[DA & DE Blog #62] Làm Chủ SQL Phân Tích Thực Chiến Từ Con Số 0: Thứ Tự Thực Thi Câu Lệnh, Phân Biệt Rạch Ròi WHERE vs. HAVING, Bản Chất Các Phép JOIN & Hàm COALESCE
![[DA & DE Blog #62] Làm Chủ SQL Phân Tích Thực Chiến Từ Con Số 0: Thứ Tự Thực Thi Câu Lệnh, Phân Biệt Rạch Ròi WHERE vs. HAVING, Bản Chất Các Phép JOIN & Hàm COALESCE](/uploads/ai-images/cover-sql-phan-tich-thuc-chien.png)
1. Bối cảnh thực tế (Context & Problem Statement)
Rất nhiều bạn mới học SQL thường gặp phải những lỗi cú pháp (Syntax Error) kỳ quặc khiến bản thân cảm thấy bế tắc:
- Bạn vừa đặt tên bí danh cho một cột:
SELECT amount * 0.1 AS discount ...nhưng khi xuống mệnh đềWHERE discount > 50thì Database lập tức báo lỗi:Column 'discount' does not exist! - Bạn muốn lọc các phòng ban có hơn 10 nhân viên bằng câu lệnh:
WHERE COUNT(employee_id) > 10và Database lại báo lỗi:Aggregate functions not allowed in WHERE! - Bạn thực hiện phép
LEFT JOINđể lấy toàn bộ danh sách khách hàng kể cả người chưa mua hàng, nhưng sau khi thêm một điều kiện lọc ởWHERE, kết quả lại bị mất sạch những người chưa mua, biếnLEFT JOINthànhINNER JOINmột cách khó hiểu!
Tất cả những lỗi trên đều bắt nguồn từ một sự thật cốt lõi: Thứ tự bạn viết câu lệnh SQL (Lexical Order) hoàn toàn khác biệt với Thứ tự mà Database Engine thực sự tính toán (Logical Execution Order).
2. Các Khái Niệm & Cơ Chế Cốt Lõi
2.1. Bản Đồ Thứ Tự Thực Thi Của Câu Lệnh SQL (SQL Logical Order of Execution)
Một câu lệnh SQL hoàn chỉnh thường được viết theo thứ tự:
SELECT FROM JOIN WHERE GROUP BY HAVING ORDER BY LIMIT.
Tuy nhiên, bên dưới động cơ cơ sở dữ liệu, thứ tự tính toán thực tế lại diễn ra qua 8 bước tuần tự chặt chẽ:
+-----------------------------------------------------------------------------------+
| THỨ TỰ THỰC THI THỰC TẾ TRONG DATABASE (LOGICAL ORDER OF EXECUTION) |
+-----------------------------------------------------------------------------------+
Bước 1: FROM & JOIN ---> Tìm và ghép các bảng dữ liệu nguồn lại với nhau
Bước 2: WHERE ---> Lọc từng dòng dữ liệu thô thỏa mãn điều kiện
Bước 3: GROUP BY ---> Gom các dòng dữ liệu vào các nhóm (ví dụ theo Phòng ban)
Bước 4: HAVING ---> Lọc bỏ các nhóm không thỏa mãn tiêu chí tổng hợp
Bước 5: SELECT ---> Chỉ định các cột cần lấy và tính toán biểu thức (Đặt Alias)
Bước 6: DISTINCT ---> Loại bỏ các dòng bị trùng lặp kết quả
Bước 7: ORDER BY ---> Sắp xếp kết quả tăng dần (ASC) hoặc giảm dần (DESC)
Bước 8: LIMIT / OFFSET---> Cắt lấy số lượng dòng hiển thị cuối cùng
💡 Giải mã câu đố:
Tại sao không dùng được Alias ởWHERE?
VìWHEREchạy ở Bước 2, trong khiSELECT(nơi đặt Alias) mãi tận Bước 5 mới chạy! Tại Bước 2, Database hoàn toàn chưa biết cái tên Alias đó là gì!
2.2. Phân Biệt Tuyệt Đối: WHERE vs. HAVING
- Mệnh đề
WHERE(Lọc trước khi gom nhóm):- Chạy ở Bước 2.
- Tác động lên từng dòng dữ liệu đơn lẻ (Row-level) trước khi bất kỳ phép gom nhóm nào diễn ra.
- Tuyệt đối không được chứa các hàm tổng hợp như
SUM(),COUNT(),AVG(),MAX(),MIN().
- Mệnh đề
HAVING(Lọc sau khi gom nhóm):- Chạy ở Bước 4 (ngay sau
GROUP BY). - Tác động lên cả một nhóm dữ liệu tổng hợp (Group-level).
- Bắt buộc phải đi kèm hàm tổng hợp hoặc cột nằm trong
GROUP BY.
- Chạy ở Bước 4 (ngay sau
Tập dữ liệu thô ---> [ Lọc WHERE: Chỉ lấy đơn năm 2026 ] ---> [ GROUP BY: Gom theo Khách hàng ]
|
v
[ Danh sách cuối cùng ] <--- [ Lọc HAVING: Chỉ lấy khách có SUM(tiền) > 10M ]
2.3. Bản Chất Trực Quan Của Các Phép JOIN
Giả sử có 2 bảng: Bảng bên trái là Khach_Hang (chứa 10 người), Bảng bên phải là Don_Hang (chứa các đơn mua).
1. INNER JOIN (Giao tập hợp):
Chỉ giữ lại những dòng có xuất hiện ở CẢ HAI BẢNG.
(Khách nào có mua hàng thì mới hiện; khách chưa mua bị loại bỏ hoàn toàn).
2. LEFT JOIN (Ưu tiên bảng trái):
Giữ lại 100% khách hàng ở Bảng Trái.
- Nếu khách có mua hàng: Hiển thị thông tin đơn hàng.
- Nếu khách chưa từng mua: Các cột thông tin đơn hàng sẽ hiển thị là NULL.
3. RIGHT JOIN (Ưu tiên bảng phải):
Giữ lại 100% dữ liệu ở Bảng Phải (Ít dùng, vì thường đổi vị trí bảng và dùng LEFT JOIN cho thuận mắt).
4. FULL OUTER JOIN (Hợp nhất toàn bộ):
Lấy tất cả mọi dòng từ cả 2 bảng. Chỗ nào không khớp sẽ điền NULL.
2.4. Phân Nhánh Logic Bằng CASE WHEN & Đếm Có Điều Kiện (Conditional Aggregation)
Hàm CASE WHEN tương đương với cấu trúc IF - ELSE IF - ELSE trong lập trình, giúp bạn biến đổi dữ liệu mềm dẻo ngay trong câu query:
CASE
WHEN diem_so >= 8.5 THEN 'Giỏi'
WHEN diem_so >= 6.5 THEN 'Khá'
ELSE 'Trung bình'
END AS xep_loai
Kỹ thuật Đỉnh Cao: Đếm có điều kiện (COUNT(CASE WHEN ...)):
Thay vì phải viết nhiều câu query rồi dùng UNION, bạn có thể đếm nhiều nhóm tiêu chí khác nhau trong cùng 1 câu lệnh duy nhất:
SELECT
COUNT(order_id) AS tong_so_don,
-- Đếm số đơn hoàn thành:
COUNT(CASE WHEN status = 'COMPLETED' THEN order_id END) AS don_thanh_cong,
-- Đếm số đơn bị hủy:
COUNT(CASE WHEN status = 'CANCELLED' THEN order_id END) AS don_bi_huy
FROM orders;
2.5. Xử Lý Giá Trị NULL & Hàm Cứu Cánh COALESCE()
Trong SQL, NULL không phải là số 0, cũng không phải là chuỗi rỗng "". NULL đại diện cho "Không biết" hoặc "Chưa có thông tin".
- Cạm bẫy toán học của
NULL(Tam trị luận - Three-Valued Logic):- Trong SQL, phép so sánh:
NULL = NULLsẽ không trả vềTRUEmà trả vềUNKNOWN! - Do đó, câu lệnh:
WHERE email = NULLsẽ luôn luôn không trả về dòng nào cả! - Cách viết chuẩn xác duy nhất: Luôn dùng toán tử
IS NULLhoặcIS NOT NULL.
- Trong SQL, phép so sánh:
- Hàm
COALESCE(val1, val2, val3, ...):- Trả về giá trị đầu tiên khác
NULLtrong danh sách truyền vào. - Ứng dụng thực tế: Khi
LEFT JOINbảng khách hàng với bảng đơn hàng, khách chưa mua sẽ cóSUM(amount) IS NULL. Sử dụngCOALESCE(SUM(amount), 0)sẽ tự động thay thế sốNULLxấu xí thành số0tròn trĩnh!
- Trả về giá trị đầu tiên khác
3. Các Bảng Markdown So Sánh Chi Tiết
📊 Bảng 1: Thứ Tự Viết Code SQL vs. Thứ Tự Database Thực Thi
| Thứ tự viết code (Lexical Order) | Thứ tự Database thực thi (Logical Order) | Nhiệm vụ của từng bước |
|---|---|---|
1. SELECT | Bước 1: FROM & JOIN | Nạp bảng nguồn và kết nối các bảng theo điều kiện ON |
2. FROM | Bước 2: WHERE | Quét và lọc từng dòng dữ liệu thô trước khi gom nhóm |
3. JOIN ... ON | Bước 3: GROUP BY | Gom các dòng dữ liệu có cùng giá trị vào một nhóm |
4. WHERE | Bước 4: HAVING | Lọc bỏ các nhóm không thỏa mãn điều kiện hàm tổng hợp |
5. GROUP BY | Bước 5: SELECT | Trích xuất các cột cần lấy, tính toán biểu thức, gán Alias |
6. HAVING | Bước 6: DISTINCT | Loại bỏ các dòng kết quả bị trùng lặp |
7. ORDER BY | Bước 7: ORDER BY | Sắp xếp thứ tự các dòng (Ở bước này ĐÃ dùng được Alias!) |
8. LIMIT | Bước 8: LIMIT | Lấy ra đúng số lượng dòng hiển thị mong muốn |
📊 Bảng 2: So Sánh Toàn Diện Mệnh Đề WHERE vs. HAVING
| Tiêu chí | Mệnh đề WHERE | Mệnh đề HAVING |
|---|---|---|
| Thời điểm thực thi | Chạy TRƯỚC khi gom nhóm (Bước 2) | Chạy SAU khi gom nhóm (Bước 4) |
| Đối tượng tác động | Từng dòng dữ liệu riêng lẻ (Row-level) | Cả một nhóm dữ liệu tổng hợp (Group-level) |
Chứa hàm tổng hợp (SUM, COUNT)? | TUYỆT ĐỐI KHÔNG | CÓ HỖ TRỢ (Ví dụ: HAVING COUNT(*) > 5) |
Đi kèm mệnh đề GROUP BY? | Có thể dùng độc lập, không cần GROUP BY | Thường bắt buộc phải đi kèm với GROUP BY |
| Tác động hiệu năng truy vấn | Rất nhanh (Lọc sớm giúp giảm bớt dữ liệu cần gom nhóm) | Chậm hơn (Database phải gom nhóm xong xuôi rồi mới lọc) |
📊 Bảng 3: Ma Trận So Sánh Các Phép JOIN Căn Bản
| Loại JOIN | Kết quả trả về | Xử lý các dòng không khớp (Unmatched Rows) | Ví dụ ứng dụng thực tế |
|---|---|---|---|
INNER JOIN | Chỉ các dòng khớp nhau ở cả 2 bảng | Loại bỏ hoàn toàn khỏi kết quả | Tìm danh sách các đơn hàng đã có người mua cụ thể |
LEFT JOIN | Toàn bộ bảng Trái + Phần khớp ở bảng Phải | Điền NULL cho các cột của bảng Phải nếu không khớp | Tìm danh sách khách hàng chưa mua đơn hàng nào |
RIGHT JOIN | Phần khớp ở bảng Trái + Toàn bộ bảng Phải | Điền NULL cho các cột của bảng Trái nếu không khớp | Ít dùng (Có thể đảo vị trí bảng và dùng LEFT JOIN) |
FULL JOIN | Toàn bộ dữ liệu của cả 2 bảng | Điền NULL cho bất kỳ bên nào bị thiếu | Đối soát dữ liệu giữa 2 hệ thống kế toán độc lập |
4. Thực Hành Thực Chiến: Bài Tập SQL Phân Tích Thực Tế (Walkthrough)
📌 Tập dữ liệu mẫu:
Cho 2 bảng trong cơ sở dữ liệu bán hàng:
- Bảng
customers:customer_id,customer_name,city - Bảng
orders:order_id,customer_id,order_date,total_amount,status('COMPLETED','CANCELLED')
💻 Bài Toán 1: Tìm các khách hàng ở "Hà Nội" đã chi tiêu tổng cộng trên 10 triệu đồng trong năm 2026:
SELECT
c.customer_id,
c.customer_name,
SUM(o.total_amount) AS tong_chi_tieu
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
-- Bước 2 (WHERE): Lọc dòng thô trước - Chỉ lấy khách Hà Nội, đơn hoàn thành năm 2026:
WHERE c.city = 'Hà Nội'
AND o.status = 'COMPLETED'
AND o.order_date >= '2026-01-01' AND o.order_date < '2027-01-01'
-- Bước 3 (GROUP BY): Gom nhóm theo từng khách hàng:
GROUP BY c.customer_id, c.customer_name
-- Bước 4 (HAVING): Lọc nhóm sau khi tính toán - Chỉ lấy tổng tiền > 10 triệu:
HAVING SUM(o.total_amount) > 10000000
-- Bước 7 (ORDER BY): Sắp xếp người chi nhiều nhất lên đầu:
ORDER BY tong_chi_tieu DESC;
💻 Bài Toán 2: Tìm danh sách các khách hàng chưa từng mua bất kỳ đơn hàng nào (Lead tồn kho):
SELECT
c.customer_id,
c.customer_name,
c.city
FROM customers c
-- Dùng LEFT JOIN để giữ lại toàn bộ khách hàng:
LEFT JOIN orders o ON c.customer_id = o.customer_id
-- Mấu chốt kỹ thuật: Nếu khách chưa mua, cột khóa ngoại của bảng orders sẽ là NULL:
WHERE o.order_id IS NULL;
💻 Bài Toán 3: Báo cáo tổng hợp số lượng đơn thành công, số đơn hủy và phân loại khách hàng bằng COALESCE và CASE WHEN:
SELECT
c.customer_id,
c.customer_name,
-- Dùng COALESCE để nếu khách chưa có đơn thì hiển thị 0đ thay vì hiển thị NULL:
COALESCE(SUM(o.total_amount), 0) AS tong_tien,
-- Đếm có điều kiện:
COUNT(CASE WHEN o.status = 'COMPLETED' THEN o.order_id END) AS so_don_thanh_cong,
COUNT(CASE WHEN o.status = 'CANCELLED' THEN o.order_id END) AS so_don_huy,
-- Phân loại giá trị khách hàng:
CASE
WHEN COALESCE(SUM(o.total_amount), 0) >= 20000000 THEN 'Khách Hàng VIP'
WHEN COALESCE(SUM(o.total_amount), 0) >= 5000000 THEN 'Khách Tiềm Năng'
ELSE 'Khách Tiêu Chuẩn'
END AS phan_hang_khach
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY tong_tien DESC;
5. Góc Ôn Luyện Phỏng Vấn (Interview Corner)
❓ Câu hỏi 1: Tại sao bạn không thể sử dụng bí danh (Alias) được khai báo trong mệnh đề SELECT để làm điều kiện lọc ở mệnh đề WHERE? Nhưng tại sao ở mệnh đề ORDER BY thì lại dùng được bình thường?
- Gợi ý trả lời ngắn gọn, chuẩn xác:
- Giải thích bằng Thứ tự thực thi logic của SQL:
- Mệnh đề
WHEREđược Database thực thi ở Bước 2, trong khi mệnh đềSELECT(nơi định nghĩa các cột mới và đặt tên bí danh Alias) mãi đến Bước 5 mới được tính toán. - Do đó, tại thời điểm
WHEREđang quét các dòng dữ liệu thô, tên bí danh Alias đó hoàn toàn chưa tồn tại trong bộ nhớ của Database Engine, dẫn đến lỗiColumn does not exist. - Ngược lại, mệnh đề
ORDER BYđược thực thi ở Bước 7 (nằm sauSELECT). Tại thời điểm sắp xếp, Database đã hoàn thành việc tính toán biểu thức và đã ghi nhận đầy đủ các tên bí danh Alias, do đó chúng ta có thể sử dụng Alias trongORDER BYmột cách hoàn toàn hợp lệ.
- Mệnh đề
- Giải thích bằng Thứ tự thực thi logic của SQL:
❓ Câu hỏi 2: Sự khác nhau bản chất giữa WHERE và HAVING trong SQL là gì? Cho ví dụ một câu truy vấn thực tế bắt buộc phải sử dụng đồng thời cả WHERE lẫn HAVING.
- Gợi ý trả lời:
- Sự khác biệt bản chất:
WHERElà bộ lọc áp dụng trên từng dòng dữ liệu đơn lẻ (Row-level) trước khi dữ liệu được đưa vào gom nhóm.WHEREkhông bao giờ được chứa các hàm tổng hợp nhưSUM(),COUNT(),AVG().HAVINGlà bộ lọc áp dụng trên từng nhóm dữ liệu đã tổng hợp (Group-level) sau khi thực hiệnGROUP BY.HAVINGchuyên dùng để lọc các giá trị tính toán từ hàm tổng hợp.
- Ví dụ bắt buộc dùng cả hai:
- Bài toán: Tìm các cửa hàng có doanh thu từ các mặt hàng điện tử trong tháng 8 đạt trên 500 triệu:
SELECT store_id, SUM(amount) AS dt_dientu FROM sales -- Dùng WHERE để lọc sớm các dòng thô: Chỉ lấy ngành hàng Điện tử trong Tháng 8: WHERE category = 'Điện tử' AND sale_date BETWEEN '2026-08-01' AND '2026-08-31' GROUP BY store_id -- Dùng HAVING để lọc trên kết quả tổng cộng sau khi gom nhóm theo cửa hàng: HAVING SUM(amount) > 500000000;
- Bài toán: Tìm các cửa hàng có doanh thu từ các mặt hàng điện tử trong tháng 8 đạt trên 500 triệu:
- Sự khác biệt bản chất:
❓ Câu hỏi 3: Khi thực hiện phép LEFT JOIN, một lập trình viên đặt điều kiện lọc ở mệnh đề WHERE thay vì mệnh đề ON. Tại sao điều này lại vô tình biến câu lệnh LEFT JOIN thành INNER JOIN? Cho ví dụ minh họa.
- Gợi ý trả lời:
- Cơ chế hoạt động:
- Khi thực hiện
LEFT JOIN, những dòng ở bảng bên trái không tìm thấy bản ghi khớp ở bảng bên phải sẽ được giữ lại, và các cột của bảng bên phải sẽ được điền giá trịNULL. - Mệnh đề
ONquy định điều kiện để ghép nối dữ liệu giữa 2 bảng trong quá trình JOIN (Bước 1). - Mệnh đề
WHEREdiễn ra sau đó (Bước 2) để lọc trên tập kết quả vừa ghép xong.
- Khi thực hiện
- Cạm bẫy biến thành
INNER JOIN:- Giả sử ta muốn lấy tất cả khách hàng và các đơn hàng có trạng thái
'COMPLETED':-- ❌ CÁCH VIẾT SAI (Biến thành INNER JOIN): SELECT c.customer_id, c.customer_name, o.order_id FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id WHERE o.status = 'COMPLETED'; - Hiện tượng: Với những khách hàng chưa từng mua hàng, cột
o.statuscủa họ nhận giá trị làNULL. Khi mệnh đềWHERE o.status = 'COMPLETED'chạy, phép so sánhNULL = 'COMPLETED'trả vềUNKNOWNToàn bộ các khách hàng chưa mua hàng bị loại bỏ sạch sẽ! Câu lệnh vô tình hoạt động hệt như một phépINNER JOIN.
- Giả sử ta muốn lấy tất cả khách hàng và các đơn hàng có trạng thái
- Cách sửa đúng: Chuyển điều kiện lọc vào thẳng mệnh đề
ONcủaLEFT JOIN:-- ✅ CÁCH VIẾT ĐÚNG: SELECT c.customer_id, c.customer_name, o.order_id FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id AND o.status = 'COMPLETED';
- Cơ chế hoạt động:
6. Tóm Tắt Ghi Nhớ Nhanh (Key Takeaways)
- Nhớ nằm lòng Thứ tự thực thi SQL:
FROM/JOINWHEREGROUP BYHAVINGSELECTORDER BYLIMIT. WHERElọc từng dòng thô (chạy trước, không dùng hàm tổng hợp);HAVINGlọc cả nhóm (chạy sau, chuyên dùng choSUM/COUNT).- Dùng
LEFT JOINkết hợpWHERE right_table.id IS NULLđể tìm các bản ghi không có giao dịch phát sinh. - Trong SQL,
NULL = NULLlà vô nghĩa; luôn dùng toán tửIS NULLvà hàmCOALESCE(col, default_val)để thay thế giá trị mặc định. - Kỹ thuật
COUNT(CASE WHEN ... THEN ... END)là vũ khí tối thượng để đếm nhiều điều kiện kinh doanh trong 1 câu truy vấn duy nhất.