Appearance
🎮 Mini Game Buổi 5: Query Master
Bạn là Data Analyst mới tại sàn e-commerce ShopVN. Các phòng ban liên tục gửi yêu cầu dữ liệu qua Slack — bằng tiếng Việt. Nhiệm vụ: dịch câu hỏi kinh doanh thành SQL query chính xác — từ SELECT đơn giản đến subquery phức tạp!
🎯 Mục tiêu học tập
Sau khi hoàn thành game, bạn sẽ:
- Dịch yêu cầu kinh doanh bằng tiếng Việt thành SQL query chính xác — tư duy business → SQL
- Viết SELECT + WHERE để lọc dữ liệu theo điều kiện cụ thể
- Sử dụng ORDER BY + LIMIT để xếp hạng và lấy top N kết quả
- Áp dụng JOIN (INNER / LEFT) để kết nối nhiều bảng liên quan
- Dùng GROUP BY + aggregate functions (COUNT, SUM, AVG) để tổng hợp dữ liệu
- Lọc sau tổng hợp bằng HAVING — phân biệt WHERE vs HAVING
- Viết subquery để giải quyết câu hỏi business phức tạp nhiều bước
📜 Luật chơi
┌──────────────────────────────────────────────────────┐
│ BẠN = Query Master 🗄️ │
│ DATABASE = Hệ thống e-commerce ShopVN │
│ MỖI VÒNG = 1 yêu cầu business → chọn SQL đúng │
│ 3 LỰA CHỌN mỗi vòng — chỉ 1 đáp án tốt nhất │
│ THỜI GIAN = 90 giây/vòng (nhanh = bonus XP) │
│ MỤC TIÊU = Thu thập ≥ 85 XP để đạt hạng Gold 🥇 │
└──────────────────────────────────────────────────────┘Nguyên tắc cốt lõi: Từ câu hỏi kinh doanh → SQL, không phải ngược lại. Đọc yêu cầu tiếng Việt → xác định cần bảng nào, cột nào, điều kiện gì → 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 | +1 XP |
| > 90 giây (hết giờ) | 0 XP, tự động chọn sai |
🎲 Cơ chế game
Database Schema — ShopVN E-commerce
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────┐
│ 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 │
│ segment │ │ status │ │ │ stock_quantity │
└──────────────────┘ └──────────────────────┘ │ └──────────────────┘
│
┌──────────────────────┐ │
│ order_items │ │
├──────────────────────┤ │
│ item_id (PK) │ │
│ order_id (FK) │ │
│ product_id (FK) ────┘ │
│ quantity │
│ unit_price │
└──────────────────────┘Chỉ số theo dõi
| Chỉ số | Icon | Mô tả | Mục tiêu |
|---|---|---|---|
| Query Accuracy | 🎯 | Chọn đúng SQL query giải quyết yêu cầu business | ≥ 6/7 câu đúng |
| Efficiency | 📊 | Query tối ưu — đúng logic + clean syntax + không dư thừa | ≥ 80% |
| Speed | ⏱️ | Tổng thời gian & speed bonus | Tối đa 35 bonus XP |
Công thức XP
XP vòng = Accuracy Points + Efficiency Bonus + Speed Bonus + Random Event Modifier
XP tổng = Σ (XP vòng 1..7) + Badge BonusTổng XP tối đa lý thuyết: 90 (Accuracy) + 35 (Efficiency) + 35 (Speed) + bonus events ≈ 160+ XP
📋 Kịch bản chi tiết
🗄️ Vòng 1: "Cho anh danh sách khách hàng ở HCM"
Độ khó: ⭐ Dễ — Warm-up | Kỹ năng: SELECT + WHERE
💬 Slack từ Marketing Manager:
"Em ơi, anh cần danh sách tất cả khách hàng ở TP.HCM — lấy tên, email, và phân khúc khách hàng. Anh đang chuẩn bị campaign Tết Nguyên Đán, cần target đúng khu vực."
📊 Database context:
Bảng customers — 15,000 khách hàng trên toàn quốc:
| customer_id | customer_name | city | segment | |
|---|---|---|---|---|
| 1001 | Nguyễn Văn An | an.nguyen@gmail.com | Hồ Chí Minh | Premium |
| 1002 | Trần Thị Bình | binh.tran@yahoo.com | Hà Nội | Standard |
| 1003 | Lê Hoàng Cường | cuong.le@gmail.com | Hồ Chí Minh | VIP |
| 1004 | Phạm Thị Dung | dung.pham@outlook.com | Đà Nẵng | Standard |
| ... | ... | ... | ... | ... |
Yêu cầu rõ ràng: lọc theo city, lấy 3 cột cụ thể.
🗄️ Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Efficiency |
|---|---|---|---|---|
| A | SELECT * FROM customers WHERE city = 'Hồ Chí Minh' | +3 | +1~5 | +1 |
| B ✅ | SELECT customer_name, email, segment FROM customers WHERE city = 'Hồ Chí Minh' | +10 | +1~5 | +5 |
| C | SELECT customer_name, email, segment FROM customers | +1 | +1~5 | +0 |
🗄️ Vòng 2: "Top 10 đơn hàng giá trị cao nhất tháng 1"
Độ khó: ⭐ Dễ | Kỹ năng: SELECT + WHERE + ORDER BY + LIMIT
💬 Slack từ Sales Director:
"Em lấy giúp anh top 10 đơn hàng có giá trị cao nhất trong tháng 1/2026 — anh muốn biết đơn nào lớn nhất, sắp xếp từ cao xuống thấp. Lấy mã đơn, ngày đặt, và tổng tiền nhé."
📊 Database context:
Bảng orders — 50,000 đơn hàng:
| order_id | customer_id | order_date | total_amount | status |
|---|---|---|---|---|
| 90001 | 1001 | 2026-01-03 | 15,200,000 | Delivered |
| 90002 | 1045 | 2026-01-03 | 890,000 | Delivered |
| 90003 | 1203 | 2026-01-05 | 32,500,000 | Delivered |
| 90004 | 1002 | 2026-01-07 | 2,100,000 | Cancelled |
| ... | ... | ... | ... | ... |
Yêu cầu: lọc theo tháng, sắp xếp giảm dần, lấy top 10.
🗄️ Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Efficiency |
|---|---|---|---|---|
| A | SELECT order_id, order_date, total_amount FROM orders WHERE order_date >= '2026-01-01' ORDER BY total_amount DESC | +3 | +1~5 | +2 |
| B | SELECT * FROM orders ORDER BY total_amount DESC LIMIT 10 | +2 | +1~5 | +1 |
| C ✅ | SELECT order_id, order_date, total_amount FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2026-02-01' ORDER BY total_amount DESC LIMIT 10 | +10 | +1~5 | +5 |
🗄️ Vòng 3: "Đơn hàng kèm tên khách hàng — ai mua gì?"
Độ khó: ⭐⭐ Trung bình | Kỹ năng: INNER JOIN
💬 Slack từ Customer Success Lead:
"Em ơi, chị cần danh sách đơn hàng tháng 1/2026 kèm theo tên khách hàng và thành phố. Chị muốn biết ai ở đâu đang đặt hàng — để team CS chủ động chăm sóc khách theo khu vực."
📊 Database context:
Cần kết nối 2 bảng: orders (có customer_id) ↔ customers (có customer_name, city).
| orders.order_id | orders.customer_id | orders.order_date | orders.total_amount | customers.customer_name | customers.city |
|---|---|---|---|---|---|
| 90001 | 1001 | 2026-01-03 | 15,200,000 | Nguyễn Văn An | Hồ Chí Minh |
| 90003 | 1203 | 2026-01-05 | 32,500,000 | Lê Hoàng Cường | Hồ Chí Minh |
| 90005 | 1002 | 2026-01-08 | 5,600,000 | Trần Thị Bình | Hà Nội |
Kết quả mong đợi: mỗi dòng = 1 đơn hàng + tên khách + thành phố.
🗄️ Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Efficiency |
|---|---|---|---|---|
| A | SELECT o.order_id, o.order_date, o.total_amount, c.customer_name, c.city FROM orders o, customers c WHERE o.customer_id = c.customer_id AND o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' | +4 | +1~5 | +2 |
| B ✅ | SELECT o.order_id, o.order_date, o.total_amount, c.customer_name, c.city FROM orders o INNER JOIN customers c ON o.customer_id = c.customer_id WHERE o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' | +10 | +1~5 | +5 |
| C | SELECT order_id, order_date, total_amount, customer_name, city FROM orders INNER JOIN customers WHERE order_date >= '2026-01-01' | +1 | +1~5 | +0 |
🗄️ Vòng 4: "Sản phẩm nào bán chạy nhất — kể cả sản phẩm chưa bán được?"
Độ khó: ⭐⭐ Trung bình | Kỹ năng: LEFT JOIN + aggregate
💬 Slack từ Product Manager:
"Em ơi, anh cần danh sách TẤT CẢ sản phẩm kèm theo tổng số lượng đã bán. Quan trọng: kể cả sản phẩm chưa bán được đơn nào cũng phải hiển thị — để anh biết sản phẩm nào cần push marketing. Sắp xếp từ bán nhiều nhất xuống ít nhất."
📊 Database context:
Bảng products có 500 sản phẩm, nhưng chỉ ~420 sản phẩm có xuất hiện trong bảng order_items. Có 80 sản phẩm chưa có đơn nào.
| product_id | product_name | category | total_sold |
|---|---|---|---|
| P001 | iPhone 16 Pro | Điện thoại | 1,250 |
| P002 | MacBook Air M3 | Laptop | 680 |
| ... | ... | ... | ... |
| P489 | Ốp lưng Galaxy A | Phụ kiện | 0 ← chưa bán |
| P500 | Bàn phím cơ RGB | Phụ kiện | 0 ← chưa bán |
Từ khóa: "tất cả sản phẩm" + "kể cả chưa bán" → cần giữ lại sản phẩm không match.
🗄️ Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Efficiency |
|---|---|---|---|---|
| A | SELECT p.product_name, SUM(oi.quantity) AS total_sold FROM products p INNER JOIN order_items oi ON p.product_id = oi.product_id GROUP BY p.product_name ORDER BY total_sold DESC | +3 | +1~5 | +2 |
| B ✅ | SELECT p.product_name, COALESCE(SUM(oi.quantity), 0) AS total_sold FROM products p LEFT JOIN order_items oi ON p.product_id = oi.product_id GROUP BY p.product_name ORDER BY total_sold DESC | +10 | +1~5 | +5 |
| C | SELECT p.product_name, COUNT(oi.item_id) AS total_sold FROM products p LEFT JOIN order_items oi ON p.product_id = oi.product_id ORDER BY total_sold DESC | +2 | +1~5 | +1 |
🗄️ Vòng 5: "Doanh thu theo danh mục sản phẩm — nhóm nào mang lại nhiều tiền nhất?"
Độ khó: ⭐⭐⭐ Khó | Kỹ năng: JOIN + GROUP BY + aggregate functions
💬 Slack từ CFO:
"Em ơi, anh cần báo cáo doanh thu tháng 1/2026 theo danh mục sản phẩm. Mỗi danh mục: tổng doanh thu, số đơn hàng, giá trị trung bình mỗi đơn. Sắp xếp theo doanh thu giảm dần — anh trình bày trước board meeting chiều nay."
📊 Database context:
Cần kết nối 3 bảng: orders → order_items → products (lấy category).
Kết quả mong đợi:
| category | total_revenue | order_count | avg_order_value |
|---|---|---|---|
| Điện thoại | 12,500,000,000 | 3,200 | 3,906,250 |
| Laptop | 8,200,000,000 | 1,100 | 7,454,545 |
| Phụ kiện | 2,800,000,000 | 8,500 | 329,412 |
| Tablet | 2,100,000,000 | 650 | 3,230,769 |
| Đồng hồ | 1,500,000,000 | 420 | 3,571,429 |
🗄️ Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Efficiency |
|---|---|---|---|---|
| A | SELECT p.category, SUM(oi.quantity * oi.unit_price) AS total_revenue FROM order_items oi JOIN products p ON oi.product_id = p.product_id GROUP BY p.category ORDER BY total_revenue DESC | +4 | +1~5 | +2 |
| B ✅ | SELECT p.category, SUM(oi.quantity * oi.unit_price) AS total_revenue, COUNT(DISTINCT o.order_id) AS order_count, SUM(oi.quantity * oi.unit_price) / COUNT(DISTINCT o.order_id) AS avg_order_value FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' GROUP BY p.category ORDER BY total_revenue DESC | +15 | +1~5 | +5 |
| C | SELECT category, SUM(price) AS total_revenue, COUNT(*) AS order_count, AVG(price) AS avg_order_value FROM products GROUP BY category ORDER BY total_revenue DESC | +1 | +1~5 | +0 |
🗄️ Vòng 6: "Khách nào chi hơn 10 triệu — chỉ tính khách mua ≥ 3 đơn?"
Độ khó: ⭐⭐⭐ Khó | Kỹ năng: GROUP BY + HAVING + multiple conditions
💬 Slack từ CRM Manager:
"Em ơi, chị cần danh sách khách hàng VIP tiềm năng: tổng chi tiêu trên 10 triệu VNĐ VÀ đã mua ít nhất 3 đơn hàng trong Q4/2025. Chị cần tên, tổng chi tiêu, và số đơn — để mời vào chương trình loyalty mới."
📊 Database context:
Bảng orders Q4/2025 — cần tổng hợp theo customer_id rồi lọc sau khi GROUP BY.
Kết quả mong đợi (ví dụ):
| customer_name | total_spent | order_count |
|---|---|---|
| Nguyễn Văn An | 45,200,000 | 8 |
| Lê Hoàng Cường | 28,700,000 | 5 |
| Trần Thị Bình | 15,300,000 | 4 |
| Phạm Minh Đức | 12,100,000 | 3 |
Lưu ý: WHERE lọc trước GROUP BY (lọc dòng), HAVING lọc sau GROUP BY (lọc nhóm).
🗄️ Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Efficiency |
|---|---|---|---|---|
| A | SELECT c.customer_name, SUM(o.total_amount) AS total_spent, COUNT(*) AS order_count 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 SUM(o.total_amount) > 10000000 AND COUNT(*) >= 3 GROUP BY c.customer_name ORDER BY total_spent DESC | +1 | +1~5 | +0 |
| B | SELECT c.customer_name, SUM(o.total_amount) AS total_spent, COUNT(*) AS order_count FROM customers c JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_name HAVING total_spent > 10000000 AND order_count >= 3 ORDER BY total_spent DESC | +3 | +1~5 | +2 |
| C ✅ | SELECT c.customer_name, SUM(o.total_amount) AS total_spent, COUNT(o.order_id) AS order_count 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 SUM(o.total_amount) > 10000000 AND COUNT(o.order_id) >= 3 ORDER BY total_spent DESC | +10 | +1~5 | +5 |
🗄️ Vòng 7: "Sản phẩm nào bán trên trung bình — ai là ngôi sao thực sự?"
Độ khó: ⭐⭐⭐⭐ Rất khó — Boss Round 🏴☠️ | Kỹ năng: Subquery
💬 Slack từ CEO (tối Chủ Nhật 😱):
"Em ơi, anh chuẩn bị slide cho board meeting sáng thứ Hai. Anh cần biết: sản phẩm nào có doanh thu T1/2026 CAO HƠN doanh thu trung bình của tất cả sản phẩm trong cùng tháng? Lấy tên sản phẩm, danh mục, và doanh thu — sort giảm dần. Anh muốn biết đâu là ngôi sao thực sự, không phải trung bình."
📊 Database context:
Đây là bài toán so sánh giá trị từng sản phẩm với giá trị trung bình tổng → cần subquery để tính trung bình trước, rồi dùng kết quả đó trong WHERE.
- Bước 1: Tính doanh thu mỗi sản phẩm T1/2026
- Bước 2: Tính doanh thu trung bình trên tất cả sản phẩm
- Bước 3: Lọc chỉ lấy sản phẩm có doanh thu > trung bình
Kết quả mong đợi (ví dụ):
| product_name | category | product_revenue | avg_all_products |
|---|---|---|---|
| iPhone 16 Pro | Điện thoại | 3,200,000,000 | 540,000,000 |
| MacBook Air M3 | Laptop | 2,800,000,000 | 540,000,000 |
| Samsung Galaxy S25 | Điện thoại | 2,500,000,000 | 540,000,000 |
| AirPods Pro 3 | Phụ kiện | 1,800,000,000 | 540,000,000 |
| iPad Air | Tablet | 1,200,000,000 | 540,000,000 |
🗄️ Câu hỏi: SQL query nào đúng?
| Lựa chọn | SQL Query | Accuracy | Speed | Efficiency |
|---|---|---|---|---|
| A | SELECT p.product_name, p.category, SUM(oi.quantity * oi.unit_price) AS product_revenue FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' GROUP BY p.product_name, p.category ORDER BY product_revenue DESC | +4 | +1~5 | +2 |
| B | SELECT p.product_name, p.category, SUM(oi.quantity * oi.unit_price) AS product_revenue FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' AND SUM(oi.quantity * oi.unit_price) > AVG(oi.quantity * oi.unit_price) GROUP BY p.product_name, p.category | +1 | +1~5 | +0 |
| C ✅ | SELECT p.product_name, p.category, SUM(oi.quantity * oi.unit_price) AS product_revenue FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.order_date >= '2026-01-01' AND o.order_date < '2026-02-01' GROUP BY p.product_name, p.category HAVING SUM(oi.quantity * oi.unit_price) > (SELECT AVG(sub.product_revenue) FROM (SELECT SUM(oi2.quantity * oi2.unit_price) AS product_revenue FROM orders o2 JOIN order_items oi2 ON o2.order_id = oi2.order_id WHERE o2.order_date >= '2026-01-01' AND o2.order_date < '2026-02-01' GROUP BY oi2.product_id) sub) ORDER BY product_revenue DESC | +15 | +1~5 | +5 |
⚡ 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 query trong doanh nghiệp!
| # | Sự kiện | Xác suất | Ảnh hưởng |
|---|---|---|---|
| 1 | 🔥 Sếp gọi gấp — "Query xong chưa em? Anh cần số liệu cho meeting 10 phút nữa!" | 20% | Thời gian vòng này giảm còn 60 giây. Áp lực tăng! |
| 2 | 💡 DBA hint — Đồng nghiệp DBA gợi ý: "Bảng đó có index trên cột date nhé, dùng range query" | 15% | Loại bỏ 1 đáp án sai → còn 2 lựa chọn. Efficiency +2 bonus |
| 3 | 📋 Schema reveal — Hệ thống hiển thị thêm sample data từ bảng liên quan | 10% | Hiển thị thêm context → dễ chọn hơn. +3 Accuracy bonus nếu đúng |
| 4 | 💥 Query timeout — Database đang chạy batch job, query bị chậm! | 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 = chạy nhanh hơn |
| 5 | 📊 Bonus dataset — Phòng Finance share thêm bảng payments để cross-check | 10% | Câu hỏi bonus +5 XP nếu đáp án có logic JOIN chính xác |
| 6 | ☕ Cà phê sữa đá boost — Đồng nghiệp mua cà phê về, tinh thần lên cao! | 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 | ≥ 85 XP | "Query Master! Yêu cầu business nào cũng dịch thành SQL chính xác. Bạn sẵn sàng query trực tiếp trên production database." |
| 🥈 Silver | ≥ 60 XP | "Solid Querier — SELECT/JOIN ổn nhưng cần luyện thêm GROUP BY + HAVING + subquery. Ôn lại Phần 3–4!" |
| 🥉 Bronze | ≥ 40 XP | "Junior Querier — hiểu cơ bản nhưng cần luyện thêm JOIN và aggregate functions." |
| ❌ Fail | < 40 XP | "Query chưa chạy được... Đọc lại Buổi 5 rồi quay lại nhé!" |
🎖️ Badge đặc biệt
| Badge | Điều kiện | Mô tả |
|---|---|---|
| 🏅 Perfect Query | 7/7 câu đúng (chọn đáp án tốt nhất) | Mọi query đều chạy đúng — zero bugs |
| ⚡ Speed Runner | Tổng thời gian ≤ 3 phút 30 giây (cả 7 vòng) | Viết query nhanh hơn ChatGPT |
| 🔗 JOIN Master | Đúng Vòng 3 + Vòng 4 (INNER JOIN + LEFT JOIN) | Kết nối bảng như thở — VLOOKUP phiên bản pro |
| 📊 Aggregation Pro | Đúng Vòng 5 + Vòng 6 (GROUP BY + HAVING) | Pivot Table bằng SQL — tổng hợp dữ liệu chuyên nghiệp |
| 🧠 Subquery Thinker | Đúng Vòng 7 (subquery boss round) | Tư duy SQL nhiều tầng — sẵn sàng cho CTE và window functions |
| 🎯 Business Translator | Đúng Vòng 1 + Vòng 2 + luôn chọn cột đúng (không SELECT *) | Dịch yêu cầu business → SQL chính xác, không thừa không thiếu |
| 🛡️ Survivor | 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 Senior DA |
| 💎 Flawless | ≥ 85 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 — SELECT cột cụ thể + WHERE lọc điều kiện
Yêu cầu: Danh sách khách hàng ở HCM — tên, email, segment → SELECT 3 cột + WHERE city.
- Đáp án B chọn đúng 3 cột cần thiết (
customer_name, email, segment) + lọc đúng city = 'Hồ Chí Minh' - Đáp án A (
SELECT *) lấy tất cả cột — dư thừa, sếp không cầncustomer_id,emailformat thô. Trong thực tế,SELECT *trên bảng lớn gây chậm query và là anti-pattern trong SQL - Đáp án C thiếu
WHERE→ trả về toàn bộ 15,000 khách hàng, không lọc HCM - Nguyên tắc: Luôn SELECT đúng cột cần thiết, không dùng
*khi đã biết cột nào sếp cần - Kiến thức: SELECT, WHERE — Phần 2: SELECT — Truy vấn dữ liệu cơ bản
Vòng 2 — ORDER BY + LIMIT + lọc ngày chính xác
Yêu cầu: Top 10 đơn hàng giá trị cao nhất tháng 1/2026 → WHERE date range + ORDER BY DESC + LIMIT 10.
- Đáp án C đầy đủ nhất: (1) Lọc tháng 1 bằng range
>= '2026-01-01' AND < '2026-02-01'— chính xác hơnLIKEhayMONTH(), (2) Chỉ lấy 3 cột cần thiết, (3)ORDER BY total_amount DESCsắp xếp giảm dần, (4)LIMIT 10lấy đúng top 10 - Đáp án A thiếu
LIMIT 10→ trả về tất cả đơn tháng 1, và thiếu điều kiện end date - Đáp án B thiếu WHERE lọc tháng → top 10 trên toàn bộ database, không phải T1/2026. Cũng dùng
SELECT *không cần thiết - Tip: Lọc ngày bằng range (
>= AND <) thay vìBETWEEN(vìBETWEENbao gồm cả 2 biên, dễ sai với timestamp) hoặcMONTH(order_date) = 1(không tận dụng được index) - Kiến thức: ORDER BY, LIMIT — Phần 2: SELECT
Vòng 3 — INNER JOIN chuẩn syntax
Yêu cầu: Đơn hàng kèm tên khách hàng và thành phố → JOIN orders với customers.
- Đáp án B dùng INNER JOIN syntax chuẩn ANSI-92:
FROM orders o INNER JOIN customers c ON o.customer_id = c.customer_id— rõ ràng, dễ đọc, dễ maintain - Đáp án A dùng comma-separated JOIN (ANSI-89 style):
FROM orders o, customers c WHERE ...— chạy đúng nhưng là cú pháp cũ, khó phân biệt điều kiện JOIN vs điều kiện lọc khi query phức tạp - Đáp án C thiếu
ONclause → lỗi cú pháp. Không chỉ định rõ cột nào join với cột nào. Cũng thiếu alias nên với cột trùng tên (nhưcustomer_id) sẽ gây ambiguous error - Best practice: Luôn dùng
JOIN ... ON ...thay vì comma-separated join. Luôn dùng table alias (o,c) cho dễ đọc - Kiến thức: JOIN — Phần 3: JOIN — Kết nối nhiều bảng
Vòng 4 — LEFT JOIN giữ lại dòng không match
Yêu cầu: TẤT CẢ sản phẩm — kể cả chưa bán → LEFT JOIN (giữ tất cả hàng bên trái).
- Đáp án B dùng LEFT JOIN → giữ tất cả 500 sản phẩm, kể cả 80 sản phẩm không có trong
order_items.COALESCE(SUM(...), 0)chuyển NULL thành 0 — không bị hiển thị NULL cho sản phẩm chưa bán - Đáp án A dùng INNER JOIN → chỉ trả về 420 sản phẩm có đơn hàng — mất 80 sản phẩm chưa bán, trái yêu cầu "tất cả sản phẩm"
- Đáp án C thiếu
GROUP BY→ lỗi SQL (dùng aggregateCOUNTmà không GROUP BY). DùngCOUNTthay vìSUM(quantity)cũng sai —COUNTchỉ đếm số dòng, không tính tổng số lượng đã bán - Khi nào dùng LEFT JOIN vs INNER JOIN? LEFT JOIN khi cần giữ lại tất cả bản ghi bên trái, dù không có match bên phải. INNER JOIN khi chỉ cần bản ghi có match ở cả 2 bảng
- Kiến thức: LEFT JOIN, COALESCE — Phần 3: JOIN
Vòng 5 — Multi-table JOIN + GROUP BY + multiple aggregates
Yêu cầu: Doanh thu theo danh mục: tổng DT, số đơn, giá trị TB → JOIN 3 bảng + GROUP BY + SUM + COUNT DISTINCT.
- Đáp án B join 3 bảng đúng thứ tự:
orders → order_items → products→ lấy đượccategory. DùngCOUNT(DISTINCT o.order_id)thay vìCOUNT(*)— vì 1 đơn hàng có nhiều items,COUNT(*)sẽ đếm trùng lặp! - Đáp án A thiếu
COUNT(DISTINCT)cho số đơn hàng và thiếuavg_order_value→ chưa đủ yêu cầu CFO. Cũng thiếu WHERE lọc tháng 1/2026 - Đáp án C query trực tiếp bảng
products→ lấyprice(giá niêm yết) thay vì doanh thu thực tế (quantity × unit_price trongorder_items). Không có JOIN → hoàn toàn sai - Bài học: Với multi-table query, luôn vẽ quan hệ bảng trước khi viết SQL: bảng nào chứa gì? → JOIN qua key nào? → GROUP BY cột nào? → aggregate cột nào?
- Kiến thức: GROUP BY + aggregate functions — Phần 4: GROUP BY & Aggregation
Vòng 6 — WHERE vs HAVING: lọc dòng vs lọc nhóm
Yêu cầu: Khách chi > 10 triệu VÀ ≥ 3 đơn trong Q4/2025 → WHERE lọc thời gian + HAVING lọc tổng hợp.
- Đáp án C phân biệt đúng WHERE vs HAVING:
WHERElọc trước GROUP BY: chỉ lấy đơn hàng Q4/2025 (lọc dòng)HAVINGlọc sau GROUP BY: chỉ giữ khách có SUM > 10 triệu VÀ COUNT ≥ 3 (lọc nhóm)
- Đáp án A đặt
SUM()vàCOUNT()trongWHERE→ lỗi SQL! Aggregate functions không thể dùng trong WHERE — vì WHERE chạy trước GROUP BY, lúc đó chưa có giá trị tổng hợp - Đáp án B thiếu
WHERElọc Q4/2025 → tính trên toàn bộ lịch sử đơn hàng, không phải chỉ Q4/2025. Dùng alias (total_spent) trong HAVING — một số database cho phép nhưng không phải standard SQL - Quy tắc nhớ:
WHERE= lọc dòng (rows) trước khi nhóm.HAVING= lọc nhóm (groups) sau khi tổng hợp - Kiến thức: HAVING — Phần 4: GROUP BY & Aggregation
Vòng 7 — Subquery: so sánh với giá trị tổng hợp
Yêu cầu: Sản phẩm có doanh thu > trung bình tất cả SP → subquery tính AVG rồi dùng trong HAVING.
- Đáp án C dùng nested subquery trong HAVING:
- Query ngoài: tính doanh thu mỗi sản phẩm (JOIN 3 bảng + GROUP BY)
- Subquery trong: tính trung bình doanh thu trên tất cả sản phẩm (
SELECT AVG(sub.product_revenue) FROM (SELECT SUM(...) GROUP BY product_id) sub) - HAVING so sánh: chỉ giữ SP có revenue > trung bình
- Đáp án A tính doanh thu mỗi SP nhưng không lọc — trả về tất cả SP, kể cả dưới trung bình. Thiếu điều kiện "cao hơn trung bình"
- Đáp án B đặt
SUM()vàAVG()trong WHERE → lỗi SQL! (lặp lại lỗi Vòng 6). Không thể so sánh aggregate trong WHERE - Tại sao cần subquery lồng 2 tầng? Vì
AVG(SUM(...))không hợp lệ trong SQL — không thể aggregate trên aggregate trực tiếp. Phải tính SUM ở tầng trong (GROUP BY product_id), rồi AVG ở tầng ngoài - Nâng cao: Đây là bước đệm cho CTE (Common Table Expression) và Window Functions mà bạn sẽ học ở level tiếp theo — cách viết gọn hơn cho cùng bài toán
- Kiến thức: Subquery — Phần 5: Subquery
📚 Kiến thức liên quan
| Vòng | Chủ đề chính | Kỹ năng SQL |
|---|---|---|
| 1 | SELECT + WHERE | Lọc dữ liệu theo điều kiện, chọn cột cụ thể |
| 2 | ORDER BY + LIMIT | Sắp xếp và lấy top N, lọc ngày bằng range |
| 3 | INNER JOIN | Kết nối 2 bảng qua key chung — ANSI-92 syntax |
| 4 | LEFT JOIN + COALESCE | Giữ tất cả bản ghi bên trái, xử lý NULL |
| 5 | Multi-JOIN + GROUP BY | JOIN 3 bảng, COUNT DISTINCT, multiple aggregates |
| 6 | WHERE vs HAVING | Phân biệt lọc dòng vs lọc nhóm sau tổng hợp |
| 7 | Subquery | Truy vấn lồng — so sánh với giá trị aggregate |
Chuỗi tư duy Query Master chuyên nghiệp:
Đọc yêu cầu business (tiếng Việt)
→ Xác định: cần bảng nào? cột nào? điều kiện gì?
→ Chọn SELECT cột cụ thể (KHÔNG dùng SELECT *)
→ Cần liên kết bảng? → JOIN (INNER nếu cần match, LEFT nếu cần tất cả)
→ Cần tổng hợp? → GROUP BY + aggregate (SUM, COUNT, AVG)
→ Lọc sau tổng hợp? → HAVING (không phải WHERE)
→ So sánh với giá trị tổng? → Subquery
→ Sắp xếp + giới hạn? → ORDER BY + LIMIT
→ Review: query có trả đúng yêu cầu sếp không?🔗 Xem thêm Buổi 5
→ 📘 Nội dung chính → 📝 Blog → 🧠 Case Study → 🏆 Tiêu chuẩn → 🛠 Workshop