SQL cho Tester: tìm đơn hàng bị thiếu hoặc trùng bằng JOIN và GROUP BY

6 giờ tới · 9 phút đọc
Bài toán đối soát đơn hàng là gì?
Khi test một hệ thống thương mại điện tử hoặc thanh toán, bạn thường gặp yêu cầu: "Kiểm tra xem có đơn hàng nào chưa có bản ghi thanh toán không?" hoặc "Có đơn hàng nào bị ghi nhận thanh toán hai lần không?". Đây là bài toán đối soát dữ liệu (data reconciliation) - một kỹ năng SQL thực tế mà Tester cần nắm.
Bài này tập trung vào hai tình huống cụ thể:
- Đơn hàng bị thiếu (missing): có trong bảng
ordersnhưng không có bản ghi tương ứng trong bảngpayments. - Đơn hàng bị trùng (duplicate): cùng một
order_idxuất hiện nhiều hơn một lần trong bảngpayments.
Tất cả schema và dữ liệu trong bài là giả định minh họa, không lấy từ hệ thống thật.
Hai bảng, một kính lúp - đủ để phát hiện lỗi dữ liệu phổ biến
Công cụ cần có: bất kỳ SQL client nào hỗ trợ standard SQL - MySQL Workbench, DBeaver, pgAdmin, hoặc SQLite online đều được. Query trong bài tuân theo cú pháp SQL chuẩn (ISO/IEC 9075), không dùng stored procedure hay câu lệnh sửa đổi dữ liệu (INSERT/UPDATE/DELETE).
Schema và dữ liệu mẫu
Dưới đây là schema giả định dùng cho toàn bộ bài. Bạn có thể tạo các bảng này trong bất kỳ môi trường SQL nào để chạy thử.
Dữ liệu mẫu (giả định - dùng để minh họa):
Phân tích nhanh dữ liệu mẫu trên:
order_id1002 và 1004: có trongorders, không có trongpayments→ bị thiếu.order_id1003: xuất hiện 2 lần trongpayments→ bị trùng.order_id1001 và 1005: bình thường (1 đơn - 1 thanh toán).
Dữ liệu mẫu nhỏ nhưng đủ để test cả hai loại lỗi: thiếu và trùng
Ghi chú: trong thực tế, bảng payments thường có thêm cột status (pending/completed/failed). Bài này bỏ qua để tập trung vào logic JOIN và GROUP BY.
Tìm đơn hàng bị thiếu bằng LEFT JOIN
Tư duy: nếu một order_id có trong bảng orders nhưng không có bản ghi nào trong payments, thì khi LEFT JOIN hai bảng, phía payments sẽ trả về NULL.
Đây là câu truy vấn (chạy trên dữ liệu mẫu giả định ở trên):
Kết quả mong đợi khi chạy trên dữ liệu mẫu:
| order_id | customer_id | amount | order_date |
|---|---|---|---|
| 1002 | 2 | 180000 | 2024-06-01 |
| 1004 | 4 | 150000 | 2024-06-02 |
Tiêu chí pass/fail:
- Pass: query trả về đúng 2 dòng, với
order_idlà 1002 và 1004. - Fail: trả về số dòng khác, hoặc xuất hiện
order_id1001/1003/1005.
LEFT JOIN giữ lại tất cả dòng bên trái; NULL bên phải = chưa có thanh toán
Giới hạn cần biết: nếu một đơn hàng có bản ghi trong payments nhưng status = 'failed', query này vẫn coi là đã có thanh toán. Trong thực tế, bạn cần thêm điều kiện lọc status, ví dụ WHERE p.payment_id IS NULL OR p.status != 'completed'. Bài này dùng schema đơn giản nên bỏ qua cột status.
Tìm đơn hàng bị trùng bằng GROUP BY và HAVING
Đơn hàng bị trùng là khi cùng một order_id xuất hiện nhiều hơn một lần trong bảng payments. GROUP BY + HAVING là công cụ chuẩn để phát hiện điều này.
Kết quả mong đợi khi chạy trên dữ liệu mẫu:
| order_id | so_lan_thanh_toan |
|---|---|
| 1003 | 2 |
Tiêu chí pass/fail:
- Pass: query trả về đúng 1 dòng,
order_id = 1003,so_lan_thanh_toan = 2. - Fail: không trả về dòng nào, hoặc trả về
order_idkhác.
GROUP BY gom nhóm, HAVING lọc nhóm - bộ đôi phát hiện trùng lặp
Muốn xem chi tiết hơn (tổng số tiền thanh toán trùng so với số tiền đơn hàng), có thể JOIN thêm:
Kết quả mong đợi của query mở rộng:
| order_id | so_lan_thanh_toan | tong_tien_da_thanh_toan | tien_don_hang |
|---|---|---|---|
| 1003 | 2 | 640000 | 320000 |
Dòng này cho thấy order_id 1003 đã bị thu tiền gấp đôi so với giá trị đơn hàng - một lỗi nghiêm trọng cần báo cáo ngay.
Giới hạn: HAVING COUNT(*) > 1 chỉ phát hiện trùng theo order_id. Trong thực tế có thể cần kiểm tra thêm tổ hợp (order_id, paid_amount) nếu hệ thống cho phép thanh toán từng phần.
Kết hợp cả hai: báo cáo tổng hợp một lần query
Trong thực tế, bạn có thể muốn chạy một báo cáo duy nhất liệt kê cả đơn thiếu lẫn đơn trùng. Cách làm là dùng CTE (Common Table Expression) hoặc subquery để gộp hai kết quả lại.
CTE là gì? CTE (Common Table Expression) là cú pháp
WITH tên_cte AS (...)cho phép đặt tên cho một kết quả trung gian rồi dùng lại trong cùng câu query. Tương đương với viết subquery nhưng dễ đọc hơn. Được hỗ trợ từ MySQL 8.0+, PostgreSQL 8.4+, SQLite 3.35+.
Kết quả mong đợi (giả định, dựa trên dữ liệu mẫu):
| order_id | loai_loi | gia_tri |
|---|---|---|
| 1002 | THIEU_THANH_TOAN | 180000 |
| 1003 | TRUNG_THANH_TOAN | 640000 |
| 1004 | THIEU_THANH_TOAN | 150000 |
Tiêu chí pass/fail:
- Pass: 3 dòng, đúng
order_id, đúngloai_loi,gia_trikhớp. - Fail: thiếu dòng, sai
loai_loi, hoặcgia_tritính sai.
Một query, hai loại lỗi - UNION ALL gộp kết quả từ hai CTE
Nếu muốn tải file SQL mẫu để chạy thử ngay: Tải file SQL mẫu
Lỗi thường gặp khi viết query đối soát
Ba lỗi phổ biến nhất khi mới bắt đầu:
1. Dùng INNER JOIN thay vì LEFT JOIN khi tìm đơn thiếu
INNER JOIN chỉ trả về các dòng có match ở cả hai bảng. Nếu dùng INNER JOIN, đơn hàng chưa có thanh toán sẽ bị bỏ qua hoàn toàn - đây là lỗi logic, không phải lỗi cú pháp, nên SQL sẽ không báo lỗi.
2. Dùng WHERE thay vì HAVING để lọc sau GROUP BY
WHERE chạy trước khi GROUP BY, nên không thể lọc theo kết quả của hàm tổng hợp như COUNT(). HAVING chạy sau GROUP BY.
3. Quên điều kiện JOIN - ra Cartesian Product
Nếu viết FROM orders, payments mà không có ON hoặc WHERE kết nối, kết quả sẽ là tích Cartesian (số dòng = orders × payments). Với bảng lớn, truy vấn này có thể làm treo database.
Ba lỗi logic phổ biến - lỗi cú pháp dễ phát hiện, lỗi logic thì không
Cách tự kiểm tra sau khi viết query:
- Chạy query trên dữ liệu mẫu nhỏ (như dữ liệu giả định trong bài) trước khi áp lên dataset thật.
- Đếm số dòng kết quả và đối chiếu với kết quả mong đợi đã xác định trước.
- Thêm
EXPLAINhoặcEXPLAIN ANALYZE(tùy database) trước SELECT để kiểm tra kế hoạch thực thi.
