Bạn cần tìm khách có tổng giá trị đơn đã thanh toán từ 20 trở lên. Câu truy vấn chạy được vẫn có thể trả sai nếu điều kiện trên từng đơn bị trộn với điều kiện trên tổng. WHERE amount >= 20 và HAVING SUM(amount) >= 20 đang hỏi hai câu khác nhau.
Bài dùng số nguyên giả lập để tập trung vào ngữ nghĩa SQL, đối chiếu PostgreSQL 17 ngày 29/10/2026. Các truy vấn đọc dữ liệu được chạy trên SQLite CLI 3.54.0; chưa chạy PostgreSQL server hoặc đo kế hoạch thực thi. Giá trị trong ví dụ là đơn vị minh họa, không phải thiết kế tiền tệ hoàn chỉnh.
Chốt câu hỏi trước khi viết truy vấn
Ta có ba bước: chỉ giữ đơn paid, gom theo khách, rồi giữ nhóm có tổng từ 20. Theo Table Expressions của PostgreSQL 17, WHERE lọc dòng trước khi gom nhóm, còn HAVING lọc các nhóm đã hình thành.
Đây là thứ tự để hiểu kết quả, không phải cam kết database sẽ thực thi vật lý đúng từng bước ấy. Bộ tối ưu có thể chọn cách xử lý khác mà vẫn phải giữ ngữ nghĩa.
Tạo bảng trong database thử nghiệm riêng, không chạy phần tạo bảng lên database ứng dụng:
CREATE TABLE orders_demo (
id INTEGER PRIMARY KEY,
customer_id TEXT NOT NULL,
status TEXT NOT NULL,
amount INTEGER NOT NULL
);
INSERT INTO orders_demo VALUES
(1, 'A', 'paid', 12),
(2, 'A', 'paid', 9),
(3, 'A', 'pending', 100),
(4, 'B', 'paid', 25),
(5, 'B', 'paid', 5),
(6, 'C', 'pending', 40);
A có tổng paid là 21, B là 30. C không có đơn paid. Dòng pending trị giá 100 của A là một ca cố tình cài vào fixture để phát hiện truy vấn quên lọc trạng thái.
Viết điều kiện đúng tầng
SELECT customer_id, SUM(amount) AS paid_total
FROM orders_demo
WHERE status = 'paid'
GROUP BY customer_id
HAVING SUM(amount) >= 20
ORDER BY customer_id;
Kết quả đã kiểm tra là A|21 và B|30. Điều kiện status xác định những dòng được tham gia tính tổng. Điều kiện trong HAVING quyết định nhóm nào xuất hiện trong kết quả cuối.
Lặp lại SUM(amount) trong HAVING thay vì dựa vào alias paid_total giúp ví dụ dùng được với PostgreSQL. Việc một engine chấp nhận alias ở vị trí nào không tự bảo đảm engine khác cũng chấp nhận.
Nếu muốn quan sát bước trung gian, chạy lại truy vấn trên nhưng bỏ dòng HAVING. Bạn vẫn nhận A và B với tổng tương ứng; C không xuất hiện vì đã không còn dòng nào sau WHERE. Nếu cần hiện cả khách không có đơn paid với tổng bằng không, đó là yêu cầu khác, thường cần bảng khách và outer join.
Một thay đổi nhỏ làm mất khách A
SELECT customer_id, SUM(amount) AS paid_total
FROM orders_demo
WHERE status = 'paid' AND amount >= 20
GROUP BY customer_id
ORDER BY customer_id;
Truy vấn này chỉ còn B|25. Cả hai đơn paid của A đều nhỏ hơn 20 nên bị loại trước khi cộng. Đơn trị giá 5 của B cũng bị loại. Nó trả tổng của những đơn paid riêng lẻ đạt ngưỡng, không phải tổng mọi đơn paid của khách đạt ngưỡng.
Một lỗi khác là bỏ WHERE và chỉ dùng HAVING trên tổng. Khi đó pending cũng đi vào phép cộng: A thành 121, C thành 40. Cả hai lỗi đều có thể bị che nếu dữ liệu thử chỉ gồm mỗi khách một đơn paid.
Kiểm tra bằng các ca có chủ đích
Fixture trên có ba đặc điểm đáng giữ trong test của ứng dụng: một khách đạt ngưỡng nhờ nhiều đơn nhỏ, một khách có cả đơn lớn lẫn nhỏ, một khách chỉ có đơn chưa thanh toán. Thêm ca tổng đúng 20 để kiểm tra ranh giới >= thay vì >.
Đọc cả danh sách khách và tổng, đừng chỉ kiểm tra số dòng. Hai truy vấn có thể cùng trả hai khách nhưng tổng đã sai. ORDER BY làm thứ tự hiển thị ổn định cho bài kiểm tra; GROUP BY không tự hứa sắp xếp kết quả.
Các hàm aggregate có quy tắc riêng với NULL và tập rỗng. Tài liệu Aggregate Functions là nguồn cần đọc khi chuyển ví dụ sang cột cho phép NULL. Ở đây amount NOT NULL là lựa chọn có chủ đích để không nhập nhằng giữa bài toán lọc và bài toán dữ liệu thiếu.
Giới hạn và cách áp dụng
SQLite cho phép một số cách chọn cột ngoài GROUP BY mà PostgreSQL không chấp nhận. Ví dụ chỉ chọn khóa nhóm và biểu thức aggregate, tránh dựa vào hành vi chọn một dòng bất kỳ. Tài liệu SELECT của SQLite mô tả các khác biệt này.
Trong API thật, dùng tham số truy vấn cho trạng thái và ngưỡng thay vì nối chuỗi từ input. Nếu kết quả sai, trước tiên kiểm tra dòng nào được phép tham gia tổng; sau đó mới tối ưu index hoặc đọc EXPLAIN. Sửa hiệu năng trước khi chốt ngữ nghĩa thường chỉ làm một truy vấn sai chạy nhanh hơn.
Khi review, đọc WHERE thành “những dòng nào được tính” và HAVING thành “những nhóm nào được trả”. Cách đọc đó giúp phát hiện việc vô tình loại các đơn nhỏ trước khi cộng.

