Skip to content

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

  1. 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
  2. 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
  3. 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
  4. 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ụLinkGhi chú
DB Fiddledb-fiddle.comOnline, không cần cài đặt — chọn SQLite
SQLite Onlinesqliteonline.comGiao diện đơn giản, phù hợp người mới
BigQuery Sandboxcloud.google.com/bigquery/docs/sandboxMiễn phí 1 TB/tháng — dùng nếu muốn trải nghiệm cloud

Setup trước buổi học

  1. Truy cập DB Fiddle (db-fiddle.com) → chọn SQLite v3.39
  2. 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)
  3. Click Run để tạo database
  4. Viết query ở ô Query SQL (bên phải) → click Run để xem kết quả
  5. 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ảngSố dòngMô tả
customers15Khách hàng — 4 vùng: Bắc (5), Nam (4), Trung (3), Tây Nguyên (2), có 3 email NULL
products12Sản phẩm — 5 danh mục: Thời Trang, Điện Tử, Gia Dụng, Thực Phẩm, Mỹ Phẩm
orders30Đơn hàng — 01/2025 → 10/2025, status: completed/shipped/cancelled
order_items53Chi tiết đơn — liên kết order ↔ product, có quantity & unit_price

⏱️ Thời lượng

PhầnThời gianNội dung
Setup & kiểm tra database10 phútTạo database trên DB Fiddle, chạy thử SELECT *
Bài tập 1: SELECT & Filtering25 phút5 query — SELECT, WHERE, ORDER BY, LIMIT, NULL
Bài tập 2: JOIN20 phút3 query — INNER JOIN, LEFT JOIN, multi-table JOIN
Bài tập 3: Aggregation25 phút4 query — GROUP BY, HAVING, monthly trend, subquery
Review & Feedback10 phútTrình bày insight, nhận xét chéo
Tổng90 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_idnameemailregionsignup_date
1Nguyễn Văn Anan.nguyen@email.comBắc2024-01-15
2Trần Thị Bìnhbinh.tran@email.comNam2024-02-20
3Lê Hoàng Cườngcuong.le@email.comTrung2024-03-10
...............
15Tô Thị Rạngrang.to@email.comTrung2025-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'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_idcustomer_idorder_datetotal_amount
101412025-05-1512500000
1026102025-09-153600000
101292025-04-182400000
100822025-03-151850000
102272025-08-051680000
100222025-01-121300000

📌 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_idorder_datestatustotal_amount
10302025-10-30completed280000
10292025-10-18completed630000
10282025-10-05shipped1900000
10272025-09-28completed150000
10262025-09-15completed3600000

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_idnameregion
8Bùi Quang HuyTrung
11Lý Thị NgọcTây Nguyên
14Huỳnh Thanh QuânBắ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_namecategoryprice
Sạc dự phòng 10000mAhĐiện Tử280000
Tai nghe BluetoothĐiện Tử350000
Nồi cơm điện 1.8LGia Dụng850000

📌 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

#QuerySố dòng kết quảĐã hoàn thành?
1SELECT * customers15
2WHERE completed AND > 1M6
3ORDER BY + LIMIT 55
4IS NULL email3
5IN + BETWEEN3

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_idcustomer_nameregionorder_datetotal_amount
1014Nguyễn Văn AnBắc2025-05-1512500000
1026Trịnh Văn MạnhBắc2025-09-153600000
1012Ngô Thị LanNam2025-04-182400000
1008Trần Thị BìnhNam2025-03-151850000
1022Đặng Thu HàBắc2025-08-051680000
1002Trần Thị BìnhNam2025-01-121300000
1020Trần Thị BìnhNam2025-07-14960000
1025Võ Đức PhongTây Nguyên2025-09-01850000
1005Phạm Minh DũngBắc2025-02-14850000
1016Phạm Minh DũngBắc2025-06-01720000
1001Nguyễn Văn AnBắc2025-01-05700000
1029Bùi Quang HuyTrung2025-10-18630000
1009Đặng Thu HàBắc2025-03-22620000
1004Nguyễn Văn AnBắc2025-02-03530000
1019Mai Thị PhượngNam2025-07-03470000
1024Ngô Thị LanNam2025-08-22540000
1010Bùi Quang HuyTrung2025-04-01350000
1003Lê Hoàng CườngTrung2025-01-20350000
1021Huỳnh Thanh QuânBắc2025-07-28350000
1030Lý Thị NgọcTây Nguyên2025-10-30280000
1007Võ Đức PhongTây Nguyên2025-03-05280000
1017Dương Công ƠnTrung2025-06-12215000
1013Trịnh Văn MạnhBắc2025-05-02180000
1027Lê Hoàng CườngTrung2025-09-28150000

📌 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_idproduct_namecategorytotal_sold
107Cà phê rang xay 500gThực Phẩm9
103Tai nghe BluetoothĐiện Tử7
106Bình giữ nhiệt 500mlGia Dụng6
108Trà oolong hộp 100 túiThực Phẩm8
101Áo thun nam basicThời Trang6
104Sạc dự phòng 10000mAhĐiện Tử5
109Kem chống nắng SPF50Mỹ Phẩm5
110Sữa rửa mặt trà xanhMỹ Phẩm5
102Quần jean slim fitThời Trang5
105Nồi cơm điện 1.8LGia Dụng3
111Laptop văn phòng 14"Điện Tử3
112Bà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_nameregionorder_dateproduct_namecategoryquantityunit_priceline_total
Nguyễn Văn AnBắc2025-01-05Áo thun nam basicThời Trang2250000500000
Nguyễn Văn AnBắc2025-01-05Bình giữ nhiệt 500mlGia Dụng1180000180000
Trần Thị BìnhNam2025-01-12Quần jean slim fitThời Trang2450000900000
Trần Thị BìnhNam2025-01-12Tai nghe BluetoothĐiện Tử1350000350000
Lê Hoàng CườngTrung2025-01-20Tai nghe BluetoothĐiện Tử1350000350000
Nguyễn Văn AnBắc2025-02-03Sạc dự phòng 10000mAhĐiện Tử1280000280000
Nguyễn Văn AnBắc2025-02-03Áo thun nam basicThời Trang1250000250000
Phạm Minh DũngBắc2025-02-14Nồi cơm điện 1.8LGia Dụng1850000850000
Võ Đức PhongTây Nguyên2025-03-05Sạc dự phòng 10000mAhĐiện Tử1280000280000
Trần Thị BìnhNam2025-03-15Nồi cơm điện 1.8LGia Dụng1850000850000

📌 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

#QueryKỹ thuật JOINĐã hoàn thành?
6Orders + CustomersINNER JOIN
7Products + Order_itemsLEFT JOIN + COALESCE
84 bảng chi tiếtMulti-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òng
  • SUM(column) — tổng cộng
  • AVG(column) — trung bình
  • MIN(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:

regiontotal_orderstotal_revenueavg_order_valuemin_ordermax_order
Bắc1021730000217300018000012500000
Nam6752000012533334700002400000
Trung41745000436250150000630000
Tây Nguyên41410000352500280000850000

📌 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:

categorytotal_orderstotal_qtytotal_revenue
Điện Tử141440960000
Thời Trang784350000
Gia Dụng883790000
Mỹ Phẩm672350000

📌 Đ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:

monthtotal_ordersmonthly_revenueavg_order_value
2025-0132350000783333
2025-0221380000690000
2025-0332750000916667
2025-04227500001375000
2025-052126800006340000
2025-062935000467500
2025-0731780000593333
2025-08222200001110000
2025-09346000001533333
2025-102910000455000

📌 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_idnameregiontotal_orderstotal_spent
1Nguyễn Văn AnBắc313730000
2Trần Thị BìnhNam34110000
10Trịnh Văn MạnhBắc23780000
9Ngô Thị LanNam22940000
7Đặng Thu HàBắc22300000

📌 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

#QueryKỹ thuậtĐã hoàn thành?
9Revenue by RegionGROUP BY + SUM/COUNT/AVG
10Revenue by CategoryGROUP BY + HAVING
11Monthly Trendstrftime + GROUP BY
12Top CustomersSubquery + 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:

categoryproduct_nametotal_soldrank_in_category
Điện TửTai nghe Bluetooth61
Điện TửSạc dự phòng 10000mAh42
Gia DụngBình giữ nhiệt 500ml51
Gia DụngNồi cơm điện 1.8L32
Mỹ PhẩmKem chống nắng SPF5041
Mỹ PhẩmSữa rửa mặt trà xanh42
Thời TrangÁo thun nam basic51
Thời TrangQuần jean slim fit42
Thực PhẩmTrà oolong hộp 100 túi71
Thực PhẩmCà phê rang xay 500g72

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_idnameemailregionsignup_date
7Đặng Thu Hàha.dang@email.comBắc2024-07-30
8Bùi Quang HuyChưa cung cấpTrung2024-08-14
9Ngô Thị Lanlan.ngo@email.comNam2024-09-01
...............
11Lý Thị NgọcChưa cung cấpTây Nguyên2024-11-05
...............
14Huỳnh Thanh QuânChưa cung cấpBắc2025-02-14

📌 COALESCE rấ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 dungFormat
1File SQL — tất cả 12 query (có comment giải thích)HoTen_Buoi05_SQL.sql
2Bảng kết quả — screenshot hoặc copy kết quả 10 query chínhPaste vào file Word/Google Doc
33 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 được30%12 query đều chạy không lỗi trên DB Fiddle
Kết quả chính xác30%Output khớp với bảng kết quả mong đợi
Comment giải thích15%Mỗi query có comment mô tả mục đích, kỹ thuật dùng
3 Insight có chất lượng20%Có số liệu cụ thể, có so sánh, có kiến nghị hành động
Bonus5%Hoàn thành 1–3 query bonus
Tổng100%

Rubric chi tiết

MứcĐiểmMô tả
Xuất sắc9–1012/12 query đúng, comment rõ ràng, 3 insight sâu sắc có số liệu + kiến nghị, có bonus
Tốt7–810–11/12 query đúng, comment đầy đủ, 3 insight có số liệu
Đạt5–68–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ỗiNguyên nhânCách sửa
no such columnSai tên cột hoặc chưa JOIN bảng chứa cột đóKiểm tra schema, thêm JOIN
ambiguous column2 bảng có cùng tên cột, SQL không biết lấy bảng nàoThêm prefix: o.customer_id thay vì customer_id
not an aggregateCột trong SELECT không nằm trong GROUP BY và không có aggregateThêm cột vào GROUP BY hoặc bọc trong aggregate function
Kết quả trốngWHERE / HAVING lọc quá chặt, hoặc không có dữ liệu khớpKiểm tra lại điều kiện, thử bỏ bớt filter
Kết quả sai số dòngJOIN tạo duplicate do quan hệ 1:NDùng COUNT(DISTINCT ...) hoặc kiểm tra lại logic JOIN

🔗 Tài liệu tham khảo


🔗 Quay lại: Buổi 5: SQL — SELECT, JOIN & Aggregation