Hàm cửa sổ trong SQL

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.

Bên trái các dòng doanh thu sản phẩm của ba cửa hàng xếp lẫn lộn; ở giữa chúng được chia thành ba khối theo cửa hàng và sắp giảm dần theo doanh thu; bên phải mỗi dòng có thêm cột hạng bắt đầu lại từ 1 ở mỗi khối

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.

Đồ thị đường doanh thu theo tháng từ 1/2024 đến 12/2025, dao động quanh 2,2 đến 2,4 tỷ đồng, có một đỉnh 2,80 tỷ ở tháng 1/2025 và một đáy 1,98 tỷ ở tháng 2/2025; đường trung bình trượt ba tháng mượt hơn, chỉ nhích nhẹ quanh Tết và cho thấy xu hướng đi lên trong nửa sau năm 2025

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

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

Enjoyed this article?

Share it with your network