HÀM CỬA SỔ TRONG SQL
Xếp hạng, so với kỳ trước và cộng dồn ngay trong một câu truy vấn mà không làm mất từng dòng dữ liệu
Có những câu hỏi kinh doanh nghe rất đơn giản nhưng lại khó viết bằng SQL cơ bản. "Ba sản phẩm bán chạy nhất ở mỗi cửa hàng là gì?" "Doanh thu tháng này tăng bao nhiêu phần trăm so với tháng trước?" "Từ đầu năm đến giờ đã bán được bao nhiêu?" Người mới học SQL thường xử lý những câu như vậy bằng cách xuất dữ liệu ra Excel rồi kéo công thức. Cách đó chạy được với vài nghìn dòng, nhưng trở nên chậm, dễ sai và khó lặp lại khi dữ liệu lên tới hàng trăm nghìn hóa đơn.
Điểm chung của ba câu hỏi trên là chúng cần tính toán trên một nhóm dòng nhưng vẫn phải giữ lại từng dòng. GROUP BY làm được vế đầu nhưng làm mất vế sau, vì nó dồn mỗi nhóm thành một dòng. Hàm cửa sổ (window function) được đưa vào chuẩn SQL từ phiên bản 2003 để lấp đúng khoảng trống này và ngày nay có mặt trong hầu hết các hệ quản trị cơ sở dữ liệu phổ biến (ISO/IEC, 2023). Với người làm phân tích, đây là nhóm kỹ thuật đáng học nhất sau JOIN và GROUP BY.
Bài viết đi theo trình tự sau. Mục 1 cho thấy vì sao GROUP BY không đủ và hàm cửa sổ khác ở đâu. Mục 2 giải thích cú pháp của mệnh đề OVER. Ba mục tiếp theo đi qua ba nhóm việc hay gặp nhất: xếp hạng, so với kỳ trước, cộng dồn và trung bình trượt. Mục 6 bàn về tỷ trọng trong nhóm và phân tích Pareto. Mục 7 gom các cạm bẫy thường gặp. Mọi ví dụ chạy trên bộ dữ liệu mô phỏng của Sen Vàng Mart, một chuỗi siêu thị giả định có 18 cửa hàng, 399.148 hóa đơn và 939.668 dòng hàng trong hai năm 2024 và 2025, lưu trong SQLite; các bảng kết quả trong bài là kết quả thật của những câu lệnh đó, tiền tính bằng đồng.
1. Vì sao GROUP BY không đủ
Trước khi đi vào cú pháp, cần thấy rõ giới hạn của công cụ quen thuộc. Mục này đặt GROUP BY và hàm cửa sổ cạnh nhau trên cùng một câu hỏi.
Giả sử quản lý vùng muốn biết sản phẩm nào đứng đầu doanh thu ở từng cửa hàng. GROUP BY theo cửa hàng và sản phẩm cho ra doanh thu của từng cặp. Nhưng để biết sản phẩm nào đứng thứ nhất trong cửa hàng của nó, ta cần so mỗi dòng với các dòng khác cùng cửa hàng. Nếu GROUP BY tiếp theo cửa hàng, các sản phẩm bị dồn lại và không còn gì để so. Không có hàm cửa sổ, người viết phải tự kết nối bảng với chính nó hoặc viết truy vấn lồng tương quan, vừa khó đọc vừa chậm.
Hàm cửa sổ làm khác. Nó tính một giá trị cho mỗi dòng dựa trên một tập dòng liên quan đến dòng đó, gọi là cửa sổ và không dồn dòng nào lại (SQLite, n.d.). Hình 1 minh họa: các dòng được chia thành từng phần theo cửa hàng, sắp thứ tự trong mỗi phần, rồi mỗi dòng nhận thêm một cột hạng được tính lại từ 1 ở mỗi cửa hàng.

Hình 1. PARTITION BY chia dòng thành từng phần, ORDER BY sắp thứ tự trong mỗi phần, hàm cửa sổ gắn kết quả vào từng dòng.
Điều quan trọng nhất trong Hình 1 là số dòng ở đầu ra bằng số dòng ở đầu vào. Đó là khác biệt căn bản giữa hàm cửa sổ và hàm tổng hợp thông thường và cũng là lý do hàm cửa sổ trả lời được những câu hỏi "so với nhóm của nó" mà GROUP BY không trả lời được.
2. Cú pháp của mệnh đề OVER
Mọi hàm cửa sổ đều có chung một khuôn: tên hàm, theo sau là mệnh đề OVER mô tả cửa sổ. Mục này tách khuôn đó thành ba thành phần.
ten_ham(...) OVER (
PARTITION BY cot_chia_nhom
ORDER BY cot_sap_xep
ROWS BETWEEN ... AND ...
)
Bảng 1 tóm tắt vai trò của từng thành phần. Cả ba đều không bắt buộc: SUM(DoanhThu) OVER () với cặp ngoặc rỗng nghĩa là cửa sổ gồm toàn bộ kết quả, rất tiện để tính tỷ trọng.
Bảng 1. Ba thành phần của mệnh đề OVER
| Thành phần | Vai trò | Nếu bỏ qua |
|---|---|---|
| PARTITION BY | Chia dòng thành các phần độc lập; hàm được tính lại từ đầu ở mỗi phần | Cả kết quả là một phần duy nhất |
| ORDER BY | Sắp thứ tự dòng trong mỗi phần; cần cho xếp hạng, LAG, LEAD, cộng dồn | Các dòng trong phần không có thứ tự; hàm tổng hợp tính trên cả phần |
| Khung cửa sổ (ROWS, RANGE) | Giới hạn cửa sổ vào một đoạn quanh dòng hiện tại, ví dụ ba dòng gần nhất | Khi có ORDER BY: từ đầu phần đến dòng hiện tại và các dòng ngang giá trị với nó |
Nguồn: dựa trên SQLite (n.d.) và ISO/IEC (2023).
Dòng cuối của Bảng 1 có một chi tiết nhỏ nhưng gây ra nhiều lỗi khó thấy: khung mặc định tính cả các dòng có cùng giá trị ORDER BY với dòng hiện tại. Mục 7 sẽ cho thấy hậu quả của chi tiết này bằng số liệu thật.
Khi một câu lệnh dùng cùng một cửa sổ nhiều lần, có thể đặt tên cho nó bằng mệnh đề WINDOW để tránh viết lặp, ví dụ WINDOW w AS (ORDER BY Thang) rồi viết LAG(DoanhThu) OVER w. Cú pháp này có trong PostgreSQL, MySQL 8.0 và SQLite; SQL Server hỗ trợ từ bản 2022.
3. Xếp hạng trong nhóm
Xếp hạng là ứng dụng quen thuộc nhất của hàm cửa sổ. Mục này so sánh ba hàm xếp hạng rồi giải bài toán "top N trong mỗi nhóm".
3.1. ROW_NUMBER, RANK và DENSE_RANK
Ba hàm này cho cùng kết quả khi mọi giá trị khác nhau và chỉ tách ra khi có giá trị bằng nhau. Để thấy khác biệt, câu lệnh dưới đây xếp 18 cửa hàng theo số nhân viên được phân công, một cột có nhiều giá trị trùng.
WITH nv AS (
SELECT MaCH, COUNT(*) AS SoNV FROM PHANCONG GROUP BY MaCH
)
SELECT MaCH, SoNV,
ROW_NUMBER() OVER (ORDER BY SoNV DESC, MaCH) AS RowNum,
RANK() OVER (ORDER BY SoNV DESC) AS Rnk,
DENSE_RANK() OVER (ORDER BY SoNV DESC) AS DenseRnk
FROM nv
ORDER BY SoNV DESC, MaCH;
Bảng 2 trích chín dòng ở giữa kết quả, nơi các giá trị trùng xuất hiện.
Bảng 2. Ba hàm xếp hạng trên số nhân viên của mỗi cửa hàng (trích dòng 5 đến 13)
| MaCH | SoNV | RowNum | Rnk | DenseRnk |
|---|---|---|---|---|
| CH01 | 26 | 5 | 5 | 5 |
| CH03 | 26 | 6 | 5 | 5 |
| CH07 | 26 | 7 | 5 | 5 |
| CH04 | 24 | 8 | 8 | 6 |
| CH13 | 23 | 9 | 9 | 7 |
| CH02 | 22 | 10 | 10 | 8 |
| CH09 | 22 | 11 | 10 | 8 |
| CH14 | 22 | 12 | 10 | 8 |
| CH16 | 22 | 13 | 10 | 8 |
Nguồn: kết quả chạy trên SQLite với bộ dữ liệu Sen Vàng Mart.
Ba cửa hàng cùng có 26 nhân viên. ROW_NUMBER vẫn đánh 5, 6, 7 vì nó luôn cho số liên tục; thứ tự giữa ba cửa hàng này do cột phụ MaCH trong ORDER BY quyết định, nếu không có cột phụ thì thứ tự là tùy ý và có thể đổi giữa hai lần chạy. RANK cho cả ba hạng 5 rồi nhảy sang hạng 8, giống cách xếp hạng trong thi đấu thể thao. DENSE_RANK cũng cho cả ba hạng 5 nhưng cửa hàng kế tiếp nhận hạng 6, không nhảy số. Chọn hàm nào phụ thuộc vào câu hỏi: "lấy đúng một dòng mỗi nhóm" cần ROW_NUMBER; "ai đứng trong top 5, kể cả đồng hạng" cần RANK; "có bao nhiêu mức khác nhau" cần DENSE_RANK.
3.2. Top N trong mỗi nhóm
Quay lại câu hỏi ở mục 1. Câu lệnh sau tính doanh thu năm 2025 của từng sản phẩm ở từng cửa hàng, đánh số trong mỗi cửa hàng, rồi giữ ba dòng đầu.
WITH dt AS (
SELECT h.MaCH, d.MaSP, SUM(d.SoLuong * d.DonGiaBan) AS DoanhThu
FROM CHITIETHD d JOIN HOADON h ON h.SoHD = d.SoHD
WHERE h.NgayGio >= '2025-01-01' AND h.NgayGio < '2026-01-01'
GROUP BY h.MaCH, d.MaSP
),
xh AS (
SELECT MaCH, MaSP, DoanhThu,
ROW_NUMBER() OVER (PARTITION BY MaCH ORDER BY DoanhThu DESC) AS Hang
FROM dt
)
SELECT x.MaCH, x.Hang, s.TenSP, x.DoanhThu
FROM xh x JOIN SANPHAM s ON s.MaSP = x.MaSP
WHERE x.Hang <= 3 AND x.MaCH IN ('CH01', 'CH08', 'CH15')
ORDER BY x.MaCH, x.Hang;
Câu lệnh có ba bước, mỗi bước là một khối WITH có tên: gộp doanh thu, xếp hạng, lọc. Điều kiện Hang <= 3 buộc phải nằm ở câu ngoài cùng, vì hàm cửa sổ được tính ở bước SELECT, tức sau WHERE; trong cùng một câu, WHERE chưa "nhìn thấy" cột Hang. Bảng 3 là kết quả cho ba cửa hàng ở ba thành phố.
Bảng 3. Ba sản phẩm doanh thu cao nhất năm 2025 ở ba cửa hàng
| MaCH | Hang | TenSP | DoanhThu |
|---|---|---|---|
| CH01 | 1 | Kem chống nắng loại nhỏ | 32595000 |
| CH01 | 2 | Vang đỏ 750ml | 32012000 |
| CH01 | 3 | Vang đỏ hộp 3L | 30655500 |
| CH08 | 1 | Vang đỏ hộp 3L | 36099000 |
| CH08 | 2 | Kem chống nắng loại nhỏ | 31402500 |
| CH08 | 3 | Vang đỏ 750ml | 23956000 |
| CH15 | 1 | Kem chống nắng loại nhỏ | 29150000 |
| CH15 | 2 | Vang đỏ hộp 3L | 26644500 |
| CH15 | 3 | Vang đỏ loại phổ thông | 20325000 |
Nguồn: kết quả chạy trên SQLite với bộ dữ liệu Sen Vàng Mart.
Bảng 3 cho thấy cùng một nhóm hàng giá cao đứng đầu ở cả ba nơi, nhưng thứ tự khác nhau: ở Hải Châu (CH08), hộp vang 3 lít vượt kem chống nắng. Đây là loại chi tiết mà một bảng tổng toàn chuỗi che mất. Cùng khuôn câu lệnh này giải được nhiều câu hỏi khác chỉ bằng cách đổi cột chia nhóm và cột sắp xếp: hóa đơn đầu tiên của mỗi khách hàng, nhân viên bán giỏi nhất mỗi tháng, nhà cung cấp giao hàng chậm nhất mỗi quý.
4. So với kỳ trước bằng LAG và LEAD
Câu hỏi "tăng bao nhiêu so với tháng trước" xuất hiện trong gần như mọi báo cáo. Mục này dùng LAG để trả lời nó và cho thấy vì sao so với cùng kỳ năm trước đôi khi quan trọng hơn.
LAG(cột, k) lấy giá trị của dòng đứng trước dòng hiện tại k vị trí trong cửa sổ; LEAD(cột, k) lấy giá trị của dòng đứng sau. Khi bỏ trống k, hệ quản trị hiểu là 1. Câu lệnh dưới đây tính doanh thu từng tháng của cả chuỗi, tăng trưởng so với tháng trước (LAG 1) và so với cùng tháng năm trước (LAG 12).
WITH th AS (
SELECT strftime('%Y-%m', h.NgayGio) AS Thang,
SUM(d.SoLuong * d.DonGiaBan) AS DoanhThu
FROM CHITIETHD d JOIN HOADON h ON h.SoHD = d.SoHD
GROUP BY Thang
)
SELECT Thang, DoanhThu,
ROUND(100.0 * (DoanhThu - LAG(DoanhThu) OVER w) / LAG(DoanhThu) OVER w, 1) AS SoThangTruoc,
ROUND(100.0 * (DoanhThu - LAG(DoanhThu, 12) OVER w) / LAG(DoanhThu, 12) OVER w, 1) AS SoCungKy,
SUM(DoanhThu) OVER (PARTITION BY substr(Thang, 1, 4) ORDER BY Thang) AS LuyKeNam
FROM th
WINDOW w AS (ORDER BY Thang)
ORDER BY Thang;
Câu lệnh trả về 24 tháng. Bảng 4 trích bốn tháng quanh Tết năm 2025, giai đoạn mà ba cột so sánh kể ba câu chuyện khác nhau.
Bảng 4. Doanh thu quanh Tết 2025 với ba cách so sánh (%)
| Thang | DoanhThu | SoThangTruoc | SoCungKy | LuyKeNam |
|---|---|---|---|---|
| 2024-12 | 2287984500 | 4.2 | NULL | 27143804200 |
| 2025-01 | 2795754300 | 22.2 | 13.4 | 2795754300 |
| 2025-02 | 1976508800 | -29.3 | -14.4 | 4772263100 |
| 2025-03 | 2304073300 | 16.6 | 3.5 | 7076336400 |
Nguồn: kết quả chạy trên SQLite với bộ dữ liệu Sen Vàng Mart.
Nhìn cột so với tháng trước, tháng 1/2025 tăng 22,2% và tháng 2 giảm 29,3%, đủ để một bản tin nội bộ viết hai tiêu đề trái ngược nhau trong hai tháng liên tiếp. Cột so với cùng kỳ cũng dao động mạnh: tháng 1 tăng 13,4%, tháng 2 giảm 14,4%. Lý do nằm ngoài dữ liệu: Tết Giáp Thìn rơi vào ngày 10/2/2024, còn Tết Ất Tỵ rơi vào ngày 29/1/2025, nên đợt mua sắm trước Tết chuyển từ tháng 2 sang tháng 1. Cột lũy kế từ đầu năm cho câu trả lời bình tĩnh hơn: hết tháng 2, chuỗi đạt 4.772 triệu đồng năm 2025, gần như bằng đúng 4.775 triệu đồng của cùng kỳ năm 2024. Mùa Tết không làm doanh thu tăng hay giảm; nó chỉ dời doanh thu từ tháng này sang tháng khác.
Bài học ở đây vượt ra ngoài SQL. Với dữ liệu có mùa vụ âm lịch, phép so sánh theo tháng dương lịch dễ đánh lừa; lũy kế nhiều tháng, hoặc so theo khoảng cách đến ngày Tết, ổn định hơn. Hàm cửa sổ giúp tính cả ba cách trong một câu lệnh, để người đọc báo cáo thấy được bức tranh đầy đủ.
5. Cộng dồn và trung bình trượt
Hai phép tính quen thuộc trong bảng tính là cộng dồn và trung bình trượt đều có thể viết bằng hàm tổng hợp kèm OVER. Mục này giải thích khung cửa sổ, thành phần thứ ba trong Bảng 1.
SUM(DoanhThu) OVER (ORDER BY Thang) cho tổng cộng dồn từ tháng đầu tiên đến tháng hiện tại; cột LuyKeNam ở Bảng 4 thêm PARTITION BY năm để cộng dồn bắt đầu lại từ tháng 1 mỗi năm. Muốn giới hạn cửa sổ vào một đoạn ngắn, ta khai báo khung cửa sổ (window frame). Trung bình trượt ba tháng là trung bình của tháng hiện tại và hai tháng trước nó:
AVG(DoanhThu) OVER (ORDER BY Thang ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
Hình 2 vẽ doanh thu 24 tháng của Sen Vàng Mart cùng đường trung bình trượt ba tháng tính bằng câu lệnh trên.

Hình 2. Doanh thu theo tháng và trung bình trượt ba tháng của Sen Vàng Mart, 2024 và 2025.
Đường trung bình trượt trong Hình 2 làm hai việc. Nó nén cú sốc Tết từ biên độ hơn 800 triệu đồng giữa tháng 1 và tháng 2/2025 xuống còn khoảng 70 triệu đồng và nó làm lộ ra một xu hướng mà đường doanh thu gốc che mất: cả bảy tháng từ tháng 6/2025 đều cao hơn cùng kỳ năm trước từ 4,6% đến 7,6%, khớp với việc cửa hàng CH07 khai trương ngày 1/6/2025. Cả năm 2025, chuỗi đạt 28.083 triệu đồng, tăng 3,5% so với 27.144 triệu đồng của năm 2024. Trung bình trượt là kỹ thuật làm mượt đơn giản nhất trong dự báo chuỗi thời gian; nó cũng có giới hạn là luôn đi chậm hơn dữ liệu thật vài kỳ (Hyndman & Athanasopoulos, 2021). Bài Dự báo doanh số bằng chuỗi thời gian cho doanh nghiệp đi tiếp từ điểm này.
Hai tháng đầu của chuỗi là trường hợp biên cần ghi chú: tháng 1/2024 chỉ có một tháng để tính trung bình, tháng 2/2024 có hai. Hàm cửa sổ không báo lỗi trong trường hợp này mà lặng lẽ tính trên ít dòng hơn, nên khi trình bày, nên bỏ hai điểm đầu hoặc ghi rõ.
6. Tỷ trọng trong nhóm và phân tích Pareto
Mục này dùng hàm cửa sổ để trả lời một câu hỏi quản trị cổ điển: một số ít nhóm hàng có tạo ra phần lớn doanh thu không.
Tỷ trọng của một dòng trong tổng là phép chia cho SUM(...) OVER (), tổng trên toàn bộ kết quả. Tỷ trọng lũy kế, nền tảng của phân tích Pareto, là tổng cộng dồn theo thứ tự giảm dần chia cho cùng mẫu số đó.
WITH n AS (
SELECT s.MaNhom, SUM(d.SoLuong * d.DonGiaBan) AS DoanhThu
FROM CHITIETHD d
JOIN HOADON h ON h.SoHD = d.SoHD
JOIN SANPHAM s ON s.MaSP = d.MaSP
WHERE h.NgayGio >= '2025-01-01' AND h.NgayGio < '2026-01-01'
GROUP BY s.MaNhom
)
SELECT g.TenNhom, n.DoanhThu,
ROUND(100.0 * n.DoanhThu / SUM(n.DoanhThu) OVER (), 1) AS TyTrong,
ROUND(100.0 * SUM(n.DoanhThu) OVER (ORDER BY n.DoanhThu DESC ROWS UNBOUNDED PRECEDING)
/ SUM(n.DoanhThu) OVER (), 1) AS LuyKe
FROM n JOIN NHOMHANG g ON g.MaNhom = n.MaNhom
ORDER BY n.DoanhThu DESC;
Câu lệnh trả về 25 nhóm hàng. Bảng 5 trích sáu nhóm đầu và hai nhóm cuối.
Bảng 5. Tỷ trọng và tỷ trọng lũy kế doanh thu theo nhóm hàng năm 2025 (%, trích)
| TenNhom | DoanhThu | TyTrong | LuyKe |
|---|---|---|---|
| Rượu vang | 5051171100 | 18.0 | 18.0 |
| Mỹ phẩm | 2471714000 | 8.8 | 26.8 |
| Đồ dùng trẻ em | 1907900800 | 6.8 | 33.6 |
| Đồ ăn nhanh tại quầy | 1759455500 | 6.3 | 39.8 |
| Đồ uống pha chế | 1608273200 | 5.7 | 45.6 |
| Đồ dùng nhà bếp | 1356112600 | 4.8 | 50.4 |
| Thực phẩm chế biến | 409055900 | 1.5 | 98.8 |
| Snack | 335939300 | 1.2 | 100.0 |
Nguồn: kết quả chạy trên SQLite với bộ dữ liệu Sen Vàng Mart.
Sáu trên 25 nhóm hàng, tức 24% số nhóm, tạo ra 50,4% doanh thu; riêng rượu vang chiếm 18%. Mức tập trung này thấp hơn câu "20% tạo ra 80%" thường được nhắc, một lời nhắc rằng quy tắc Pareto là câu hỏi cần kiểm chứng trên dữ liệu chứ không phải định luật. Chú ý cụm ROWS UNBOUNDED PRECEDING trong câu lệnh: nó buộc cộng dồn theo từng dòng. Nếu hai nhóm có doanh thu bằng nhau mà bỏ cụm này, khung mặc định sẽ cộng cả hai cùng lúc, đúng loại lỗi mà mục sau phân tích.
7. Những cạm bẫy thường gặp
Hàm cửa sổ hiếm khi báo lỗi; khi viết sai, nó thường trả về một con số trông hợp lý nhưng sai. Mục này gom ba cạm bẫy gặp nhiều nhất, mỗi cạm bẫy kèm số liệu thật.
7.1. Lọc trước khi tính
Nếu đặt điều kiện "từ tháng 1/2025" trong cùng câu với LAG, tháng 1/2025 sẽ có giá trị tháng trước là NULL, dù dữ liệu tháng 12/2024 vẫn nằm trong cơ sở dữ liệu. Lý do là WHERE chạy trước bước SELECT, nên dòng tháng 12/2024 đã bị loại trước khi LAG kịp nhìn thấy nó. Câu lệnh ở mục 4 tránh lỗi này bằng cách tính LAG trên toàn bộ dữ liệu trong khối WITH, rồi mới lọc ở câu ngoài. Quy tắc chung: tính hàm cửa sổ trên tất cả các dòng mà nó cần, lọc sau.
7.2. Khung mặc định gộp các dòng ngang giá trị
Đây là cạm bẫy khó thấy nhất. Câu lệnh sau cộng dồn tiền của từng hóa đơn ở cửa hàng CH07 trong ngày khai trương 1/6/2025, sắp theo ngày, bằng hai cách viết.
WITH hd AS (
SELECT h.SoHD, date(h.NgayGio) AS Ngay, SUM(d.SoLuong * d.DonGiaBan) AS Tien
FROM HOADON h JOIN CHITIETHD d ON d.SoHD = h.SoHD
WHERE h.MaCH = 'CH07' AND h.NgayGio < '2025-06-03'
GROUP BY h.SoHD
)
SELECT SoHD, Ngay, Tien,
SUM(Tien) OVER (ORDER BY Ngay) AS MacDinh,
SUM(Tien) OVER (ORDER BY Ngay ROWS UNBOUNDED PRECEDING) AS TheoDong
FROM hd
ORDER BY Ngay, SoHD;
Bảng 6 là năm dòng đầu của kết quả.
Bảng 6. Cộng dồn với khung mặc định và khung theo dòng, cửa hàng CH07 ngày 1/6/2025
| SoHD | Ngay | Tien | MacDinh | TheoDong |
|---|---|---|---|---|
| HD0280649 | 2025-06-01 | 83000 | 3087000 | 83000 |
| HD0280650 | 2025-06-01 | 108000 | 3087000 | 191000 |
| HD0280651 | 2025-06-01 | 88000 | 3087000 | 279000 |
| HD0280652 | 2025-06-01 | 202000 | 3087000 | 481000 |
| HD0280653 | 2025-06-01 | 360000 | 3087000 | 841000 |
Nguồn: kết quả chạy trên SQLite với bộ dữ liệu Sen Vàng Mart.
Cột MacDinh cho cùng một con số 3.087.000 đồng ở mọi hóa đơn, chính là tổng của cả 21 hóa đơn trong ngày. Khi có ORDER BY mà không khai báo khung, chuẩn SQL dùng khung RANGE từ đầu phần đến dòng hiện tại, tính cả mọi dòng có cùng giá trị sắp xếp (SQLite, n.d.). Vì 21 hóa đơn cùng một ngày, chúng "ngang hàng" nhau và nhận chung một tổng. Cột TheoDong, với ROWS UNBOUNDED PRECEDING, mới cho cộng dồn từng hóa đơn như người viết mong đợi. Lỗi này không lộ ra khi cột sắp xếp không có giá trị trùng, như cột tháng ở mục 4, nên một câu lệnh có thể chạy đúng nhiều tháng rồi bỗng sai khi ai đó đổi từ dữ liệu theo tháng sang dữ liệu theo ngày.
7.3. Thứ tự không xác định và chi phí sắp xếp
Cạm bẫy thứ ba đã thấy ở mục 3.1: nếu cột ORDER BY có giá trị trùng, ROW_NUMBER đánh số tùy ý giữa các dòng trùng, nên "hóa đơn đầu tiên của mỗi khách" có thể đổi giữa hai lần chạy. Cách phòng là luôn thêm một cột phụ có giá trị duy nhất, như mã hóa đơn, vào ORDER BY. Về hiệu năng, mỗi cửa sổ có PARTITION BY hay ORDER BY mới đòi hệ quản trị sắp xếp lại dữ liệu; câu lệnh dùng nhiều cửa sổ khác nhau sẽ sắp xếp nhiều lần. Đặt tên cửa sổ bằng WINDOW và dùng chung khi có thể vừa gọn vừa giúp bộ tối ưu nhận ra các cửa sổ giống nhau.
Bảng 7 tóm tắt các hàm cửa sổ hay dùng nhất cùng câu hỏi kinh doanh mà mỗi hàm trả lời.
Bảng 7. Các hàm cửa sổ thường dùng trong phân tích kinh doanh
| Hàm | Trả về | Câu hỏi kinh doanh |
|---|---|---|
| ROW_NUMBER() | Số thứ tự liên tục trong phần | Hóa đơn đầu tiên của mỗi khách; lấy đúng một dòng mỗi nhóm |
| RANK(), DENSE_RANK() | Thứ hạng, xử lý đồng hạng theo hai cách | Top 3 sản phẩm mỗi cửa hàng, kể cả đồng hạng |
| LAG(), LEAD() | Giá trị của dòng trước, dòng sau | Tăng trưởng so với tháng trước, cùng kỳ năm trước |
| SUM() OVER (ORDER BY ...) | Tổng cộng dồn | Doanh thu lũy kế từ đầu năm |
| AVG() OVER (... ROWS BETWEEN ...) | Trung bình trên khung trượt | Trung bình trượt ba tháng |
| SUM() OVER () | Tổng của toàn bộ kết quả | Tỷ trọng, phân tích Pareto |
| NTILE(n) | Số thứ tự nhóm khi chia đều thành n nhóm | Chia khách hàng thành năm nhóm theo chi tiêu |
Nguồn: dựa trên SQLite (n.d.) và Jukić (2025).
Kết luận
Hàm cửa sổ giải quyết một nhu cầu rất phổ biến mà GROUP BY không giải được: tính trên nhóm nhưng giữ lại từng dòng. Chỉ với mệnh đề OVER và ba thành phần của nó, người phân tích có thể xếp hạng trong từng nhóm, so với kỳ trước, cộng dồn, làm mượt và tính tỷ trọng ngay trong cơ sở dữ liệu, trên hàng trăm nghìn dòng, bằng những câu lệnh chạy lại được mỗi tháng thay vì những trang tính kéo công thức thủ công.
Những ví dụ trên dữ liệu Sen Vàng Mart cũng cho thấy kỹ thuật chỉ là một nửa câu chuyện. Cùng một cột LAG có thể kể câu chuyện tăng 22% hay giảm 29%, trong khi lũy kế nói rằng mùa Tết không làm thay đổi gì; cùng một hàm SUM có thể cộng dồn đúng hay sai chỉ vì một cụm từ khai báo khung. Người viết hàm cửa sổ giỏi là người luôn hỏi cửa sổ của mình đang chứa những dòng nào, đã bị lọc mất dòng nào và có dòng nào ngang hàng với nhau.
Đọc thêm
- SQL từ cơ bản đến nâng cao
- Phân tích cohort trong giữ chân khách hàng
- Dự báo doanh số bằng chuỗi thời gian cho doanh nghiệp
- Lược đồ hình sao, bảng sự kiện và bảng chiều
- Lộ trình Data Analyst năm 2027 từ Excel đến Python
Tài liệu tham khảo
Hyndman, R. J., & Athanasopoulos, G. (2021). Forecasting: Principles and practice (3rd ed.). OTexts. https://otexts.com/fpp3/
ISO/IEC. (2023). ISO/IEC 9075:2023 Information technology: Database languages SQL. International Organization for Standardization. https://www.iso.org/standard/76583.html
Jukić, N. (2025). Database systems: Introduction to databases and data warehouses (3rd ed.). Prospect Press. https://www.prospectpressvt.com/textbooks/jukic-database-systems-introduction-to-databases-and-data-warehouses-3-0
SQLite. (n.d.). Window functions. https://www.sqlite.org/windowfunctions.html