Window Functions là một trong những công cụ mạnh mẽ nhất trong SQL Server, cho phép bạn thực hiện các phép tính như tổng, trung bình, xếp hạng, và truy xuất giá trị từ các dòng liên quan trong một tập kết quả mà không cần
GROUP BY. Trong bài viết này, bạn sẽ tìm hiểu cách hoạt động của Window Functions, các loại phổ biến, và ví dụ thực tế dễ hiểu
1. Window Functions là gì?
Window Function thực hiện tính toán trên một “cửa sổ” của các hàng liên quan đến hàng hiện tại. Khác với Aggregate Functions (SUM, AVG, COUNT…), các Window Function không làm gộp dữ liệu lại, mà vẫn giữ nguyên từng hàng.
Cú pháp chung:
<Window_Function>() OVER (
[PARTITION BY partition_expression]
[ORDER BY sort_expression]
[ROWS|RANGE BETWEEN ...]
)
PARTITION BY: Chia tập dữ liệu thành các nhóm con (partition).ORDER BY: Xác định thứ tự trong mỗi partition.ROWS|RANGE: Xác định phạm vi của cửa số.
2. Cách sử dụng Window Functions
2.1 PARTITION BY - Phân nhóm dữ liệu
PARTITION BYchia dữ liệu thành các nhóm (tương tựGROUP BY) nhưng không gộp kết quả.
Ví dụ: Tính tổng doanh thu theo từng phòng ban mà không gộp hàng.
SELECT
employee_id,
department,
salary,
SUM(salary) OVER (PARTITION BY department) AS total_salary_by_dept
FROM employees;
Kết quả:
| name | department | salary | total_salary_by_dept |
|---|---|---|---|
| An | Sales | 5000 | 15000 |
| Bình | Sales | 7000 | 15000 |
| Chi | Sales | 3000 | 15000 |
| Dũng | IT | 6000 | 10000 |
| Huy | IT | 4000 | 10000 |
Giải thích kết quả:
- Các nhân viên thuộc Sales đều nhận được giá trị
total_salary_by_deptbằng tổng lương của 3 người 5000 + 7000 + 3000 = 15000. - Các nhân viên thuộc IT đều nhận được giá trị
total_salary_by_deptbằng tổng lương của 2 người 6000 + 4000 = 10000.
2.2 ORDER BY - Sắp xếp
ORDER BYxác định thứ tự các hàng trong cửa sổ trước khi áp dụng hàm.
Ví dụ: Tính luỹ kế lương theo từng phòng ban.
SELECT
employee_id,
department,
salary,
SUM(salary) OVER (
PARTITION BY department
ORDER BY employee_id
) AS running_total_salary
FROM employees;
Kết quả:
| name | department | salary | running_total_salary |
|---|---|---|---|
| An | Sales | 5000 | 5000 |
| Bình | Sales | 7000 | 12000 |
| Chi | Sales | 3000 | 15000 |
| Dũng | IT | 6000 | 6000 |
| Huy | IT | 4000 | 10000 |
Giải thích kết quả:
Đối với Sales:
- An: lương 5000 => luỹ kế = 5000.
- Bình: lương 7000 => luỹ kế 5000 + 7000 = 12000.
- Chi: lương 3000 => luỹ kế 5000 + 7000 + 3000 = 15000.
Đối với IT:
- Dũng: lương 6000 => luỹ kế = 6000.
- Huy: lương 4000 => luỹ kế 6000 + 4000 = 10000.
2.3 Hàm xếp hạng: RANK() vs DENSE_RANK()
Cả hai hàm RANK() và DENSE_RANK() đều dùng để xếp hạng các hàng trong một nhóm. Khác biệt chính:
RANK(): Nếu có nhiều hàng bằng nhau, chúng nhận cùng một hạng, và thứ hạng tiếp theo sẽ bị bỏ qua.DENSE_RANK(): Cũng gán hạng băng nhau cho các giá trị giống nhau, nhưng không nhảy thứ hạng kế tiếp.
Ví dụ:
SELECT
name,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rank
FROM employees;
Giải thích:
PARTITION BY departmentNhân viên được nhóm theo phòng ban.ORDER BY salary DESCTrong mỗi phòng ban, sắp xếp theo lương giảm dẫn.RANK()gán thứ hạng, nhưng nếu có giá trị trùng nhau, thì các dòng đó chia sẻ cùng một hạng và nhảy qua hạng tiếp theo.DENSE_RANK()cũng gán cùng hạng cho các giá trị trùng, nhưng không nhảy hạng.
Kết quả:
| name | department | salary | rank | dense_rank |
|---|---|---|---|---|
| An | Sales | 8000 | 1 | 1 |
| Bình | Sales | 8000 | 1 | 1 |
| Chi | Sales | 6000 | 3 | 2 |
| Dũng | IT | 7000 | 1 | 1 |
| Huy | IT | 6800 | 2 | 2 |
Giải thích kết quả:
Đối với Sales:
- An và Bình đều có lương 8000, nên:
- Cả hai đều được
RANK=DENSE_RANK= 1. - Với
RANK(), hạng tiếp theo sau hai người hạng 1 sẽ là hạng 3 (vì đã chiếm 2 dòng). - Với
DENSE_RANK(), hạng tiếp theo vẫn là hạng 2 (không bị nhảy).
- Cả hai đều được
- Chi có lương 6000, thấp hơn:
RANK= 3.DENSE_RANK)= 2.
Đối với IT:
- Dũng: có lương cao nhất (7000), nên
RANK=DENSE_RANK= 1 - Huy: có lương thấp hơn (6800), nên:
RANK=DENSE_RANK= 2- Không có trùng lương trong nhóm này, nên
RANK()vàDENSE_RANK()cho ra kết quả giống nhau.
2.4 ROW_NUMBER() - Đánh số thứ tự duy nhất
- Hàm
ROW_NUMBER()gán số thứ tự duy nhất cho từng hàng trong mỗi partition, không quan tâm đến giá trị có trùng nhau.
Ví dụ:
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num
FROM employees;
Giải thích:
- Dữ liệu được chia theo từng phòng ban
- Sắp xếp lương giảm dần.
- Mỗi hàng được đánh số từ 1 tăng dần, không nhảy số.
Kết quả:
| name | department | salary | row_num |
|---|---|---|---|
| An | Sales | 8000 | 1 |
| Bình | Sales | 8000 | 2 |
| Chi | Sales | 6000 | 3 |
| Dũng | IT | 7000 | 1 |
| Huy | IT | 6800 | 2 |
2.4 NTILE(n) - Chia nhóm phần bằng nhau
NTILE(n)chia dữ liệu thànhnnhóm có kích thước bằng nhau.
Ví dụ:
SELECT
name,
department,
salary,
NTILE(4) OVER (ORDER BY salary DESC) AS salary_quartile
FROM employees;
Giải thích:
- Toàn bộ dữ liệu được sắp xếp theo lương giảm dần
- Chia thành 4 nhóm (quartiles).
- Nhóm 1 có mức lương cao nhât, nhóm 4 thấp nhất.
Kết quả:
| name | department | salary | salary_quartile |
|---|---|---|---|
| An | Sales | 8000 | 1 |
| Bình | Sales | 8000 | 1 |
| Chi | Sales | 6000 | 2 |
| Dũng | IT | 7000 | 1 |
| Huy | IT | 6800 | 2 |
2.5 LAG() và LEAD() - Truy xuất dòng trước hoặc sau
LAG(): Lấy giá trị của hàng trước đó trog cùng partition.LEAD(): Lấy giá trị của hàng kế tiếp trong cùng partiton.
Ví dụ LAG(): Lấy mức lương của nhân viên trước đó trong phòng ban để so sánh.
SELECT
name,
department,
salary,
LAG(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS prev_salary
FROM employees;
Kết quả:
| name | department | salary | prev_salary |
|---|---|---|---|
| An | Sales | 8000 | NULL |
| Bình | Sales | 8000 | 8000 |
| Chi | Sales | 6000 | 8000 |
| Dũng | IT | 7000 | NULL |
| Huy | IT | 6800 | 7000 |
Ví dụ LEAD(): Lấy mức lương của nhân viên kế tiếp trong phòng ban để so sánh.
SELECT
name,
department,
salary,
LEAD(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS next_salary
FROM employees;
Kết quả:
| name | department | salary | next_salary |
|---|---|---|---|
| An | Sales | 8000 | 8000 |
| Bình | Sales | 8000 | 6000 |
| Chi | Sales | 6000 | NULL |
| Dũng | IT | 7000 | 6800 |
| Huy | IT | 6800 | NULL |
2.6 Các hàm tổng hợp: SUM(), AVG(), MIN(), MAX()
Ví dụ: Tổng lương được cộng dồn theo thứ tự lương giảm dần trong mỗi phòng ban.
SELECT
name,
department,
salary,
SUM(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS running_total
FROM employees;
Kết quả:
| name | department | salary | running_total |
|---|---|---|---|
| An | Sales | 8000 | 8000 |
| Bình | Sales | 8000 | 16000 |
| Chi | Sales | 6000 | 22000 |
| Dũng | IT | 7000 | 7000 |
| Huy | IT | 6800 | 13800 |
Giải thích kết quả:
Đối với Sales:
- An: có lương 8000 => tổng tích lũy là 8000
- Bình: cũng có lương 8000 => cộng thêm vào tổng → 8000 + 8000 = 16000.
- Chi: có lương 6000 => tổng cộng dồn là 8000 + 8000 + 6000 = 22000.
Đối với IT:
- Dũng: có lương 7000 => tổng tích lũy là 7000
- Huy: có lương 6800 => cộng thêm vào → 7000 + 6800 = 13800.
Ví dụ: Trung bình lương được tính tích luỹ từ dòng đầu đến dòng hiện tại.
SELECT
name,
department,
salary,
AVG(salary) OVER (
PARTITION BY department
ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_avg
FROM employees;
Kết quả:
| name | department | salary | running_avg |
|---|---|---|---|
| An | Sales | 8000 | 8000.00 |
| Bình | Sales | 8000 | 8000.00 |
| Chi | Sales | 6000 | 7333.33 |
| Dũng | IT | 7000 | 7000.00 |
| Huy | IT | 6800 | 6900.00 |
Giải thích kết quả:
Đối với Sales:
- An: chỉ có 1 dòng => trung bình = 8000
- Bình: AVG(8000, 8000) = 8000
- Chi: AVG(8000, 8000, 6000) = (8000 + 8000 + 6000)/3 = 7333.33
Đối với IT:
- Dũng: chỉ có 1 dòng => 7000
- Huy: AVG(7000, 6800) = (7000 + 6800)/2 = 6900
3. Lưu ý khi sử dụng Window Functions
- Luôn cần từ khóa
OVER(). - Không thể sử dụng trực tiếp trong
WHERE(dùng trongSELECThoặcORDER BY). - Hiệu suất phụ thuộc vào việc phân vùng và sắp xếp dữ liệu.
- Tối ưu chỉ khi có chỉ mục phù hợp và dữ liệu không quá lớn.
4. Tổng kết
Window Functions là công cụ mạnh mẽ giúp bạn xử lý các bài toán phức tạp trong SQL Server mà không cần phải viết nhiều câu lệnh con hay join phức tạp. Hiểu rõ cách hoạt động và áp dụng đúng cách sẽ giúp bạn tối ưu hiệu suất và mở rộng khả năng phân tích dữ liệu.