Chuyển đến nội dung
SQL
Hàm cửa sổ trong SQL

Hàm cửa sổ trong SQL

Báo cáo cần từng đơn của An, kèm tổng tiền An đã mua. Nếu GROUP BY khách, hai đơn sẽ bị gom thành một dòng. Window function cho mình tính trên nhóm dòng liên quan mà vẫn giữ từng dòng ban đầu.

Ví dụ dùng cafe-v1, SQLite 3.25 trở lên; tạo database theo bài SQL đầu tiên.

Tổng của khách trên từng đơn

SELECT order_id, customer_id, amount_vnd,
       SUM(amount_vnd) OVER (
         PARTITION BY customer_id
       ) AS customer_total
FROM orders
WHERE status = 'paid'
ORDER BY customer_id, order_id;
order_id customer_id amount_vnd customer_total
101 1 40000 100000
102 1 60000 100000
103 2 50000 50000

PARTITION BY chia các dòng thành nhóm để tính. Nó không phải partition trên đĩa. Tổng của An lặp lại ở hai dòng, nên không cộng cột customer_total lần nữa để tìm doanh thu toàn quán.

Tổng lũy kế cần một thứ tự rõ ràng

SELECT order_id, customer_id,
       SUM(amount_vnd) OVER (
         PARTITION BY customer_id
         ORDER BY business_date, order_id
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS running_total
FROM orders
WHERE status = 'paid'
ORDER BY customer_id, business_date, order_id;

Lần lượt là 40000, 100000 và 50000. ROWS ở đây nói lấy các dòng từ đầu nhóm đến dòng hiện tại. Thêm order_id giúp thứ tự xác định khi nhiều đơn có cùng ngày. Thứ tự trong OVER phục vụ phép tính; ORDER BY ngoài cùng phục vụ cách hiển thị.

Nếu chỉ sắp xếp theo ngày và để frame mặc định, các dòng đồng hạng có thể cùng nhận tổng đến hết nhóm đồng hạng. Vì vậy, nên viết frame mình muốn khi tính lũy kế. Tổng theo thứ tự mã đơn là ví dụ học cơ chế; trong hệ thống thật, bạn cần timestamp và quy tắc khi timestamp trùng.

Lấy một đơn mỗi khách

WITH ranked AS (
  SELECT order_id, customer_id,
         ROW_NUMBER() OVER (
           PARTITION BY customer_id
           ORDER BY business_date DESC, order_id DESC
         ) AS rn
  FROM orders
  WHERE status = 'paid'
)
SELECT order_id, customer_id
FROM ranked
WHERE rn = 1
ORDER BY customer_id;

Kết quả là đơn 102 của khách 1, đơn 103 của khách 2. Mình tính số thứ tự trong CTE rồi lọc ở ngoài; không dùng alias rn trong WHERE của cùng mức truy vấn. ROW_NUMBER chọn một dòng, còn RANK có thể giữ nhiều dòng đồng hạng nếu lọc hạng 1.

Khi dùng cách này để bỏ trùng, phải có quy tắc chọn bản ghi mới nhất: thời gian cập nhật, số phiên bản, rồi tiêu chí phá hòa. “Chọn dòng đầu” khi thứ tự không xác định có thể làm kết quả thay đổi qua mỗi lần chạy.

Thử bỏ WHERE status = 'paid': bạn sẽ có thêm nhóm khách NULL. Đây là lý do phải quyết định phạm vi dữ liệu trước khi tính window.

Tham khảo: SQLite window functions và PostgreSQL window tutorial.