aedev.net
  • Trang chủ
  • AI
  • Mobile
  • Flutter
  • React Native
  • Backend
  • DevOps
  • Hướng dẫn
No Result
View All Result
SAVED POSTS
aedev.net
  • Trang chủ
  • AI
  • Mobile
  • Flutter
  • React Native
  • Backend
  • DevOps
  • Hướng dẫn
No Result
View All Result
aedev.net
No Result
View All Result
Minh họa kiến trúc backend API gọn gàng cho sản phẩm nhỏ

LEFT JOIN và COUNT: đừng đếm khách chưa có đơn thành một đơn hàng

Suong Nguyen by Suong Nguyen
26/10/2026
in Backend
0
585
SHARES
3.2k
VIEWS
Summarize with ChatGPTShare to Facebook

Báo cáo cần hiển thị mọi khách hàng, kể cả người chưa có đơn. Bạn dùng LEFT JOIN rồi COUNT(*), nhưng khách chưa mua gì lại có số đơn bằng 1. SQL vẫn chạy thành công; điều sai nằm ở thứ bạn đang đếm sau phép join.

Bài đối chiếu PostgreSQL 17 và SQLite ngày 26/10/2026. Các fixture SQL được chạy trên SQLite CLI 3.54.0. Chưa chạy PostgreSQL trong lượt biên tập, nên các kết luận về PostgreSQL dựa trên tài liệu chính thức; không có execution plan hay benchmark PostgreSQL được tạo từ kết quả SQLite.

1. Quan sát hàng đầu vào của phép đếm

Theo tài liệu LEFT JOIN của PostgreSQL, khi không có hàng bên phải khớp, hàng bên trái vẫn được giữ và các cột bên phải mang NULL. Theo tham chiếu aggregate, COUNT(*) đếm hàng đầu vào, còn COUNT(biểu thức) chỉ đếm giá trị không NULL.

Như vậy, một hàng đại diện khách chưa có đơn vẫn là một hàng của kết quả join. Nếu muốn đếm đơn, bạn cần chọn một cột đại diện chắc chắn có giá trị khi một đơn thực sự khớp, chẳng hạn khóa chính của bảng đơn.

2. Dựng fixture hai khách hàng

Chạy trong database thử, cùng một phiên kết nối để giữ các bảng tạm:

CREATE TEMP TABLE count_customers (
    id integer PRIMARY KEY,
    name text NOT NULL
);
CREATE TEMP TABLE count_orders (
    id integer PRIMARY KEY,
    customer_id integer NOT NULL,
    status text NOT NULL
);

INSERT INTO count_customers VALUES (1, 'An'), (2, 'Binh');
INSERT INTO count_orders VALUES (101, 1, 'paid'), (102, 1, 'pending');

SELECT c.id, c.name,
       COUNT(*) AS joined_rows,
       COUNT(o.id) AS order_count
FROM count_customers AS c
LEFT JOIN count_orders AS o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.id;

Fixture đã chạy cho An hai joined rows và hai orders; Binh có một joined row nhưng không có order. Hai cột đếm khác nhau vì chúng trả lời hai câu hỏi khác nhau. Không nên đổi alias của COUNT(*) thành order_count rồi xem tên cột là bằng chứng phép tính đã đúng.

Tài liệu aggregate của SQLite cũng mô tả sự khác biệt COUNT(*) với COUNT(X). Hãy chọn cột khóa không NULL của đối tượng cần đếm; nếu đếm một cột tùy chọn như ghi chú đơn, bạn có thể bỏ sót đơn vẫn tồn tại nhưng chưa có ghi chú.

3. Đếm đơn đã thanh toán mà vẫn giữ khách chưa có đơn

Đưa điều kiện xác định đơn cần khớp vào ON:

SELECT c.id, c.name, COUNT(o.id) AS paid_order_count
FROM count_customers AS c
LEFT JOIN count_orders AS o
    ON o.customer_id = c.id AND o.status = 'paid'
GROUP BY c.id, c.name
ORDER BY c.id;

Kết quả đã kiểm tra là An có 1, Binh có 0. Ý định của truy vấn là giữ tập khách hàng, rồi chỉ nối những đơn thỏa điều kiện để đếm. Đây không phải quy tắc “mọi filter đều phải đặt trong ON”; nó phù hợp với báo cáo đang cần hiển thị cả khách không có đơn paid.

So sánh với biến thể sau:

SELECT c.id, c.name, COUNT(o.id) AS paid_order_count
FROM count_customers AS c
LEFT JOIN count_orders AS o ON o.customer_id = c.id
WHERE o.status = 'paid'
GROUP BY c.id, c.name
ORDER BY c.id;

Trong fixture, biến thể này chỉ còn An. Hàng không có order mang status NULL không thỏa điều kiện WHERE. Nếu báo cáo chỉ cần những khách đã có đơn paid, đó có thể là kết quả đúng; nếu cần mọi khách, nó không đáp ứng yêu cầu ban đầu.

4. Rà hiện tượng nhân hàng khi nối thêm bảng

Giả sử sau này một đơn có nhiều dòng sản phẩm và bạn nối cả bảng dòng sản phẩm. Một đơn có thể xuất hiện nhiều lần trong kết quả trung gian. COUNT(o.id) khi đó đếm số lần ID xuất hiện sau join, chưa chắc là số đơn duy nhất.

Bạn cần xác định đơn vị báo cáo trước: khách hàng, đơn hàng hay dòng sản phẩm. Tùy ý định, có thể cần tổng hợp ở cấp đơn trước khi join hoặc dùng COUNT(DISTINCT o.id). Không thêm DISTINCT chỉ để số “trông hợp lý” mà chưa giải thích nguyên nhân nhân hàng.

Kiểm tra trước khi đưa vào báo cáo

Tạo ca khách không có đơn, chỉ có đơn pending, có một đơn paid và có nhiều đơn paid. Ghi số mong đợi theo từng khách. Sau đó thêm ca có nhiều dòng chi tiết cho cùng một đơn nếu truy vấn thật có join cấp đó.

Kiểm tra cả danh sách khách xuất hiện lẫn số đếm. Chỉ đối chiếu tổng cuối cùng có thể bỏ qua việc một khách bị mất trong khi khách khác bị đếm thừa. Với báo cáo lớn, giữ một tập dữ liệu nhỏ có thể đọc bằng mắt để rà ngữ nghĩa trước khi đo hiệu năng.

Lỗi thường gặp

COUNT(1) không tự sửa vấn đề hàng rỗng của LEFT JOIN: số 1 vẫn là một biểu thức không NULL trên hàng đó. Thay COUNT(*) bằng COUNT(c.id) cũng vẫn đếm khách, vì c.id thuộc phía được giữ lại. Hãy nhìn vào đối tượng cần đếm và cột đại diện của phía phù hợp.

Nếu số đếm đúng nhưng thiếu khách, rà WHERE và ON. Nếu số tăng sau khi nối bảng mới, xem kết quả trung gian trước GROUP BY. Bài này không đưa ra kết luận tốc độ giữa các dạng truy vấn; tối ưu cần dữ liệu và plan của database thực tế.

Kết luận

Hãy đếm đối tượng theo ý nghĩa của báo cáo thay vì mặc định đếm mọi hàng sau join. Một fixture có cả trường hợp không khớp sẽ giúp bạn nhận ra sai lệch trước khi con số được đưa lên dashboard.

Nguồn tham khảo

  • PostgreSQL 17: LEFT JOIN và ON/WHERE
  • PostgreSQL 17: aggregate functions
  • SQLite: aggregate functions

Tags: Báo cáo dữ liệuPostgreSQLSQL
SummarizeShare234
Suong Nguyen

Suong Nguyen

Related Stories

Minh họa kiến trúc backend API gọn gàng cho sản phẩm nhỏ

SQL NOT IN gặp NULL: vì sao truy vấn loại trừ trả về rỗng?

by Suong Nguyen
25/10/2026
0

Dựng một fixture nhỏ để hiểu NOT IN với NULL, chọn NOT EXISTS theo nghiệp vụ và kiểm tra cả cột bên trái lẫn bên phải.

Minh họa kiến trúc backend API gọn gàng cho sản phẩm nhỏ

Đọc PostgreSQL EXPLAIN ANALYZE: đừng nhầm cost với mili giây

by Suong Nguyen
24/10/2026
0

Đọc cost, rows, loops và buffers theo đúng ý nghĩa trước khi quyết định thêm index cho truy vấn PostgreSQL.

Minh họa kiến trúc backend API gọn gàng cho sản phẩm nhỏ

Optimistic locking trong PostgreSQL: tránh ghi đè thay đổi của người khác

by Suong Nguyen
23/10/2026
0

Dùng cột version và UPDATE có điều kiện để phát hiện một biểu mẫu đang lưu trên phiên bản dữ liệu đã cũ.

Minh họa kiến trúc backend API gọn gàng cho sản phẩm nhỏ

Idempotency-Key cho API tạo đơn: retry mà không tạo trùng nghiệp vụ

by Suong Nguyen
22/10/2026
0

Thiết kế phạm vi khóa, transaction và response lưu lại để API tạo đơn xử lý an toàn khi client gửi lại request.

0 0 votes
Article Rating
Subscribe
Login
Notify of
guest
guest
0 Comments
Oldest
Newest Most Voted
AE Dev

AE Dev mang đến các bài viết về lập trình, mobile app, backend, frontend, WordPress và công nghệ, phù hợp cho developer muốn học hỏi và cập nhật kiến thức thực tế.

Recent Posts

  • React Native Image: cover và contain khác nhau trong cùng một khung ảnh
  • JavaScript sort: vì sao số 10 đứng trước 2 và mảng gốc bị đổi?
  • Android plurals: vì sao getQuantityString cần truyền count hai lần?

Categories

  • AI
  • Backend
  • Đánh giá
  • DevOps
  • Flutter
  • Hướng dẫn
  • Lập trình Mobile
  • Phỏng vấn
  • React Native
  • Uncategorized

Weekly Newsletter

  • Buy JNews
  • Support Forum
  • Pre-sale Question
  • Contact Us

© 2026 AE Dev - Blog công nghệ, lập trình và chia sẻ kiến thức IT.

Welcome Back!

Login to your account below

Forgotten Password?

Retrieve your password

Please enter your username or email address to reset your password.

Log In
No Result
View All Result
  • Trang chủ
  • AI
  • Mobile
  • React Native
  • Backend
  • DevOps
  • Hướng dẫn
  • Flutter

© 2026 AE Dev - Blog công nghệ, lập trình và chia sẻ kiến thức IT.

wpDiscuz
0
0
Would love your thoughts, please comment.x
()
x
| Reply