Bài tập: số đơn mỗi khách
Bạn sẽ học được
- Ghép
JOIN,GROUP BYvàORDER BYđể trả lời một câu hỏi nhiều bảng. - Chọn
INNER JOINhayLEFT JOINdựa vào một từ trong đề: "chỉ gồm". - Gom nhóm an toàn bằng cả
idvà tên. - Viết đúng thứ tự sắp xếp phụ để kết quả xác định.
- Tự kiểm tra kết quả trước khi nộp.
Khái niệm cốt lõi
Đề bài Số đơn mỗi khách:
Liệt kê tên khách hàng và số đơn (
orders), chỉ gồm khách đã có đơn; sắp xếp số đơn giảm dần rồi theo tên.
Checklist:
| Câu hỏi | Trả lời | Thành phần SQL |
|---|---|---|
| Bảng nào? | customers, orders | FROM customers c JOIN orders o |
| Cột nào? | name, COUNT(o.id) AS orders | SELECT c.name, COUNT(o.id) AS orders |
| Lọc gì? | Khách chưa có đơn bị loại | JOIN (inner), không cần WHERE |
| Nhóm theo gì? | Khách | GROUP BY c.id, c.name |
| Thứ tự? | Số đơn giảm dần, rồi tên | ORDER BY orders DESC, c.name |
Đáp án:
SELECT c.name, COUNT(o.id) AS orders
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY orders DESC, c.name;Kết quả:
| name | orders |
|---|---|
| Bình Trần | 3 |
| An Nguyễn | 2 |
| Chi Lê | 2 |
| Giang Đặng | 2 |
| Dũng Phạm | 1 |
| Em Võ | 1 |
| Inh Đỗ | 1 |
| Khánh Hồ | 1 |
| Linh Ngô | 1 |
| Minh Dương | 1 |
| Nga Lý | 1 |
Mười một dòng. Hà Bùi không có đơn nên không xuất hiện — đúng yêu cầu "chỉ gồm khách đã có đơn".
Bốn quyết định trong câu lệnh, và lý do của từng quyết định:
Một, JOIN chứ không LEFT JOIN. Đề nói "chỉ gồm khách đã có đơn". INNER JOIN tự loại Hà Bùi mà không cần WHERE. Nếu dùng LEFT JOIN, Hà Bùi xuất hiện với orders = 0 — sai đề. Đây là chỗ đề bài kiểm tra xem bạn có đọc kỹ cụm "chỉ gồm" hay không.
Hai, GROUP BY c.id, c.name. Gom theo id để hai khách trùng tên không bị gộp thành một nhóm. Trên dữ liệu hiện tại không có tên trùng, nên GROUP BY c.name cho cùng kết quả — nhưng cách viết đúng về nguyên tắc là gom theo khóa. Nhiều bài chấm chạy trên bộ dữ liệu ẩn có thêm khách; nếu hai người trùng tên xuất hiện, GROUP BY c.name sẽ gộp họ và sai.
Ba, COUNT(o.id), không phải COUNT(*). Với INNER JOIN, mọi dòng đều có o.id, nên hai cách cho cùng số. Nhưng COUNT(o.id) diễn đạt đúng ý "đếm đơn" và không phụ thuộc vào việc bạn dùng loại join nào. Thói quen tốt: đếm cột của bảng đang được đếm.
Bốn, ORDER BY orders DESC, c.name. orders là bí danh của cột đếm, dùng được trong ORDER BY vì mệnh đề này chạy sau SELECT. Sắp phụ theo c.name để các khách cùng số đơn có thứ tự xác định — nếu chỉ sắp theo số đơn, bốn khách 1 đơn có thể đổi chỗ giữa các lần chạy. Đề nói "rồi theo tên", nên đây là phần bắt buộc của đáp án, không phải tùy chọn.
Vì sao bí danh cột phải là orders? Đề ghi rõ "số đơn (orders)" — tên cột trong ngoặc là tên cột kết quả. Bài chấm so sánh theo tên cột; đặt so_don sẽ bị báo sai dù con số đúng.
Một điểm tinh tế: orders trùng tên với bảng orders. Trong ORDER BY orders DESC, PostgreSQL ưu tiên hiểu đây là tên cột trong danh sách SELECT, nên câu lệnh chạy đúng. Trong SELECT và GROUP BY, bạn vẫn phải dùng bí danh bảng o cho cột của bảng. Đây là trường hợp hiếm nhưng hợp lệ; đừng lo, chỉ cần biết vì sao nó chạy được.
Bộ dữ liệu ẩn có thêm khách và thêm đơn, nên kết quả sẽ khác về số dòng và số đơn. Đừng hardcode; hãy để JOIN và GROUP BY tự làm việc. Câu lệnh đúng quy tắc sẽ đúng trên cả hai bộ dữ liệu.
Ví dụ chạy được
Kiểm tra trước khi gom nhóm — xem các cặp khách/đơn:
SELECT c.name, o.id AS don
FROM customers c
JOIN orders o ON o.customer_id = c.id
ORDER BY c.name, o.id;Mười sáu dòng. Bình Trần xuất hiện ba lần (đơn 2, 6, 12); An Nguyễn hai lần (1, 4). Đếm số lần xuất hiện bằng mắt là cách kiểm tra kết quả gom nhóm — nếu một khách có ba dòng ở đây mà kết quả cuối ghi 2, bạn đã gom sai.
Bản gom nhóm không sắp xếp:
SELECT c.name, COUNT(o.id) AS orders
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;Mười một dòng với con số đúng nhưng thứ tự không đảm bảo. Thêm ORDER BY để đạt yêu cầu đề.
Bản sai dùng LEFT JOIN:
SELECT c.name, COUNT(o.id) AS orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY orders DESC, c.name;Mười hai dòng, có thêm Hà Bùi với orders = 0 ở cuối. Câu này đúng cho câu hỏi "mọi khách kèm số đơn" nhưng sai đề bài hiện tại. Đọc lại đề để chọn đúng loại join là bước đầu tiên, không phải bước chỉnh sửa cuối cùng.
Bản sai dùng COUNT(*) nhưng vẫn INNER JOIN:
SELECT c.name, COUNT(*) AS orders
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;Trên dữ liệu này cho cùng kết quả, vì mỗi dòng nối ứng với đúng một đơn. Nhưng nếu ai đó sửa câu lệnh thành LEFT JOIN, COUNT(*) âm thầm cho Hà Bùi 1 đơn. Dùng COUNT(o.id) để câu lệnh miễn nhiễm với thay đổi đó.
Kiểm tra tổng:
SELECT SUM(so_don) AS tong_don
FROM (
SELECT COUNT(o.id) AS so_don
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id
) t;Kết quả: 16, bằng số dòng bảng orders. Phép kiểm chứng này phát hiện cả hai lỗi: nếu nhỏ hơn 16, có đơn bị mất do JOIN sai; nếu lớn hơn, có dòng bị nhân. Luôn chạy nó sau khi viết xong một báo cáo gom nhóm.
Thử thách nhỏ: sắp xếp theo số đơn tăng dần rồi tên, xem danh sách đảo lại. Đổi DESC thành ASC chỉ đổi chiều của cột đầu; c.name vẫn tăng dần. Đây là hành vi đúng của sắp xếp nhiều cột — mỗi cột có chiều riêng.
Thực hành
- Nộp đáp án cho bài Số đơn mỗi khách (
orders-per-customer). - Viết biến thể trả về cả khách chưa có đơn, với
orders = 0, và giải thích khác biệt với đáp án chính. - Viết biến thể đếm số đơn
completedmỗi khách, vẫn chỉ gồm khách đã có đơn hoàn tất. Kết quả có 8 dòng; Bình Trần vẫn đứng đầu với 3. - Đổi bí danh cột thành
so_donvà quan sát bài chấm báo lỗi tên cột. Đổi lạiordersđể nộp.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Kết quả có khách 0 đơn | Dùng LEFT JOIN | Đổi sang JOIN |
| Chấm báo sai cột | Bí danh không phải orders | COUNT(o.id) AS orders |
| Thứ tự không đúng khi bằng số đơn | Thiếu sắp xếp phụ theo tên | ORDER BY orders DESC, c.name |
| Hai khách trùng tên bị gộp | GROUP BY c.name | GROUP BY c.id, c.name |
| Tổng số đơn khác 16 | JOIN sai khóa hoặc nhân dòng | Kiểm tra o.customer_id = c.id |
Lỗi "kết quả có khách 0 đơn" là lỗi phổ biến nhất của bài này vì LEFT JOIN là cách viết quen thuộc khi nghĩ về "khách hàng". Hãy đọc đề trước khi chọn loại join: "chỉ gồm" nghĩa là loại những người không khớp.
Tóm tắt
- "Chỉ gồm khách đã có đơn" →
INNER JOIN, không cầnWHERE. - Đếm bằng
COUNT(o.id), đặt bí danh đúng tên đề yêu cầu (orders). - Gom theo
c.id, c.nameđể an toàn với tên trùng. - Sắp
orders DESCrồic.nameđể thứ tự xác định. - Kiểm tra tổng số đơn bằng 16 trước khi nộp.
Bạn đã hoàn thành chương JOIN. Chương cuối của khóa SQL cơ bản nói về tư duy phân tích: dùng CASE để phân loại và trả lời những câu hỏi nghiệp vụ phức tạp hơn một phép đếm đơn giản.
Use ← → to move between lessons