INNER JOIN
Bạn sẽ học được
- Viết
INNER JOINvới điều kiệnONđúng khóa. - Phân biệt
JOIN,INNER JOINvàCROSS JOIN. - Dùng
USINGkhi hai bảng có cột khóa cùng tên. - Ghép nhiều điều kiện trong
ONvà hiểu vì sao vớiINNER JOINchúng tương đươngWHERE. - Tránh lỗi bỏ quên
ONlàm kết quả bùng nổ.
Khái niệm cốt lõi
INNER JOIN — thường viết tắt là JOIN — chỉ giữ những cặp dòng khớp điều kiện ở cả hai bảng. Dòng không tìm được cặp bị loại hoàn toàn:
SELECT c.name, o.id AS don
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id
ORDER BY o.id;Mười sáu dòng — mỗi đơn một dòng. Hà Bùi không có đơn nên không xuất hiện. Đây chính là khác biệt với LEFT JOIN: nếu đổi thành LEFT JOIN, kết quả có thêm một dòng cho Hà Bùi với don là NULL.
JOIN và INNER JOIN là một. Viết INNER cho rõ ý, nhất là trong câu có cả LEFT JOIN; viết tắt khi ngữ cảnh đã rõ. Không có sự khác biệt về kết quả hay hiệu năng.
CROSS JOIN là trường hợp khác hẳn: nó ghép mọi dòng với mọi dòng, không cần điều kiện:
SELECT COUNT(*) FROM customers CROSS JOIN orders;Kết quả: 12 × 16 = 192. Đây là tích Descartes. CROSS JOIN đôi khi hữu ích để tạo lưới ngày × sản phẩm cho báo cáo, nhưng gần như không bao giờ là thứ bạn muốn vô tình nhận được.
Bỏ quên ON trong JOIN cũng cho kết quả tương tự:
SELECT COUNT(*) FROM customers c JOIN orders o;PostgreSQL chấp nhận cú pháp này và trả 192 dòng — mỗi khách ghép với mọi đơn. Không có thông báo lỗi. Đây là lý do phải kiểm tra số dòng kết quả sau mỗi lần viết JOIN: nếu nó lớn hơn nhiều so với dự đoán, khả năng cao bạn thiếu ON hoặc ON sai khóa.
Khi hai bảng có cột khóa cùng tên, dùng USING cho gọn:
SELECT c.name, o.id
FROM customers c
JOIN orders o USING (customer_id);Câu này chỉ chạy được nếu bảng customers cũng có cột customer_id — trong dữ liệu của khóa học, bảng khách dùng id, nên phải viết ON o.customer_id = c.id. Nhưng trong các hệ thống khác, cặp customer_id ở cả hai bảng là thiết kế phổ biến, và USING khi đó gộp cột thành một, tránh phải viết bí danh khi chọn.
ON nhận nhiều điều kiện nối bằng AND:
SELECT c.name, o.id, o.order_date
FROM customers c
JOIN orders o
ON o.customer_id = c.id
AND o.order_date >= '2025-06-01'
ORDER BY o.order_date;Với INNER JOIN, điều kiện trong ON và trong WHERE cho kết quả giống nhau — cả hai đều loại dòng trước khi trả kết quả. Khác biệt chỉ xuất hiện với LEFT JOIN, khi điều kiện đặt sai chỗ có thể biến nó thành INNER JOIN (bài sau sẽ phân tích kỹ). Thói quen tốt: điều kiện ghép bảng đặt trong ON, điều kiện lọc dữ liệu đặt trong WHERE. Cách viết này giúp người đọc thấy ngay ý định và tránh bẫy với outer join.
Một lưu ý về hiệu năng: ON nên dùng cột có chỉ mục ở cả hai bên. Khóa chính luôn có chỉ mục; khóa ngoại như orders.customer_id cần được tạo chỉ mục riêng (Prisma tạo giúp khi khai báo quan hệ). Không có chỉ mục, PostgreSQL có thể chọn nested loop chậm trên bảng lớn. Với dữ liệu vài chục dòng trong khóa học, điều này không ảnh hưởng kết quả, nhưng sẽ quan trọng khi làm việc thật.
Cuối cùng, INNER JOIN có thể nối nhiều bảng liên tiếp. Mỗi JOIN thêm một mảnh dữ liệu:
SELECT c.name, o.id, oi.quantity
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id;Kết quả 29 dòng — số dòng hàng. Mỗi đơn của mỗi khách được "nở" ra theo số món. Bài bốn của chương sẽ nói kỹ về cách đọc kết quả nối nhiều bảng và tránh đếm trùng.
Ví dụ chạy được
Đơn đã hoàn tất kèm tên khách:
SELECT o.id, c.name, o.order_date
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'completed'
ORDER BY o.id;Mười ba dòng. Khách 1 xuất hiện hai lần (đơn 1 và 4), khách 2 ba lần (đơn 2, 6, 12). Số dòng là số đơn, không phải số khách — kết quả nối giữ hạt của bảng orders.
Đếm khách khác nhau đã có đơn hoàn tất:
SELECT COUNT(DISTINCT c.id) AS so_khach
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'completed';Kết quả: 8. Mười ba đơn hoàn tất đến từ tám khách: An, Bình, Chi, Dũng, Giang, Khánh, Linh, Minh. Em và Nga chỉ có đơn pending, Inh có đơn cancelled, Hà chưa mua. Đây là bài tập phân biệt "đếm dòng" và "đếm thực thể" — hai con số đúng cho hai câu hỏi khác nhau.
Sản phẩm kèm số dòng hàng đã bán:
SELECT p.name, COUNT(*) AS so_dong_ban
FROM products p
JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.name
ORDER BY so_dong_ban DESC, p.name;Tám dòng — Giá kê laptop không xuất hiện vì chưa bán lần nào. Tai nghe đứng đầu với 5 dòng hàng; Bàn phím, Nồi chiên, Sách mỗi sản phẩm 4 dòng. Điều quan trọng không phải con số mà là việc JOIN tự loại sản phẩm chưa bán — nếu câu hỏi là "mọi sản phẩm kể cả chưa bán", phải dùng LEFT JOIN.
Lọc theo thuộc tính của bảng thứ hai:
SELECT p.category, COUNT(*) AS so_dong_ban
FROM products p
JOIN order_items oi ON oi.product_id = p.id
WHERE p.category = 'Điện tử'
GROUP BY p.category;Kết quả: 12 dòng bán thuộc nhóm Điện tử (Tai nghe 5, Bàn phím 4, Chuột 3). Điều kiện lọc nằm trên products nhưng vẫn dùng được sau khi nối — WHERE chạy trên kết quả nối.
ON không chỉ so sánh bằng. Có thể ghép theo khoảng, dù hiếm dùng:
SELECT o.id, p.name, oi.unit_price
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
WHERE oi.unit_price > p.price
ORDER BY o.id;Câu này tìm các dòng bán có đơn giá cao hơn giá niêm yết hiện tại — trên dữ liệu hiện tại trả 0 dòng vì đơn giá luôn bằng giá sản phẩm. Nó minh họa rằng ON có thể là bất kỳ điều kiện boolean nào, không nhất thiết là đẳng thức khóa.
Kiểm tra lỗi thiếu ON:
SELECT COUNT(*) FROM customers c JOIN orders o;192 dòng. Nếu bạn thấy con số này trong một báo cáo thật, gần như chắc chắn có một JOIN thiếu điều kiện. Hãy xem lại từng ON.
Thực hành
- Viết
INNER JOINgiữaordersvàcustomers, đếm số dòng. Kết quả 16 — mỗi đơn một dòng. - Đếm số khách khác nhau đã có đơn hoàn tất. Kết quả 8. Sau đó đếm số đơn hoàn tất — 13 — và giải thích vì sao hai số khác nhau.
- Nối
order_itemsvớiproducts, lọccategory = 'Gia dụng', đếm số dòng. Kết quả 10 (Nồi chiên 4, Bình giữ nhiệt 3, Đèn bàn 3). - Thử chạy
SELECT COUNT(*) FROM customers CROSS JOIN orders;rồi bỏCROSSvà viếtJOINkhông cóON. Xác nhận hai kết quả bằng nhau và rút ra quy tắc kiểm tra.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Kết quả bùng nổ về số dòng | Thiếu ON hoặc ON sai | Kiểm tra điều kiện ghép giữa hai khóa |
| Mất dòng không có cặp | INNER JOIN loại chúng | Dùng LEFT JOIN nếu cần giữ |
column reference is ambiguous | Cột trùng tên | Thêm bí danh bảng |
| Đếm bị nhân đôi | Nối bảng một-nhiều rồi COUNT(*) | COUNT(DISTINCT ...) hoặc đếm ở hạt đúng |
Kết quả khác giữa ON và WHERE | Truy vấn có LEFT JOIN ở chỗ khác | Giữ điều kiện ghép trong ON |
Lỗi "kết quả khác giữa ON và WHERE" không xảy ra với INNER JOIN thuần túy, nhưng xuất hiện ngay khi câu lệnh có thêm outer join. Vì vậy hãy áp dụng quy ước tách bạch ngay từ đầu: ON để ghép, WHERE để lọc.
Tóm tắt
INNER JOINchỉ giữ dòng khớp ở cả hai bảng.JOIN=INNER JOIN;CROSS JOINghép mọi cặp và rất dễ xảy ra khi quênON.USINGdùng khi hai bảng có cột khóa cùng tên.- Với
INNER JOIN, điều kiện trongONvàWHEREtương đương — nhưng hãy tách bạch để tránh bẫy outer join. - Luôn so số dòng kết quả với dự đoán để phát hiện ghép sai.
Dùng ← → để chuyển bài