SQL & Database — Tư tưởng & Nguyên lý
19/52
Bài 19 / 52~22 phútJoin, aggregation & windowLộ trình · chặng 10/21Miễn phí lượt xem

Window functions intro — OVER + PARTITION BY giữ row + tính cross-row

Window = cửa sổ nhìn nhóm row liên quan, vẫn đứng tại 1 row. Khác GROUP BY collapse. 4 use case: ranking, aggregate over window, lag/lead, frame. WHERE rn=1 pitfall.

TL;DR: Window function = "cửa sổ nhìn nhóm" — tính metric cross-row (rank, aggregate, lag/lead) mà KHÔNG collapse row như GROUP BY. Cú pháp: func() OVER (PARTITION BY ... ORDER BY ...). Bug phổ biến: WHERE rn = 1 không hoạt động vì window function nằm trong SELECT (bước 5), WHERE chạy ở bước 2 — wrap subquery hoặc CTE. Window function là ANSI SQL chuẩn, hoạt động trên mọi RDBMS hiện đại (PostgreSQL, MySQL 8+, SQL Server, Oracle, SQLite 3.25+).

TaskFlow cần báo cáo: "task nào có due_at gần nhất trong từng project?" Câu truy vấn đầu tiên bạn thử dùng GROUP BY project_id — gom 30 task về 1 row per project, mất toàn bộ thông tin từng task. Bạn không thể giữ id, title, assignee_id vì chúng không nằm trong GROUP BY và không được aggregate.

Window function giải quyết bài toán này: giữ nguyên 30 row, thêm column rn tính từ thứ tự due_at trong từng project. Filter rn = 1 cho bạn đúng task muốn mà không mất chi tiết. Đây là pattern xuất hiện trong 79% job description analytics SQL — ranking per group, aggregate kết hợp so sánh cross-row, lag/lead giữa kỳ.

Bài này map khái niệm cửa sổ, cú pháp OVER, 4 use case cơ bản, và 1 pitfall mà hầu hết dev mới đều dính lần đầu.

1. Analogy — Cửa sổ nhìn xung quanh

Hãy tưởng tượng một bữa tiệc công ty: mọi người ngồi theo bàn (project). GROUP BY giống ban tổ chức gom mọi người thành từng bàn rồi chỉ báo cáo tổng số người mỗi bàn — bạn mất danh sách từng người. Window function giống mỗi người vẫn ngồi tại chỗ của mình, nhìn quanh bàn để biết mình đứng thứ mấy trong nhóm — identity vẫn giữ nguyên, thêm thông tin cross-row.

Bữa tiệcSQL
Ban tổ chức gom bàn, báo tổng số ngườiGROUP BY — collapse nhiều row thành 1
Mỗi người ngồi tại chỗ, nhìn quanh bànWindow function — giữ row, thêm cross-row metric
"Bàn A" thay vì từng tênAggregate result — mất individual identity
Rank trong bàn của mìnhROW_NUMBER() OVER (PARTITION BY project_id ORDER BY due_at)
💡 Cách nhớ

Window = "cửa sổ nhìn nhóm" — bạn vẫn đứng tại row của mình, nhưng thấy được thông tin của cả nhóm xung quanh. GROUP BY collapse nhóm thành 1 row. Window không collapse — mỗi row vẫn xuất hiện trong kết quả.

"Không collapse" nghe trừu tượng, nhưng nó đo được bằng số hàng đầu ra — cùng bốn hàng đầu vào, một bên trả về 1 hàng, một bên trả về 4:

Bốn hàng tasks của project 7 với 5, 3, 4 và 8 giờ rẽ hai nhánh: GROUP BY project_id trả về 1 hàng project 7 với AVG 5.0 và bốn id không còn trong kết quả; OVER PARTITION BY project_id vẫn trả 4 hàng, mỗi hàng thêm cột AVG 5.0 và id vẫn còn để so hàng với nhóm

2. Cú pháp + cơ chế

SQL
SELECT
  id,
  project_id,
  title,
  due_at,
  ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY due_at) AS rn
FROM tasks;

Ba phần trong OVER(...):

PhầnVai tròGhi chú
PARTITION BY project_idChia rows thành group theo projectGiống GROUP BY nhưng KHÔNG collapse
ORDER BY due_atThứ tự trong từng windowQuyết định rank, lag/lead, frame
(frame clause)Phạm vi rows tính trong windowMặc định hoặc chỉ định — bài 7 deep dive

Output minh họa:

id | project_id | title       | due_at     | rn
---+------------+-------------+------------+----
 1 |          1 | Deploy v1   | 2026-05-01 |  1
 2 |          1 | Fix auth    | 2026-05-03 |  2
 3 |          1 | Update docs | 2026-05-10 |  3
 4 |          2 | Mockup home | 2026-05-02 |  1
 5 |          2 | Logo design | 2026-05-05 |  2

Mỗi row vẫn xuất hiện đầy đủ. rn reset cho mỗi project_idPARTITION BY project_id. Task id=4 của project 2 có rn=1 độc lập với project 1.

3. So sánh window vs aggregate vs subquery

SQL
-- AGGREGATE: collapse, mat row chi tiet
SELECT project_id, AVG(EXTRACT(DAY FROM (updated_at - created_at)))
FROM tasks
GROUP BY project_id;
-- Output: 1 row per project, khong xem duoc task individual
SQL
-- WINDOW: giu row + add column cross-row
SELECT
  id,
  title,
  project_id,
  EXTRACT(DAY FROM (updated_at - created_at))              AS my_days,
  AVG(EXTRACT(DAY FROM (updated_at - created_at)))
    OVER (PARTITION BY project_id)                         AS project_avg
FROM tasks;
-- Output: moi row task + column project_avg tinh tren ca project
SQL
-- SUBQUERY: verbose hon, plan thuong cham hon voi bang lon
SELECT
  t.id,
  t.title,
  t.project_id,
  EXTRACT(DAY FROM (t.updated_at - t.created_at))          AS my_days,
  (SELECT AVG(EXTRACT(DAY FROM (updated_at - created_at)))
   FROM tasks
   WHERE project_id = t.project_id)                        AS project_avg
FROM tasks t;
-- Output: same result nhung correlated subquery chay lai cho moi row

Window là lựa chọn tốt nhất cho pattern "giữ row chi tiết + thêm cross-row metric". Subquery correlated đạt cùng kết quả nhưng thường chậm hơn với bảng lớn vì chạy lại cho mỗi row.

4. 4 use case — tổng quan

#PatternFunction ví dụUse case
1RankingROW_NUMBER, RANK, DENSE_RANKTop N per group, leaderboard
2Aggregate over windowSUM/AVG/COUNT OVERRow detail + group metric song song
3Lag/LeadLAG, LEADSo sánh với row trước/sau
4Frame (running/moving)SUM OVER ROWS BETWEENRunning total, moving average

Bài tiếp theo (Module 3 bài 7 của khoá này) đi sâu cả 4 pattern với ví dụ thực chiến. Bài này focus intro + pattern 1 (ranking) + pattern 2 (aggregate over window) để xây nền.

Thử ngẫmreport "task due gần nhất per project" đổi yêu cầu thành "3 task due gần nhất per project". Trong bốn pattern ở bảng trên, bạn đụng đúng một pattern — nhưng đổi gì trong đó?

5. Pattern 1 — Ranking với ROW_NUMBER

SQL
-- "Task co due_at som nhat per project"
SELECT
  id,
  project_id,
  title,
  due_at,
  ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY due_at) AS rn
FROM tasks;
-- rn=1 la task due som nhat trong project do

Filter rn = 1 cho top-1 per project. Nhưng WHERE rn = 1 trực tiếp báo lỗi — đây là pitfall quan trọng, xem section 6.

Ba hàm ranking hay dùng:

HàmTie handlingVí dụ với tie
ROW_NUMBER()Luôn unique, tie break tùy ý1, 2, 3, 4
RANK()Gap sau tie1, 2, 2, 4
DENSE_RANK()Không gap sau tie1, 2, 2, 3

6. Pitfall — WHERE rn = 1 không hợp lệ

Pitfall — WHERE không thấy window function result

Window function nằm trong SELECT (bước 5 của logical order). WHERE chạy ở bước 2 — trước khi window được tính. Dùng WHERE rn = 1 sau ROW_NUMBER() OVER (...) AS rn sẽ báo column "rn" does not exist.

SQL
-- BUG: WHERE chay TRUOC SELECT (trong do chua window result)
SELECT
  id,
  project_id,
  title,
  ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY due_at) AS rn
FROM tasks
WHERE rn = 1;
-- ERROR: column "rn" does not exist

Ba cách fix:

SQL
-- Fix 1: wrap subquery -- universal, moi dialect
SELECT * FROM (
  SELECT
    id,
    project_id,
    title,
    ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY due_at) AS rn
  FROM tasks
) sub
WHERE rn = 1;
SQL
-- Fix 2: CTE (Module 8 cua khoa nay -- readable hon cho query phuc tap)
WITH ranked AS (
  SELECT
    id,
    project_id,
    title,
    ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY due_at) AS rn
  FROM tasks
)
SELECT * FROM ranked WHERE rn = 1;
SQL
-- Fix 3: DISTINCT ON (PostgreSQL-only) -- ngan nhat cho top-1 per group
SELECT DISTINCT ON (project_id)
  id, project_id, title, due_at
FROM tasks
ORDER BY project_id, due_at;
-- Chi PostgreSQL. MySQL/SQL Server: dung subquery/CTE voi ROW_NUMBER (Fix 1/2)

DISTINCT ON là lựa chọn ngắn gọn nhất cho "top-1 per group" trong PostgreSQL — không portable sang MySQL/SQL Server. Window function (Fix 1/2) cần thiết khi muốn top-N (N vượt 1) hoặc khi cần giữ rank value trong kết quả, và là cách portable nhất.

Thử ngẫmbạn dùng Fix 3 (DISTINCT ON) cho một dashboard nội bộ. Sáu tháng sau PM yêu cầu "top 3 thay vì top 1". Chi phí đổi code lúc đó lớn cỡ nào so với nếu ban đầu chọn Fix 1?

7. Pattern 2 — Aggregate over window

SQL
-- Per task: so ngay hoan thanh cua task nay + trung binh cua project + tong cua assignee
SELECT
  id,
  title,
  EXTRACT(DAY FROM (updated_at - created_at))              AS my_days,
  AVG(EXTRACT(DAY FROM (updated_at - created_at)))
    OVER (PARTITION BY project_id)                         AS project_avg,
  SUM(EXTRACT(DAY FROM (updated_at - created_at)))
    OVER (PARTITION BY assignee_id)                        AS my_total_days
FROM tasks
WHERE status = 'done';

Hai window khác nhau (PARTITION BY project_idPARTITION BY assignee_id) trong cùng một query hoàn toàn hợp lệ. PostgreSQL tính từng window function độc lập — mỗi OVER(...) định nghĩa một cửa sổ riêng.

8. Applied — TaskFlow leaderboard nâng cao

SQL
-- "Per user: so task done + rank trong project theo so task done"
SELECT
  u.id,
  u.name,
  t.project_id,
  COUNT(t.id) FILTER (WHERE t.status = 'done')           AS done_count,
  RANK() OVER (
    PARTITION BY t.project_id
    ORDER BY COUNT(t.id) FILTER (WHERE t.status = 'done') DESC
  )                                                        AS project_rank
FROM users u
LEFT JOIN tasks t ON t.assignee_id = u.id
GROUP BY u.id, u.name, t.project_id;

Điểm đáng chú ý: window function chạy sau GROUP BY trong logical order (GROUP BY là bước 3, window function nằm trong SELECT là bước 5). Vì vậy RANK() OVER (...) có thể dùng kết quả của COUNT(...) FILTER (...) — aggregate được tính xong trước khi window function đọc giá trị đó.

id | name    | project_id | done_count | project_rank
---+---------+------------+------------+--------------
 1 | An      |          1 |          8 |            1
 2 | Binh    |          1 |          5 |            2
 3 | Chi     |          1 |          5 |            2
 4 | An      |          2 |          3 |            1

RANK() tạo gap: project_rank = 4 không xuất hiện sau hai người rank 2. Dùng DENSE_RANK() nếu muốn liên tục không gap.

9. Deep Dive — Window functions

📚 Deep Dive — Window functions
  • Modern SQL — Window Functions — Markus Winand giải thích window function cross-vendor (PostgreSQL, MySQL 8+, SQL Server, Oracle, SQLite 3.25+): cú pháp OVER, PARTITION BY, frame clause. Agnostic, có bảng so sánh dialect.
  • SQL Standard ISO/IEC 9075 — Window Functions — định nghĩa chuẩn ANSI SQL cho window function calls, frame clause grammar, và FILTER.
  • Itzik Ben-Gan — "T-SQL Window Functions: For SQL Server and Azure SQL" (sách) — deep dive về frame clause, gap-and-island pattern. Viết cho SQL Server nhưng concept áp dụng cho mọi RDBMS tuân ANSI SQL.

Ghi chú: Modern SQL cho cross-vendor quick reference; ISO spec cho nguồn gốc chuẩn; Ben-Gan khi muốn master frame clause và advanced pattern như gap-and-island.

10. Tóm tắt

  • Window function = "cửa sổ nhìn nhóm row liên quan" — KHÔNG collapse như GROUP BY, mỗi row vẫn xuất hiện trong kết quả.
  • Cú pháp: func() OVER (PARTITION BY ... ORDER BY ...) — PARTITION BY chia group, ORDER BY xác định thứ tự trong window.
  • 4 use case: ranking (ROW_NUMBER/RANK/DENSE_RANK), aggregate over window (SUM/AVG/COUNT OVER), lag/lead so sánh row trước/sau, frame cho running total và moving average.
  • WHERE không thấy window function result — wrap subquery hoặc CTE để filter theo column window.
  • DISTINCT ON (col) là alternative ngắn gọn cho "top-1 per group" trong PostgreSQL (không portable). Window function + subquery/CTE là cách portable cho top-N và khi cần rank value.
  • Window function chạy sau GROUP BY trong logical order — có thể OVER trên aggregate result.
  • Module 3 bài 7 của khoá này: rank/lag/running total — 4 pattern thực chiến. Module 4 của khoá này: CTE wrap pattern cho window filter.

11. Tự kiểm tra

Tự kiểm tra
0/6 câu đã trả lời
  1. Q1
    Vì sao window function giữ row trong khi GROUP BY collapse? Khi nào dùng window, khi nào dùng GROUP BY?
  2. Q2
    Vì sao WHERE rn = 1 sau ROW_NUMBER() OVER (...) AS rn báo lỗi? Nêu 3 cách fix và tradeoff.
  3. Q3
    Phân biệt AVG(x) OVER (PARTITION BY p) vs SELECT AVG(x) FROM t GROUP BY p. Cho ví dụ output khác nhau.
  4. Q4
    DISTINCT ON vs ROW_NUMBER + WHERE rn=1 — khi nào dùng cái nào?
  5. Q5
    Window function chạy ở vị trí nào trong SQL logical processing order? Điều này ảnh hưởng gì đến việc dùng window kết hợp GROUP BY?
  6. Q6
    TaskFlow cần '1 query trả về task due gần nhất per assignee, kèm median due (tính bằng ngày từ hôm nay) của toàn project đó'. Có thể viết 1 query không? Phác thảo approach.

Bài tiếp theo: Window rank/lag/running total — 4 pattern phổ biến nhất analytics

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Window patterns — RANK + LAG/LEAD + running total + moving average