Appearance
🛠 Workshop Buổi 5: Query cơ sở dữ liệu E-commerce
Kết nối database mẫu, viết 10+ SQL query từ cơ bản đến nâng cao — lọc, sắp xếp, JOIN bảng, tổng hợp dữ liệu — rút ra insight kinh doanh từ dữ liệu E-commerce thực tế!
🎯 Mục tiêu
Sau khi hoàn thành workshop này, bạn sẽ:
- Viết SELECT query với WHERE, ORDER BY, LIMIT, NULL handling để lọc và sắp xếp dữ liệu từ database E-commerce
- Sử dụng JOIN — INNER JOIN, LEFT JOIN để kết nối bảng orders + customers + products, trích xuất thông tin đa chiều
- Tổng hợp dữ liệu với GROUP BY + COUNT/SUM/AVG, HAVING, subquery — phân tích revenue by region và monthly trend
- Rút ra 3 insight kinh doanh từ kết quả query — có số liệu cụ thể, sẵn sàng đưa vào portfolio Capstone
🧰 Công cụ & Setup
| Công cụ | Link | Ghi chú |
|---|---|---|
| DB Fiddle | db-fiddle.com | Online, không cần cài đặt — chọn SQLite |
| SQLite Online | sqliteonline.com | Giao diện đơn giản, phù hợp người mới |
| BigQuery Sandbox | cloud.google.com/bigquery/docs/sandbox | Miễn phí 1 TB/tháng — dùng nếu muốn trải nghiệm cloud |
Setup trước buổi học
- Truy cập DB Fiddle (db-fiddle.com) → chọn SQLite v3.39
- Copy toàn bộ phần CREATE TABLE + INSERT ở mục Dataset bên dưới → dán vào ô Schema SQL (bên trái)
- Click Run để tạo database
- Viết query ở ô Query SQL (bên phải) → click Run để xem kết quả
- Lưu file .sql trên máy: mở Notepad / VS Code → dán từng query → lưu thành
HoTen_Buoi05_SQL.sql
💡 Tip: DB Fiddle cho phép share link — sau khi hoàn thành, click Share để lưu lại và chia sẻ với giảng viên.
📦 Dataset: E-commerce Database
Database mô phỏng hệ thống e-commerce tại Việt Nam gồm 4 bảng liên kết với nhau:
Sơ đồ quan hệ (ERD)
┌──────────────┐ ┌──────────────┐
│ customers │ │ products │
├──────────────┤ ├──────────────┤
│ customer_id │ (PK) │ product_id │ (PK)
│ name │ │ product_name │
│ email │ │ category │
│ region │ │ price │
│ signup_date │ │ stock_qty │
└──────┬───────┘ └──────┬───────┘
│ 1:N │ 1:N
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ orders │ │ order_items │
├──────────────┤ ├──────────────┤
│ order_id │ (PK) │ item_id │ (PK)
│ customer_id │ (FK) │ order_id │ (FK)
│ order_date │ │ product_id │ (FK)
│ status │ │ quantity │
│ total_amount │ │ unit_price │
└──────┬───────┘ └──────────────┘
│ 1:N ▲
└──────────────────────┘Schema & Sample Data
Copy toàn bộ block SQL dưới đây vào ô Schema SQL của DB Fiddle:
sql
-- =============================================
-- E-COMMERCE DATABASE — Workshop Buổi 5
-- 4 bảng: customers, products, orders, order_items
-- =============================================
-- 1. BẢNG CUSTOMERS — Thông tin khách hàng
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
region TEXT,
signup_date DATE
);
INSERT INTO customers (customer_id, name, email, region, signup_date) VALUES
(1, 'Nguyễn Văn An', 'an.nguyen@email.com', 'Bắc', '2024-01-15'),
(2, 'Trần Thị Bình', 'binh.tran@email.com', 'Nam', '2024-02-20'),
(3, 'Lê Hoàng Cường', 'cuong.le@email.com', 'Trung', '2024-03-10'),
(4, 'Phạm Minh Dũng', 'dung.pham@email.com', 'Bắc', '2024-04-05'),
(5, 'Hoàng Thị Em', 'em.hoang@email.com', 'Nam', '2024-05-18'),
(6, 'Võ Đức Phong', 'phong.vo@email.com', 'Tây Nguyên', '2024-06-22'),
(7, 'Đặng Thu Hà', 'ha.dang@email.com', 'Bắc', '2024-07-30'),
(8, 'Bùi Quang Huy', NULL, 'Trung', '2024-08-14'),
(9, 'Ngô Thị Lan', 'lan.ngo@email.com', 'Nam', '2024-09-01'),
(10, 'Trịnh Văn Mạnh', 'manh.trinh@email.com', 'Bắc', '2024-10-12'),
(11, 'Lý Thị Ngọc', NULL, 'Tây Nguyên', '2024-11-05'),
(12, 'Dương Công Ơn', 'on.duong@email.com', 'Trung', '2024-12-20'),
(13, 'Mai Thị Phượng', 'phuong.mai@email.com', 'Nam', '2025-01-08'),
(14, 'Huỳnh Thanh Quân', NULL, 'Bắc', '2025-02-14'),
(15, 'Tô Thị Rạng', 'rang.to@email.com', 'Trung', '2025-03-22');
-- 2. BẢNG PRODUCTS — Danh mục sản phẩm
CREATE TABLE products (
product_id INTEGER PRIMARY KEY,
product_name TEXT NOT NULL,
category TEXT,
price REAL,
stock_qty INTEGER
);
INSERT INTO products (product_id, product_name, category, price, stock_qty) VALUES
(101, 'Áo thun nam basic', 'Thời Trang', 250000, 120),
(102, 'Quần jean slim fit', 'Thời Trang', 450000, 80),
(103, 'Tai nghe Bluetooth', 'Điện Tử', 350000, 200),
(104, 'Sạc dự phòng 10000mAh', 'Điện Tử', 280000, 150),
(105, 'Nồi cơm điện 1.8L', 'Gia Dụng', 850000, 60),
(106, 'Bình giữ nhiệt 500ml', 'Gia Dụng', 180000, 300),
(107, 'Cà phê rang xay 500g', 'Thực Phẩm', 120000, 500),
(108, 'Trà oolong hộp 100 túi', 'Thực Phẩm', 95000, 400),
(109, 'Kem chống nắng SPF50', 'Mỹ Phẩm', 320000, 180),
(110, 'Sữa rửa mặt trà xanh', 'Mỹ Phẩm', 150000, 250),
(111, 'Laptop văn phòng 14"', 'Điện Tử', 12500000, 25),
(112, 'Bàn phím cơ gaming', 'Điện Tử', 1200000, 70);
-- 3. BẢNG ORDERS — Đơn hàng
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
order_date DATE,
status TEXT,
total_amount REAL,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
INSERT INTO orders (order_id, customer_id, order_date, status, total_amount) VALUES
(1001, 1, '2025-01-05', 'completed', 700000),
(1002, 2, '2025-01-12', 'completed', 1300000),
(1003, 3, '2025-01-20', 'completed', 350000),
(1004, 1, '2025-02-03', 'completed', 530000),
(1005, 4, '2025-02-14', 'completed', 850000),
(1006, 5, '2025-02-28', 'cancelled', 450000),
(1007, 6, '2025-03-05', 'completed', 280000),
(1008, 2, '2025-03-15', 'completed', 1850000),
(1009, 7, '2025-03-22', 'completed', 620000),
(1010, 8, '2025-04-01', 'completed', 350000),
(1011, 3, '2025-04-10', 'shipped', 950000),
(1012, 9, '2025-04-18', 'completed', 2400000),
(1013, 10, '2025-05-02', 'completed', 180000),
(1014, 1, '2025-05-15', 'completed', 12500000),
(1015, 11, '2025-05-20', 'cancelled', 320000),
(1016, 4, '2025-06-01', 'completed', 720000),
(1017, 12, '2025-06-12', 'completed', 215000),
(1018, 5, '2025-06-25', 'shipped', 1050000),
(1019, 13, '2025-07-03', 'completed', 470000),
(1020, 2, '2025-07-14', 'completed', 960000),
(1021, 14, '2025-07-28', 'completed', 350000),
(1022, 7, '2025-08-05', 'completed', 1680000),
(1023, 15, '2025-08-18', 'cancelled', 250000),
(1024, 9, '2025-08-22', 'completed', 540000),
(1025, 6, '2025-09-01', 'completed', 850000),
(1026, 10, '2025-09-15', 'completed', 3600000),
(1027, 3, '2025-09-28', 'completed', 150000),
(1028, 1, '2025-10-05', 'shipped', 1900000),
(1029, 8, '2025-10-18', 'completed', 630000),
(1030, 11, '2025-10-30', 'completed', 280000);
-- 4. BẢNG ORDER_ITEMS — Chi tiết từng sản phẩm trong đơn hàng
CREATE TABLE order_items (
item_id INTEGER PRIMARY KEY,
order_id INTEGER,
product_id INTEGER,
quantity INTEGER,
unit_price REAL,
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
INSERT INTO order_items (item_id, order_id, product_id, quantity, unit_price) VALUES
(1, 1001, 101, 2, 250000),
(2, 1001, 106, 1, 180000),
(3, 1002, 102, 2, 450000),
(4, 1002, 103, 1, 350000),
(5, 1003, 103, 1, 350000),
(6, 1004, 104, 1, 280000),
(7, 1004, 101, 1, 250000),
(8, 1005, 105, 1, 850000),
(9, 1006, 102, 1, 450000),
(10, 1007, 104, 1, 280000),
(11, 1008, 105, 1, 850000),
(12, 1008, 109, 2, 320000),
(13, 1008, 106, 1, 180000),
(14, 1009, 107, 3, 120000),
(15, 1009, 110, 1, 150000),
(16, 1009, 106, 1, 180000),
(17, 1010, 103, 1, 350000),
(18, 1011, 109, 1, 320000),
(19, 1011, 107, 2, 120000),
(20, 1011, 108, 3, 95000),
(21, 1012, 111, 1, 12500000),
(22, 1013, 106, 1, 180000),
(23, 1014, 111, 1, 12500000),
(24, 1015, 109, 1, 320000),
(25, 1016, 101, 1, 250000),
(26, 1016, 103, 1, 350000),
(27, 1016, 107, 1, 120000),
(28, 1017, 108, 1, 95000),
(29, 1017, 107, 1, 120000),
(30, 1018, 102, 1, 450000),
(31, 1018, 109, 1, 320000),
(32, 1018, 104, 1, 280000),
(33, 1019, 110, 2, 150000),
(34, 1019, 108, 1, 95000),
(35, 1020, 103, 2, 350000),
(36, 1020, 106, 1, 180000),
(37, 1021, 103, 1, 350000),
(38, 1022, 112, 1, 1200000),
(39, 1022, 101, 1, 250000),
(40, 1022, 108, 2, 95000),
(41, 1023, 101, 1, 250000),
(42, 1024, 104, 1, 280000),
(43, 1024, 110, 1, 150000),
(44, 1025, 105, 1, 850000),
(45, 1026, 111, 1, 12500000),
(46, 1027, 110, 1, 150000),
(47, 1028, 112, 1, 1200000),
(48, 1028, 102, 1, 450000),
(49, 1028, 101, 1, 250000),
(50, 1029, 107, 2, 120000),
(51, 1029, 106, 1, 180000),
(52, 1029, 108, 1, 95000),
(53, 1030, 104, 1, 280000);Tổng quan dữ liệu
| Bảng | Số dòng | Mô tả |
|---|---|---|
| customers | 15 | Khách hàng — 4 vùng: Bắc (5), Nam (4), Trung (3), Tây Nguyên (2), có 3 email NULL |
| products | 12 | Sản phẩm — 5 danh mục: Thời Trang, Điện Tử, Gia Dụng, Thực Phẩm, Mỹ Phẩm |
| orders | 30 | Đơn hàng — 01/2025 → 10/2025, status: completed/shipped/cancelled |
| order_items | 53 | Chi tiết đơn — liên kết order ↔ product, có quantity & unit_price |
⏱️ Thời lượng
| Phần | Thời gian | Nội dung |
|---|---|---|
| Setup & kiểm tra database | 10 phút | Tạo database trên DB Fiddle, chạy thử SELECT * |
| Bài tập 1: SELECT & Filtering | 25 phút | 5 query — SELECT, WHERE, ORDER BY, LIMIT, NULL |
| Bài tập 2: JOIN | 20 phút | 3 query — INNER JOIN, LEFT JOIN, multi-table JOIN |
| Bài tập 3: Aggregation | 25 phút | 4 query — GROUP BY, HAVING, monthly trend, subquery |
| Review & Feedback | 10 phút | Trình bày insight, nhận xét chéo |
| Tổng | 90 phút |
Bài tập 1: SELECT & Filtering — Truy vấn cơ bản (5 query)
Hướng dẫn
Mục tiêu: Thành thạo viết SELECT query với đầy đủ clause — WHERE lọc điều kiện, ORDER BY sắp xếp, LIMIT giới hạn kết quả, và xử lý giá trị NULL.
📌 Sau mỗi query, kiểm tra kết quả trả về có đúng với bảng kết quả mong đợi bên dưới.
Query 1: Xem toàn bộ khách hàng
Yêu cầu: Lấy tất cả thông tin từ bảng customers, sắp xếp theo customer_id.
sql
-- Query 1: Xem toàn bộ khách hàng
-- Mục đích: Làm quen với SELECT *, kiểm tra dữ liệu đã được tạo đúng
SELECT *
FROM customers
ORDER BY customer_id;Kết quả mong đợi (15 dòng):
| customer_id | name | region | signup_date | |
|---|---|---|---|---|
| 1 | Nguyễn Văn An | an.nguyen@email.com | Bắc | 2024-01-15 |
| 2 | Trần Thị Bình | binh.tran@email.com | Nam | 2024-02-20 |
| 3 | Lê Hoàng Cường | cuong.le@email.com | Trung | 2024-03-10 |
| ... | ... | ... | ... | ... |
| 15 | Tô Thị Rạng | rang.to@email.com | Trung | 2025-03-22 |
✅ Nếu thấy 15 dòng đầy đủ — database đã sẵn sàng!
Query 2: Lọc đơn hàng đã hoàn thành với doanh thu > 1 triệu
Yêu cầu: Lấy order_id, customer_id, order_date, total_amount từ bảng orders — chỉ những đơn có status = 'completed' VÀ total_amount > 1000000. Sắp xếp theo total_amount giảm dần.
sql
-- Query 2: Lọc đơn hàng completed có doanh thu > 1 triệu
-- Mục đích: Thực hành WHERE với nhiều điều kiện (AND) + ORDER BY DESC
SELECT order_id,
customer_id,
order_date,
total_amount
FROM orders
WHERE status = 'completed'
AND total_amount > 1000000
ORDER BY total_amount DESC;Kết quả mong đợi:
| order_id | customer_id | order_date | total_amount |
|---|---|---|---|
| 1014 | 1 | 2025-05-15 | 12500000 |
| 1026 | 10 | 2025-09-15 | 3600000 |
| 1012 | 9 | 2025-04-18 | 2400000 |
| 1008 | 2 | 2025-03-15 | 1850000 |
| 1022 | 7 | 2025-08-05 | 1680000 |
| 1002 | 2 | 2025-01-12 | 1300000 |
📌 6 đơn hàng có status completed với total > 1 triệu. Đơn lớn nhất là 12.5 triệu (Laptop).
Query 3: Top 5 đơn hàng gần nhất
Yêu cầu: Lấy 5 đơn hàng mới nhất (theo order_date), hiển thị order_id, order_date, status, total_amount.
sql
-- Query 3: Top 5 đơn hàng gần đây nhất
-- Mục đích: Thực hành ORDER BY + LIMIT
SELECT order_id,
order_date,
status,
total_amount
FROM orders
ORDER BY order_date DESC
LIMIT 5;Kết quả mong đợi:
| order_id | order_date | status | total_amount |
|---|---|---|---|
| 1030 | 2025-10-30 | completed | 280000 |
| 1029 | 2025-10-18 | completed | 630000 |
| 1028 | 2025-10-05 | shipped | 1900000 |
| 1027 | 2025-09-28 | completed | 150000 |
| 1026 | 2025-09-15 | completed | 3600000 |
Query 4: Tìm khách hàng chưa có email (NULL handling)
Yêu cầu: Lấy customer_id, name, region từ bảng customers — chỉ những khách có email IS NULL.
sql
-- Query 4: Tìm khách hàng thiếu email
-- Mục đích: Thực hành IS NULL — xử lý giá trị thiếu trong database
SELECT customer_id,
name,
region
FROM customers
WHERE email IS NULL;Kết quả mong đợi:
| customer_id | name | region |
|---|---|---|
| 8 | Bùi Quang Huy | Trung |
| 11 | Lý Thị Ngọc | Tây Nguyên |
| 14 | Huỳnh Thanh Quân | Bắc |
📌 3 khách hàng thiếu email. Trong thực tế, đây là data cần bổ sung — có thể liên hệ khách qua kênh khác hoặc tạo chiến dịch "Update profile" để thu thập email.
Query 5: Lọc sản phẩm thuộc danh mục Điện Tử hoặc Gia Dụng, giá từ 200K–1 triệu
Yêu cầu: Lấy product_name, category, price từ bảng products — chỉ sản phẩm thuộc danh mục 'Điện Tử' hoặc 'Gia Dụng' VÀ có price nằm trong khoảng 200,000 – 1,000,000. Sắp xếp theo price tăng dần.
sql
-- Query 5: Lọc sản phẩm Điện Tử / Gia Dụng, giá 200K–1M
-- Mục đích: Thực hành IN + BETWEEN — 2 clause lọc rất hay dùng
SELECT product_name,
category,
price
FROM products
WHERE category IN ('Điện Tử', 'Gia Dụng')
AND price BETWEEN 200000 AND 1000000
ORDER BY price ASC;Kết quả mong đợi:
| product_name | category | price |
|---|---|---|
| Sạc dự phòng 10000mAh | Điện Tử | 280000 |
| Tai nghe Bluetooth | Điện Tử | 350000 |
| Nồi cơm điện 1.8L | Gia Dụng | 850000 |
📌 3 sản phẩm phù hợp. Bình giữ nhiệt (180K) bị loại vì dưới 200K. Laptop (12.5M) và Bàn phím cơ (1.2M) bị loại vì vượt 1M.
✅ Checklist Bài tập 1
| # | Query | Số dòng kết quả | Đã hoàn thành? |
|---|---|---|---|
| 1 | SELECT * customers | 15 | ☐ |
| 2 | WHERE completed AND > 1M | 6 | ☐ |
| 3 | ORDER BY + LIMIT 5 | 5 | ☐ |
| 4 | IS NULL email | 3 | ☐ |
| 5 | IN + BETWEEN | 3 | ☐ |
Bài tập 2: JOIN — Kết nối bảng (3 query)
Hướng dẫn
Mục tiêu: Sử dụng JOIN để kết nối dữ liệu từ nhiều bảng — đây là kỹ năng cốt lõi của SQL. Tương tự VLOOKUP trong Excel nhưng mạnh hơn rất nhiều.
📌 Nhắc lại:
- INNER JOIN: chỉ lấy dòng có giá trị khớp ở CẢ HAI bảng
- LEFT JOIN: lấy TẤT CẢ dòng bên trái + dòng khớp bên phải (không khớp → NULL)
Query 6: INNER JOIN — Đơn hàng kèm tên khách hàng và khu vực
Yêu cầu: Kết nối bảng orders với customers qua customer_id. Hiển thị order_id, name (khách hàng), region, order_date, total_amount. Chỉ lấy đơn completed. Sắp xếp theo total_amount DESC.
sql
-- Query 6: INNER JOIN orders + customers
-- Mục đích: Kết nối 2 bảng để xem ai đặt đơn nào, ở vùng nào
SELECT o.order_id,
c.name AS customer_name,
c.region,
o.order_date,
o.total_amount
FROM orders o
INNER JOIN customers c
ON o.customer_id = c.customer_id
WHERE o.status = 'completed'
ORDER BY o.total_amount DESC;Kết quả mong đợi:
| order_id | customer_name | region | order_date | total_amount |
|---|---|---|---|---|
| 1014 | Nguyễn Văn An | Bắc | 2025-05-15 | 12500000 |
| 1026 | Trịnh Văn Mạnh | Bắc | 2025-09-15 | 3600000 |
| 1012 | Ngô Thị Lan | Nam | 2025-04-18 | 2400000 |
| 1008 | Trần Thị Bình | Nam | 2025-03-15 | 1850000 |
| 1022 | Đặng Thu Hà | Bắc | 2025-08-05 | 1680000 |
| 1002 | Trần Thị Bình | Nam | 2025-01-12 | 1300000 |
| 1020 | Trần Thị Bình | Nam | 2025-07-14 | 960000 |
| 1025 | Võ Đức Phong | Tây Nguyên | 2025-09-01 | 850000 |
| 1005 | Phạm Minh Dũng | Bắc | 2025-02-14 | 850000 |
| 1016 | Phạm Minh Dũng | Bắc | 2025-06-01 | 720000 |
| 1001 | Nguyễn Văn An | Bắc | 2025-01-05 | 700000 |
| 1029 | Bùi Quang Huy | Trung | 2025-10-18 | 630000 |
| 1009 | Đặng Thu Hà | Bắc | 2025-03-22 | 620000 |
| 1004 | Nguyễn Văn An | Bắc | 2025-02-03 | 530000 |
| 1019 | Mai Thị Phượng | Nam | 2025-07-03 | 470000 |
| 1024 | Ngô Thị Lan | Nam | 2025-08-22 | 540000 |
| 1010 | Bùi Quang Huy | Trung | 2025-04-01 | 350000 |
| 1003 | Lê Hoàng Cường | Trung | 2025-01-20 | 350000 |
| 1021 | Huỳnh Thanh Quân | Bắc | 2025-07-28 | 350000 |
| 1030 | Lý Thị Ngọc | Tây Nguyên | 2025-10-30 | 280000 |
| 1007 | Võ Đức Phong | Tây Nguyên | 2025-03-05 | 280000 |
| 1017 | Dương Công Ơn | Trung | 2025-06-12 | 215000 |
| 1013 | Trịnh Văn Mạnh | Bắc | 2025-05-02 | 180000 |
| 1027 | Lê Hoàng Cường | Trung | 2025-09-28 | 150000 |
📌 24 đơn hàng completed (loại 3 cancelled + 3 shipped). Nguyễn Văn An (Bắc) có đơn lớn nhất: 12.5M.
Query 7: LEFT JOIN — Tất cả sản phẩm, kể cả chưa được mua
Yêu cầu: Hiển thị tất cả sản phẩm từ bảng products, kèm tổng số lượng đã bán (SUM(quantity)) từ order_items. Sản phẩm chưa bán ra hiển thị 0. Sắp xếp theo số lượng bán giảm dần.
sql
-- Query 7: LEFT JOIN products + order_items
-- Mục đích: Xem sản phẩm nào bán chạy, sản phẩm nào chưa ai mua
-- LEFT JOIN giữ TẤT CẢ sản phẩm, kể cả chưa xuất hiện trong order_items
SELECT p.product_id,
p.product_name,
p.category,
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_id, p.product_name, p.category
ORDER BY total_sold DESC;Kết quả mong đợi:
| product_id | product_name | category | total_sold |
|---|---|---|---|
| 107 | Cà phê rang xay 500g | Thực Phẩm | 9 |
| 103 | Tai nghe Bluetooth | Điện Tử | 7 |
| 106 | Bình giữ nhiệt 500ml | Gia Dụng | 6 |
| 108 | Trà oolong hộp 100 túi | Thực Phẩm | 8 |
| 101 | Áo thun nam basic | Thời Trang | 6 |
| 104 | Sạc dự phòng 10000mAh | Điện Tử | 5 |
| 109 | Kem chống nắng SPF50 | Mỹ Phẩm | 5 |
| 110 | Sữa rửa mặt trà xanh | Mỹ Phẩm | 5 |
| 102 | Quần jean slim fit | Thời Trang | 5 |
| 105 | Nồi cơm điện 1.8L | Gia Dụng | 3 |
| 111 | Laptop văn phòng 14" | Điện Tử | 3 |
| 112 | Bàn phím cơ gaming | Điện Tử | 2 |
📌 Tất cả 12 sản phẩm đều đã bán ít nhất 1 lần. Cà phê rang xay và Trà oolong bán chạy nhất (hàng tiêu dùng nhanh, giá thấp, mua lặp lại).
Query 8: Multi-table JOIN — Chi tiết đơn hàng đầy đủ
Yêu cầu: Kết nối 3 bảng: orders + customers + order_items + products để tạo bảng chi tiết đầy đủ. Hiển thị: tên khách, ngày đặt, tên sản phẩm, danh mục, số lượng, đơn giá, thành tiền. Chỉ lấy đơn completed. Hiển thị 10 dòng đầu.
sql
-- Query 8: Multi-table JOIN — 4 bảng
-- Mục đích: Tạo bảng chi tiết bán hàng hoàn chỉnh bằng cách JOIN 4 bảng
-- orders → customers (ai mua?) → order_items (mua gì?) → products (thông tin SP?)
SELECT c.name AS customer_name,
c.region,
o.order_date,
p.product_name,
p.category,
oi.quantity,
oi.unit_price,
(oi.quantity * oi.unit_price) AS line_total
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE o.status = 'completed'
ORDER BY o.order_date, o.order_id
LIMIT 10;Kết quả mong đợi (10 dòng đầu):
| customer_name | region | order_date | product_name | category | quantity | unit_price | line_total |
|---|---|---|---|---|---|---|---|
| Nguyễn Văn An | Bắc | 2025-01-05 | Áo thun nam basic | Thời Trang | 2 | 250000 | 500000 |
| Nguyễn Văn An | Bắc | 2025-01-05 | Bình giữ nhiệt 500ml | Gia Dụng | 1 | 180000 | 180000 |
| Trần Thị Bình | Nam | 2025-01-12 | Quần jean slim fit | Thời Trang | 2 | 450000 | 900000 |
| Trần Thị Bình | Nam | 2025-01-12 | Tai nghe Bluetooth | Điện Tử | 1 | 350000 | 350000 |
| Lê Hoàng Cường | Trung | 2025-01-20 | Tai nghe Bluetooth | Điện Tử | 1 | 350000 | 350000 |
| Nguyễn Văn An | Bắc | 2025-02-03 | Sạc dự phòng 10000mAh | Điện Tử | 1 | 280000 | 280000 |
| Nguyễn Văn An | Bắc | 2025-02-03 | Áo thun nam basic | Thời Trang | 1 | 250000 | 250000 |
| Phạm Minh Dũng | Bắc | 2025-02-14 | Nồi cơm điện 1.8L | Gia Dụng | 1 | 850000 | 850000 |
| Võ Đức Phong | Tây Nguyên | 2025-03-05 | Sạc dự phòng 10000mAh | Điện Tử | 1 | 280000 | 280000 |
| Trần Thị Bình | Nam | 2025-03-15 | Nồi cơm điện 1.8L | Gia Dụng | 1 | 850000 | 850000 |
📌 Mỗi dòng là 1 sản phẩm trong 1 đơn hàng.
line_total = quantity × unit_price. Đây chính là bảng chi tiết sales — tương tự sheet "Sales Data" trong Excel mà bạn đã quen từ Buổi 3–4.
✅ Checklist Bài tập 2
| # | Query | Kỹ thuật JOIN | Đã hoàn thành? |
|---|---|---|---|
| 6 | Orders + Customers | INNER JOIN | ☐ |
| 7 | Products + Order_items | LEFT JOIN + COALESCE | ☐ |
| 8 | 4 bảng chi tiết | Multi-table INNER JOIN | ☐ |
Bài tập 3: Aggregation — Tổng hợp dữ liệu (4 query)
Hướng dẫn
Mục tiêu: Dùng GROUP BY + aggregate functions để phân tích doanh thu theo vùng, theo danh mục, theo tháng. Thực hành HAVING để lọc nhóm, và viết subquery.
📌 Nhắc lại aggregate functions:
COUNT(*)— đếm số dòngSUM(column)— tổng cộngAVG(column)— trung bìnhMIN(column)/MAX(column)— nhỏ nhất / lớn nhất
Query 9: Revenue by Region — Doanh thu theo khu vực
Yêu cầu: Tính tổng doanh thu (SUM), số đơn (COUNT), doanh thu trung bình mỗi đơn (AVG) theo từng region. Chỉ tính đơn completed. Sắp xếp theo tổng doanh thu giảm dần.
sql
-- Query 9: Revenue by Region
-- Mục đích: GROUP BY + nhiều aggregate functions — phân tích doanh thu theo vùng
SELECT c.region,
COUNT(o.order_id) AS total_orders,
SUM(o.total_amount) AS total_revenue,
ROUND(AVG(o.total_amount)) AS avg_order_value,
MIN(o.total_amount) AS min_order,
MAX(o.total_amount) AS max_order
FROM orders o
INNER JOIN customers c
ON o.customer_id = c.customer_id
WHERE o.status = 'completed'
GROUP BY c.region
ORDER BY total_revenue DESC;Kết quả mong đợi:
| region | total_orders | total_revenue | avg_order_value | min_order | max_order |
|---|---|---|---|---|---|
| Bắc | 10 | 21730000 | 2173000 | 180000 | 12500000 |
| Nam | 6 | 7520000 | 1253333 | 470000 | 2400000 |
| Trung | 4 | 1745000 | 436250 | 150000 | 630000 |
| Tây Nguyên | 4 | 1410000 | 352500 | 280000 | 850000 |
📌 Insight #1: Khu vực Bắc chiếm 67% tổng doanh thu (21.7M / 32.4M) với chỉ 10 đơn — AOV cao nhất do có khách mua Laptop 12.5M. Nếu loại đơn outlier này, AOV Bắc giảm về ~1M — gần ngang Nam.
Query 10: Doanh thu theo danh mục, lọc danh mục > 2 triệu (HAVING)
Yêu cầu: Tính doanh thu theo category (từ bảng order_items + products), chỉ hiển thị danh mục có tổng doanh thu > 2,000,000. Chỉ tính đơn completed.
sql
-- Query 10: Revenue by Category + HAVING
-- Mục đích: GROUP BY + HAVING — lọc kết quả sau khi gom nhóm
-- HAVING khác WHERE: WHERE lọc TRƯỚC khi gom nhóm, HAVING lọc SAU khi gom nhóm
SELECT p.category,
COUNT(DISTINCT o.order_id) AS total_orders,
SUM(oi.quantity) AS total_qty,
SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY p.category
HAVING total_revenue > 2000000
ORDER BY total_revenue DESC;Kết quả mong đợi:
| category | total_orders | total_qty | total_revenue |
|---|---|---|---|
| Điện Tử | 14 | 14 | 40960000 |
| Thời Trang | 7 | 8 | 4350000 |
| Gia Dụng | 8 | 8 | 3790000 |
| Mỹ Phẩm | 6 | 7 | 2350000 |
📌 Điện Tử chiếm ~73% doanh thu — chủ yếu nhờ 3 đơn Laptop (mỗi đơn 12.5M). Thực Phẩm không xuất hiện vì doanh thu < 2M (bị HAVING lọc ra).
Query 11: Monthly Revenue Trend — Xu hướng doanh thu theo tháng
Yêu cầu: Tính doanh thu và số đơn theo từng tháng (01/2025 → 10/2025). Chỉ tính đơn completed.
sql
-- Query 11: Monthly Revenue Trend
-- Mục đích: Phân tích xu hướng theo thời gian — dùng strftime() trong SQLite
-- strftime('%Y-%m', date) trích năm-tháng từ cột date
SELECT strftime('%Y-%m', o.order_date) AS month,
COUNT(o.order_id) AS total_orders,
SUM(o.total_amount) AS monthly_revenue,
ROUND(AVG(o.total_amount)) AS avg_order_value
FROM orders o
WHERE o.status = 'completed'
GROUP BY strftime('%Y-%m', o.order_date)
ORDER BY month;Kết quả mong đợi:
| month | total_orders | monthly_revenue | avg_order_value |
|---|---|---|---|
| 2025-01 | 3 | 2350000 | 783333 |
| 2025-02 | 2 | 1380000 | 690000 |
| 2025-03 | 3 | 2750000 | 916667 |
| 2025-04 | 2 | 2750000 | 1375000 |
| 2025-05 | 2 | 12680000 | 6340000 |
| 2025-06 | 2 | 935000 | 467500 |
| 2025-07 | 3 | 1780000 | 593333 |
| 2025-08 | 2 | 2220000 | 1110000 |
| 2025-09 | 3 | 4600000 | 1533333 |
| 2025-10 | 2 | 910000 | 455000 |
📌 Insight #2: Tháng 5/2025 có doanh thu đột biến (12.68M) do 1 đơn Laptop 12.5M. Nếu loại outlier, doanh thu tháng 5 chỉ 180K — thực tế là tháng thấp nhất. Tháng 9 có doanh thu ổn định cao (4.6M, 3 đơn) — có thể là mùa back-to-school.
Query 12: Khách hàng có tổng chi tiêu cao nhất (Subquery)
Yêu cầu: Tìm khách hàng có tổng total_amount (chỉ đơn completed) cao hơn trung bình chi tiêu của tất cả khách hàng. Sử dụng subquery.
sql
-- Query 12: Khách hàng chi tiêu trên trung bình — dùng subquery
-- Mục đích: Thực hành subquery trong HAVING
-- Bước 1: Subquery tính trung bình tổng chi tiêu mỗi khách
-- Bước 2: Query chính lọc khách có tổng chi tiêu > trung bình
SELECT c.customer_id,
c.name,
c.region,
COUNT(o.order_id) AS total_orders,
SUM(o.total_amount) AS total_spent
FROM orders o
INNER JOIN customers c
ON o.customer_id = c.customer_id
WHERE o.status = 'completed'
GROUP BY c.customer_id, c.name, c.region
HAVING total_spent > (
-- Subquery: tính trung bình tổng chi tiêu mỗi khách
SELECT AVG(customer_total)
FROM (
SELECT SUM(total_amount) AS customer_total
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
)
)
ORDER BY total_spent DESC;Kết quả mong đợi:
| customer_id | name | region | total_orders | total_spent |
|---|---|---|---|---|
| 1 | Nguyễn Văn An | Bắc | 3 | 13730000 |
| 2 | Trần Thị Bình | Nam | 3 | 4110000 |
| 10 | Trịnh Văn Mạnh | Bắc | 2 | 3780000 |
| 9 | Ngô Thị Lan | Nam | 2 | 2940000 |
| 7 | Đặng Thu Hà | Bắc | 2 | 2300000 |
📌 Insight #3: Chỉ 5/15 khách hàng (33%) có chi tiêu trên trung bình, nhưng nhóm này đóng góp ~83% tổng doanh thu. Đây là nhóm khách VIP — cần chương trình loyalty riêng. Nguyễn Văn An là khách #1 với 13.7M (chiếm 42% tổng doanh thu!).
✅ Checklist Bài tập 3
| # | Query | Kỹ thuật | Đã hoàn thành? |
|---|---|---|---|
| 9 | Revenue by Region | GROUP BY + SUM/COUNT/AVG | ☐ |
| 10 | Revenue by Category | GROUP BY + HAVING | ☐ |
| 11 | Monthly Trend | strftime + GROUP BY | ☐ |
| 12 | Top Customers | Subquery + HAVING | ☐ |
🌟 Bonus: Nâng cao (tùy chọn)
Dành cho bạn hoàn thành sớm hoặc muốn thử thách thêm. Không bắt buộc nhưng rất có giá trị trong thực tế.
Bonus 1: Top 2 sản phẩm bán chạy nhất mỗi danh mục (Top N per Group)
sql
-- Bonus 1: Top 2 sản phẩm mỗi category — dùng window function hoặc subquery
-- Trong SQLite, dùng cách tiếp cận ROW_NUMBER() (SQLite 3.25+)
SELECT *
FROM (
SELECT p.category,
p.product_name,
SUM(oi.quantity) AS total_sold,
ROW_NUMBER() OVER (
PARTITION BY p.category
ORDER BY SUM(oi.quantity) DESC
) AS rank_in_category
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY p.category, p.product_name
)
WHERE rank_in_category <= 2
ORDER BY category, rank_in_category;Kết quả mong đợi:
| category | product_name | total_sold | rank_in_category |
|---|---|---|---|
| Điện Tử | Tai nghe Bluetooth | 6 | 1 |
| Điện Tử | Sạc dự phòng 10000mAh | 4 | 2 |
| Gia Dụng | Bình giữ nhiệt 500ml | 5 | 1 |
| Gia Dụng | Nồi cơm điện 1.8L | 3 | 2 |
| Mỹ Phẩm | Kem chống nắng SPF50 | 4 | 1 |
| Mỹ Phẩm | Sữa rửa mặt trà xanh | 4 | 2 |
| Thời Trang | Áo thun nam basic | 5 | 1 |
| Thời Trang | Quần jean slim fit | 4 | 2 |
| Thực Phẩm | Trà oolong hộp 100 túi | 7 | 1 |
| Thực Phẩm | Cà phê rang xay 500g | 7 | 2 |
Bonus 2: Khách hàng mua cùng sản phẩm (Self JOIN concept)
sql
-- Bonus 2: Tìm cặp khách hàng cùng mua 1 sản phẩm
-- Dùng self-join trên order_items thông qua product_id
SELECT DISTINCT
c1.name AS customer_1,
c2.name AS customer_2,
p.product_name
FROM order_items oi1
INNER JOIN order_items oi2 ON oi1.product_id = oi2.product_id
AND oi1.order_id <> oi2.order_id
INNER JOIN orders o1 ON oi1.order_id = o1.order_id
INNER JOIN orders o2 ON oi2.order_id = o2.order_id
INNER JOIN customers c1 ON o1.customer_id = c1.customer_id
INNER JOIN customers c2 ON o2.customer_id = c2.customer_id
INNER JOIN products p ON oi1.product_id = p.product_id
WHERE c1.customer_id < c2.customer_id -- tránh trùng cặp A-B và B-A
ORDER BY p.product_name, c1.name
LIMIT 10;📌 Kỹ thuật này dùng trong recommendation system: "Khách mua sản phẩm X cũng mua sản phẩm Y" — nền tảng của collaborative filtering.
Bonus 3: COALESCE — Thay thế NULL bằng giá trị mặc định
sql
-- Bonus 3: Hiển thị danh sách khách hàng, thay NULL email bằng 'Chưa cung cấp'
-- COALESCE(value, default) — trả về value nếu không NULL, ngược lại trả default
SELECT customer_id,
name,
COALESCE(email, 'Chưa cung cấp') AS email,
region,
signup_date
FROM customers
ORDER BY customer_id;Kết quả mong đợi (trích):
| customer_id | name | region | signup_date | |
|---|---|---|---|---|
| 7 | Đặng Thu Hà | ha.dang@email.com | Bắc | 2024-07-30 |
| 8 | Bùi Quang Huy | Chưa cung cấp | Trung | 2024-08-14 |
| 9 | Ngô Thị Lan | lan.ngo@email.com | Nam | 2024-09-01 |
| ... | ... | ... | ... | ... |
| 11 | Lý Thị Ngọc | Chưa cung cấp | Tây Nguyên | 2024-11-05 |
| ... | ... | ... | ... | ... |
| 14 | Huỳnh Thanh Quân | Chưa cung cấp | Bắc | 2025-02-14 |
📌
COALESCErất hữu ích khi tạo báo cáo — thay NULL bằng text có nghĩa giúp dữ liệu dễ đọc hơn.
📋 Deliverable — Bài nộp
Yêu cầu nộp
| # | Nội dung | Format |
|---|---|---|
| 1 | File SQL — tất cả 12 query (có comment giải thích) | HoTen_Buoi05_SQL.sql |
| 2 | Bảng kết quả — screenshot hoặc copy kết quả 10 query chính | Paste vào file Word/Google Doc |
| 3 | 3 insight kinh doanh — rút ra từ kết quả query, có số liệu cụ thể | Viết trong cùng file Word/Google Doc |
Template file .sql
sql
-- =============================================
-- Workshop Buổi 5: Query cơ sở dữ liệu E-commerce
-- Họ tên: [Điền tên]
-- Ngày: [Điền ngày]
-- =============================================
-- === BÀI TẬP 1: SELECT & FILTERING ===
-- Query 1: Xem toàn bộ khách hàng
-- [Paste query ở đây]
-- Query 2: Lọc đơn hàng completed > 1 triệu
-- [Paste query ở đây]
-- Query 3: Top 5 đơn gần nhất
-- [Paste query ở đây]
-- Query 4: Khách hàng thiếu email (NULL)
-- [Paste query ở đây]
-- Query 5: Lọc sản phẩm IN + BETWEEN
-- [Paste query ở đây]
-- === BÀI TẬP 2: JOIN ===
-- Query 6: INNER JOIN orders + customers
-- [Paste query ở đây]
-- Query 7: LEFT JOIN products + order_items
-- [Paste query ở đây]
-- Query 8: Multi-table JOIN (4 bảng)
-- [Paste query ở đây]
-- === BÀI TẬP 3: AGGREGATION ===
-- Query 9: Revenue by Region
-- [Paste query ở đây]
-- Query 10: Revenue by Category + HAVING
-- [Paste query ở đây]
-- Query 11: Monthly Revenue Trend
-- [Paste query ở đây]
-- Query 12: Top Customers (subquery)
-- [Paste query ở đây]
-- === BONUS (tùy chọn) ===
-- [Paste query bonus nếu có]
-- === 3 INSIGHT ===
-- Insight 1: [Viết ở đây]
-- Insight 2: [Viết ở đây]
-- Insight 3: [Viết ở đây]3 Insight mẫu (tham khảo)
Insight 1 — Revenue tập trung ở khu vực Bắc: Khu vực Bắc đóng góp 67% tổng doanh thu (21.7M / 32.4M) với 10 đơn hàng completed. Tuy nhiên, nếu loại đơn Laptop 12.5M (outlier), AOV Bắc ~1M — tương đương khu vực Nam. → Kiến nghị: cần phân tích sâu hơn để tách biệt ảnh hưởng của đơn lớn (high-ticket items).
Insight 2 — Tháng 5 có doanh thu đột biến nhưng thiếu bền vững: Tháng 5/2025 đạt 12.68M — cao nhất 10 tháng, nhưng 98.6% đến từ 1 đơn Laptop. Tháng 9 (4.6M từ 3 đơn) có cấu trúc revenue healthy hơn. → Kiến nghị: đặt alert cho đơn > 5M để theo dõi riêng, tránh bị "skew" khi phân tích trend.
Insight 3 — 33% khách VIP tạo 83% doanh thu (quy luật Pareto): 5/15 khách hàng có tổng chi tiêu trên trung bình, đóng góp ~83% revenue. Nguyễn Văn An (13.7M, 42% revenue) là khách #1. → Kiến nghị: xây chương trình loyalty (tích điểm, ưu đãi riêng) cho top 5 khách, đồng thời tạo campaign "kích hoạt" 10 khách còn lại.
📝 Tiêu chí chấm
| Tiêu chí | Trọng số | Chi tiết |
|---|---|---|
| Query đúng cú pháp & chạy được | 30% | 12 query đều chạy không lỗi trên DB Fiddle |
| Kết quả chính xác | 30% | Output khớp với bảng kết quả mong đợi |
| Comment giải thích | 15% | Mỗi query có comment mô tả mục đích, kỹ thuật dùng |
| 3 Insight có chất lượng | 20% | Có số liệu cụ thể, có so sánh, có kiến nghị hành động |
| Bonus | 5% | Hoàn thành 1–3 query bonus |
| Tổng | 100% |
Rubric chi tiết
| Mức | Điểm | Mô tả |
|---|---|---|
| Xuất sắc | 9–10 | 12/12 query đúng, comment rõ ràng, 3 insight sâu sắc có số liệu + kiến nghị, có bonus |
| Tốt | 7–8 | 10–11/12 query đúng, comment đầy đủ, 3 insight có số liệu |
| Đạt | 5–6 | 8–9/12 query đúng, comment tối thiểu, 2 insight cơ bản |
| Chưa đạt | < 5 | < 8 query đúng hoặc thiếu insight |
💡 Tips & Best Practices
SQL Writing Style
sql
-- ✅ TỐT: Mỗi clause trên 1 dòng, camelCase cho alias, có comment
SELECT c.name AS customer_name,
c.region,
SUM(o.total_amount) AS total_revenue
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status = 'completed'
GROUP BY c.name, c.region
ORDER BY total_revenue DESC;
-- ❌ XẤU: Viết 1 dòng dài, không comment, không alias
SELECT name, region, sum(total_amount) FROM orders INNER JOIN customers ON orders.customer_id = customers.customer_id WHERE status = 'completed' GROUP BY name, region ORDER BY sum(total_amount) DESC;Thứ tự thực thi SQL (quan trọng!)
Thứ tự VIẾT: Thứ tự CHẠY:
1. SELECT 5. SELECT
2. FROM 1. FROM / JOIN
3. WHERE 2. WHERE
4. GROUP BY 3. GROUP BY
5. HAVING 4. HAVING
6. ORDER BY 6. ORDER BY
7. LIMIT 7. LIMIT📌 Đây là lý do bạn không thể dùng alias từ SELECT trong WHERE — vì WHERE chạy TRƯỚC SELECT!
Lỗi thường gặp
| Lỗi | Nguyên nhân | Cách sửa |
|---|---|---|
no such column | Sai tên cột hoặc chưa JOIN bảng chứa cột đó | Kiểm tra schema, thêm JOIN |
ambiguous column | 2 bảng có cùng tên cột, SQL không biết lấy bảng nào | Thêm prefix: o.customer_id thay vì customer_id |
not an aggregate | Cột trong SELECT không nằm trong GROUP BY và không có aggregate | Thêm cột vào GROUP BY hoặc bọc trong aggregate function |
| Kết quả trống | WHERE / HAVING lọc quá chặt, hoặc không có dữ liệu khớp | Kiểm tra lại điều kiện, thử bỏ bớt filter |
| Kết quả sai số dòng | JOIN tạo duplicate do quan hệ 1:N | Dùng COUNT(DISTINCT ...) hoặc kiểm tra lại logic JOIN |
🔗 Tài liệu tham khảo
- 📖 SQLite Documentation — Reference chính thức
- 🎮 SQLBolt — Bài tập tương tác từ cơ bản đến nâng cao
- 🎮 SQL Murder Mystery — Học SQL qua giải câu đố
- 📝 W3Schools SQL Tutorial — Tra cứu cú pháp nhanh
- 🛠 DB Fiddle — Viết & test SQL trực tuyến
- 📊 Mode SQL Tutorial — Bài tập SQL cho DA
🔗 Quay lại: Buổi 5: SQL — SELECT, JOIN & Aggregation