SQL Window Functions — Phân Tích Dữ Liệu Không Cần Subquery
Window functions trong SQL: ROW_NUMBER, RANK, LEAD, LAG, SUM OVER — phân tích dữ liệu mạnh mẽ mà không cần subquery.
Window functions tính toán trên một tập hợp các dòng liên quan đến dòng hiện tại — nhưng không gộp chúng lại như GROUP BY.
NOTE: “Window” có nghĩa là tập hợp các dòng được tính toán.
Window Function vs GROUP BY#
-- GROUP BY — gộp dòng, mất chi tiết
SELECT department, AVG(salary)
FROM employees
GROUP BY department;
-- Window function — giữ nguyên dòng, thêm cột tính toán
SELECT name, department, salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg
FROM employees;sqlOVER và PARTITION BY#
SELECT
name,
department,
salary,
-- AVG trong từng department
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
-- Global AVG
AVG(salary) OVER () AS company_avg,
-- Salary so với department average
ROUND(salary - AVG(salary) OVER (PARTITION BY department)) AS diff_from_dept_avg
FROM employees;sqlROW_NUMBER, RANK, DENSE_RANK#
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
RANK() OVER (ORDER BY salary DESC) AS rank,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;sql| salary | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 10000 | 1 | 1 | 1 |
| 9000 | 2 | 2 | 2 |
| 9000 | 3 | 2 | 2 |
| 8000 | 4 | 4 | 3 |
ROW_NUMBER— unique, không trùngRANK— trùng nhau, bỏ qua sốDENSE_RANK— trùng nhau, không bỏ số
Ví Dụ — Top-N Per Group#
-- Top 3 nhân viên lương cao nhất mỗi phòng ban
WITH ranked AS (
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn
FROM employees
)
SELECT name, department, salary
FROM ranked
WHERE rn <= 3;sqlLEAD và LAG — So Sánh Với Dòng Trước/Sau#
SELECT
date,
revenue,
LAG(revenue) OVER (ORDER BY date) AS prev_day_revenue,
revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change,
LEAD(revenue) OVER (ORDER BY date) AS next_day_revenue
FROM daily_revenue
ORDER BY date;sql| date | revenue | prev_day | change | next_day |
|---|---|---|---|---|
| 2024-01-01 | 1000 | NULL | NULL | 1200 |
| 2024-01-02 | 1200 | 1000 | 200 | 1100 |
| 2024-01-03 | 1100 | 1200 | -100 | 1300 |
Ví Dụ — User Session Duration#
-- Tính thời gian giữa các hành động của user
SELECT
user_id,
action,
created_at,
LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_action,
EXTRACT(EPOCH FROM created_at - LAG(created_at)
OVER (PARTITION BY user_id ORDER BY created_at)) AS seconds_since_prev
FROM user_actions;sqlFIRST_VALUE và LAST_VALUE#
SELECT
department,
name,
salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC) AS highest_paid,
LAST_VALUE(name) OVER (
PARTITION BY department
ORDER BY salary DESC
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS lowest_paid
FROM employees;sqlRunning Total — Cumulative SUM#
SELECT
date,
revenue,
SUM(revenue) OVER (ORDER BY date) AS running_total,
SUM(revenue) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS weekly_moving_avg
FROM daily_revenue;sqlNTILE — Chia Nhóm#
-- Chia employees thành 4 nhóm (quartile) theo lương
SELECT
name,
salary,
NTILE(4) OVER (ORDER BY salary DESC) AS salary_quartile
FROM employees;sql-- Phân trang API dùng NTILE
WITH page_groups AS (
SELECT *,
NTILE(10) OVER (ORDER BY id) AS page
FROM products
)
SELECT * FROM page_groups WHERE page = 1;sqlCUME_DIST và PERCENT_RANK#
SELECT
score,
CUME_DIST() OVER (ORDER BY score DESC) AS cumulative_dist, -- % của scores <= hiện tại
PERCENT_RANK() OVER (ORDER BY score DESC) AS percent_rank -- relative rank (0-1)
FROM exam_scores;sqlVí Dụ Thực Tế#
1. Detect Thay Đổi Trạng Thái#
WITH changes AS (
SELECT
user_id,
status,
created_at,
LAG(status) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_status
FROM subscriptions
)
SELECT *
FROM changes
WHERE status != prev_status OR prev_status IS NULL;sql2. Fill Missing Dates#
-- Tạo date series và fill missing values
WITH dates AS (
SELECT generate_series(
'2024-01-01'::date,
'2024-01-31'::date,
'1 day'::interval
) AS date
)
SELECT
dates.date,
COALESCE(sales.amount, 0) AS amount,
SUM(COALESCE(sales.amount, 0)) OVER (ORDER BY dates.date) AS running_total
FROM dates
LEFT JOIN sales ON sales.date = dates.date
ORDER BY dates.date;sql3. So Sánh Tháng Này vs Tháng Trước#
WITH monthly AS (
SELECT
DATE_TRUNC('month', order_date)::date AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY month
)
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
ROUND((revenue - LAG(revenue) OVER (ORDER BY month))
/ NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 2) AS growth_pct
FROM monthly;sql4. First Purchase per User#
SELECT
user_id,
order_date,
amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS order_rank
FROM orders;
-- WHERE order_rank = 1 → first purchasesqlPerformance#
-- Window functions cần sort — đảm bảo có index phù hợp
CREATE INDEX idx_employees_dept_salary ON employees(department, salary DESC);
CREATE INDEX idx_daily_revenue_date ON daily_revenue(date);sqlKết Luận#
Window functions là một trong những tính năng SQL mạnh nhất:
ROW_NUMBER()— xếp hạng, top-N per groupLAG/LEAD— so sánh với dòng trước/sauSUM OVER (ORDER BY)— running totalNTILE— phân nhóm percentileFIRST_VALUE/LAST_VALUE— giá trị biên
Học window functions = giảm 80% subquery và self-join trong code.