
Chia sẻ
"Hướng dẫn SQL nâng cao toàn diện: Window Functions (ROW_NUMBER, RANK, LAG, LEAD), CTE recursive, Running totals, PIVOT, và kỹ thuật tối ưu query. Kèm ví dụ thực tế cho Data Analyst."
Window Functions và CTE là hai kỹ năng SQL nâng cao được hỏi nhiều nhất trong phỏng vấn Data Analyst 2026. Nếu bạn đã thành thạo SELECT, JOIN, GROUP BY — đây là bước tiếp theo giúp bạn giải quyết bài toán phức tạp hơn: xếp hạng, so sánh dòng trước-sau, running total, và truy vấn đệ quy.
Bài viết này bao gồm tất cả những gì bạn cần: từ cú pháp, ví dụ thực tế, đến kỹ thuật tối ưu performance. Tất cả ví dụ đều dùng cú pháp chuẩn SQL (tương thích MySQL 8+, PostgreSQL, SQL Server).

1. Window Functions — Tính Toán Trên "Cửa Sổ" Dữ Liệu
Window Functions thực hiện phép tính trên một "cửa sổ" (window) các dòng liên quan đến dòng hiện tại — KHÔNG gộp dòng như GROUP BY. Kết quả trả số liệu cho từng dòng.
-- Cú pháp tổng quát
function_name(expression) OVER (
[PARTITION BY column1, column2, ...] -- Chia nhóm (tùy chọn)
[ORDER BY column3 ASC|DESC] -- Sắp xếp trong nhóm
[ROWS/RANGE frame_clause] -- Khung cửa sổ (tùy chọn)
)1.1 ROW_NUMBER — Đánh số thứ tự
-- STT trong mỗi phòng ban, sắp theo lương giảm dần
SELECT
HoTen,
PhongBan,
Luong,
ROW_NUMBER() OVER (PARTITION BY PhongBan ORDER BY Luong DESC) AS STT
FROM NhanVien;
-- Kết quả:
-- Nguyễn Văn An | Kinh Doanh | 25,000,000 | 1
-- Trần Thị Bình | Kinh Doanh | 22,000,000 | 2
-- Lê Văn Cường | Kỹ Thuật | 30,000,000 | 1
-- Phạm Hồng Đức | Kỹ Thuật | 28,000,000 | 21.2 RANK vs DENSE_RANK — Xếp hạng
-- So sánh 3 loại ranking
SELECT
SanPham,
DoanhThu,
ROW_NUMBER() OVER (ORDER BY DoanhThu DESC) AS row_num, -- 1,2,3,4
RANK() OVER (ORDER BY DoanhThu DESC) AS rank_val, -- 1,2,2,4 (skip)
DENSE_RANK() OVER (ORDER BY DoanhThu DESC) AS dense_val -- 1,2,2,3 (không skip)
FROM BanHang;
-- ROW_NUMBER: luôn duy nhất (1,2,3,4...)
-- RANK: trùng thì cùng hạng, skip số tiếp theo (1,2,2,4)
-- DENSE_RANK: trùng thì cùng hạng, KHÔNG skip (1,2,2,3)1.3 LAG / LEAD — So sánh dòng trước và sau
-- Doanh thu tháng trước + Tăng trưởng %
SELECT
Thang,
DoanhThu,
LAG(DoanhThu, 1) OVER (ORDER BY Thang) AS DoanhThu_ThangTruoc,
DoanhThu - LAG(DoanhThu, 1) OVER (ORDER BY Thang) AS ChenhLech,
ROUND(
(DoanhThu - LAG(DoanhThu, 1) OVER (ORDER BY Thang))
/ LAG(DoanhThu, 1) OVER (ORDER BY Thang) * 100, 1
) AS TangTruong_Pct
FROM DoanhThu_TheoThang;
-- LAG(col, 1): lấy giá trị dòng TRƯỚC 1 vị trí
-- LAG(col, 2): lấy giá trị dòng TRƯỚC 2 vị trí
-- LEAD(col, 1): lấy giá trị dòng SAU 1 vị trí1.4 Running Total — Tổng lũy kế
-- Doanh thu lũy kế theo tháng
SELECT
Thang,
DoanhThu,
SUM(DoanhThu) OVER (ORDER BY Thang ROWS UNBOUNDED PRECEDING) AS LuyKe
FROM DoanhThu_TheoThang;
-- Lũy kế trong từng năm (reset đầu năm)
SELECT
Nam, Thang, DoanhThu,
SUM(DoanhThu) OVER (
PARTITION BY Nam
ORDER BY Thang
ROWS UNBOUNDED PRECEDING
) AS LuyKe_Theo_Nam
FROM DoanhThu_TheoThang;1.5 Moving Average — Trung bình trượt
-- Trung bình trượt 3 tháng
SELECT
Thang,
DoanhThu,
AVG(DoanhThu) OVER (
ORDER BY Thang
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS MA_3Thang
FROM DoanhThu_TheoThang;2. CTE (Common Table Expression) — Tách Query Phức Tạp
CTE cho phép đặt tên cho một subquery, giúp query phức tạp dễ đọc, dễ maintain. CTE cũng hỗ trợ đệ quy (recursive) — giải quyết bài toán cây phân cấp, sơ đồ tổ chức.
2.1 CTE cơ bản
-- Top 3 nhân viên doanh thu cao nhất mỗi phòng ban
WITH RankedSales AS (
SELECT
NV.HoTen,
NV.PhongBan,
SUM(DH.ThanhTien) AS TongDoanhThu,
RANK() OVER (PARTITION BY NV.PhongBan ORDER BY SUM(DH.ThanhTien) DESC) AS Hang
FROM NhanVien NV
JOIN DonHang DH ON NV.MaNV = DH.MaNV
GROUP BY NV.HoTen, NV.PhongBan
)
SELECT * FROM RankedSales WHERE Hang <= 3;2.2 Multiple CTEs
-- Tổng hợp: Doanh thu vs Chi phí vs Lợi nhuận theo tháng
WITH DoanhThu AS (
SELECT
FORMAT(NgayBan, 'yyyy-MM') AS Thang,
SUM(ThanhTien) AS TongDoanhThu
FROM DonHang
GROUP BY FORMAT(NgayBan, 'yyyy-MM')
),
ChiPhi AS (
SELECT
FORMAT(NgayChi, 'yyyy-MM') AS Thang,
SUM(SoTien) AS TongChiPhi
FROM ChiPhi
GROUP BY FORMAT(NgayChi, 'yyyy-MM')
)
SELECT
DT.Thang,
DT.TongDoanhThu,
CP.TongChiPhi,
DT.TongDoanhThu - ISNULL(CP.TongChiPhi, 0) AS LoiNhuan
FROM DoanhThu DT
LEFT JOIN ChiPhi CP ON DT.Thang = CP.Thang
ORDER BY DT.Thang;2.3 Recursive CTE — Sơ đồ tổ chức
-- Hiển thị cây phân cấp nhân viên
WITH OrgChart AS (
-- Anchor: Giám đốc (không có quản lý)
SELECT MaNV, HoTen, MaQuanLy, 0 AS Cap, CAST(HoTen AS NVARCHAR(500)) AS DuongDan
FROM NhanVien
WHERE MaQuanLy IS NULL
UNION ALL
-- Recursive: nhân viên dưới quyền
SELECT NV.MaNV, NV.HoTen, NV.MaQuanLy, OC.Cap + 1,
CAST(OC.DuongDan + ' → ' + NV.HoTen AS NVARCHAR(500))
FROM NhanVien NV
JOIN OrgChart OC ON NV.MaQuanLy = OC.MaNV
)
SELECT
REPLICATE(' ', Cap) + HoTen AS SoDoToChuc,
Cap AS CapBac,
DuongDan
FROM OrgChart
ORDER BY DuongDan;3. PIVOT — Chuyển Dòng Thành Cột
-- Doanh thu theo Sản Phẩm (dòng) → Quý (cột)
SELECT * FROM (
SELECT SanPham, Quy, DoanhThu
FROM BanHang_TheoQuy
) AS src
PIVOT (
SUM(DoanhThu)
FOR Quy IN ([Q1], [Q2], [Q3], [Q4])
) AS pvt;
-- Kết quả:
-- SanPham | Q1 | Q2 | Q3 | Q4
-- Laptop | 500,000,000 | 600,000,000 | 450,000,000 | 800,000,000
-- Điện thoại | 300,000,000 | 350,000,000 | 400,000,000 | 500,000,000Lưu ý: Cú pháp PIVOT chỉ có trong SQL Server. MySQL dùng CASE WHEN, PostgreSQL dùng crosstab() hoặc FILTER.
4. Kỹ Thuật Tối Ưu Query
4.1 Sử dụng Index hiệu quả
Tạo index cho cột trong WHERE, JOIN, ORDER BY
Composite index: đặt cột có selectivity cao trước
Tránh dùng hàm trên cột có index: WHERE YEAR(NgayBan) = 2026 → WHERE NgayBan >= '2026-01-01'
Dùng INCLUDE index cho cột cần SELECT nhưng không cần filter
4.2 Tránh các anti-patterns
-- ❌ Bad: SELECT * (lấy hết cột)
SELECT * FROM DonHang WHERE MaKH = 1001;
-- ✅ Good: chỉ lấy cột cần
SELECT MaDH, NgayBan, ThanhTien FROM DonHang WHERE MaKH = 1001;
-- ❌ Bad: Subquery trong SELECT (chạy N lần)
SELECT HoTen, (SELECT COUNT(*) FROM DonHang WHERE MaNV = NV.MaNV) AS SoDon
FROM NhanVien NV;
-- ✅ Good: JOIN + GROUP BY (chạy 1 lần)
SELECT NV.HoTen, COUNT(DH.MaDH) AS SoDon
FROM NhanVien NV
LEFT JOIN DonHang DH ON NV.MaNV = DH.MaNV
GROUP BY NV.HoTen;
-- ❌ Bad: LIKE '%keyword%' (full table scan)
-- ✅ Good: LIKE 'keyword%' (sử dụng index) hoặc Full-Text Search4.3 Đọc Execution Plan
SET STATISTICS IO ON — xem số lần đọc trang (logical reads)
Actual Execution Plan (Ctrl+M trong SSMS) — xem flow xử lý
Tìm Table Scan / Clustered Index Scan → cần thêm index
Tìm Key Lookup → cần INCLUDE cột trong index
Kiểm tra Estimated vs Actual Rows — chênh lệch lớn = statistics cần update
Câu Hỏi Thường Gặp (FAQ)
Window Functions có chậm hơn GROUP BY không?
Không nhất thiết. Window Functions và GROUP BY phục vụ mục đích khác nhau. GROUP BY gộp dòng (trả ít dòng hơn), Window Functions giữ nguyên số dòng. Performance phụ thuộc vào index, data volume, và cách viết query. Với index phù hợp, Window Functions rất nhanh.
CTE có nhanh hơn Subquery không?
CTE và Subquery thường có performance tương đương — SQL engine tối ưu chúng giống nhau. CTE vượt trội ở khả năng đọc hiểu (dễ maintain) và hỗ trợ recursive. Một số trường hợp CTE chậm hơn: khi CTE được tham chiếu nhiều lần (SQL Server evaluate lại mỗi lần) — lúc đó dùng temp table.
Nên học SQL nào: MySQL, PostgreSQL, hay SQL Server?
Tất cả đều dùng cú pháp chuẩn SQL cho Window Functions và CTE. Khác biệt chủ yếu ở PIVOT (SQL Server), lateral join (PostgreSQL), và một số hàm riêng. Cho beginner: PostgreSQL (free, đầy đủ tính năng). Cho doanh nghiệp VN: SQL Server (phổ biến nhất).
Có cần biết SQL nâng cao để phỏng vấn Data Analyst?
Có. Theo khảo sát 2026, 85% job posting Data Analyst yêu cầu Window Functions và 70% yêu cầu CTE. Đây là kỹ năng phân biệt junior với mid-level. Ngoài ra, knowledge về query optimization là điểm cộng lớn khi phỏng vấn senior positions.
Kết Luận
Window Functions, CTE, và Query Optimization là bộ ba kỹ năng SQL nâng cao mà mọi Data Analyst cần thành thạo. Hãy bắt đầu từ ROW_NUMBER và LAG (dùng nhiều nhất), rồi mở rộng sang CTE recursive và PIVOT.
Đọc thêm bài SQL cho người mới bắt đầu trên Trà Đá Data nếu bạn cần ôn lại kiến thức cơ bản nhé! 🍵
Mục lục
- 1. Window Functions — Tính Toán Trên "Cửa Sổ" Dữ Liệu
- 1.1 ROW_NUMBER — Đánh số thứ tự
- 1.2 RANK vs DENSE_RANK — Xếp hạng
- 1.3 LAG / LEAD — So sánh dòng trước và sau
- 1.4 Running Total — Tổng lũy kế
- 1.5 Moving Average — Trung bình trượt
- 2. CTE (Common Table Expression) — Tách Query Phức Tạp
- 2.1 CTE cơ bản
- 2.2 Multiple CTEs
- 2.3 Recursive CTE — Sơ đồ tổ chức
- 3. PIVOT — Chuyển Dòng Thành Cột
- 4. Kỹ Thuật Tối Ưu Query
- 4.1 Sử dụng Index hiệu quả
- 4.2 Tránh các anti-patterns
- 4.3 Đọc Execution Plan
- Câu Hỏi Thường Gặp (FAQ)
- Window Functions có chậm hơn GROUP BY không?
- CTE có nhanh hơn Subquery không?
- Nên học SQL nào: MySQL, PostgreSQL, hay SQL Server?
- Có cần biết SQL nâng cao để phỏng vấn Data Analyst?
- Kết Luận
Muốn làm chủ SQL?
Tham gia khóa học E-Learning của Trà Đá Data để được hướng dẫn chi tiết từ A-Z với Case Study thực tế.
Tìm hiểu ngayBình luận
Đăng nhập để tham gia bình luận
Đăng nhậpNhận bài viết mới nhất
Đăng ký để nhận thông báo khi có bài viết mới. Không spam, chỉ kiến thức chất lượng.
Bài viết liên quan
Khám phá thêm các bài viết cùng chủ đề

SQL Cho Người Mới Bắt Đầu: SELECT, JOIN, GROUP BY Và Subquery Từ A Đến Z
Hướng dẫn SQL từ cơ bản đến trung cấp: SELECT, WHERE, JOIN, GROUP BY, HAVING, Subquery, Window Functions. Ví dụ thực tế với dữ liệu kinh doanh, so sánh với Excel.
Stored Procedure Trong SQL: Viết Thủ Tục Lưu Trữ Và Tự Động Hoá Truy Vấn
Hướng dẫn tạo Stored Procedure trong SQL Server và PostgreSQL: parameters, variables, control flow, error handling — tự động hoá truy vấn phức tạp.
Index Trong SQL: Tăng Tốc Truy Vấn Gấp 100 Lần Và Những Sai Lầm Cần Tránh
Hướng dẫn tạo và quản lý Index trong SQL — Clustered, Non-Clustered, Composite Index, khi nào nên dùng, khi nào tránh, và cách đo hiệu năng.
