Window Functions trong PostgreSQL

Window Functions trong PostgreSQL là một trong những tính năng mạnh mẽ nhất của hệ quản trị cơ sở dữ liệu PostgreSQL, đặc biệt khi làm việc với các bài toán phân tích dữ liệu. Nếu bạn đang sử dụng SQL để phân tích doanh thu, xếp hạng dữ liệu, tính tổng tích lũy hoặc xử lý chuỗi thời gian, thì việc hiểu và sử dụng thành thạo Window Functions sẽ giúp bạn viết các truy vấn hiệu quả và linh hoạt hơn rất nhiều.

Window Functions trong PostgreSQL

Khác với các hàm tổng hợp truyền thống như SUM(), COUNT() hay AVG() thường được sử dụng cùng GROUP BY, Window Functions cho phép thực hiện các phép tính trên một tập các dòng dữ liệu liên quan nhưng không làm mất đi các dòng dữ liệu ban đầu. Điều này rất quan trọng trong các tình huống phân tích, nơi chúng ta vừa cần kết quả tính toán vừa cần giữ lại toàn bộ dữ liệu chi tiết.

Trong bài viết này, chúng ta sẽ cùng tìm hiểu chi tiết về Window Functions trong PostgreSQL, cách chúng hoạt động, cú pháp cơ bản, cũng như một số hàm phổ biến thường được sử dụng trong thực tế.

Window Functions trong PostgreSQL là gì?

Window Functions trong PostgreSQL là các hàm cho phép thực hiện tính toán trên một nhóm các dòng dữ liệu 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ả truy vấn.

Nói một cách đơn giản hơn, mỗi dòng dữ liệu sẽ có một “cửa sổ” (window) bao gồm nhiều dòng khác. Hàm window sẽ thực hiện phép tính trên cửa sổ đó và trả kết quả cho từng dòng riêng lẻ.

Điểm khác biệt quan trọng giữa Window Functions và GROUP BY là:

  • GROUP BY sẽ gộp nhiều dòng thành một dòng kết quả
  • Window Functions giữ nguyên tất cả các dòng dữ liệu

Ví dụ giả sử ta có bảng dữ liệu nhân viên:

employeedepartmentsalary
AIT1000
BIT1500
CHR1200

Nếu sử dụng GROUP BY department, ta chỉ nhận được tổng lương theo phòng ban. Nhưng với Window Functions, ta có thể hiển thị tổng lương phòng ban ngay trên từng dòng nhân viên, giúp việc phân tích trở nên chi tiết và trực quan hơn.

Cú pháp cơ bản của Window Functions trong PostgreSQL

Cú pháp chung của một window function trong PostgreSQL thường có dạng:

function_name(expression)
OVER(
    PARTITIONBYcolumn
    ORDERBYcolumn
)

Trong đó, phần OVER() đóng vai trò cực kỳ quan trọng vì nó xác định phạm vi dữ liệu mà hàm sẽ thực hiện tính toán.

OVER()

OVER() là thành phần bắt buộc của window function. Nó xác định cửa sổ dữ liệu mà hàm sẽ áp dụng. Nếu không có OVER(), PostgreSQL sẽ xem hàm đó như một hàm tổng hợp thông thường.

Bên trong OVER() có thể bao gồm các thành phần như:

  • PARTITION BY
  • ORDER BY
  • ROWS BETWEEN

Những thành phần này giúp định nghĩa cách dữ liệu được chia nhóm và cách cửa sổ dữ liệu được hình thành.

PARTITION BY

PARTITION BY dùng để chia dữ liệu thành nhiều nhóm nhỏ gọi là partition. Mỗi partition sẽ được xử lý độc lập. Điểm quan trọng là mặc dù dữ liệu được chia nhóm để tính toán, nhưng các dòng dữ liệu vẫn được giữ nguyên trong kết quả.

Giả sử chúng ta có bảng dữ liệu sales như sau:

idemployeedepartmentsale_daterevenue
1AliceIT2024-01-01500
2BobIT2024-01-02700
3CharlieIT2024-01-03700
4DavidHR2024-01-01400
5EvaHR2024-01-02600
6FrankHR2024-01-03600
7GraceMarketing2024-01-01450
8HelenMarketing2024-01-02650
9IanMarketing2024-01-03800
10AliceIT2024-01-04550
11BobIT2024-01-05720
12EvaHR2024-01-04620
13DavidHR2024-01-05500
14HelenMarketing2024-01-04700
15IanMarketing2024-01-05850
CREATE TABLE sales (
    id SERIAL PRIMARY KEY,
    employee VARCHAR(50),  -- tên nhân viên
    department VARCHAR(50), -- bộ phận
    sale_date DATE,
    revenue INT --doanh số bán hàng
);
INSERT INTO sales (employee, department, sale_date, revenue) VALUES
('Alice', 'IT', '2024-01-01', 500),
('Bob', 'IT', '2024-01-02', 700),
('Charlie', 'IT', '2024-01-03', 700),

('David', 'HR', '2024-01-01', 400),
('Eva', 'HR', '2024-01-02', 600),
('Frank', 'HR', '2024-01-03', 600),

('Grace', 'Marketing', '2024-01-01', 450),
('Helen', 'Marketing', '2024-01-02', 650),
('Ian', 'Marketing', '2024-01-03', 800),

('Alice', 'IT', '2024-01-04', 550),
('Bob', 'IT', '2024-01-05', 720),

('Eva', 'HR', '2024-01-04', 620),
('David', 'HR', '2024-01-05', 500),

('Helen', 'Marketing', '2024-01-04', 700),
('Ian', 'Marketing', '2024-01-05', 850);

Ví dụ 1:

SELECT
employee,
department,
revenue,
SUM(revenue) OVER (PARTITION BY department) AS total_department_revenue
FROM sales;

Trong ví dụ này:

  • Dữ liệu được chia theo department
  • Tổng doanh thu sẽ được tính riêng cho từng phòng ban
  • Kết quả tổng sẽ xuất hiện trên từng dòng nhân viên trong cùng phòng ban

Điều này giúp chúng ta dễ dàng so sánh doanh thu cá nhân với tổng doanh thu của phòng ban.

ORDER BY trong Window Functions

ORDER BY bên trong OVER() xác định thứ tự của các dòng trong mỗi partition.

Thứ tự này đặc biệt quan trọng khi sử dụng các hàm như:

  • ROW_NUMBER()
  • RANK()
  • LAG()
  • LEAD()
  • Running totals

Ví dụ:

SELECT
employee,
revenue,
ROW_NUMBER() OVER (ORDER BY revenue DESC) AS rank
FROM sales;

Ở đây, dữ liệu được sắp xếp theo revenue giảm dần và mỗi dòng sẽ được gán một số thứ tự tương ứng.

Các Window Functions phổ biến trong PostgreSQL

ROW_NUMBER()

ROW_NUMBER() là một trong những window function đơn giản và phổ biến nhất. Hàm này đánh số thứ tự cho từng dòng dựa trên thứ tự được xác định bởi ORDER BY. Ví dụ:

SELECT
employee,
revenue,
ROW_NUMBER() OVER (ORDER BY revenue DESC) AS rank
FROM sales;

Kết quả sẽ hiển thị thứ hạng của từng giao dịch dựa trên doanh thu, từ cao xuống thấp.

Hàm này thường được sử dụng trong các bài toán như:

  • Lấy top N bản ghi
  • Phân trang dữ liệu
  • Xếp hạng dữ liệu

Top nhân viên trong mỗi phòng ban

SELECT
employee,
department,
revenue,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY revenue DESC
) AS rank
FROM sales;

RANK()

RANK() cũng được dùng để xếp hạng dữ liệu nhưng khác với ROW_NUMBER(), hàm này cho phép các giá trị giống nhau có cùng thứ hạng. Ví dụ:

revenuerank
7001
7001
6003

Ta thấy rằng nếu hai dòng có cùng doanh thu, chúng sẽ có cùng thứ hạng và thứ hạng tiếp theo sẽ bị bỏ qua.

SELECT
employee,
revenue,
RANK() OVER (ORDER BY revenue DESC) AS rank
FROM sales;

DENSE_RANK()

DENSE_RANK() hoạt động tương tự RANK() nhưng không bỏ qua thứ hạng. Ví dụ:

revenuedense_rank
7001
7001
6002

Hàm này thường được sử dụng khi cần xếp hạng liên tục mà không có khoảng trống trong thứ tự.

SELECT
employee,
revenue,
DENSE_RANK() OVER (ORDER BY revenue DESC) AS rank
FROM sales;

LAG()

LAG() là một window function cực kỳ hữu ích khi làm việc với dữ liệu theo thời gian. Hàm này cho phép lấy giá trị của dòng trước đó trong cùng một cửa sổ dữ liệu. Ví dụ:

SELECT
employee,
revenue,
LAG(revenue) OVER (ORDER BY id) AS previous_revenue
FROM sales;

Với truy vấn này, mỗi dòng sẽ hiển thị thêm doanh thu của dòng trước đó. Điều này rất hữu ích khi phân tích:

  • Tăng trưởng doanh thu
  • So sánh dữ liệu giữa các thời điểm
  • Phân tích chuỗi thời gian

Doanh thu hôm nay vs hôm qua của cùng nhân viên

SELECT
employee,
revenue,
LAG(revenue) OVER ( PARTITION BY employee ORDER BY sale_date ) AS previous_revenue
FROM sales;

LEAD()

LEAD() là hàm ngược lại với LAG(). Thay vì lấy giá trị của dòng trước, nó lấy giá trị của dòng tiếp theo. Ví dụ:

SELECT
employee,
revenue,
LEAD(revenue) OVER (ORDER BY id) AS next_revenue
FROM sales;

Hàm này thường được sử dụng trong các bài toán như:

  • Dự đoán xu hướng
  • So sánh dữ liệu giữa các mốc thời gian

Ví dụ thực tế: Running Total

Một ứng dụng phổ biến của Window Functions trong PostgreSQL là tính tổng tích lũy (running total). Ví dụ:

SELECT
sale_date,
revenue,
SUM(revenue) OVER (ORDER BY sale_date) AS cumulative_revenue
FROM sales;

Kết quả sẽ cho thấy tổng doanh thu tích lũy theo thời gian.

Ví dụ:

daterevenuecumulative_revenue
01-01100100
02-01150250
03-01200450

Running total thường được sử dụng trong:

  • Phân tích doanh thu
  • Theo dõi tăng trưởng
  • Báo cáo kinh doanh

Doanh thu tích lũy của từng phòng ban

SELECT
sale_date,
department,
revenue,
SUM(revenue) OVER ( PARTITION BY department ORDER BY sale_date ) AS cumulative_revenue
FROM sales;

Ứng dụng của Window Functions trong phân tích dữ liệu

Trong thực tế, Window Functions trong PostgreSQL được sử dụng rất nhiều trong các hệ thống phân tích dữ liệu và Business Intelligence.

Một số ứng dụng phổ biến bao gồm:

  • Xếp hạng khách hàng theo doanh thu
  • Tính tổng tích lũy theo thời gian
  • So sánh dữ liệu giữa các dòng
  • Phân tích chuỗi thời gian
  • Tạo các đặc trưng dữ liệu cho machine learning

Nhờ khả năng tính toán trên từng dòng mà vẫn giữ nguyên dữ liệu chi tiết, Window Functions trở thành công cụ không thể thiếu đối với các Data Analyst, Data Engineer và Data Scientist.

Bài tập thực hành

Sau khi đã làm quen với các Window Functions trong PostgreSQL như ROW_NUMBER(), RANK(), LAG(), LEAD() và SUM() OVER(), chúng ta hãy thử áp dụng các hàm này vào một vài tình huống thực tế. Dưới đây là một số bài tập nhỏ để bạn luyện tập thêm với bảng sales đã tạo ở trên.

Bài tập 1: Xếp hạng doanh thu theo phòng ban

Hãy viết truy vấn SQL để xếp hạng doanh thu của từng nhân viên trong mỗi phòng ban, từ cao xuống thấp. Gợi ý:

  • Sử dụng RANK() hoặc ROW_NUMBER()
  • Kết hợp với PARTITION BY department

Bài tập 2: Tìm nhân viên có doanh thu cao nhất mỗi phòng ban

Từ kết quả xếp hạng ở bài trên, hãy tìm nhân viên có doanh thu cao nhất trong từng phòng ban. Gợi ý:

  • Sử dụng ROW_NUMBER()
  • Lọc kết quả với rank = 1

Bài tập 3: So sánh doanh thu với ngày trước đó

Hãy tính doanh thu của ngày trước đó cho mỗi nhân viên để so sánh sự thay đổi theo thời gian. Gợi ý:

  • Sử dụng LAG()
  • Partition theo employee
  • Sắp xếp theo sale_date

Bài tập 4: Tính doanh thu tích lũy theo thời gian

Hãy viết truy vấn để tính tổng doanh thu tích lũy theo ngày. Gợi ý:

  • Sử dụng SUM(revenue) OVER
  • ORDER BY sale_date

Bài tập 5: Tính tỷ lệ đóng góp doanh thu của mỗi nhân viên

Hãy tính tỷ lệ đóng góp doanh thu của từng dòng dữ liệu so với tổng doanh thu của phòng ban. Gợi ý:

revenue / SUM(revenue) OVER (PARTITION BY department)

Kết luận

Window Functions trong PostgreSQL là một công cụ cực kỳ mạnh mẽ khi làm việc với dữ liệu phân tích bằng SQL. Thay vì phải viết các truy vấn phức tạp hoặc sử dụng nhiều subquery, Window Functions cho phép chúng ta thực hiện các phép tính nâng cao ngay trên từng dòng dữ liệu một cách trực quan và hiệu quả.

Những hàm như ROW_NUMBER(), RANK(), LAG(), LEAD() hay SUM() OVER() giúp giải quyết rất nhiều bài toán thực tế trong phân tích dữ liệu, từ xếp hạng, tính tổng tích lũy cho đến xử lý dữ liệu theo thời gian.

Nếu bạn đang làm việc với SQL trong lĩnh vực phân tích dữ liệu, việc hiểu rõ và thành thạo Window Functions trong PostgreSQL sẽ giúp bạn nâng cao đáng kể khả năng viết truy vấn và khai thác dữ liệu một cách hiệu quả hơn.