Appearance
🎮 Mini Game Buổi 6: SQL Challenge — Window Function Speed Run
Bạn là Senior Data Analyst tại sàn e-commerce ShopVN. CEO chuẩn bị board meeting và cần 7 phân tích nâng cao trong 1 buổi sáng. Nhiệm vụ: viết SQL với Window Functions, CTE, Star Schema query — nhanh, chính xác, và phức tạp!
🎯 Mục tiêu học tập
Sau khi hoàn thành game, bạn sẽ:
- Dùng ROW_NUMBER + PARTITION BY để xếp hạng sản phẩm trong mỗi category — Top N per group
- Sử dụng RANK / DENSE_RANK để phân biệt cách xếp hạng khi có tied values
- Áp dụng LAG/LEAD để so sánh MoM revenue growth — truy cập dòng trước/sau
- Tính Running Total bằng SUM OVER với frame clause — doanh thu tích lũy
- Tính Percent of Total bằng SUM OVER (PARTITION BY) — tỷ trọng doanh thu
- Query trên Star Schema — JOIN fact + dimension tables theo đúng Kimball pattern
- Viết CTE (Common Table Expression) — chia query phức tạp thành các bước logic rõ ràng
📜 Luật chơi
┌──────────────────────────────────────────────────────┐
│ BẠN = SQL Challenge Master 🏆 │
│ DATABASE = Hệ thống e-commerce ShopVN (nâng cao) │
│ MỖI VÒNG = 1 yêu cầu phân tích → chọn SQL đúng │
│ 3 LỰA CHỌN mỗi vòng — chỉ 1 đáp án tốt nhất │
│ THỜI GIAN = 120 giây/vòng (nhanh = bonus XP) │
│ MỤC TIÊU = Thu thập ≥ 90 XP để đạt hạng Gold 🥇 │
└──────────────────────────────────────────────────────┘Nguyên tắc cốt lõi: Từ câu hỏi kinh doanh nâng cao → SQL Window Functions / CTE / Star Schema query. Đọc yêu cầu → xác định cần hàm nào, partition nào, frame nào → chọn query.
Cách tính điểm mỗi vòng:
| Thời gian trả lời | Speed Bonus |
|---|---|
| ≤ 30 giây | +5 XP ⚡ |
| 31–60 giây | +3 XP |
| 61–90 giây | +2 XP |
| 91–120 giây | +1 XP |
| > 120 giây (hết giờ) | 0 XP, tự động chọn sai |
🎲 Cơ chế game
Database Schema — ShopVN E-commerce (nâng cao)
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────┐
│ customers │ │ orders │ │ products │
├──────────────────┤ ├──────────────────────┤ ├──────────────────┤
│ customer_id (PK) │───┐ │ order_id (PK) │ ┌───│ product_id (PK) │
│ customer_name │ └──>│ customer_id (FK) │ │ │ product_name │
│ email │ │ order_date │ │ │ category │
│ city │ │ total_amount │ │ │ price │
│ region │ │ status │ │ │ stock_quantity │
│ segment │ │ shipping_fee │ │ │ brand │
└──────────────────┘ └──────────────────────┘ │ └──────────────────┘
│
┌──────────────────────┐ │
│ order_items │ │
├──────────────────────┤ │
│ item_id (PK) │ │
│ order_id (FK) │ │
│ product_id (FK) ────┘ │
│ quantity │
│ unit_price │
│ discount │
└──────────────────────┘
Star Schema (cho Vòng 6):
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ dim_product │──>│ fact_daily_sales │<──│ dim_date │
├──────────────┤ ├──────────────────┤ ├──────────────┤
│ product_key │ │ date_key (FK) │ │ date_key │
│ product_name │ │ product_key (FK) │ │ full_date │
│ category │ │ store_key (FK) │ │ month_name │
│ brand │ │ quantity_sold │ │ quarter │
└──────────────┘ │ revenue │ │ year │
│ cost │ │ is_weekend │
┌──────────────┐ │ discount_amount │ └──────────────┘
│ dim_store │──>└──────────────────┘
├──────────────┤
│ store_key │
│ store_name │
│ city │
│ region │
└──────────────┘Chỉ số theo dõi
| Chỉ số | Icon | Mô tả | Mục tiêu |
|---|---|---|---|
| Accuracy | 🎯 | Chọn đúng SQL query giải quyết yêu cầu business | ≥ 6/7 câu đúng |
| Speed | ⏱️ | Tốc độ trả lời — speed bonus XP | Tối đa 35 bonus XP |
| Complexity Score | 📊 | Mức độ phức tạp SQL — Window Function > GROUP BY | Chọn query tối ưu nhất |
Công thức XP
XP vòng = Accuracy Points + Complexity Bonus + Speed Bonus + Random Event Modifier
XP tổng = Σ (XP vòng 1..7) + Badge BonusTổng XP tối đa lý thuyết: 100 (Accuracy) + 35 (Complexity) + 35 (Speed) + bonus events ≈ 170+ XP
📋 Kịch bản chi tiết
🏆 Vòng 1: "Top 3 sản phẩm bán chạy nhất mỗi danh mục"
Độ khó: ⭐⭐ Trung bình | Kỹ năng: ROW_NUMBER + PARTITION BY
💬 Slack từ Product Manager:
"Em ơi, anh cần top 3 sản phẩm có doanh thu cao nhất trong MỖI danh mục (Điện Tử, Thời Trang, Gia Dụng...) — tháng 1/2026. Anh muốn biết sản phẩm nào đang lead trong từng category, để allocate budget marketing cho đúng."
📊 Database context:
Bảng products có 500 sản phẩm thuộc 8 danh mục. Bảng order_items chứa chi tiết bán hàng. Cần xếp hạng trong mỗi nhóm — GROUP BY đơn thuần không đủ!
Kết quả mong đợi:
| category | product_name | revenue | rank_in_category |
|---|---|---|---|
| Điện Tử | iPhone 16 Pro | 3,200,000,000 | 1 |
| Điện Tử | Samsung Galaxy S25 | 2,500,000,000 | 2 |
| Điện Tử | MacBook Air M3 | 1,800,000,000 | 3 |
| Thời Trang | Áo polo Routine | 850,000,000 | 1 |
| Thời Trang | Giày Nike Air | 720,000,000 | 2 |
| Thời Trang | Quần jean Levi's | 680,000,000 | 3 |
| ... | ... | ... | ... |
🏆 Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Complexity |
|---|---|---|---|---|
| A | SELECT p.category, p.product_name, SUM(oi.quantity * oi.unit_price) AS revenue FROM order_items oi JOIN products p ON oi.product_id = p.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' GROUP BY p.category, p.product_name ORDER BY p.category, revenue DESC LIMIT 3 | +3 | +1~5 | +1 |
| B ✅ | WITH product_revenue AS (SELECT p.category, p.product_name, SUM(oi.quantity * oi.unit_price) AS revenue, ROW_NUMBER() OVER (PARTITION BY p.category ORDER BY SUM(oi.quantity * oi.unit_price) DESC) AS rank_in_category FROM order_items oi JOIN products p ON oi.product_id = p.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' GROUP BY p.category, p.product_name) SELECT category, product_name, revenue, rank_in_category FROM product_revenue WHERE rank_in_category <= 3 ORDER BY category, rank_in_category | +12 | +1~5 | +5 |
| C | SELECT p.category, p.product_name, SUM(oi.quantity * oi.unit_price) AS revenue, RANK() OVER (ORDER BY SUM(oi.quantity * oi.unit_price) DESC) AS rank_pos FROM order_items oi JOIN products p ON oi.product_id = p.product_id GROUP BY p.category, p.product_name HAVING rank_pos <= 3 | +2 | +1~5 | +2 |
🏆 Vòng 2: "Xếp hạng nhân viên sales — ai bán nhiều nhất mỗi vùng?"
Độ khó: ⭐⭐ Trung bình | Kỹ năng: DENSE_RANK + PARTITION BY
💬 Slack từ Sales Director:
"Em ơi, anh cần xếp hạng nhân viên bán hàng theo doanh thu Q4/2025, PHÂN THEO TỪNG VÙNG (Bắc, Trung, Nam). Nếu 2 nhân viên cùng doanh thu thì cùng hạng — nhưng hạng tiếp theo không bị bỏ qua. Anh cần để trao giải cuối năm."
📊 Database context:
Bảng orders có cột salesperson_id, kết nối với bảng employees có region. Từ khóa: cùng hạng, không bỏ qua → DENSE_RANK, phân theo vùng → PARTITION BY region.
Kết quả mong đợi:
| region | employee_name | total_sales | sales_rank |
|---|---|---|---|
| Bắc | Trần Minh Tuấn | 2,500,000,000 | 1 |
| Bắc | Nguyễn Hải Yến | 2,500,000,000 | 1 |
| Bắc | Phạm Văn Linh | 1,800,000,000 | 2 |
| Nam | Lê Thị Hương | 3,100,000,000 | 1 |
| Nam | Võ Đức Trí | 2,200,000,000 | 2 |
🏆 Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Complexity |
|---|---|---|---|---|
| A | SELECT e.region, e.employee_name, SUM(o.total_amount) AS total_sales, ROW_NUMBER() OVER (PARTITION BY e.region ORDER BY SUM(o.total_amount) DESC) AS sales_rank FROM employees e JOIN orders o ON e.employee_id = o.salesperson_id WHERE o.order_date >= '2025-10-01' AND o.order_date < '2026-01-01' GROUP BY e.region, e.employee_name | +4 | +1~5 | +3 |
| B | SELECT e.region, e.employee_name, SUM(o.total_amount) AS total_sales, RANK() OVER (PARTITION BY e.region ORDER BY SUM(o.total_amount) DESC) AS sales_rank FROM employees e JOIN orders o ON e.employee_id = o.salesperson_id WHERE o.order_date >= '2025-10-01' AND o.order_date < '2026-01-01' GROUP BY e.region, e.employee_name | +5 | +1~5 | +3 |
| C ✅ | SELECT e.region, e.employee_name, SUM(o.total_amount) AS total_sales, DENSE_RANK() OVER (PARTITION BY e.region ORDER BY SUM(o.total_amount) DESC) AS sales_rank FROM employees e JOIN orders o ON e.employee_id = o.salesperson_id WHERE o.order_date >= '2025-10-01' AND o.order_date < '2026-01-01' GROUP BY e.region, e.employee_name | +12 | +1~5 | +5 |
🏆 Vòng 3: "Doanh thu tháng này so tháng trước — tăng hay giảm?"
Độ khó: ⭐⭐⭐ Khó | Kỹ năng: LAG + tính % tăng trưởng MoM
💬 Slack từ CFO:
"Em ơi, anh cần bảng doanh thu theo tháng từ T7/2025 đến T12/2025, kèm theo doanh thu tháng trước và % tăng trưởng MoM (Month-over-Month). Anh trình board meeting chiều nay — cần biết tháng nào tăng, tháng nào giảm."
📊 Database context:
Cần tổng doanh thu theo tháng, rồi so sánh mỗi tháng với tháng trước đó bằng LAG. Dòng đầu tiên (T7/2025) sẽ có prev_month = NULL vì không có tháng trước.
Kết quả mong đợi:
| month | revenue | prev_month_revenue | mom_growth_pct |
|---|---|---|---|
| 2025-07 | 1,430,000,000 | NULL | NULL |
| 2025-08 | 1,270,000,000 | 1,430,000,000 | -11.2 |
| 2025-09 | 1,600,000,000 | 1,270,000,000 | 26.0 |
| 2025-10 | 1,810,000,000 | 1,600,000,000 | 13.1 |
| 2025-11 | 2,100,000,000 | 1,810,000,000 | 16.0 |
| 2025-12 | 2,450,000,000 | 2,100,000,000 | 16.7 |
🏆 Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Complexity |
|---|---|---|---|---|
| A | SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount) AS revenue FROM orders WHERE order_date >= '2025-07-01' AND order_date < '2026-01-01' AND status = 'completed' GROUP BY month ORDER BY month | +3 | +1~5 | +1 |
| B | SELECT m.month, m.revenue, m2.revenue AS prev_month_revenue, ROUND((m.revenue - m2.revenue) * 100.0 / m2.revenue, 1) AS mom_growth_pct FROM monthly_revenue m LEFT JOIN monthly_revenue m2 ON m.month = m2.month + INTERVAL '1 month' ORDER BY m.month | +5 | +1~5 | +2 |
| C ✅ | WITH monthly AS (SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount) AS revenue FROM orders WHERE order_date >= '2025-07-01' AND order_date < '2026-01-01' AND status = 'completed' GROUP BY DATE_TRUNC('month', order_date)) SELECT month, revenue, LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue, ROUND((revenue - LAG(revenue, 1) OVER (ORDER BY month)) * 100.0 / LAG(revenue, 1) OVER (ORDER BY month), 1) AS mom_growth_pct FROM monthly ORDER BY month | +15 | +1~5 | +5 |
🏆 Vòng 4: "Doanh thu tích lũy theo ngày — đã đạt bao nhiêu % target?"
Độ khó: ⭐⭐⭐ Khó | Kỹ năng: Running Total — SUM OVER + frame clause
💬 Slack từ CEO:
"Em ơi, target doanh thu tháng 1/2026 là 3 tỷ VNĐ. Anh cần bảng doanh thu tích lũy theo từng ngày — để xem đến ngày 15 đã đạt bao nhiêu phần trăm target. Đính kèm cả tỷ lệ % so với target nhé."
📊 Database context:
Cần: (1) doanh thu hàng ngày, (2) running total (tích lũy), (3) % so với target 3 tỷ. Running total dùng SUM() OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW).
Kết quả mong đợi:
| order_date | daily_revenue | running_total | pct_of_target |
|---|---|---|---|
| 2026-01-01 | 85,000,000 | 85,000,000 | 2.8% |
| 2026-01-02 | 120,000,000 | 205,000,000 | 6.8% |
| 2026-01-03 | 95,000,000 | 300,000,000 | 10.0% |
| ... | ... | ... | ... |
| 2026-01-15 | 110,000,000 | 1,580,000,000 | 52.7% |
| ... | ... | ... | ... |
| 2026-01-31 | 150,000,000 | 2,850,000,000 | 95.0% |
🏆 Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Complexity |
|---|---|---|---|---|
| A | SELECT order_date, SUM(total_amount) AS daily_revenue, SUM(SUM(total_amount)) OVER (ORDER BY order_date) AS running_total FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2026-02-01' GROUP BY order_date ORDER BY order_date | +5 | +1~5 | +3 |
| B ✅ | WITH daily AS (SELECT order_date, SUM(total_amount) AS daily_revenue FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2026-02-01' AND status = 'completed' GROUP BY order_date) SELECT order_date, daily_revenue, SUM(daily_revenue) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total, ROUND(SUM(daily_revenue) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) * 100.0 / 3000000000, 1) AS pct_of_target FROM daily ORDER BY order_date | +15 | +1~5 | +5 |
| C | SELECT order_date, SUM(total_amount) AS daily_revenue, SUM(total_amount) OVER () AS running_total, ROUND(SUM(total_amount) OVER () * 100.0 / 3000000000, 1) AS pct_of_target FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2026-02-01' GROUP BY order_date ORDER BY order_date | +2 | +1~5 | +1 |
🏆 Vòng 5: "Tỷ trọng doanh thu mỗi sản phẩm trong category — ai chiếm bao nhiêu %?"
Độ khó: ⭐⭐⭐ Khó | Kỹ năng: Percent of Total — SUM OVER (PARTITION BY) + Running Total kết hợp
💬 Slack từ Marketing Director:
"Em ơi, chị cần biết tỷ trọng doanh thu mỗi sản phẩm trong category của nó — sản phẩm nào chiếm bao nhiêu % doanh thu category. Kèm theo running total tích lũy % (để xem top 2–3 sản phẩm đã chiếm bao nhiêu % rồi). Chị cần cho strategic planning Q2."
📊 Database context:
Cần: (1) doanh thu mỗi sản phẩm, (2) tổng doanh thu category dùng SUM OVER (PARTITION BY category), (3) % = revenue / category_total, (4) cumulative % dùng running SUM.
Kết quả mong đợi:
| category | product_name | revenue | pct_of_category | cumulative_pct |
|---|---|---|---|---|
| Điện Tử | iPhone 16 Pro | 3,200M | 35.6% | 35.6% |
| Điện Tử | Samsung Galaxy S25 | 2,500M | 27.8% | 63.4% |
| Điện Tử | MacBook Air M3 | 1,800M | 20.0% | 83.4% |
| Điện Tử | Tai nghe Sony | 1,500M | 16.7% | 100.0% |
🏆 Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Complexity |
|---|---|---|---|---|
| A | SELECT p.category, p.product_name, SUM(oi.quantity * oi.unit_price) AS revenue, ROUND(SUM(oi.quantity * oi.unit_price) * 100.0 / (SELECT SUM(quantity * unit_price) FROM order_items), 1) AS pct_of_total FROM order_items oi JOIN products p ON oi.product_id = p.product_id GROUP BY p.category, p.product_name ORDER BY p.category, revenue DESC | +4 | +1~5 | +2 |
| B ✅ | WITH product_rev AS (SELECT p.category, p.product_name, SUM(oi.quantity * oi.unit_price) AS revenue FROM order_items oi JOIN products p ON oi.product_id = p.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' GROUP BY p.category, p.product_name) SELECT category, product_name, revenue, ROUND(revenue * 100.0 / SUM(revenue) OVER (PARTITION BY category), 1) AS pct_of_category, ROUND(SUM(revenue) OVER (PARTITION BY category ORDER BY revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) * 100.0 / SUM(revenue) OVER (PARTITION BY category), 1) AS cumulative_pct FROM product_rev ORDER BY category, revenue DESC | +15 | +1~5 | +5 |
| C | SELECT p.category, p.product_name, SUM(oi.quantity * oi.unit_price) AS revenue, ROUND(SUM(oi.quantity * oi.unit_price) * 100.0 / SUM(SUM(oi.quantity * oi.unit_price)) OVER (PARTITION BY p.category), 1) AS pct_of_category FROM order_items oi JOIN products p ON oi.product_id = p.product_id GROUP BY p.category, p.product_name ORDER BY p.category, revenue DESC | +6 | +1~5 | +3 |
🏆 Vòng 6: "Query trên Star Schema — doanh thu theo quý, vùng miền, category"
Độ khó: ⭐⭐⭐⭐ Rất khó | Kỹ năng: Star Schema query — JOIN fact + dimension tables
💬 Slack từ BI Lead:
"Em ơi, Data Engineer vừa deploy star schema mới trên BigQuery. Anh cần em viết query trên star schema: doanh thu theo quý, theo vùng miền, theo category — năm 2025. Query phải JOIN fact table với 3 dimension tables. Đây là test đầu tiên trên DW mới."
📊 Database context:
Star schema mới: fact_daily_sales (FK: date_key, product_key, store_key) → JOIN với dim_date, dim_product, dim_store. Cần dùng đúng surrogate keys, lọc trên dimension.
Kết quả mong đợi:
| year | quarter | region | category | total_revenue | total_items |
|---|---|---|---|---|---|
| 2025 | Q1 | Bắc | Điện Tử | 5,200M | 12,500 |
| 2025 | Q1 | Bắc | Thời Trang | 2,100M | 8,200 |
| 2025 | Q1 | Nam | Điện Tử | 4,800M | 11,000 |
| ... | ... | ... | ... | ... | ... |
🏆 Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Complexity |
|---|---|---|---|---|
| A | SELECT d.year, d.quarter, s.region, p.category, SUM(f.revenue) AS total_revenue, SUM(f.quantity_sold) AS total_items FROM fact_daily_sales f, dim_date d, dim_product p, dim_store s WHERE f.date_key = d.date_key AND f.product_key = p.product_key AND f.store_key = s.store_key AND d.year = 2025 GROUP BY d.year, d.quarter, s.region, p.category ORDER BY d.quarter, total_revenue DESC | +5 | +1~5 | +2 |
| B ✅ | SELECT d.year, CONCAT('Q', d.quarter) AS quarter, s.region, p.category, SUM(f.revenue) AS total_revenue, SUM(f.quantity_sold) AS total_items, ROUND(AVG(f.revenue), 0) AS avg_daily_revenue FROM fact_daily_sales f JOIN dim_date d ON f.date_key = d.date_key JOIN dim_product p ON f.product_key = p.product_key JOIN dim_store s ON f.store_key = s.store_key WHERE d.year = 2025 GROUP BY d.year, d.quarter, s.region, p.category ORDER BY d.quarter, total_revenue DESC | +15 | +1~5 | +5 |
| C | SELECT EXTRACT(QUARTER FROM order_date) AS quarter, region, category, SUM(revenue) AS total_revenue FROM sales WHERE EXTRACT(YEAR FROM order_date) = 2025 GROUP BY quarter, region, category ORDER BY quarter, total_revenue DESC | +2 | +1~5 | +1 |
🏆 Vòng 7: "Khách hàng nào có doanh thu tăng liên tục 3 tháng — ai là 'rising star'?"
Độ khó: ⭐⭐⭐⭐ Rất khó — Boss Round 🏴☠️ | Kỹ năng: CTE + LAG + logic phức tạp
💬 Slack từ CEO (tối Chủ Nhật 😱):
"Em ơi, anh muốn biết khách hàng nào có doanh thu tăng liên tục 3 tháng gần nhất (T10, T11, T12/2025). Đây là 'rising star' — khách đang mua ngày càng nhiều. Anh cần tên khách, doanh thu 3 tháng, và tỷ lệ tăng trưởng. Query phải dùng CTE cho dễ đọc — anh muốn team DE review được."
📊 Database context:
Bài toán nhiều bước:
- Tính doanh thu mỗi khách mỗi tháng (CTE 1)
- Dùng LAG lấy doanh thu tháng trước (CTE 2)
- Kiểm tra 3 tháng liên tiếp đều tăng (WHERE)
Kết quả mong đợi:
| customer_name | oct_revenue | nov_revenue | dec_revenue | growth_oct_nov | growth_nov_dec |
|---|---|---|---|---|---|
| Nguyễn Văn An | 5,200,000 | 6,800,000 | 8,500,000 | +30.8% | +25.0% |
| Lê Thị Hương | 3,100,000 | 3,500,000 | 4,200,000 | +12.9% | +20.0% |
| Trần Minh Tuấn | 2,800,000 | 3,000,000 | 3,800,000 | +7.1% | +26.7% |
🏆 Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Complexity |
|---|---|---|---|---|
| A | SELECT c.customer_name, SUM(CASE WHEN EXTRACT(MONTH FROM o.order_date) = 10 THEN o.total_amount END) AS oct_revenue, SUM(CASE WHEN EXTRACT(MONTH FROM o.order_date) = 11 THEN o.total_amount END) AS nov_revenue, SUM(CASE WHEN EXTRACT(MONTH FROM o.order_date) = 12 THEN o.total_amount END) AS dec_revenue FROM customers c JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= '2025-10-01' AND o.order_date < '2026-01-01' GROUP BY c.customer_name HAVING oct_revenue < nov_revenue AND nov_revenue < dec_revenue | +6 | +1~5 | +3 |
| B ✅ | WITH monthly_customer AS (SELECT c.customer_id, c.customer_name, DATE_TRUNC('month', o.order_date) AS month, SUM(o.total_amount) AS monthly_revenue FROM customers c JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= '2025-10-01' AND o.order_date < '2026-01-01' AND o.status = 'completed' GROUP BY c.customer_id, c.customer_name, DATE_TRUNC('month', o.order_date)), with_lag AS (SELECT *, LAG(monthly_revenue, 1) OVER (PARTITION BY customer_id ORDER BY month) AS prev_month_revenue, ROUND((monthly_revenue - LAG(monthly_revenue, 1) OVER (PARTITION BY customer_id ORDER BY month)) * 100.0 / LAG(monthly_revenue, 1) OVER (PARTITION BY customer_id ORDER BY month), 1) AS mom_growth_pct FROM monthly_customer) SELECT customer_name, month, monthly_revenue, prev_month_revenue, mom_growth_pct FROM with_lag WHERE mom_growth_pct > 0 GROUP BY customer_id HAVING COUNT(*) = 2 ORDER BY customer_name | +15 | +1~5 | +5 |
| C | SELECT c.customer_name, SUM(o.total_amount) AS total_q4 FROM customers c JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= '2025-10-01' AND o.order_date < '2026-01-01' GROUP BY c.customer_name ORDER BY total_q4 DESC | +2 | +1~5 | +1 |
⚡ Sự kiện ngẫu nhiên (Random Events)
Mỗi vòng có 20% xác suất kích hoạt 1 sự kiện ngẫu nhiên. Sự kiện mô phỏng áp lực thực tế khi viết SQL nâng cao trong doanh nghiệp!
| # | Sự kiện | Xác suất | Ảnh hưởng |
|---|---|---|---|
| 1 | 🔥 Board meeting trong 5 phút — CEO gọi: "Query xong chưa? Anh cần slide ngay!" | 20% | Thời gian vòng này giảm còn 60 giây. Áp lực tăng! |
| 2 | 💡 Data Engineer hint — "Bảng đó có window function index, dùng PARTITION BY sẽ nhanh hơn" | 15% | Loại bỏ 1 đáp án sai → còn 2 lựa chọn. Complexity +2 bonus |
| 3 | 📋 Execution plan reveal — Hệ thống hiển thị explain plan cho 2 query | 10% | Thấy query nào nhanh hơn → dễ chọn hơn. +3 Accuracy bonus nếu đúng |
| 4 | 💥 BigQuery quota exhausted — DW đang chạy batch job hàng đêm, query bị throttle! | 20% | Thời gian giảm còn 45 giây, nhưng nếu đúng: Speed ×2. Bài học: query tối ưu = ít resource hơn |
| 5 | 📊 Star schema unlocked — DE deploy thêm dim_promotion table | 10% | Câu hỏi bonus +5 XP nếu đáp án JOIN chuẩn Kimball |
| 6 | ☕ Trà đá office boost — Đồng nghiệp mua trà đá, cả team chill 5 phút! | 15% | +3 XP bonus cho câu tiếp theo bất kể đúng sai |
🏆 Hệ thống xếp hạng
| Rank | Điều kiện | Mô tả |
|---|---|---|
| 🥇 Gold | ≥ 90 XP | "Window Function Master! Bạn viết SQL nâng cao như thở — ROW_NUMBER, LAG, Running Total, Star Schema đều pro. Sẵn sàng làm Senior DA." |
| 🥈 Silver | ≥ 65 XP | "Solid Analyst — Window Functions cơ bản ổn nhưng cần luyện thêm CTE phức tạp và Star Schema query. Ôn lại Phần 2–3!" |
| 🥉 Bronze | ≥ 40 XP | "Junior Analyst — hiểu concept nhưng cần practice thêm. DataLemur và LeetCode sẽ giúp bạn!" |
| ❌ Fail | < 40 XP | "Window Function chưa mở được... Đọc lại Buổi 6 rồi quay lại nhé!" |
🎖️ Badge đặc biệt
| Badge | Điều kiện | Mô tả |
|---|---|---|
| 🏅 Perfect Window | 7/7 câu đúng (chọn đáp án tốt nhất) | Mọi window function đều chính xác — zero bugs |
| ⚡ Speed Demon | Tổng thời gian ≤ 5 phút (cả 7 vòng) | Viết SQL nâng cao nhanh hơn co-pilot |
| 🔢 Ranking Pro | Đúng Vòng 1 + Vòng 2 (ROW_NUMBER + DENSE_RANK) | Phân biệt ranking functions như chuyên gia |
| 📈 Trend Analyst | Đúng Vòng 3 + Vòng 4 (LAG + Running Total) | MoM growth, cumulative revenue — bảng số nào cũng handle |
| 📊 Portfolio Master | Đúng Vòng 5 (Percent of Total + cumulative %) | Phân tích cơ cấu — biết ai chiếm bao nhiêu % |
| ⭐ Star Schema Navigator | Đúng Vòng 6 (Star Schema query) | Query trên DW như Senior DE |
| 🧠 CTE Architect | Đúng Vòng 7 (CTE boss round) | Viết CTE nhiều tầng — code review-ready |
| 🛡️ Pressure Proof | Gặp ≥ 3 sự kiện ngẫu nhiên bất lợi mà vẫn đạt Gold | Bình tĩnh dưới áp lực — tố chất Lead DA |
| 💎 Flawless Diamond | ≥ 90 XP + không bị event nào trừ điểm | Hoàn hảo trong mọi tình huống |
💡 Giải thích đáp án
Vòng 1 — ROW_NUMBER + PARTITION BY → Top N per group
Yêu cầu: Top 3 sản phẩm bán chạy nhất trong MỖI category → ROW_NUMBER() OVER (PARTITION BY category) + CTE + WHERE rank <= 3.
- Đáp án B dùng CTE +
ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC)— chính xác nhất:- CTE
product_revenue: tính doanh thu mỗi SP, gánrank_in_categorybằng ROW_NUMBER - Query ngoài: lọc
WHERE rank_in_category <= 3→ top 3 mỗi category PARTITION BY category→ xếp hạng độc lập trong mỗi danh mục
- CTE
- Đáp án A dùng
LIMIT 3→ chỉ lấy top 3 tổng cộng, không phải top 3 mỗi category. LIMIT áp dụng cho toàn bộ result set, không theo nhóm - Đáp án C dùng
RANK() OVER (ORDER BY ...)không có PARTITION BY → xếp hạng toàn cục. Thêm vào đó,HAVING rank_pos <= 3không hợp lệ — window function alias không dùng được trong HAVING - Bài học: Top N per group là bài toán kinh điển — luôn cần
PARTITION BY+ CTE (hoặc subquery) để lọc - Kiến thức: ROW_NUMBER, PARTITION BY — Phần 1: Window Functions
Vòng 2 — DENSE_RANK vs RANK vs ROW_NUMBER
Yêu cầu: Xếp hạng nhân viên sales — cùng hạng, không bỏ qua → DENSE_RANK.
- Đáp án C dùng
DENSE_RANK()— khi 2 người cùng doanh thu → cùng hạng (ví dụ: hạng 1, 1), người tiếp theo → hạng 2 (không bỏ qua) - Đáp án A dùng
ROW_NUMBER()— mỗi dòng 1 số duy nhất, kể cả khi cùng doanh thu → 2 người 2.5 tỷ sẽ nhận hạng 1 và 2 ngẫu nhiên. Không phù hợp yêu cầu "cùng hạng" - Đáp án B dùng
RANK()— cùng hạng nhưng bỏ qua: 2 người hạng 1 → người tiếp theo hạng 3 (bỏ qua hạng 2). Yêu cầu nói "không bỏ qua" - Quy tắc nhớ:
ROW_NUMBER: 1, 2, 3, 4 (luôn unique)RANK: 1, 1, 3, 4 (tied → skip)DENSE_RANK: 1, 1, 2, 3 (tied → no skip)
- Kiến thức: Ranking functions — Phần 1: Window Functions
Vòng 3 — LAG + MoM Growth
Yêu cầu: Doanh thu theo tháng + doanh thu tháng trước + % tăng trưởng → LAG + CTE.
- Đáp án C đầy đủ nhất:
- CTE
monthly: tổng doanh thu theo tháng (GROUP BY DATE_TRUNC) LAG(revenue, 1) OVER (ORDER BY month)— lấy doanh thu tháng trước- Tính
mom_growth_pct= (current - previous) / previous × 100 - Lọc chỉ
completedorders — đúng business logic
- CTE
- Đáp án A chỉ tính doanh thu theo tháng — thiếu LAG, thiếu MoM growth, chưa trả lời câu hỏi
- Đáp án B dùng self-join (
LEFT JOIN ... ON month + INTERVAL) — chạy đúng nhưng phức tạp hơn LAG, và giả sử đã có bảngmonthly_revenuesẵn (không thực tế) - Tại sao LAG tốt hơn self-join? LAG chạy trong 1 pass, self-join cần 2 lần scan bảng. Trên bảng lớn, LAG nhanh hơn đáng kể
- Kiến thức: LAG, LEAD — Phần 1: Window Functions
Vòng 4 — Running Total + frame clause
Yêu cầu: Doanh thu tích lũy theo ngày + % so target → SUM OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW).
- Đáp án B dùng CTE + explicit frame clause:
- CTE
daily: tổng doanh thu hàng ngày, lọc completed SUM(daily_revenue) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)— running total chính xác- Tính
pct_of_target= running_total / 3,000,000,000 × 100
- CTE
- Đáp án A dùng
SUM(SUM(...)) OVER (ORDER BY ...)— syntax hợp lệ (aggregate + window) nhưng thiếu explicit frame clause và thiếu % target - Đáp án C dùng
SUM(...) OVER ()không có ORDER BY → tổng toàn bộ (grand total), KHÔNG phải running total. Đây là lỗi phổ biến nhất! - Bài học:
SUM() OVER ()= grand total.SUM() OVER (ORDER BY date)= running total (default frame). ExplicitROWS BETWEENlà best practice - Kiến thức: Running Total — Phần 2: Phân tích nâng cao
Vòng 5 — Percent of Total + cumulative %
Yêu cầu: Tỷ trọng % mỗi SP trong category + cumulative % → SUM OVER (PARTITION BY) + SUM OVER (PARTITION BY + ORDER BY).
- Đáp án B dùng CTE + 2 window functions khác nhau:
SUM(revenue) OVER (PARTITION BY category)→ tổng category (để tính %)SUM(revenue) OVER (PARTITION BY category ORDER BY revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)→ cumulative sum (để tính cumulative %)- Chia cho category total → pct_of_category và cumulative_pct
- Đáp án A dùng subquery
(SELECT SUM(...) FROM order_items)→ tính % so tổng toàn bộ, không phải tổng category. Và thiếu cumulative % - Đáp án C không tính cumulative %, chỉ tính pct_of_category — chưa đầy đủ yêu cầu
- Bài học: PARTITION BY không có ORDER BY → grand total per group. PARTITION BY có ORDER BY → cumulative (running) per group
- Kiến thức: Percent of Total — Phần 2: Phân tích nâng cao
Vòng 6 — Star Schema query
Yêu cầu: Query doanh thu theo quý + vùng + category trên star schema → JOIN fact + 3 dim tables.
- Đáp án B dùng explicit
JOIN ... ONsyntax (ANSI-92) + lọc trên dimension (WHERE d.year = 2025) + thêmavg_daily_revenue:FROM fact_daily_sales f— bắt đầu từ fact tableJOIN dim_date d ON f.date_key = d.date_key— join bằng surrogate keyJOIN dim_product p ON f.product_key = p.product_keyJOIN dim_store s ON f.store_key = s.store_key- GROUP BY dimension attributes, SUM fact measures
- Đáp án A dùng comma-separated join (ANSI-89) — chạy đúng nhưng anti-pattern trên star schema. Khó phân biệt JOIN condition vs filter condition
- Đáp án C query trực tiếp bảng
sales(không phải star schema) — dùngEXTRACT(YEAR FROM order_date)thay vì JOIN dim_date. Không đúng yêu cầu "query trên star schema" - Best practice Kimball: Luôn FROM fact → JOIN dim, lọc trên dim (không EXTRACT trên fact column), aggregate trên fact measures
- Kiến thức: Star Schema — Phần 3: Data Warehouse Basics
Vòng 7 — CTE multi-step + LAG + logic phức tạp
Yêu cầu: Khách hàng có doanh thu tăng liên tục 3 tháng → CTE chained + LAG + filter.
- Đáp án B dùng multi-step CTE:
- CTE
monthly_customer: tính doanh thu mỗi khách mỗi tháng - CTE
with_lag: dùngLAG(monthly_revenue) OVER (PARTITION BY customer_id ORDER BY month)→ lấy revenue tháng trước - Tính
mom_growth_pct— % tăng trưởng - Filter:
WHERE mom_growth_pct > 0→ chỉ tháng tăng,HAVING COUNT(*) = 2→ 2 lần tăng liên tiếp (T10→T11, T11→T12)
- CTE
- Đáp án A dùng CASE WHEN pivot — chạy đúng logic nhưng không dùng window function, hardcode tháng, và
HAVING oct_revenue < nov_revenuedùng alias trong HAVING (không phải standard SQL ở mọi hệ thống) - Đáp án C chỉ tính tổng Q4 — hoàn toàn không trả lời "tăng liên tục" vì không so sánh từng tháng
- Bài học: Multi-step CTE giúp chia bài toán phức tạp thành từng bước rõ ràng — giống viết hàm trong programming. Dễ đọc, dễ debug, dễ review
- Kiến thức: CTE — Phần 4: CTE
📚 Kiến thức liên quan
| Vòng | Chủ đề chính | Kỹ năng SQL |
|---|---|---|
| 1 | ROW_NUMBER + PARTITION BY | Top N per group — CTE + window + filter |
| 2 | DENSE_RANK vs RANK vs ROW_NUMBER | Phân biệt ranking functions cho tied values |
| 3 | LAG + MoM Growth | Truy cập dòng trước, tính % tăng trưởng |
| 4 | Running Total (SUM OVER) | Frame clause, ROWS BETWEEN, cumulative sum |
| 5 | Percent of Total + Cumulative % | SUM OVER PARTITION BY, running % |
| 6 | Star Schema Query | Fact + dimension JOIN, Kimball pattern |
| 7 | CTE multi-step + LAG | Chain CTEs, phân tích trend theo khách hàng |
Chuỗi tư duy SQL Challenge chuyên nghiệp:
Đọc yêu cầu business nâng cao
→ Cần ranking trong nhóm? → ROW_NUMBER/RANK/DENSE_RANK + PARTITION BY
→ Cần so sánh với kỳ trước? → LAG + ORDER BY
→ Cần tích lũy? → SUM OVER + ROWS BETWEEN UNBOUNDED PRECEDING
→ Cần tỷ trọng? → value / SUM OVER (PARTITION BY group)
→ Data từ star schema? → FROM fact JOIN dim₁ JOIN dim₂ JOIN dim₃
→ Query phức tạp nhiều bước? → CTE (WITH ... AS)
→ Review: kết quả business-ready? Format đúng? NULL xử lý chưa?🔗 Xem thêm Buổi 6
→ 📘 Nội dung chính → 📝 Blog → 🧠 Case Study → 🏆 Tiêu chuẩn → 🛠 Workshop