Appearance
📘 Buổi 6: SQL nâng cao — Window Function & Tư duy Data Warehouse
Level up SQL: phân tích ranking, running total, và hiểu kho dữ liệu doanh nghiệp.
🎯 Mục tiêu buổi học
Sau buổi này, học viên sẽ:
- Sử dụng Window Functions: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD
- Tính running total, moving average, percent of total bằng SQL
- Hiểu kiến trúc Data Warehouse: star schema, dimension vs fact table
- Viết CTE (Common Table Expression) để query phức tạp dễ đọc
📋 Tổng quan
Ở Buổi 5, bạn đã nắm vững SQL cơ bản: viết SELECT, JOIN nhiều bảng, và dùng GROUP BY + aggregate functions để tổng hợp dữ liệu. Bạn biết cách đếm đơn hàng theo thành phố (COUNT ... GROUP BY city), tính doanh thu trung bình (AVG(total_amount)), và kết nối 3–4 bảng bằng JOIN. Đó là nền tảng vững chắc — nhưng thực tế công việc DA đòi hỏi nhiều hơn thế.
Hãy tưởng tượng sếp hỏi: "Cho anh bảng xếp hạng top sản phẩm theo doanh thu trong mỗi category" — GROUP BY không đủ, vì bạn cần xếp hạng trong mỗi nhóm. Hoặc: "Tính doanh thu tích lũy theo ngày để xem tiến độ so với target" — cần running total, không chỉ tổng theo ngày. Hay: "So sánh doanh thu tháng này với tháng trước (MoM growth)" — cần truy cập dòng trước đó trong kết quả. Tất cả những bài toán này đều được giải quyết bằng Window Functions — tính năng SQL nâng cao mà 90% Data Analyst sử dụng hàng ngày.
Song song đó, khi dữ liệu doanh nghiệp tăng lên hàng triệu–hàng tỷ dòng, hệ thống database giao dịch (OLTP) không còn phù hợp cho phân tích. Doanh nghiệp cần một kiến trúc riêng gọi là Data Warehouse — nơi dữ liệu được tổ chức theo mô hình Star Schema (fact table + dimension table) tối ưu cho truy vấn phân tích. Hiểu Data Warehouse giúp bạn biết dữ liệu đến từ đâu, tại sao schema trông khác so với database giao dịch, và bạn cần JOIN bảng nào để trả lời câu hỏi kinh doanh.
Cuối cùng, query phức tạp với nhiều bước tính toán dễ trở nên khó đọc, khó debug. CTE (Common Table Expression) là cách viết SQL "sạch" — chia query thành từng bước logic rõ ràng, mỗi bước có tên — giống cách bạn chia hàm trong lập trình. CTE là kỹ năng bắt buộc khi làm việc với real-world data.
Buổi này đánh dấu bước chuyển từ SQL cơ bản → SQL phân tích chuyên nghiệp. Sau buổi học, bạn sẽ viết được những query mà trước đây phải xuất ra Excel rồi tính tay — giờ chỉ cần 1 câu SQL.
📌 Phần 1: Window Functions — Sức mạnh phân tích trong SQL
Khái niệm
Window Function (hàm cửa sổ) là loại hàm SQL thực hiện phép tính trên một tập hợp các dòng liên quan đến dòng hiện tại — mà không gom nhóm (collapse) kết quả như GROUP BY. Mỗi dòng vẫn giữ nguyên trong output, nhưng kèm thêm giá trị tính toán từ "cửa sổ" dữ liệu xung quanh.
Cú pháp tổng quát:
sql
function_name(expression) OVER (
[PARTITION BY cột_phân_nhóm] -- Chia dữ liệu thành nhóm (tùy chọn)
[ORDER BY cột_sắp_xếp] -- Sắp xếp trong mỗi nhóm (tùy chọn)
[ROWS/RANGE BETWEEN ... AND ...] -- Khung cửa sổ (tùy chọn)
)So sánh GROUP BY vs Window Function:
| Đặc điểm | GROUP BY + Aggregate | Window Function |
|---|---|---|
| Kết quả | Gom nhiều dòng thành 1 dòng/nhóm | Giữ nguyên tất cả dòng |
| Cột output | Chỉ cột trong GROUP BY + aggregate | Tất cả cột gốc + giá trị window |
| Ví dụ | Doanh thu theo tháng (1 dòng/tháng) | Doanh thu từng đơn + tổng tháng kèm theo |
| Từ khóa | GROUP BY | OVER() |
| Use case | Tổng hợp, báo cáo summary | Ranking, running total, so sánh dòng |
Ba nhóm Window Functions:
| Nhóm | Hàm | Mô tả |
|---|---|---|
| Ranking | ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE(n) | Xếp hạng dòng trong mỗi nhóm |
| Offset | LAG(), LEAD(), FIRST_VALUE(), LAST_VALUE() | Truy cập dòng trước/sau dòng hiện tại |
| Aggregate | SUM() OVER, AVG() OVER, COUNT() OVER, MIN() OVER, MAX() OVER | Hàm tổng hợp nhưng giữ nguyên dòng gốc |
Tại sao quan trọng cho Data Analyst?
Window Functions là kỹ năng SQL được hỏi nhiều nhất trong phỏng vấn DA — theo DataLemur (2025), 70%+ bài SQL interview yêu cầu window functions. Lý do: chúng giải quyết chính xác các bài toán phân tích thực tế mà GROUP BY không làm được:
- Ranking: Top N sản phẩm trong mỗi category, xếp hạng nhân viên theo KPI
- Running total: Doanh thu tích lũy từ đầu tháng — theo dõi tiến độ vs target
- Period comparison: So sánh MoM, YoY bằng LAG — không cần self-join
- Percent of total: Tỷ trọng mỗi sản phẩm trong tổng doanh thu danh mục
Nhớ lại Buổi 5, bạn dùng GROUP BY để tính tổng doanh thu theo thành phố. Window Function là bước tiếp theo: bạn giữ nguyên chi tiết từng đơn hàng, nhưng thêm cột tổng thành phố bên cạnh — để mỗi đơn hàng tự biết nó chiếm bao nhiêu phần trăm doanh thu thành phố đó.
Áp dụng thực tế
Ranking Functions — phân biệt:
| Hàm | Cách xếp hạng | Khi có giá trị bằng nhau | Ví dụ: [100, 90, 90, 80] |
|---|---|---|---|
ROW_NUMBER() | Mỗi dòng 1 số duy nhất | Không quan tâm — gán ngẫu nhiên | 1, 2, 3, 4 |
RANK() | Cùng hạng cho giá trị bằng, bỏ qua hạng kế tiếp | 2 người hạng 2 → tiếp theo là hạng 4 | 1, 2, 2, 4 |
DENSE_RANK() | Cùng hạng cho giá trị bằng, không bỏ qua | 2 người hạng 2 → tiếp theo vẫn hạng 3 | 1, 2, 2, 3 |
NTILE(n) | Chia đều thành n nhóm | Phân bổ đều | NTILE(4) → chia thành 4 phần tư (quartile) |
sql
-- Xếp hạng sản phẩm theo doanh thu trong mỗi category
SELECT
category,
product_name,
revenue,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) AS row_num,
RANK() OVER (PARTITION BY category ORDER BY revenue DESC) AS rank_pos,
DENSE_RANK() OVER (PARTITION BY category ORDER BY revenue DESC) AS dense_rank_pos
FROM product_sales;| category | product_name | revenue | row_num | rank_pos | dense_rank_pos |
|---|---|---|---|---|---|
| Điện thoại | iPhone 16 | 5000000 | 1 | 1 | 1 |
| Điện thoại | Samsung S25 | 4200000 | 2 | 2 | 2 |
| Điện thoại | Xiaomi 15 | 4200000 | 3 | 2 | 2 |
| Điện thoại | Oppo Find | 3100000 | 4 | 4 | 3 |
| Laptop | MacBook Pro | 8500000 | 1 | 1 | 1 |
| Laptop | Dell XPS | 6000000 | 2 | 2 | 2 |
Offset Functions — LAG & LEAD:
sql
-- So sánh doanh thu tháng hiện tại vs tháng trước (MoM Growth)
SELECT
month,
revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue,
ROUND(
(revenue - LAG(revenue, 1) OVER (ORDER BY month))
/ LAG(revenue, 1) OVER (ORDER BY month) * 100, 1
) AS mom_growth_pct
FROM monthly_revenue;| month | revenue | prev_month_revenue | next_month_revenue | mom_growth_pct |
|---|---|---|---|---|
| 2026-01 | 500000000 | NULL | 580000000 | NULL |
| 2026-02 | 580000000 | 500000000 | 620000000 | 16.0 |
| 2026-03 | 620000000 | 580000000 | 550000000 | 6.9 |
| 2026-04 | 550000000 | 620000000 | NULL | -11.3 |
Aggregate Windows — SUM OVER, AVG OVER:
sql
-- Running total (doanh thu tích lũy) theo ngày
SELECT
order_date,
daily_revenue,
SUM(daily_revenue) OVER (ORDER BY order_date) AS running_total,
AVG(daily_revenue) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7d
FROM daily_sales;📌 Phần 2: Phân tích nâng cao — Running Total, Moving Average, Percent of Total
Khái niệm
Phân tích nâng cao trong SQL tận dụng Window Functions để tính toán các chỉ số mà business cần hàng ngày nhưng GROUP BY đơn thuần không thể tạo ra. Bốn kỹ thuật quan trọng nhất:
| Kỹ thuật | English | Mô tả | Dùng khi |
|---|---|---|---|
| Tích lũy | Running Total | Tổng cộng dồn từ đầu kỳ đến dòng hiện tại | Theo dõi tiến độ doanh thu vs target |
| Trung bình trượt | Moving Average | Trung bình N ngày/tuần gần nhất — làm mượt biến động | Phân tích trend, loại bỏ noise |
| Tỷ trọng | Percent of Total | Tỷ lệ % của mỗi dòng so với tổng nhóm | Hiểu cơ cấu doanh thu, portfolio mix |
| Tăng trưởng kỳ | MoM / YoY Growth | So sánh giá trị hiện tại với kỳ trước (tháng, năm) | Đánh giá xu hướng tăng/giảm |
Tại sao quan trọng cho Data Analyst?
Đây là 4 kỹ thuật xuất hiện trong mọi báo cáo phân tích của doanh nghiệp:
- CFO cần running total để biết "Tính đến hôm nay, đã đạt bao nhiêu % mục tiêu doanh thu quý?"
- Product Manager cần moving average 7 ngày để xem DAU (Daily Active Users) có xu hướng tăng hay giảm — bỏ qua biến động cuối tuần
- Marketing Director cần percent of total để biết "Kênh nào đóng góp nhiều nhất vào tổng chi phí marketing?"
- CEO cần MoM/YoY growth trong slide deck hàng tháng
Trước Buổi 6, bạn phải xuất data ra Excel, dùng ô tham chiếu để tính running total, tạo cột helper cho moving average. Giờ — 1 câu SQL làm tất cả, chạy trên hàng triệu dòng trong vài giây.
Áp dụng thực tế
Running Total — doanh thu tích lũy theo ngày trong tháng:
sql
SELECT
order_date,
daily_revenue,
SUM(daily_revenue) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_revenue
FROM daily_sales
WHERE order_date BETWEEN '2026-01-01' AND '2026-01-31';Moving Average 7 ngày — làm mượt DAU:
sql
SELECT
log_date,
dau,
ROUND(
AVG(dau) OVER (
ORDER BY log_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 0
) AS dau_ma7
FROM app_daily_metrics
ORDER BY log_date;Percent of Total — tỷ trọng doanh thu mỗi sản phẩm trong category:
sql
SELECT
category,
product_name,
revenue,
SUM(revenue) OVER (PARTITION BY category) AS category_total,
ROUND(
revenue * 100.0 / SUM(revenue) OVER (PARTITION BY category), 1
) AS pct_of_category
FROM product_sales
ORDER BY category, revenue DESC;| category | product_name | revenue | category_total | pct_of_category |
|---|---|---|---|---|
| Điện thoại | iPhone 16 | 5000000 | 16500000 | 30.3 |
| Điện thoại | Samsung S25 | 4200000 | 16500000 | 25.5 |
| Điện thoại | Xiaomi 15 | 4200000 | 16500000 | 25.5 |
| Điện thoại | Oppo Find | 3100000 | 16500000 | 18.8 |
MoM Growth bằng LAG — tăng trưởng doanh thu qua từng tháng:
sql
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month))
* 100.0 / LAG(revenue) OVER (ORDER BY month), 1
) AS mom_growth_pct
FROM monthly_revenue
ORDER BY month;YoY Growth — so sánh cùng kỳ năm trước:
sql
SELECT
month,
revenue,
LAG(revenue, 12) OVER (ORDER BY month) AS same_month_last_year,
ROUND(
(revenue - LAG(revenue, 12) OVER (ORDER BY month))
* 100.0 / LAG(revenue, 12) OVER (ORDER BY month), 1
) AS yoy_growth_pct
FROM monthly_revenue
ORDER BY month;📌 Phần 3: Data Warehouse Basics — Kiến trúc kho dữ liệu
Khái niệm
Data Warehouse (kho dữ liệu) là hệ thống lưu trữ dữ liệu tập trung được thiết kế riêng cho phân tích và báo cáo — khác biệt hoàn toàn với database giao dịch (OLTP) mà ứng dụng sử dụng hàng ngày. Data warehouse thu thập dữ liệu từ nhiều nguồn (ERP, CRM, website, app...), biến đổi và tổ chức lại theo mô hình tối ưu cho query phân tích.
OLTP vs OLAP — hai thế giới database:
| Đặc điểm | OLTP (Online Transaction Processing) | OLAP (Online Analytical Processing) |
|---|---|---|
| Mục đích | Xử lý giao dịch — INSERT, UPDATE hàng ngày | Phân tích, báo cáo — SELECT phức tạp |
| Người dùng | Ứng dụng, developer | Data Analyst, BI team, management |
| Query | Đơn giản, nhanh: SELECT * WHERE id = 123 | Phức tạp, quét nhiều data: JOIN 5 bảng + GROUP BY + Window |
| Schema | Normalized (3NF) — ít dư thừa, nhiều bảng | Denormalized — Star/Snowflake schema |
| Dữ liệu | Dữ liệu hiện tại (current state) | Dữ liệu lịch sử (historical) |
| Kích thước | GB → TB | TB → PB |
| Ví dụ | MySQL cho app Tiki, PostgreSQL cho VNPay | BigQuery, Redshift, Snowflake |
Star Schema — mô hình cốt lõi của Data Warehouse:
Star schema tổ chức dữ liệu thành 2 loại bảng:
| Loại bảng | English | Mô tả | Đặc điểm | Ví dụ |
|---|---|---|---|---|
| Bảng sự kiện | Fact Table | Chứa các số liệu đo lường (metrics) — mỗi dòng là 1 sự kiện kinh doanh | Nhiều dòng, nhiều FK, ít cột text | fact_sales, fact_orders, fact_page_views |
| Bảng chiều | Dimension Table | Chứa thông tin mô tả (context) cho fact — ai, cái gì, ở đâu, khi nào | Ít dòng, nhiều cột text, có PK | dim_customer, dim_product, dim_date, dim_store |
┌─────────────┐
│ dim_product │
│─────────────│
│ product_key │──┐
│ product_name │ │
│ category │ │ ┌──────────────────────────┐
│ brand │ │ │ fact_sales │
└─────────────┘ │ │──────────────────────────│
├───>│ sale_id │
┌─────────────┐ │ │ date_key (FK) ──────│───> dim_date
│ dim_customer │ │ │ customer_key (FK) ──────│───> dim_customer
│─────────────│ │ │ product_key (FK) ──────│───> dim_product
│ customer_key │──┘ │ store_key (FK) ──────│───> dim_store
│ customer_name│ │ quantity │
│ segment │ │ unit_price │
│ city │ │ total_amount │
│ join_date │ │ discount │
└─────────────┘ └──────────────────────────┘
│
┌─────────────┐ │ ┌──────────┐
│ dim_date │ │ │ dim_store │
│─────────────│ │ │──────────│
│ date_key │<─────────────────┘ │ store_key│
│ full_date │ │ store_name│
│ day_of_week │ │ region │
│ month │ │ city │
│ quarter │ └──────────┘
│ year │
│ is_weekend │
└─────────────┘Các khái niệm liên quan:
| Khái niệm | English | Mô tả |
|---|---|---|
| ETL | Extract – Transform – Load | Quy trình lấy dữ liệu từ nguồn (Extract), biến đổi (Transform), nạp vào warehouse (Load) |
| Data Mart | Data Mart | "Warehouse nhỏ" cho 1 bộ phận — ví dụ: Marketing Data Mart, Finance Data Mart |
| Data Lake | Data Lake | Kho lưu trữ dữ liệu thô (raw), chưa xử lý — structured + unstructured — giá rẻ, linh hoạt |
| Surrogate Key | Surrogate Key | Khóa nhân tạo (thường integer tăng tự động) thay thế natural key trong dimension table |
| Slowly Changing Dimension | SCD | Kỹ thuật xử lý khi thông tin dimension thay đổi (khách hàng đổi địa chỉ, sản phẩm đổi giá) |
Tại sao quan trọng cho Data Analyst?
Là DA, bạn không xây data warehouse — đó là việc của Data Engineer. Nhưng bạn query từ warehouse mỗi ngày. Hiểu kiến trúc giúp bạn:
- Biết JOIN bảng nào: fact_sales JOIN dim_product → lấy tên sản phẩm cho báo cáo doanh thu
- Hiểu tại sao schema "lạ": dim_date có cột
is_weekend,quarter— vì đã được thiết kế sẵn cho phân tích - Tránh query sai: Không
GROUP BYtrên fact table mà quên JOIN dimension → kết quả vô nghĩa - Nói cùng ngôn ngữ với Data Engineer, BI Developer — sử dụng thuật ngữ chính xác khi yêu cầu dữ liệu
Theo Kimball Dimensional Modeling — tiêu chuẩn thiết kế Data Warehouse phổ biến nhất — Star Schema là mô hình tối ưu vì: đơn giản, dễ hiểu, và query nhanh (ít JOIN hơn snowflake schema).
Áp dụng thực tế
Query Star Schema — Doanh thu theo danh mục sản phẩm, năm, quý:
sql
SELECT
dd.year,
dd.quarter,
dp.category,
SUM(fs.total_amount) AS total_revenue,
COUNT(DISTINCT fs.sale_id) AS num_transactions,
COUNT(DISTINCT fs.customer_key) AS num_customers
FROM fact_sales fs
JOIN dim_date dd ON fs.date_key = dd.date_key
JOIN dim_product dp ON fs.product_key = dp.product_key
WHERE dd.year = 2025
GROUP BY dd.year, dd.quarter, dp.category
ORDER BY dd.quarter, total_revenue DESC;So sánh Data Lake vs Data Warehouse vs Data Mart:
| Đặc điểm | Data Lake | Data Warehouse | Data Mart |
|---|---|---|---|
| Dữ liệu | Thô (raw), mọi định dạng | Đã xử lý, có cấu trúc | Tập con của warehouse |
| Schema | Schema-on-read | Schema-on-write (Star/Snowflake) | Star schema đơn giản |
| Người dùng | Data Scientist, Data Engineer | Data Analyst, BI team | Bộ phận cụ thể |
| Công nghệ | S3, Azure Data Lake, GCS | BigQuery, Redshift, Snowflake | Derived từ warehouse |
| Chi phí lưu trữ | Thấp | Trung bình – Cao | Thấp (ít data) |
📌 Phần 4: CTE & Advanced Query — Viết SQL sạch, dễ bảo trì
Khái niệm
CTE (Common Table Expression) là bảng tạm có tên được định nghĩa ngay trong câu query bằng mệnh đề WITH ... AS (...). CTE giúp chia query phức tạp thành nhiều bước logic — mỗi bước có tên rõ ràng — sau đó kết hợp lại trong query cuối.
Cú pháp CTE:
sql
WITH tên_cte AS (
SELECT ...
FROM ...
WHERE ...
)
SELECT *
FROM tên_cte;So sánh: Subquery vs CTE:
| Đặc điểm | Subquery | CTE |
|---|---|---|
| Vị trí | Nằm trong FROM hoặc WHERE | Khai báo trước query chính bằng WITH |
| Đọc hiểu | Khó khi lồng nhiều tầng | Dễ — mỗi bước có tên |
| Tái sử dụng | Phải viết lại nếu dùng nhiều lần | Đặt tên 1 lần, dùng nhiều lần trong query |
| Debug | Khó tách ra kiểm tra | Dễ — chạy riêng từng CTE |
| Performance | Tương đương | Tương đương (hầu hết RDBMS) |
Nested CTE — nhiều bước nối tiếp:
sql
WITH step1 AS (
-- Bước 1: Tính doanh thu theo tháng
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(total_amount) AS monthly_revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', order_date)
),
step2 AS (
-- Bước 2: Thêm MoM growth
SELECT
month,
monthly_revenue,
LAG(monthly_revenue) OVER (ORDER BY month) AS prev_month,
ROUND(
(monthly_revenue - LAG(monthly_revenue) OVER (ORDER BY month))
* 100.0 / LAG(monthly_revenue) OVER (ORDER BY month), 1
) AS mom_growth_pct
FROM step1
)
-- Bước 3: Chỉ lấy tháng tăng trưởng âm
SELECT *
FROM step2
WHERE mom_growth_pct < 0
ORDER BY month;CASE WHEN — logic điều kiện trong SQL:
sql
SELECT
customer_name,
total_spent,
CASE
WHEN total_spent >= 10000000 THEN 'VIP'
WHEN total_spent >= 5000000 THEN 'Gold'
WHEN total_spent >= 1000000 THEN 'Silver'
ELSE 'Standard'
END AS customer_tier
FROM customer_summary;UNION / INTERSECT / EXCEPT — phép toán tập hợp:
| Phép toán | Mô tả | Ví dụ |
|---|---|---|
UNION | Gộp kết quả 2 query, loại trùng | Gộp danh sách khách hàng từ 2 source |
UNION ALL | Gộp kết quả, giữ trùng | Gộp log từ nhiều bảng — cần tất cả dòng |
INTERSECT | Chỉ lấy dòng có ở cả 2 query | Tìm khách hàng mua cả online lẫn offline |
EXCEPT | Lấy dòng ở query 1 mà không có ở query 2 | Tìm khách hàng tháng trước nhưng không mua tháng này (churned) |
Tại sao quan trọng cho Data Analyst?
CTE là tiêu chuẩn viết SQL trong mọi team DA chuyên nghiệp. Query thực tế thường có 3–5 bước tính toán — nếu viết thành subquery lồng nhau, không ai review hoặc debug được. CTE giúp:
- Code review dễ hơn: đồng nghiệp đọc từng bước logic rõ ràng
- Debug dễ hơn: chạy riêng từng CTE để kiểm tra kết quả trung gian
- Bảo trì dễ hơn: thay đổi 1 bước không ảnh hưởng bước khác
CASE WHEN là cách bạn tạo business logic trong SQL — phân nhóm khách hàng, gán label, tạo bucket — thay vì phải làm bên ngoài (Excel, Python).
Áp dụng thực tế
sql
-- CTE kết hợp Window Function + CASE WHEN
-- Bài toán: Phân tích RFM (Recency, Frequency, Monetary) bằng SQL
WITH customer_metrics AS (
SELECT
customer_id,
MAX(order_date) AS last_order_date,
COUNT(order_id) AS frequency,
SUM(total_amount) AS monetary
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
),
rfm_scores AS (
SELECT
customer_id,
NTILE(5) OVER (ORDER BY last_order_date ASC) AS r_score, -- 5 = gần nhất
NTILE(5) OVER (ORDER BY frequency ASC) AS f_score, -- 5 = mua nhiều nhất
NTILE(5) OVER (ORDER BY monetary ASC) AS m_score -- 5 = chi nhiều nhất
FROM customer_metrics
)
SELECT
customer_id,
r_score, f_score, m_score,
CASE
WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN 'Champions'
WHEN r_score >= 4 AND f_score >= 3 THEN 'Loyal'
WHEN r_score >= 4 THEN 'Recent'
WHEN r_score <= 2 AND f_score >= 3 THEN 'At Risk'
WHEN r_score <= 2 AND f_score <= 2 THEN 'Lost'
ELSE 'Other'
END AS rfm_segment
FROM rfm_scores
ORDER BY r_score DESC, f_score DESC, m_score DESC;📊 Framework tổng hợp
Window Function Syntax Map
┌─────────────────────────────────────────────────────────────────────┐
│ WINDOW FUNCTION SYNTAX │
│ │
│ function(expr) OVER ( │
│ ┌─ PARTITION BY col ──── Chia nhóm (như GROUP BY nhưng giữ │
│ │ nguyên dòng) │
│ │ │
│ ├─ ORDER BY col ──────── Sắp xếp trong mỗi nhóm │
│ │ │
│ └─ ROWS BETWEEN ─────── Khung cửa sổ (frame) │
│ ├─ UNBOUNDED PRECEDING ── từ đầu nhóm │
│ ├─ N PRECEDING ────────── N dòng trước │
│ ├─ CURRENT ROW ────────── dòng hiện tại │
│ ├─ N FOLLOWING ────────── N dòng sau │
│ └─ UNBOUNDED FOLLOWING ── đến cuối nhóm │
│ ) │
│ │
│ ┌──────────────┬──────────────┬──────────────────┐ │
│ │ RANKING │ OFFSET │ AGGREGATE │ │
│ ├──────────────┼──────────────┼──────────────────┤ │
│ │ ROW_NUMBER() │ LAG(col, n) │ SUM(col) OVER │ │
│ │ RANK() │ LEAD(col, n) │ AVG(col) OVER │ │
│ │ DENSE_RANK() │ FIRST_VALUE()│ COUNT(col) OVER │ │
│ │ NTILE(n) │ LAST_VALUE() │ MIN/MAX OVER │ │
│ └──────────────┴──────────────┴──────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘Star Schema — Mô hình Data Warehouse
┌──────────────┐
│ dim_date │
│ (Khi nào?) │
└──────┬───────┘
│
┌──────────────┐ ┌──────┴───────┐ ┌──────────────┐
│ dim_product │ │ FACT TABLE │ │ dim_customer │
│ (Cái gì?) │────│ (Sự kiện + │────│ (Ai?) │
│ │ │ Metrics) │ │ │
└──────────────┘ └──────┬───────┘ └──────────────┘
│
┌──────┴───────┐
│ dim_store │
│ (Ở đâu?) │
└──────────────┘
Fact table ở giữa, các Dimension tables xung quanh → hình ngôi sao ⭐🛠️ Demo — 10+ SQL Queries thực hành
Demo 1: ROW_NUMBER — Top 3 sản phẩm mỗi category
sql
WITH ranked AS (
SELECT
category,
product_name,
revenue,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY revenue DESC
) AS rn
FROM product_sales
)
SELECT category, product_name, revenue
FROM ranked
WHERE rn <= 3
ORDER BY category, rn;Demo 2: RANK vs DENSE_RANK — Xếp hạng có trùng
sql
SELECT
employee_name,
department,
sales_amount,
RANK() OVER (ORDER BY sales_amount DESC) AS rank_pos,
DENSE_RANK() OVER (ORDER BY sales_amount DESC) AS dense_rank_pos
FROM employee_sales
WHERE quarter = 'Q1-2026';Demo 3: LAG — MoM Revenue Growth
sql
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(total_amount) AS revenue,
LAG(SUM(total_amount)) OVER (
ORDER BY DATE_TRUNC('month', order_date)
) AS prev_month_rev,
ROUND(
(SUM(total_amount) - LAG(SUM(total_amount)) OVER (
ORDER BY DATE_TRUNC('month', order_date)
)) * 100.0 / NULLIF(LAG(SUM(total_amount)) OVER (
ORDER BY DATE_TRUNC('month', order_date)
), 0), 1
) AS mom_growth_pct
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;Demo 4: Running Total — Doanh thu tích lũy theo ngày
sql
SELECT
order_date,
SUM(total_amount) AS daily_revenue,
SUM(SUM(total_amount)) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_revenue
FROM orders
WHERE order_date BETWEEN '2026-01-01' AND '2026-01-31'
AND status = 'completed'
GROUP BY order_date
ORDER BY order_date;Demo 5: Moving Average 7 ngày — DAU trend
sql
SELECT
log_date,
dau,
ROUND(AVG(dau) OVER (
ORDER BY log_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 0) AS ma_7d,
ROUND(AVG(dau) OVER (
ORDER BY log_date
ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
), 0) AS ma_30d
FROM app_daily_metrics
WHERE log_date >= '2026-01-01'
ORDER BY log_date;Demo 6: Percent of Total — Tỷ trọng doanh thu theo region
sql
SELECT
region,
store_name,
revenue,
SUM(revenue) OVER () AS grand_total,
ROUND(revenue * 100.0 / SUM(revenue) OVER (), 1) AS pct_of_grand,
SUM(revenue) OVER (PARTITION BY region) AS region_total,
ROUND(revenue * 100.0 / SUM(revenue) OVER (PARTITION BY region), 1) AS pct_of_region
FROM store_revenue
ORDER BY region, revenue DESC;Demo 7: CTE + CASE WHEN — Phân nhóm khách hàng theo giá trị
sql
WITH customer_value AS (
SELECT
c.customer_id,
c.customer_name,
c.city,
COUNT(o.order_id) AS total_orders,
SUM(o.total_amount) AS total_spent,
MAX(o.order_date) AS last_order
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
AND o.status = 'completed'
GROUP BY c.customer_id, c.customer_name, c.city
)
SELECT
customer_name,
city,
total_orders,
total_spent,
last_order,
CASE
WHEN total_spent >= 20000000 THEN 'Platinum'
WHEN total_spent >= 10000000 THEN 'Gold'
WHEN total_spent >= 3000000 THEN 'Silver'
WHEN total_spent > 0 THEN 'Bronze'
ELSE 'Inactive'
END AS tier,
CASE
WHEN last_order >= CURRENT_DATE - INTERVAL '30 days' THEN 'Active'
WHEN last_order >= CURRENT_DATE - INTERVAL '90 days' THEN 'Warm'
ELSE 'Cold'
END AS engagement
FROM customer_value
ORDER BY total_spent DESC;Demo 8: LEAD — Dự báo gap giữa 2 đơn hàng liên tiếp
sql
SELECT
customer_id,
order_date,
LEAD(order_date) OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS next_order_date,
LEAD(order_date) OVER (
PARTITION BY customer_id
ORDER BY order_date
) - order_date AS days_between_orders
FROM orders
WHERE status = 'completed'
ORDER BY customer_id, order_date;Demo 9: FIRST_VALUE / LAST_VALUE — Đơn hàng đầu tiên và cuối cùng
sql
SELECT
customer_id,
order_date,
total_amount,
FIRST_VALUE(total_amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS first_order_amount,
LAST_VALUE(total_amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_order_amount
FROM orders
WHERE status = 'completed';Demo 10: Nested CTE + Window — Phân tích Cohort Retention
sql
WITH first_purchase AS (
SELECT
customer_id,
DATE_TRUNC('month', MIN(order_date)) AS cohort_month
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
),
monthly_activity AS (
SELECT
o.customer_id,
fp.cohort_month,
DATE_TRUNC('month', o.order_date) AS activity_month,
(EXTRACT(YEAR FROM DATE_TRUNC('month', o.order_date)) -
EXTRACT(YEAR FROM fp.cohort_month)) * 12 +
(EXTRACT(MONTH FROM DATE_TRUNC('month', o.order_date)) -
EXTRACT(MONTH FROM fp.cohort_month)) AS month_offset
FROM orders o
JOIN first_purchase fp ON o.customer_id = fp.customer_id
WHERE o.status = 'completed'
),
cohort_size AS (
SELECT cohort_month, COUNT(DISTINCT customer_id) AS num_customers
FROM first_purchase
GROUP BY cohort_month
)
SELECT
ma.cohort_month,
ma.month_offset,
COUNT(DISTINCT ma.customer_id) AS retained_customers,
cs.num_customers AS cohort_size,
ROUND(
COUNT(DISTINCT ma.customer_id) * 100.0 / cs.num_customers, 1
) AS retention_pct
FROM monthly_activity ma
JOIN cohort_size cs ON ma.cohort_month = cs.cohort_month
GROUP BY ma.cohort_month, ma.month_offset, cs.num_customers
ORDER BY ma.cohort_month, ma.month_offset;Demo 11: UNION + CASE WHEN — Gộp nguồn revenue
sql
WITH all_revenue AS (
SELECT 'Online' AS channel, order_date, total_amount
FROM online_orders
WHERE status = 'completed'
UNION ALL
SELECT 'Offline' AS channel, sale_date AS order_date, amount AS total_amount
FROM pos_transactions
WHERE status = 'completed'
)
SELECT
DATE_TRUNC('month', order_date) AS month,
channel,
SUM(total_amount) AS revenue,
SUM(SUM(total_amount)) OVER (
PARTITION BY channel
ORDER BY DATE_TRUNC('month', order_date)
) AS cumulative_revenue
FROM all_revenue
GROUP BY DATE_TRUNC('month', order_date), channel
ORDER BY month, channel;🏪 Ví dụ thực tế từ doanh nghiệp
🌍 Global: Spotify — Window Functions cho Music Analytics
Spotify — nền tảng streaming nhạc với 600M+ users — sử dụng SQL window functions hàng ngày để phân tích hành vi nghe nhạc, xếp hạng nghệ sĩ, và đo lường engagement.
Bài toán phân tích tại Spotify:
| Bài toán | Window Function dùng | Mô tả |
|---|---|---|
| Top 50 bài hát theo quốc gia | ROW_NUMBER() PARTITION BY country | Xếp hạng bài hát theo lượt stream trong mỗi thị trường |
| Trending songs | LAG(streams) + % change | So sánh lượt stream tuần này vs tuần trước — tăng mạnh = trending |
| Cumulative streams | SUM(streams) OVER (ORDER BY date) | Running total lượt nghe từ ngày phát hành — biểu đồ tích lũy |
| Artist ranking evolution | RANK() OVER (ORDER BY monthly_streams) theo tháng | Theo dõi thứ hạng nghệ sĩ thay đổi qua thời gian |
| Listening session gap | LEAD(session_start) - session_end | Khoảng thời gian giữa 2 lần nghe — đo engagement |
Ví dụ SQL tại Spotify (đơn giản hóa):
sql
-- Top 5 bài hát trending mỗi quốc gia (tuần này vs tuần trước)
WITH weekly_streams AS (
SELECT
country,
track_name,
artist_name,
SUM(CASE WHEN stream_date BETWEEN '2026-02-09' AND '2026-02-15'
THEN 1 ELSE 0 END) AS this_week,
SUM(CASE WHEN stream_date BETWEEN '2026-02-02' AND '2026-02-08'
THEN 1 ELSE 0 END) AS last_week
FROM streams
GROUP BY country, track_name, artist_name
),
trending AS (
SELECT
country,
track_name,
artist_name,
this_week,
last_week,
ROUND((this_week - last_week) * 100.0 / NULLIF(last_week, 0), 1) AS wow_growth,
ROW_NUMBER() OVER (
PARTITION BY country
ORDER BY (this_week - last_week) * 100.0 / NULLIF(last_week, 0) DESC
) AS trend_rank
FROM weekly_streams
WHERE last_week >= 1000 -- Chỉ bài đã có lượng nghe đáng kể
)
SELECT country, track_name, artist_name, this_week, last_week, wow_growth
FROM trending
WHERE trend_rank <= 5
ORDER BY country, trend_rank;Data Warehouse tại Spotify:
- Spotify dùng Google BigQuery + internal data platform xây trên Star Schema
- Fact table:
fact_streams(hàng tỷ dòng/ngày) — mỗi dòng = 1 lượt nghe - Dimension tables:
dim_track,dim_artist,dim_user,dim_date,dim_country - ETL pipeline xử lý 100+ TB data mỗi ngày — từ app event logs → warehouse → dashboard
🇻🇳 Việt Nam: VNPay — Window Functions cho phân tích giao dịch thanh toán
VNPay — nền tảng thanh toán số hàng đầu Việt Nam với 40M+ users — sử dụng SQL nâng cao để phân tích giao dịch, phát hiện gian lận, và theo dõi tăng trưởng merchant.
Database schema tại VNPay (đơn giản hóa — Star Schema):
| Bảng | Loại | Mô tả | Dòng (ước tính) |
|---|---|---|---|
fact_transactions | Fact | Mỗi giao dịch thanh toán | 500M+/tháng |
dim_merchant | Dimension | Cửa hàng, đối tác chấp nhận VNPay | 500K+ |
dim_customer | Dimension | Người dùng VNPay | 40M+ |
dim_date | Dimension | Ngày, tuần, tháng, quý, năm | 10K+ dòng |
dim_payment_method | Dimension | QR, thẻ, ví điện tử, chuyển khoản | 20+ phương thức |
dim_category | Dimension | Ngành hàng: F&B, retail, utility... | 50+ danh mục |
Bài toán phân tích tại VNPay:
| Bài toán | SQL Approach |
|---|---|
| GMV running total theo tháng | SUM(amount) OVER (ORDER BY date) — theo dõi mục tiêu quý |
| Top 10 merchant mỗi ngành | ROW_NUMBER() PARTITION BY category ORDER BY gmv DESC |
| MoM transaction growth | LAG(txn_count) OVER (ORDER BY month) → growth % |
| Moving avg giá trị giao dịch 7 ngày | AVG(avg_txn_value) OVER (ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) |
| Tỷ trọng phương thức thanh toán | SUM(amount) OVER (PARTITION BY method) / SUM(amount) OVER () |
| Phát hiện giao dịch bất thường | LAG + LEAD để tìm spike đột ngột trong chuỗi giao dịch |
Ví dụ SQL tại VNPay (đơn giản hóa):
sql
-- Running total GMV theo ngày trong tháng + Moving Average 7 ngày
WITH daily_gmv AS (
SELECT
dd.full_date,
dc.category_name,
SUM(ft.amount) AS daily_amount,
COUNT(ft.txn_id) AS daily_txn_count
FROM fact_transactions ft
JOIN dim_date dd ON ft.date_key = dd.date_key
JOIN dim_category dc ON ft.category_key = dc.category_key
WHERE dd.year = 2026 AND dd.month = 2
AND ft.status = 'success'
GROUP BY dd.full_date, dc.category_name
)
SELECT
full_date,
category_name,
daily_amount,
daily_txn_count,
SUM(daily_amount) OVER (
PARTITION BY category_name
ORDER BY full_date
) AS cumulative_gmv,
ROUND(AVG(daily_amount) OVER (
PARTITION BY category_name
ORDER BY full_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 0) AS gmv_ma7d,
ROUND(
daily_amount * 100.0 / SUM(daily_amount) OVER (
PARTITION BY full_date
), 1
) AS pct_of_daily_total
FROM daily_gmv
ORDER BY full_date, category_name;Bài học cho DA:
- VNPay xử lý 500M+ giao dịch/tháng — cùng cú pháp Window Function bạn đang học, nhưng chạy trên Data Warehouse (BigQuery/Redshift)
- Star Schema tại VNPay: fact_transactions ở giữa, JOIN với dim_merchant, dim_customer, dim_date, dim_payment_method
- CTE giúp query phân tích phức tạp dễ đọc, dễ review — team DA 20+ người cùng maintain codebase SQL
- Window Functions thay thế hoàn toàn việc "xuất Excel → tính running total bằng SUM ô" — nhanh hơn, chính xác hơn, tái sử dụng được
✅ Checklist buổi học
Sau buổi học này, bạn nên tự tin trả lời "Có" cho tất cả các câu hỏi dưới đây:
- [ ] Tôi phân biệt được Window Function vs GROUP BY: window giữ nguyên dòng, GROUP BY gom dòng
- [ ] Tôi viết được
OVER(),PARTITION BY,ORDER BYtrong window function - [ ] Tôi phân biệt và sử dụng được
ROW_NUMBER(),RANK(),DENSE_RANK() - [ ] Tôi dùng được
NTILE(n)để chia dữ liệu thành n nhóm bằng nhau - [ ] Tôi viết được
LAG()vàLEAD()để so sánh dòng hiện tại vs dòng trước/sau - [ ] Tôi tính được running total bằng
SUM() OVER (ORDER BY ... ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - [ ] Tôi tính được moving average 7 ngày bằng
AVG() OVER (ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) - [ ] Tôi tính được percent of total bằng
value / SUM(value) OVER () - [ ] Tôi tính được MoM/YoY growth bằng
LAG()kết hợp phép tính % - [ ] Tôi giải thích được OLTP vs OLAP và tại sao cần Data Warehouse
- [ ] Tôi hiểu Star Schema: fact table (metrics) ở giữa, dimension tables (context) xung quanh
- [ ] Tôi phân biệt được fact table vs dimension table và cho ví dụ thực tế
- [ ] Tôi giải thích được ETL, Data Mart, Data Lake là gì
- [ ] Tôi viết được CTE (
WITH ... AS) để chia query phức tạp thành bước rõ ràng - [ ] Tôi viết được nested CTE (nhiều WITH nối tiếp)
- [ ] Tôi sử dụng được
CASE WHENđể tạo logic phân nhóm trong SQL - [ ] Tôi phân biệt được
UNION,UNION ALL,INTERSECT,EXCEPT
🔑 Từ khóa quan trọng
| Tiếng Việt | English | Giải thích |
|---|---|---|
| Hàm cửa sổ | Window Function | Hàm SQL tính toán trên tập dòng liên quan mà không gom nhóm — dùng OVER() |
| Phân vùng | PARTITION BY | Chia dữ liệu thành nhóm trong window function — mỗi nhóm tính riêng |
| Xếp hạng | Ranking (ROW_NUMBER, RANK, DENSE_RANK) | Gán thứ hạng cho mỗi dòng dựa trên giá trị sắp xếp |
| Dòng trước/sau | LAG / LEAD | Truy cập giá trị dòng trước (LAG) hoặc dòng sau (LEAD) dòng hiện tại |
| Tích lũy | Running Total | Tổng cộng dồn từ đầu kỳ đến dòng hiện tại — SUM() OVER (ORDER BY) |
| Trung bình trượt | Moving Average | Trung bình N dòng/ngày gần nhất — làm mượt biến động |
| Tỷ trọng | Percent of Total | Tỷ lệ % của giá trị dòng so với tổng nhóm hoặc tổng toàn bộ |
| Tăng trưởng theo kỳ | MoM / YoY Growth | So sánh giá trị tháng này vs tháng trước (MoM) hoặc cùng kỳ năm trước (YoY) |
| Kho dữ liệu | Data Warehouse | Hệ thống lưu trữ dữ liệu tập trung, tối ưu cho phân tích — OLAP |
| Bảng sự kiện | Fact Table | Bảng chứa metrics/measures — mỗi dòng là 1 sự kiện kinh doanh |
| Bảng chiều | Dimension Table | Bảng chứa thông tin mô tả (context): ai, cái gì, ở đâu, khi nào |
| Mô hình sao | Star Schema | Kiến trúc data warehouse: fact table ở giữa, dimension tables xung quanh |
| Biểu thức bảng chung | CTE (Common Table Expression) | Bảng tạm có tên trong query — WITH ... AS (...) — giúp chia bước logic |
| Khung cửa sổ | Window Frame (ROWS BETWEEN) | Xác định phạm vi dòng cho phép tính window — PRECEDING, CURRENT ROW, FOLLOWING |
🔗 Xem thêm Buổi 6
→ 📝 Blog → 🧠 Case Study → 🏆 Tiêu chuẩn → 🛠 Workshop → 🎮 Mini Game