Skip to content

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

  1. 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
  2. Viết SELECT + WHERE để lọc dữ liệu theo điều kiện cụ thể
  3. Sử dụng ORDER BY + LIMIT để xếp hạng và lấy top N kết quả
  4. Áp dụng JOIN (INNER / LEFT) để kết nối nhiều bảng liên quan
  5. Dùng GROUP BY + aggregate functions (COUNT, SUM, AVG) để tổng hợp dữ liệu
  6. Lọc sau tổng hợp bằng HAVING — phân biệt WHERE vs HAVING
  7. 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ờiSpeed 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ốIconMô 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 bonusTố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 Bonus

Tổ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_idcustomer_nameemailcitysegment
1001Nguyễn Văn Anan.nguyen@gmail.comHồ Chí MinhPremium
1002Trần Thị Bìnhbinh.tran@yahoo.comHà NộiStandard
1003Lê Hoàng Cườngcuong.le@gmail.comHồ Chí MinhVIP
1004Phạm Thị Dungdung.pham@outlook.comĐà NẵngStandard
...............

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ọnSQL QueryAccuracySpeedEfficiency
ASELECT * FROM customers WHERE city = 'Hồ Chí Minh'+3+1~5+1
BSELECT customer_name, email, segment FROM customers WHERE city = 'Hồ Chí Minh'+10+1~5+5
CSELECT 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_idcustomer_idorder_datetotal_amountstatus
9000110012026-01-0315,200,000Delivered
9000210452026-01-03890,000Delivered
9000312032026-01-0532,500,000Delivered
9000410022026-01-072,100,000Cancelled
...............

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ọnSQL QueryAccuracySpeedEfficiency
ASELECT order_id, order_date, total_amount FROM orders WHERE order_date >= '2026-01-01' ORDER BY total_amount DESC+3+1~5+2
BSELECT * FROM orders ORDER BY total_amount DESC LIMIT 10+2+1~5+1
CSELECT 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_idorders.customer_idorders.order_dateorders.total_amountcustomers.customer_namecustomers.city
9000110012026-01-0315,200,000Nguyễn Văn AnHồ Chí Minh
9000312032026-01-0532,500,000Lê Hoàng CườngHồ Chí Minh
9000510022026-01-085,600,000Trần Thị BìnhHà 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ọnSQL QueryAccuracySpeedEfficiency
ASELECT 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
BSELECT 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
CSELECT 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_idproduct_namecategorytotal_sold
P001iPhone 16 ProĐiện thoại1,250
P002MacBook Air M3Laptop680
............
P489Ốp lưng Galaxy APhụ kiện0 ← chưa bán
P500Bàn phím cơ RGBPhụ kiện0 ← 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ọnSQL QueryAccuracySpeedEfficiency
ASELECT 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
BSELECT 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
CSELECT 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: ordersorder_itemsproducts (lấy category).

Kết quả mong đợi:

categorytotal_revenueorder_countavg_order_value
Điện thoại12,500,000,0003,2003,906,250
Laptop8,200,000,0001,1007,454,545
Phụ kiện2,800,000,0008,500329,412
Tablet2,100,000,0006503,230,769
Đồng hồ1,500,000,0004203,571,429

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

Lựa chọnSQL QueryAccuracySpeedEfficiency
ASELECT 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
BSELECT 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
CSELECT 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_nametotal_spentorder_count
Nguyễn Văn An45,200,0008
Lê Hoàng Cường28,700,0005
Trần Thị Bình15,300,0004
Phạm Minh Đức12,100,0003

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ọnSQL QueryAccuracySpeedEfficiency
ASELECT 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
BSELECT 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
CSELECT 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_namecategoryproduct_revenueavg_all_products
iPhone 16 ProĐiện thoại3,200,000,000540,000,000
MacBook Air M3Laptop2,800,000,000540,000,000
Samsung Galaxy S25Điện thoại2,500,000,000540,000,000
AirPods Pro 3Phụ kiện1,800,000,000540,000,000
iPad AirTablet1,200,000,000540,000,000

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

Lựa chọnSQL QueryAccuracySpeedEfficiency
ASELECT 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
BSELECT 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
CSELECT 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ệnXá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 quan10%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-check10%Câu hỏi bonus +5 XP nếu đáp án có logic JOIN chính xác
6Cà 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ệnMô 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ệnMô tả
🏅 Perfect Query7/7 câu đúng (chọn đáp án tốt nhất)Mọi query đều chạy đúng — zero bugs
Speed RunnerTổ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
🛡️ SurvivorGặ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 Senior DA
💎 Flawless≥ 85 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 — 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ần customer_id, email format 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ơn LIKE hay MONTH(), (2) Chỉ lấy 3 cột cần thiết, (3) ORDER BY total_amount DESC sắp xếp giảm dần, (4) LIMIT 10 lấ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ì BETWEEN bao gồm cả 2 biên, dễ sai với timestamp) hoặc MONTH(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 ON clause → 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 aggregate COUNT mà không GROUP BY). Dùng COUNT thay vì SUM(quantity) cũng sai — COUNT chỉ đế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 được category. Dùng COUNT(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ếu avg_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ấy price (giá niêm yết) thay vì doanh thu thực tế (quantity × unit_price trong order_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:
    • WHERE lọc trước GROUP BY: chỉ lấy đơn hàng Q4/2025 (lọc dòng)
    • HAVING lọ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()COUNT() trong WHERElỗ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 WHERE lọ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:
    1. Query ngoài: tính doanh thu mỗi sản phẩm (JOIN 3 bảng + GROUP BY)
    2. 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)
    3. 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()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?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)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òngChủ đề chínhKỹ năng SQL
1SELECT + WHERELọc dữ liệu theo điều kiện, chọn cột cụ thể
2ORDER BY + LIMITSắp xếp và lấy top N, lọc ngày bằng range
3INNER JOINKết nối 2 bảng qua key chung — ANSI-92 syntax
4LEFT JOIN + COALESCEGiữ tất cả bản ghi bên trái, xử lý NULL
5Multi-JOIN + GROUP BYJOIN 3 bảng, COUNT DISTINCT, multiple aggregates
6WHERE vs HAVINGPhân biệt lọc dòng vs lọc nhóm sau tổng hợp
7SubqueryTruy 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