Window function SQL: 6 hàm Data Analyst dùng mỗi ngày

Featured Image 1200×628@2x

🪟 CÙNG MỘT BẢNG, HAI CÁCH TÍNH

GROUP BY

gộp 16 dòng thành 4 dòng

chi_nhanh tong
Hà Nội 1.760
Hải Phòng 1.510
Đà Nẵng 1.150
TP.HCM 2.030

✕ Mất chi tiết từng tháng

Window function

giữ đủ 16 dòng, thêm cột mới

chi_nhanh thang doanh_thu luy_ke
Hà Nội 07/2026 420 420
Hà Nội 08/2026 450 870
Hà Nội 09/2026 410 1.280
… … … …

✓ Vừa có chi tiết, vừa có số tổng hợp

Sếp hỏi: “Xếp hạng chi nhánh theo doanh thu tháng này, so với tháng trước tăng bao nhiêu phần trăm, và mỗi chi nhánh chiếm bao nhiêu tỷ trọng?”. Nếu chỉ dùng GROUP BY, bạn sẽ phải viết vài câu truy vấn con, nối bảng với chính nó, hoặc xuất ra Excel để tính tiếp.

Window function giải quyết cả ba việc trong một câu SQL gọn gàng. Bài viết này giải thích window function trong SQL là gì, cách đọc cú pháp OVER (PARTITION BY ... ORDER BY ...), rồi đi qua 6 hàm Data Analyst dùng mỗi ngày với cùng một bộ dữ liệu mẫu, kèm kết quả để bạn đối chiếu.

Window function là gì?

Window function (hàm cửa sổ) là hàm tính toán trên một nhóm dòng có liên quan đến dòng hiện tại, nhưng vẫn giữ nguyên từng dòng trong kết quả. “Cửa sổ” chính là nhóm dòng mà hàm được phép nhìn vào khi tính cho mỗi dòng.

Khác gì GROUP BY?

  • GROUP BY gộp nhiều dòng thành một. 16 dòng doanh thu của 4 chi nhánh sau khi GROUP BY theo chi nhánh chỉ còn 4 dòng.
  • Window function tính toán trên nhóm nhưng không gộp. Vẫn đủ 16 dòng, mỗi dòng có thêm một cột mới như thứ hạng, doanh thu tháng trước hay tổng lũy kế.

Nhờ vậy bạn vừa thấy chi tiết từng dòng, vừa thấy con số so sánh với cả nhóm, đúng kiểu bảng mà báo cáo thường cần.

Cú pháp cơ bản

🗄 cu-phap.sqlSQL Server
1TEN_HAM(...) OVER (
2  PARTITION BY cot_chia_nhom   -- chia cửa sổ theo nhóm (không bắt buộc)
3  ORDER BY cot_sap_xep         -- thứ tự dòng trong mỗi nhóm
4  ROWS BETWEEN ... AND ...     -- phạm vi dòng cụ thể (không bắt buộc)
5)

PARTITION BY

giống GROUP BY nhưng không gộp dòng. Mỗi chi nhánh là một cửa sổ riêng. Bỏ đi thì cả bảng là một cửa sổ.

ORDER BY

sắp xếp dòng trong cửa sổ, bắt buộc với các hàm xếp hạng và LAG/LEAD.

ROWS BETWEEN

giới hạn cửa sổ vào một số dòng nhất định, dùng cho lũy kế và trung bình trượt.

Các ví dụ trong bài viết cho SQL Server, và chạy được gần như nguyên văn trên PostgreSQL, MySQL 8 và BigQuery.

Dữ liệu mẫu dùng trong bài

Bảng DoanhThu gồm 3 cột: chi_nhanh, thang và doanh_thu (triệu đồng, cột tháng lưu ngày đầu tháng như 2026-10-01), 16 dòng cho 4 chi nhánh trong 4 tháng. Để dễ nhìn, dưới đây trình bày dạng xoay ngang:

Chi nhánh 07/2026 08/2026 09/2026 10/2026
Hà Nội 420 450 410 480
Hải Phòng 300 340 390 480
Đà Nẵng 260 280 300 310
TP.HCM 500 470 540 520

Tất cả kết quả bên dưới đều được chạy thật trên bộ dữ liệu này, bạn có thể tự tạo bảng và chạy lại để đối chiếu.

Hàm 1: ROW_NUMBER – Đánh số thứ tự và lấy dòng đầu tiên

HÀM 1 · ROW_NUMBER

ROW_NUMBER() đánh số 1, 2, 3… cho từng dòng trong mỗi cửa sổ theo thứ tự bạn chọn, không bao giờ trùng số. Ứng dụng phổ biến nhất là lấy dòng đầu tiên của mỗi nhóm: đơn gần nhất của mỗi khách, bản ghi mới nhất khi dữ liệu bị trùng, hay tháng doanh thu cao nhất của mỗi chi nhánh.

🗄 window.sqlSQL Server
1WITH xep AS (
2  SELECT chi_nhanh, thang, doanh_thu,
3         ROW_NUMBER() OVER (PARTITION BY chi_nhanh ORDER BY doanh_thu DESC) AS stt
4  FROM DoanhThu
5)
6SELECT chi_nhanh, thang, doanh_thu
7FROM xep
8WHERE stt = 1;

▶ KẾT QUẢ (✨ cột do window function tạo ra)

chi_nhanh thang doanh_thu
Hà Nội 10/2026 480
Hải Phòng 10/2026 480
TP.HCM 09/2026 540
Đà Nẵng 10/2026 310

⚠ Lưu ý: window function không dùng trực tiếp trong WHERE được, nên ta tính trong CTE rồi lọc ở bước sau.

Hàm 2: RANK và DENSE_RANK – Xếp hạng có đồng hạng

HÀM 2 · RANK / DENSE_RANK

Khi hai dòng bằng nhau, ROW_NUMBER vẫn ép chúng thành hai số khác nhau. RANK và DENSE_RANK cho chúng cùng hạng, chỉ khác ở cách đánh số sau đó.

🗄 window.sqlSQL Server
1SELECT chi_nhanh, doanh_thu,
2       ROW_NUMBER() OVER (ORDER BY doanh_thu DESC) AS row_number,
3       RANK()       OVER (ORDER BY doanh_thu DESC) AS rank,
4       DENSE_RANK() OVER (ORDER BY doanh_thu DESC) AS dense_rank
5FROM DoanhThu
6WHERE thang = '2026-10-01';

▶ KẾT QUẢ (✨ cột do window function tạo ra)

chi_nhanh doanh_thu ✨ row_number ✨ rank ✨ dense_rank
TP.HCM 520 1 1 1
Hà Nội 480 2 2 2
Hải Phòng 480 3 2 2
Đà Nẵng 310 4 4 3
  • RANK: đồng hạng rồi nhảy số (1, 2, 2, 4), giống bảng xếp hạng thi đấu.
  • DENSE_RANK: đồng hạng nhưng không nhảy số (1, 2, 2, 3), hợp khi cần “top 3 mức doanh thu”.
  • ROW_NUMBER: Hà Nội và Hải Phòng cùng 480 nhưng bị chia số 2 và 3 một cách tùy ý. Muốn kết quả ổn định, hãy thêm cột phụ vào ORDER BY, ví dụ ORDER BY doanh_thu DESC, chi_nhanh.

Hàm 3: LAG và LEAD – So với kỳ trước

HÀM 3 · LAG / LEAD

LAG(cot) lấy giá trị của dòng trước trong cửa sổ, LEAD(cot) lấy giá trị của dòng sau. Đây là cách nhanh nhất để tính tăng trưởng so với tháng trước (MoM), khoảng cách giữa hai lần mua hay thời gian xử lý giữa hai bước.

🗄 window.sqlSQL Server
1SELECT chi_nhanh, thang, doanh_thu,
2       LAG(doanh_thu) OVER (PARTITION BY chi_nhanh ORDER BY thang) AS thang_truoc,
3       ROUND(
4         (doanh_thu - LAG(doanh_thu) OVER (PARTITION BY chi_nhanh ORDER BY thang)) * 100.0
5         / LAG(doanh_thu) OVER (PARTITION BY chi_nhanh ORDER BY thang), 1) AS tang_truong_pct
6FROM DoanhThu
7WHERE chi_nhanh = N'Hải Phòng';

▶ KẾT QUẢ (✨ cột do window function tạo ra)

chi_nhanh thang doanh_thu ✨ thang_truoc ✨ tang_truong_pct
Hải Phòng 07/2026 300 NULL NULL
Hải Phòng 08/2026 340 300 13,3
Hải Phòng 09/2026 390 340 14,7
Hải Phòng 10/2026 480 390 23,1

Dòng đầu tiên không có tháng trước nên trả về NULL. Nếu muốn thay bằng 0, dùng tham số thứ ba: LAG(doanh_thu, 1, 0). Muốn so với cùng kỳ năm trước với dữ liệu theo tháng, dùng LAG(doanh_thu, 12).

Con số trên cho thấy Hải Phòng tăng trưởng nhanh dần qua từng tháng, một nhận định khó thấy nếu chỉ nhìn bảng doanh thu thô. Khi đưa lên dashboard, bạn có thể đặt cột tăng trưởng cạnh doanh thu, theo đúng tinh thần của bài Dashboard không phải để đẹp.

Hàm 4 và 5: SUM OVER và AVG OVER – Lũy kế và trung bình trượt

HÀM 4 · SUM OVER  ·  HÀM 5 · AVG OVER

Các hàm tổng hợp quen thuộc như SUM, AVG cũng trở thành window function khi có OVER. Kết hợp với ROWS BETWEEN, bạn có hai chỉ số rất hay dùng trong báo cáo:

  • Doanh thu lũy kế (running total): cộng từ tháng đầu tiên đến tháng hiện tại, dùng để so với kế hoạch năm.
  • Trung bình trượt 3 tháng (moving average): trung bình của tháng hiện tại và 2 tháng trước, giúp làm mượt biến động để thấy xu hướng.
🗄 window.sqlSQL Server
1SELECT chi_nhanh, thang, doanh_thu,
2       SUM(doanh_thu) OVER (
3         PARTITION BY chi_nhanh ORDER BY thang
4         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS luy_ke,
5       ROUND(AVG(doanh_thu * 1.0) OVER (
6         PARTITION BY chi_nhanh ORDER BY thang
7         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 1) AS tb_truot_3_thang
8FROM DoanhThu
9WHERE chi_nhanh = N'Hà Nội';

▶ KẾT QUẢ (✨ cột do window function tạo ra)

chi_nhanh thang doanh_thu ✨ luy_ke ✨ tb_truot_3_thang
Hà Nội 07/2026 420 420 420,0
Hà Nội 08/2026 450 870 435,0
Hà Nội 09/2026 410 1.280 426,7
Hà Nội 10/2026 480 1.760 446,7

Doanh thu Hà Nội tháng 9 giảm xuống 410, nhưng trung bình trượt chỉ nhích nhẹ rồi tiếp tục tăng, cho thấy đây nhiều khả năng là dao động ngắn hạn chứ chưa phải xu hướng giảm. Lưu ý hai tháng đầu chưa đủ 3 tháng nên trung bình chỉ tính trên số dòng đang có.

Hàm 6: SUM OVER không ORDER BY – Tỷ trọng trong tổng

HÀM 6 · SUM OVER ()

Khi bỏ ORDER BY trong OVER, SUM tính tổng của cả cửa sổ và gắn con số đó vào từng dòng. Chia doanh thu của dòng cho tổng này là ra tỷ trọng.

🗄 window.sqlSQL Server
1SELECT chi_nhanh, doanh_thu,
2       SUM(doanh_thu) OVER () AS tong_toan_quoc,
3       ROUND(doanh_thu * 100.0 / SUM(doanh_thu) OVER (), 1) AS ty_trong_pct
4FROM DoanhThu
5WHERE thang = '2026-10-01';

▶ KẾT QUẢ (✨ cột do window function tạo ra)

chi_nhanh doanh_thu ✨ tong_toan_quoc ✨ ty_trong_pct
TP.HCM 520 1.790 29,1
Hà Nội 480 1.790 26,8
Hải Phòng 480 1.790 26,8
Đà Nẵng 310 1.790 17,3

Thêm PARTITION BY để tính tỷ trọng trong từng nhóm, ví dụ SUM(doanh_thu) OVER (PARTITION BY thang) cho tỷ trọng của mỗi chi nhánh trong từng tháng trên cùng một bảng.

📅 Muốn học SQL bài bản từ cơ bản đến window function và ứng dụng vào báo cáo? Xem lịch khai giảng mới nhất tại MCNA.

5 lỗi thường gặp khi dùng window function

⚠ Dùng window function trong WHERE

SQL tính WHERE trước khi tính window function, nên WHERE ROW_NUMBER() OVER (...) = 1 sẽ báo lỗi. Hãy tính trong CTE hoặc truy vấn con rồi mới lọc.

⚠ Lọc dữ liệu làm sai kết quả LAG

nếu WHERE thang = '2026-10-01' đặt cùng câu với LAG, tháng 9 đã bị loại trước nên cột tháng trước toàn là NULL. Tính LAG trên đủ dữ liệu trong CTE, lọc tháng cần xem ở bước sau.

⚠ Quên PARTITION BY

thiếu PARTITION BY chi_nhanh, LAG của dòng đầu tiên của Hải Phòng sẽ lấy nhầm doanh thu tháng cuối của chi nhánh đứng trước.

⚠ Thiếu dòng cho tháng không có dữ liệu

nếu một chi nhánh không có doanh thu tháng 8 và bảng không có dòng đó, LAG của tháng 9 sẽ so với tháng 7. Hãy tạo đủ khung tháng cho mọi chi nhánh trước khi tính.

⚠ Dùng ORDER BY trong SUM OVER mà không ghi rõ phạm vi

khi có ORDER BY mà không có ROWS BETWEEN, nhiều hệ quản trị mặc định dùng RANGE, cộng gộp các dòng có cùng giá trị sắp xếp và cho lũy kế khác mong đợi. Luôn viết rõ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

Câu hỏi thường gặp về window function

?

Window function có làm truy vấn chậm không?

Window function cần sắp xếp dữ liệu theo PARTITION BY và ORDER BY, nên với bảng rất lớn có thể tốn thời gian. Tuy vậy, so với cách cũ là nối bảng với chính nó hay dùng truy vấn con lồng nhau, window function thường nhanh và dễ đọc hơn. Lọc bớt dữ liệu không cần thiết trong CTE đầu tiên sẽ giúp cải thiện tốc độ.

?

Nên dùng RANK hay DENSE_RANK?

Dùng RANK khi muốn thứ hạng phản ánh đúng số người đứng trước, như bảng xếp hạng thi đua. Dùng DENSE_RANK khi muốn lấy “top N mức giá trị” mà không bị hụt vì đồng hạng.

?

Ngoài 6 hàm trên còn hàm nào hữu ích?

NTILE(n) chia dữ liệu thành n nhóm bằng nhau, rất tiện để chấm điểm 1–5 trong phân tích RFM. FIRST_VALUE và LAST_VALUE lấy giá trị đầu hoặc cuối trong cửa sổ, còn COUNT(*) OVER (...) đếm số dòng trong từng nhóm mà không cần GROUP BY.

?

Excel hay Power BI có làm được những việc này không?

Có. Power BI có các hàm DAX tương tự như RANKX hay nhóm hàm thời gian, và bản Power BI gần đây có thêm hàm WINDOW, OFFSET. Tuy nhiên tính sẵn bằng SQL giúp báo cáo nhẹ hơn và dùng lại được cho nhiều công cụ. Xem thêm bài Time Intelligence trong Power BI.

?

Có thể nhờ AI viết window function không?

Có, và AI viết khá tốt nếu bạn mô tả rõ cấu trúc bảng và kết quả mong muốn. Nhưng hãy tự kiểm tra PARTITION BY, ORDER BY và phạm vi ROWS, vì đây là nơi AI hay viết sai mà câu lệnh vẫn chạy được. Bài Prompt cho Data Analyst có mẫu prompt viết SQL bạn có thể dùng ngay.

Kết luận

Window function giúp Data Analyst tính toán trên cả nhóm dòng mà vẫn giữ nguyên chi tiết từng dòng. Chỉ với 6 hàm ROW_NUMBER, RANK/DENSE_RANK, LAG/LEAD, SUM OVER lũy kế, AVG OVER trượt và SUM OVER tỷ trọng, bạn đã xử lý được phần lớn câu hỏi xếp hạng, so sánh kỳ trước và tỷ trọng trong báo cáo hằng ngày.

Hãy nhớ ba điều: chia đúng PARTITION BY, sắp xếp đúng ORDER BY, và viết rõ phạm vi ROWS BETWEEN khi tính lũy kế. Tạo bảng mẫu trong bài và chạy lại từng câu là cách nhanh nhất để nắm vững.

Nếu bạn muốn học SQL bài bản và ứng dụng vào báo cáo thực tế cùng Power BI, các khóa học tại MCNA Technology School sẽ đồng hành cùng bạn. Xem lịch khai giảng mới nhất. Đọc thêm: Phân tích cohort là gì? và Phân tích RFM là gì?.

Thông tin tác giả

Võ Ngọc Vương – Data Analyst tại MCNA Technology School

MCNA Technology School – Đơn vị tiên phong đào tạo AI, Big Data, Business Intelligence tại Việt Nam. Chuyên sâu về Power BI, Python, SQL và Data Analytics — đối tác đào tạo của 300+ doanh nghiệp.

Đối tác đào tạo: Viettel Global, Masan Group, Techcombank, VPBank, Daikin.

→ Chỉ mục