Mệnh đề WHERE
Xem thửTình huống mở đầu
Cuối tháng 3, Hạnh bên kế toán cần tổng doanh thu các đơn đã thanh toán của cả tháng để đối chiếu sổ sách. Bảng orders có 10 đơn, cột placed_at ghi cả ngày lẫn giờ. Hạnh viết WHERE status = 'paid' AND placed_at BETWEEN '2026-03-01' AND '2026-03-31' và nhận tổng 2.780.000. Kế toán trưởng đối chiếu thì thấy sổ ghi 3.230.000. Lệch đúng 450.000, và không có dòng lỗi nào hiện ra.
Hãy đoán trước: đơn nào đã rơi mất, và vì sao một điều kiện nhìn rất đúng lại làm rơi nó? Cuối bài bạn sẽ thấy cách viết khoảng thời gian không bao giờ bị lệch kiểu này.
Ôn nhanh bài trước: DELETE đơn cha khi còn món con trỏ tới, với khóa ngoại mặc định, cho lỗi mã nào?
Đáp án: lỗi 1451, và không dòng nào bị xóa. Bài này quay lại thứ đứng sau cả SELECT, UPDATE và DELETE: bộ lọc WHERE.
Mục tiêu bài học
Sau bài này bạn có thể:
- Giải thích
WHEREgiữ dòng khi điều kiện choTRUE, và loại dòng khi choFALSEhoặcNULL. - Dùng cùng một
WHEREchoSELECT,UPDATEvàDELETE, và xem trước bằngSELECTtrước khi sửa hoặc xóa. - Phân biệt
WHERE(lọc dòng, trước khi gom nhóm) vớiHAVING(lọc nhóm, sau khi gom). - Viết khoảng ngày giờ bằng điều kiện nửa mở, và tránh bọc cột trong hàm khi lọc.
Bức tranh lớn
Xuyên suốt bài, WHERE là cái sàng đặt ở cổng kho: mỗi dòng của bảng đi qua một lần, lọt lỗ thì được giữ, không lọt thì bị loại. Cái sàng này chỉ là một, nhưng có ba người đứng sau nó: người chỉ nhìn (SELECT), người sửa nhãn (UPDATE) và người dọn đi (DELETE). Dự án xuyên suốt là báo cáo cuối tháng 3 của Bean Corner với bảng orders có cột placed_at kiểu DATETIME.
Hình dưới trả lời một câu hỏi: điều kiện của WHERE quyết định số phận của một dòng như thế nào?
Đang tải sơ đồ…
Cách đọc hình: đi từ ô xanh dương xuống ô cam, là điều kiện được thử trên từng dòng. Chỉ nhánh TRUE dẫn đến ô xanh lá: dòng được giữ. Nhánh FALSE hoặc NULL đều dẫn đến ô đỏ: dòng bị loại, không phân biệt. Ô tím cuối cùng nói rằng tập dòng được giữ là đầu vào chung của ba lệnh.
Chỗ ẩn dụ không còn đúng: cái sàng thật làm hỏng thứ lọt qua nó, còn WHERE chỉ chọn, và chính lệnh đứng sau mới quyết định làm gì với các dòng được chọn.
Khái niệm cốt lõi
Bài này có bốn khái niệm.
1. WHERE giữ dòng khi điều kiện là TRUE
Với mỗi dòng, điều kiện cho ra TRUE, FALSE hoặc NULL. Chỉ TRUE được giữ. Điều kiện chứa NULL thường ra NULL, nên so sánh coupon <> 'SPRING' bỏ sót dòng có coupon là NULL.
2. Một bộ lọc cho ba lệnh
SELECT, UPDATE và DELETE dùng cùng cú pháp WHERE. Nếu bạn gõ cùng một điều kiện, bạn nhận cùng một tập dòng. Vì vậy cách an toàn nhất là viết điều kiện với SELECT trước, kiểm tra tập dòng, rồi chép nguyên vào UPDATE hoặc DELETE.
3. WHERE lọc dòng, HAVING lọc nhóm
WHERE chạy trước GROUP BY, nên chỉ nhìn thấy từng dòng và không dùng được COUNT hay SUM của nhóm. HAVING chạy sau khi gom nhóm, nên dùng để lọc theo kết quả tổng hợp.
4. Viết khoảng thời gian cho đúng
Với cột ngày giờ, dùng khoảng nửa mở: placed_at >= đầu_kỳ AND placed_at < đầu_kỳ_sau. Đừng bọc cột trong hàm như MONTH(placed_at) = 3, vì chỉ mục trên cột đó sẽ không dùng được.
Ví dụ đầu tiên
Đầu tiên, dựng bảng orders với 10 đơn có cả giờ [Đã chạy: MySQL 8.0.44]:
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT NOT NULL,
total DECIMAL(10,2) NOT NULL,
status VARCHAR(10) NOT NULL,
coupon VARCHAR(10) NULL,
placed_at DATETIME NOT NULL
);
INSERT INTO orders VALUES
(101, 1, 450000, 'paid', NULL, '2026-03-01 09:15:00'),
(102, 2, 120000, 'pending', NULL, '2026-03-02 10:00:00'),
(103, 1, 890000, 'paid', 'SPRING', '2026-03-15 14:30:00'),
(104, 3, 450000, 'paid', NULL, '2026-03-20 08:45:00'),
(105, 4, 275000, 'pending', NULL, '2026-03-28 19:10:00'),
(106, 3, 990000, 'paid', 'SPRING', '2026-03-31 00:00:00'),
(107, 5, 60000, 'cancelled', NULL, '2026-03-31 11:20:00'),
(108, 2, 450000, 'paid', 'VIP10', '2026-03-31 18:30:00'),
(109, 5, 1200000, 'paid', NULL, '2026-04-01 00:00:00'),
(110, 1, 330000, 'paid', NULL, '2026-04-02 16:05:00');
Câu của Hạnh, với BETWEEN:
SELECT id, total, placed_at
FROM orders
WHERE status = 'paid'
AND placed_at BETWEEN '2026-03-01' AND '2026-03-31'
ORDER BY id;
SELECT SUM(total) AS revenue, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
AND placed_at BETWEEN '2026-03-01' AND '2026-03-31';
Dự đoán trước khi xem kết quả: có mấy đơn lọt qua, tổng là bao nhiêu?
Kết quả [Đã chạy: MySQL 8.0.44]:
+-----+-----------+---------------------+
| id | total | placed_at |
+-----+-----------+---------------------+
| 101 | 450000.00 | 2026-03-01 09:15:00 |
| 103 | 890000.00 | 2026-03-15 14:30:00 |
| 104 | 450000.00 | 2026-03-20 08:45:00 |
| 106 | 990000.00 | 2026-03-31 00:00:00 |
+-----+-----------+---------------------+
+------------+-------------+
| revenue | paid_orders |
+------------+-------------+
| 2780000.00 | 4 |
+------------+-------------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
WHERE status = 'paid' | Giữ các đơn đã thanh toán | Điều kiện thứ nhất, loại pending và cancelled |
AND placed_at BETWEEN '2026-03-01' AND '2026-03-31' | So placed_at với hai cận, mỗi cận là 00:00:00 của ngày ghi | Với cột DATETIME, chuỗi chỉ có ngày được hiểu là nửa đêm của ngày đó |
ORDER BY id; | Xếp kết quả theo mã đơn | Để so sánh dễ với bảng gốc |
SUM(total) ... COUNT(*) | Tổng tiền và số đơn trong tập vừa lọc | Hai con số mà Hạnh đưa lên sổ |
Bốn đơn lọt qua, tổng 2.780.000. Đơn 106 đặt đúng 31/3 lúc 00:00:00 nên vừa khít cận trên. Đơn 108 đặt 18:30 cùng ngày, muộn hơn cận trên, nên rơi mất. Cách viết đúng dùng khoảng nửa mở [Đã chạy: MySQL 8.0.44]:
SELECT SUM(total) AS revenue, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
AND placed_at >= '2026-03-01' -- gồm đầu tháng
AND placed_at < '2026-04-01'; -- loại đầu tháng sau
+------------+-------------+
| revenue | paid_orders |
+------------+-------------+
| 3230000.00 | 5 |
+------------+-------------+
Năm đơn, tổng 3.230.000: đúng số trên sổ. Cận trên là 00:00 ngày 1/4 và bị loại, nên mọi giờ của ngày 31/3 đều lọt vào.
Xem từng bước
Animation dưới đây cho năm đơn đi qua điều kiện BETWEEN, rồi qua khoảng nửa mở. Hãy trả lời câu dự đoán trước khi bấm bước đầu tiên.
Đang tải Animation…
Bảng trace bằng chữ [Đã chạy: MySQL 8.0.44, năm đơn paid tháng 3]:
| Đơn | Thời điểm đặt | BETWEEN '...03-01' AND '...03-31' | >= '...03-01' AND < '...04-01' |
|---|---|---|---|
| 101 | 01/3 09:15 | Giữ | Giữ |
| 103 | 15/3 14:30 | Giữ | Giữ |
| 104 | 20/3 08:45 | Giữ | Giữ |
| 106 | 31/3 00:00 | Giữ (đúng cận trên) | Giữ |
| 108 | 31/3 18:30 | Loại | Giữ |
Bạn vừa làm được: giải thích vì sao BETWEEN với ngày cuối tháng làm rơi đơn trong ngày đó, và viết lại điều kiện cho đúng.
Bây giờ thấy WHERE là một bộ lọc dùng chung. Cùng điều kiện status = 'cancelled' AND total < 100000, trước hết xem [Đã chạy: MySQL 8.0.44]:
SELECT id, status, total
FROM orders
WHERE status = 'cancelled' AND total < 100000;
+-----+-----------+----------+
| id | status | total |
+-----+-----------+----------+
| 107 | cancelled | 60000.00 |
+-----+-----------+----------+
Một dòng: đơn 107. Cùng điều kiện đó gắn vào UPDATE (trên bảng gốc) chạm đúng một dòng:
UPDATE orders
SET status = 'archived'
WHERE status = 'cancelled' AND total < 100000;
SELECT ROW_COUNT() AS updated;
SELECT id, status FROM orders WHERE id = 107;
+---------+
| updated |
+---------+
| 1 |
+---------+
+-----+----------+
| id | status |
+-----+----------+
| 107 | archived |
+-----+----------+
Và gắn vào DELETE cũng xóa đúng một dòng:
DELETE FROM orders
WHERE status = 'cancelled' AND total < 100000;
SELECT ROW_COUNT() AS deleted;
SELECT COUNT(*) AS orders_left FROM orders;
+---------+
| deleted |
+---------+
| 1 |
+---------+
+-------------+
| orders_left |
+-------------+
| 9 |
+-------------+
Concept Check
Dùng bảng orders mười đơn ở đầu bài.
Câu 1. WHERE coupon <> 'SPRING' trả về những đơn nào? (Cột coupon có SPRING ở đơn 103 và 106, VIP10 ở đơn 108, còn lại là NULL.)
- A. Chỉ đơn 108, vì dòng
NULLlàm điều kiện raNULL. - B. Chín đơn, trừ đơn 103 và 106 có
SPRING. - C. Tám đơn, vì
NULLkhácSPRINGnên được giữ. - D. Không đơn nào, vì cột
couponcó chứaNULL.
Câu 2. Cột placed_at kiểu DATETIME. placed_at < '2026-04-01' giữ đơn nào?
- A. Mọi đơn đặt trước 00:00 ngày 1/4, kể cả các giờ của ngày 31/3.
- B. Chỉ đơn đặt trước ngày 31/3.
- C. Mọi đơn đặt trong tháng 4.
- D. Mọi đơn đặt từ 00:00 ngày 1/4 trở đi.
Câu 3. Vì sao WHERE COUNT(*) >= 2 bị lỗi?
- A. Vì
COUNTchỉ dùng được vớiGROUP BYở cuối câu. - B. Vì
WHERElọc từng dòng trước khi gom nhóm, nên chưa có tổng của nhóm. - C. Vì dấu
>=không dùng được với hàm. - D. Vì
COUNT(*)phải viết hoa toàn bộ mới hợp lệ.
Đáp án và giải thích:
- Câu 1: A. B và C quên rằng
NULL <> 'SPRING'raNULL, không phảiTRUE. D sai vì đơn 108 cóVIP10khácSPRINGnên được giữ. - Câu 2: A. B và C sai vì cả hai bỏ sót hoặc thêm nhầm đơn. D là chiều ngược lại, đó là điều kiện
>=. - Câu 3: B. A sai vì lỗi nằm ở chỗ đặt
COUNTtrongWHERE, không ởGROUP BY. C và D bịa ra quy tắc không tồn tại.
Làm cùng mình
Hạnh hỏi tiếp: doanh thu tuần từ 15/3 đến hết ngày 21/3 là bao nhiêu? Lần này cô muốn viết đúng ngay.
Bước 1. Vẽ ra giấy
Trước khi gõ lệnh, hãy viết ra các đơn paid nằm trong tuần đó (xem bảng đầu bài), cộng tay tổng tiền, và xác định cận dưới cùng cận trên của khoảng nửa mở.
Bước 2. Điền chỗ trống
Điền hai điều kiện còn thiếu:
SELECT SUM(total) AS revenue, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
-- TODO 1: giữ đơn từ 00:00 ngày 15/3/2026
-- TODO 2: loại đơn từ 00:00 ngày 22/3/2026 trở đi
;
Gợi ý: cận dưới dùng dấu >=, cận trên dùng dấu < và là 00:00 ngày kế tiếp sau khoảng cần lấy.
Bước 3. Chạy và đối chiếu
Dự đoán trước: tổng và số đơn là bao nhiêu? Rồi đối chiếu với mục Lời giải và đáp án.
Bước 4. Vẽ lại ra giấy
Ghi một câu: vì sao cận trên là 22/3 chứ không phải 21/3 dù tuần kết thúc ngày 21/3.
Thử phá nó đi
Các lỗi người mới hay gặp với WHERE [Đã chạy: MySQL 8.0.44]:
| Code lỗi | Thông báo hoặc hiện tượng | Nguyên nhân | Cách sửa |
|---|---|---|---|
placed_at BETWEEN '2026-03-01' AND '2026-03-31' | Không lỗi, thiếu đơn sau 00:00 ngày 31/3 | Cận trên là nửa đêm đầu ngày 31 | Khoảng nửa mở, < '2026-04-01' |
WHERE coupon <> 'SPRING' | Không lỗi, thiếu các đơn không có mã giảm giá | NULL <> 'SPRING' ra NULL | Thêm OR coupon IS NULL |
WHERE COUNT(*) >= 2 | ERROR 1111 Invalid use of group function | WHERE chạy trước khi gom nhóm | Dùng HAVING COUNT(*) >= 2 |
WHERE MONTH(placed_at) = 3 | Kết quả đúng nhưng đọc cả bảng khi bảng lớn | Hàm bọc cột nên chỉ mục không dùng được | Viết khoảng ngày |
Lỗi thứ hai chạy thật [Đã chạy: MySQL 8.0.44]:
SELECT id, coupon FROM orders WHERE coupon <> 'SPRING';
+-----+--------+
| id | coupon |
+-----+--------+
| 108 | VIP10 |
+-----+--------+
Chỉ đơn 108 được giữ, trong khi bảy đơn không có mã giảm giá bị bỏ sót. Muốn có chúng, thêm OR coupon IS NULL:
SELECT id, coupon FROM orders WHERE coupon <> 'SPRING' OR coupon IS NULL ORDER BY id;
+-----+--------+
| id | coupon |
+-----+--------+
| 101 | NULL |
| 102 | NULL |
| 104 | NULL |
| 105 | NULL |
| 107 | NULL |
| 108 | VIP10 |
| 109 | NULL |
| 110 | NULL |
+-----+--------+
Lỗi thứ ba, với HAVING là cách đúng (WHERE đặt trước gom nhóm, HAVING đặt sau) [Đã chạy: MySQL 8.0.44]:
SELECT customer_id, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid' -- lọc từng dòng trước khi gom nhóm
GROUP BY customer_id
HAVING COUNT(*) >= 2; -- lọc nhóm sau khi gom
+-------------+-------------+
| customer_id | paid_orders |
+-------------+-------------+
| 1 | 3 |
| 3 | 2 |
+-------------+-------------+
Rồi cách sai:
SELECT customer_id, COUNT(*) AS paid_orders
FROM orders
WHERE COUNT(*) >= 2
GROUP BY customer_id;
ERROR 1111 (HY000) at line 22: Invalid use of group function
Hiểu lầm quan trọng nhất ở đây là nghĩ WHERE thấy được tổng của nhóm. Hình dưới đối chiếu mô hình sai với mô hình đúng.
Đang tải sơ đồ…
Cách đọc hình: hai chuỗi độc lập. Chuỗi đỏ là niềm tin sai, dẫn đến lỗi 1111. Chuỗi xanh là thực tế: WHERE xét một dòng tại một thời điểm, còn HAVING xét kết quả tổng hợp của cả nhóm.
Cách thứ tư cũng cho kết quả đúng nhưng ngầm: lọc bằng hàm. Với bảng 10 dòng bạn không thấy khác biệt [Đã chạy: MySQL 8.0.44]:
SELECT COUNT(*) AS march_orders FROM orders WHERE MONTH(placed_at) = 3;
+--------------+
| march_orders |
+--------------+
| 8 |
+--------------+
Phần Đào sâu có thí nghiệm trên bảng 20000 dòng để thấy khác biệt.
Tự tay làm
Dùng chung bảng orders mười đơn của bài. Mỗi bài nêu đầu vào, kết quả mong đợi và tiêu chí đạt.
Cơ bản
Bài 1. Đếm số dòng. Đầu vào: năm điều kiện dưới đây, mỗi điều kiện đếm số đơn trên bảng đầu bài.
| Điều kiện | Số đơn |
|---|---|
status = 'paid' AND total >= 450000 | ? |
status <> 'paid' | ? |
coupon <> 'SPRING' | ? |
placed_at < '2026-04-01' | ? |
total BETWEEN 300000 AND 450000 | ? |
Tiêu chí đạt: đúng cả năm ô. Gợi ý: nhớ dòng có coupon là NULL và rằng BETWEEN gồm cả hai đầu mút.
Vận dụng
Bài 2. Doanh thu tuần. Đầu vào: bảng orders. Tính tổng tiền và số đơn paid đặt từ 00:00 ngày 15/3 đến hết ngày 21/3. Tiêu chí đạt: viết đúng khoảng nửa mở, kết quả khớp với tính tay. Gợi ý: cận trên là 00:00 ngày 22/3.
Thử thách (tùy chọn)
Bài 3. Khách thân thiết tháng 3. Đầu vào: bảng orders. Liệt kê khách có ít nhất 2 đơn paid trong tháng 3 và tổng tiền ít nhất 1.000.000, kèm số đơn và tổng tiền. Tiêu chí đạt: kết quả đúng hai khách. Gợi ý: điều kiện từng dòng (paid, tháng 3) đặt ở WHERE, điều kiện theo nhóm đặt ở HAVING.
Tự kiểm tra
Trả lời bằng lời của bạn, không nhìn lại bài. Mỗi câu ghi mục tiêu được kiểm tra.
- (Mục tiêu 1, nhớ lại)
WHEREgiữ dòng khi điều kiện cho ra giá trị nào, và loại dòng khi cho ra những giá trị nào? - (Mục tiêu 2, dự đoán)
SELECTvới mộtWHEREtrả 4 dòng. Chép nguyênWHEREđó vàoDELETEthì xóa mấy dòng? - (Mục tiêu 4, dự đoán trạng thái) Cột
DATETIME, đơn đặt lúc 31/3 lúc 23:59:59.BETWEEN '2026-03-01' AND '2026-03-31'giữ hay loại đơn đó? - (Mục tiêu 3, giải thích cho bạn chưa học) Khi nào dùng
WHERE, khi nào dùngHAVING? - (Mục tiêu 4, tìm lỗi) Ai đó lọc đơn năm 2026 bằng
WHERE YEAR(placed_at) = 2026trên bảng 20 triệu dòng có chỉ mụcplaced_at. Có vấn đề gì và sửa thế nào?
Điều cần nhớ
WHEREgiữ dòng khi điều kiện làTRUE;FALSEvàNULLđều bị loại.SELECT,UPDATE,DELETEdùng chungWHERE: viết và kiểm tra bằngSELECTtrước.WHERElọc dòng trước khi gom nhóm;HAVINGlọc nhóm sau khi gom.- Với cột ngày giờ, dùng khoảng nửa mở
>= đầu_kỳ AND < đầu_kỳ_sau;BETWEENngày cuối làm rơi các giờ sau 00:00. - Đừng bọc cột trong hàm khi lọc nếu cột đó có chỉ mục.
Cheat sheet:
| Muốn | Viết | Ghi chú |
|---|---|---|
| Lọc một tháng | >= '2026-03-01' AND < '2026-04-01' | Đúng với mọi độ chính xác của giờ |
| Lọc tính cả NULL | col <> 'x' OR col IS NULL | <> bỏ sót NULL |
| Lọc theo tổng nhóm | GROUP BY ... HAVING COUNT(*) >= 2 | WHERE không dùng được hàm nhóm |
| Xem trước khi sửa | SELECT ... WHERE cùng_điều_kiện | Rồi chép sang UPDATE hoặc DELETE |
| Tránh hàm trên cột | col >= ... AND col < ... | Thay cho MONTH(col) = 3 |
Hình tóm tắt của bài là sơ đồ ở mục Bức tranh lớn.
Câu hỏi để dành cho bài sau: cột tính toán như total * 0.1 trong báo cáo cần một cái tên dễ đọc. Đặt tên ở đâu, và vì sao nhiều khi bạn dùng tên đó được ở ORDER BY nhưng không dùng được ở WHERE?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. Với bảng lớn có chỉ mục trên
placed_at, điều kiện khoảng cho phép MySQL nhảy thẳng vào đoạn của tháng 3 trong chỉ mục, còn hàm bọc cột buộc nó tính hàm cho từng dòng. Thí nghiệm trên bảngbig20000 dòng, kế hoạch thực thi của hai cách viết [Có thể khác nhau: kế hoạch và chi phí tùy phiên bản và dữ liệu]:
EXPLAIN FORMAT=TREE
SELECT SUM(total) FROM big
WHERE placed_at >= '2026-03-01' AND placed_at < '2026-04-01'\G
*************************** 1. row ***************************
EXPLAIN: -> Aggregate: sum(big.total) (cost=614 rows=1)
-> Index range scan on big using idx_placed over ('2026-03-01 00:00:00' <= placed_at < '2026-04-01 00:00:00'), with index condition: ((big.placed_at >= TIMESTAMP'2026-03-01 00:00:00') and (big.placed_at < TIMESTAMP'2026-04-01 00:00:00')) (cost=502 rows=1116)
EXPLAIN FORMAT=TREE
SELECT SUM(total) FROM big
WHERE MONTH(placed_at) = 3 AND YEAR(placed_at) = 2026\G
*************************** 1. row ***************************
EXPLAIN: -> Aggregate: sum(big.total) (cost=4000 rows=1)
-> Filter: ((month(big.placed_at) = 3) and (year(big.placed_at) = 2026)) (cost=2000 rows=20000)
-> Table scan on big (cost=2000 rows=20000)
Hai cách trả cùng kết quả [Đã chạy: MySQL 8.0.44]:
SELECT COUNT(*) AS by_range FROM big WHERE placed_at >= '2026-03-01' AND placed_at < '2026-04-01';
SELECT COUNT(*) AS by_function FROM big WHERE MONTH(placed_at) = 3 AND YEAR(placed_at) = 2026;
+----------+
| by_range |
+----------+
| 1116 |
+----------+
+-------------+
| by_function |
+-------------+
| 1116 |
+-------------+
Kế hoạch đầu quét một đoạn của chỉ mục, kế hoạch sau quét toàn bộ bảng (Table scan). Bảng 20000 dòng vẫn nhanh, nhưng 20 triệu dòng thì không.
- So sánh với lựa chọn khác. PostgreSQL cũng dính cùng bẫy với cột
TIMESTAMP: đơn 108 lúc 18:30 ngày 31/3 vẫn rơi mất, chỉ đơn 106 lúc nửa đêm được giữ [Đã chạy: PostgreSQL 16.15]:
SELECT id, placed_at FROM orders
WHERE status = 'paid' AND placed_at BETWEEN '2026-03-01' AND '2026-03-31'
ORDER BY id;
id | placed_at
-----+---------------------
106 | 2026-03-31 00:00:00
(1 row)
Viết cận trên là '2026-03-31 23:59:59' cũng chưa an toàn, vì một giá trị như 23:59:59.500 (cột có phần giây lẻ) vẫn lớn hơn cận. Khoảng nửa mở không có vấn đề này.
- Hiểu lầm nâng cao.
WHERE a OR b AND ctínhANDtrước, đã gặp ở bàiToán tử. VớiUPDATEvàDELETEthì ngoặc quan trọng hơn nhiều, vì sai điều kiện nghĩa là sửa hoặc xóa nhầm dòng thật. - Giới hạn của bài. Bài này chưa nói đến truy vấn con trong
WHEREhayEXISTS, và chưa nói kỹ về chỉ mục. Hai chủ đề đó có chương riêng. - Đọc thêm. MySQL 8.0 Reference Manual, mục SELECT Statement và mục Date and Time Functions: https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html và mục Optimizing SELECT Statements: https://dev.mysql.com/doc/refman/8.0/en/select-optimization.html
Bài tiếp theo
Bài sau là Bí danh (ALIAS): đặt tên tạm cho cột và bảng, và quy tắc tên nào dùng được ở mệnh đề nào. Mini-dự án nối tiếp: làm báo cáo doanh thu tháng 3 của Bean Corner với cột có tên dễ đọc.
Lời giải và đáp án
Làm cùng mình, Bước 2. Hai điều kiện cần điền, rồi kết quả:
AND placed_at >= '2026-03-15'
AND placed_at < '2026-03-22'
SELECT SUM(total) AS revenue, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
AND placed_at >= '2026-03-15'
AND placed_at < '2026-03-22';
+------------+-------------+
| revenue | paid_orders |
+------------+-------------+
| 1340000.00 | 2 |
+------------+-------------+
Hai đơn paid nằm trong tuần: 103 (890.000) và 104 (450.000), tổng 1.340.000. Cận trên là 22/3 vì khoảng nửa mở loại cận trên: mọi giờ của ngày 21/3 nhỏ hơn 00:00 ngày 22/3 nên được giữ.
Bài 1. Năm câu đếm:
SELECT COUNT(*) AS a FROM orders WHERE status = 'paid' AND total >= 450000;
SELECT COUNT(*) AS b FROM orders WHERE status <> 'paid';
SELECT COUNT(*) AS c FROM orders WHERE coupon <> 'SPRING';
SELECT COUNT(*) AS d FROM orders WHERE placed_at < '2026-04-01';
SELECT COUNT(*) AS e FROM orders WHERE total BETWEEN 300000 AND 450000;
+---+
| a |
+---+
| 6 |
+---+
+---+
| b |
+---+
| 3 |
+---+
+---+
| c |
+---+
| 1 |
+---+
+---+
| d |
+---+
| 8 |
+---+
+---+
| e |
+---+
| 4 |
+---+
| Điều kiện | Số đơn | Giải thích |
|---|---|---|
status = 'paid' AND total >= 450000 | 6 | Đơn 101, 103, 104, 106, 108, 109 |
status <> 'paid' | 3 | Đơn 102, 105, 107; không có status nào là NULL |
coupon <> 'SPRING' | 1 | Chỉ đơn 108; bảy đơn NULL bị loại |
placed_at < '2026-04-01' | 8 | Đơn 101 đến 108, mọi giờ của 31/3 |
total BETWEEN 300000 AND 450000 | 4 | Đơn 101, 104, 108 (450.000) và 110 (330.000) |
Bài 2.
SELECT SUM(total) AS revenue, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
AND placed_at >= '2026-03-15'
AND placed_at < '2026-03-22';
+------------+-------------+
| revenue | paid_orders |
+------------+-------------+
| 1340000.00 | 2 |
+------------+-------------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
status = 'paid' | Giữ đơn đã thanh toán | Điều kiện nghiệp vụ |
placed_at >= '2026-03-15' | Giữ đơn từ 00:00 ngày 15/3 | Cận dưới gồm cả mốc |
placed_at < '2026-03-22' | Loại đơn từ 00:00 ngày 22/3 | Cận trên không gồm mốc, nên cả ngày 21/3 nằm trong |
Bài 3.
SELECT customer_id, COUNT(*) AS paid_orders, SUM(total) AS revenue
FROM orders
WHERE status = 'paid'
AND placed_at >= '2026-03-01'
AND placed_at < '2026-04-01'
GROUP BY customer_id
HAVING COUNT(*) >= 2 AND SUM(total) >= 1000000
ORDER BY customer_id;
+-------------+-------------+------------+
| customer_id | paid_orders | revenue |
+-------------+-------------+------------+
| 1 | 2 | 1340000.00 |
| 3 | 2 | 1440000.00 |
+-------------+-------------+------------+
Hai khách thỏa: khách 1 (đơn 101 và 103, tổng 1.340.000) và khách 3 (đơn 104 và 106, tổng 1.440.000). Khách 2 chỉ có một đơn paid trong tháng 3 nên không đủ. Điều kiện về trạng thái và tháng nằm ở WHERE, còn điều kiện về số đơn và tổng nằm ở HAVING.
Tự kiểm tra.
- Giữ khi
TRUE; loại khiFALSEhoặcNULL. - Xóa đúng 4 dòng, vì cùng điều kiện cho cùng tập dòng.
- Loại, vì cận trên là 00:00:00 ngày 31/3 và 23:59:59 muộn hơn nó.
WHEREđể lọc từng dòng trước khi gom nhóm.HAVINGđể lọc nhóm dựa trên kết quả nhưCOUNThoặcSUM.- Hàm
YEAR()bọc cột nên MySQL không dùng được chỉ mục và quét cả 20 triệu dòng. Sửa thànhplaced_at >= '2026-01-01' AND placed_at < '2027-01-01'.