Skip to content

🎮 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ẽ:

  1. Dùng ROW_NUMBER + PARTITION BY để xếp hạng sản phẩm trong mỗi category — Top N per group
  2. Sử dụng RANK / DENSE_RANK để phân biệt cách xếp hạng khi có tied values
  3. Áp dụng LAG/LEAD để so sánh MoM revenue growth — truy cập dòng trước/sau
  4. Tính Running Total bằng SUM OVER với frame clause — doanh thu tích lũy
  5. Tính Percent of Total bằng SUM OVER (PARTITION BY) — tỷ trọng doanh thu
  6. Query trên Star Schema — JOIN fact + dimension tables theo đúng Kimball pattern
  7. 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ờiSpeed 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ốIconMô 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 XPTối đa 35 bonus XP
Complexity Score📊Mức độ phức tạp SQL — Window Function > GROUP BYChọ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 Bonus

Tổ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ómGROUP BY đơn thuần không đủ!

Kết quả mong đợi:

categoryproduct_namerevenuerank_in_category
Điện TửiPhone 16 Pro3,200,000,0001
Điện TửSamsung Galaxy S252,500,000,0002
Điện TửMacBook Air M31,800,000,0003
Thời TrangÁo polo Routine850,000,0001
Thời TrangGiày Nike Air720,000,0002
Thời TrangQuần jean Levi's680,000,0003
............

🏆 Câu hỏi: SQL query nào đúng?

Lựa chọnSQL QueryAccuracySpeedComplexity
ASELECT 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
BWITH 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
CSELECT 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 employeesregion. Từ khóa: cùng hạng, không bỏ quaDENSE_RANK, phân theo vùngPARTITION BY region.

Kết quả mong đợi:

regionemployee_nametotal_salessales_rank
BắcTrần Minh Tuấn2,500,000,0001
BắcNguyễn Hải Yến2,500,000,0001
BắcPhạm Văn Linh1,800,000,0002
NamLê Thị Hương3,100,000,0001
NamVõ Đức Trí2,200,000,0002

🏆 Câu hỏi: SQL query nào đúng?

Lựa chọnSQL QueryAccuracySpeedComplexity
ASELECT 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
BSELECT 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
CSELECT 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:

monthrevenueprev_month_revenuemom_growth_pct
2025-071,430,000,000NULLNULL
2025-081,270,000,0001,430,000,000-11.2
2025-091,600,000,0001,270,000,00026.0
2025-101,810,000,0001,600,000,00013.1
2025-112,100,000,0001,810,000,00016.0
2025-122,450,000,0002,100,000,00016.7

🏆 Câu hỏi: SQL query nào đúng?

Lựa chọnSQL QueryAccuracySpeedComplexity
ASELECT 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
BSELECT 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
CWITH 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_datedaily_revenuerunning_totalpct_of_target
2026-01-0185,000,00085,000,0002.8%
2026-01-02120,000,000205,000,0006.8%
2026-01-0395,000,000300,000,00010.0%
............
2026-01-15110,000,0001,580,000,00052.7%
............
2026-01-31150,000,0002,850,000,00095.0%

🏆 Câu hỏi: SQL query nào đúng?

Lựa chọnSQL QueryAccuracySpeedComplexity
ASELECT 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
BWITH 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
CSELECT 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:

categoryproduct_namerevenuepct_of_categorycumulative_pct
Điện TửiPhone 16 Pro3,200M35.6%35.6%
Điện TửSamsung Galaxy S252,500M27.8%63.4%
Điện TửMacBook Air M31,800M20.0%83.4%
Điện TửTai nghe Sony1,500M16.7%100.0%

🏆 Câu hỏi: SQL query nào đúng?

Lựa chọnSQL QueryAccuracySpeedComplexity
ASELECT 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
BWITH 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
CSELECT 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:

yearquarterregioncategorytotal_revenuetotal_items
2025Q1BắcĐiện Tử5,200M12,500
2025Q1BắcThời Trang2,100M8,200
2025Q1NamĐiện Tử4,800M11,000
..................

🏆 Câu hỏi: SQL query nào đúng?

Lựa chọnSQL QueryAccuracySpeedComplexity
ASELECT 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
BSELECT 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
CSELECT 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:

  1. Tính doanh thu mỗi khách mỗi tháng (CTE 1)
  2. Dùng LAG lấy doanh thu tháng trước (CTE 2)
  3. Kiểm tra 3 tháng liên tiếp đều tăng (WHERE)

Kết quả mong đợi:

customer_nameoct_revenuenov_revenuedec_revenuegrowth_oct_novgrowth_nov_dec
Nguyễn Văn An5,200,0006,800,0008,500,000+30.8%+25.0%
Lê Thị Hương3,100,0003,500,0004,200,000+12.9%+20.0%
Trần Minh Tuấn2,800,0003,000,0003,800,000+7.1%+26.7%

🏆 Câu hỏi: SQL query nào đúng?

Lựa chọnSQL QueryAccuracySpeedComplexity
ASELECT 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
BWITH 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
CSELECT 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ệnXá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 query10%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 table10%Câu hỏi bonus +5 XP nếu đáp án JOIN chuẩn Kimball
6Trà đá 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ệnMô 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ệnMô tả
🏅 Perfect Window7/7 câu đúng (chọn đáp án tốt nhất)Mọi window function đều chính xác — zero bugs
Speed DemonTổ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 ProofGặp ≥ 3 sự kiện ngẫu nhiên bất lợi mà vẫn đạt GoldBì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ểmHoà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:
    1. CTE product_revenue: tính doanh thu mỗi SP, gán rank_in_category bằng ROW_NUMBER
    2. Query ngoài: lọc WHERE rank_in_category <= 3 → top 3 mỗi category
    3. PARTITION BY category → xếp hạng độc lập trong mỗi danh mục
  • Đá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 <= 3 khô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ỏ quaDENSE_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:
    1. CTE monthly: tổng doanh thu theo tháng (GROUP BY DATE_TRUNC)
    2. LAG(revenue, 1) OVER (ORDER BY month) — lấy doanh thu tháng trước
    3. Tính mom_growth_pct = (current - previous) / previous × 100
    4. Lọc chỉ completed orders — đúng business logic
  • Đá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ảng monthly_revenue sẵ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:
    1. CTE daily: tổng doanh thu hàng ngày, lọc completed
    2. SUM(daily_revenue) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) — running total chính xác
    3. Tính pct_of_target = running_total / 3,000,000,000 × 100
  • Đá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). Explicit ROWS BETWEEN là 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:
    1. SUM(revenue) OVER (PARTITION BY category) → tổng category (để tính %)
    2. SUM(revenue) OVER (PARTITION BY category ORDER BY revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) → cumulative sum (để tính cumulative %)
    3. 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 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 ... ON syntax (ANSI-92) + lọc trên dimension (WHERE d.year = 2025) + thêm avg_daily_revenue:
    1. FROM fact_daily_sales f — bắt đầu từ fact table
    2. JOIN dim_date d ON f.date_key = d.date_key — join bằng surrogate key
    3. JOIN dim_product p ON f.product_key = p.product_key
    4. JOIN dim_store s ON f.store_key = s.store_key
    5. 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ùng EXTRACT(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:
    1. CTE monthly_customer: tính doanh thu mỗi khách mỗi tháng
    2. CTE with_lag: dùng LAG(monthly_revenue) OVER (PARTITION BY customer_id ORDER BY month) → lấy revenue tháng trước
    3. Tính mom_growth_pct — % tăng trưởng
    4. 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)
  • Đá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_revenue dù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òngChủ đề chínhKỹ năng SQL
1ROW_NUMBER + PARTITION BYTop N per group — CTE + window + filter
2DENSE_RANK vs RANK vs ROW_NUMBERPhân biệt ranking functions cho tied values
3LAG + MoM GrowthTruy cập dòng trước, tính % tăng trưởng
4Running Total (SUM OVER)Frame clause, ROWS BETWEEN, cumulative sum
5Percent of Total + Cumulative %SUM OVER PARTITION BY, running %
6Star Schema QueryFact + dimension JOIN, Kimball pattern
7CTE multi-step + LAGChain 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