6 loại JOIN trong SQL bạn phải biết (với ví dụ đơn giản và rõ ràng)
Tìm hiểu 6 loại JOIN trong SQL mà mọi nhà phân tích nên biết — INNER, LEFT, RIGHT, FULL OUTER, CROSS và SELF — với ví dụ thực tế và lỗi thường gặp.

JOIN trong SQL là gì và tại sao bạn dùng chúng
Một SQL JOIN cho phép bạn kết hợp các hàng từ hai (hoặc nhiều) bảng thành một kết quả bằng cách khớp chúng theo một cột liên quan — thường là một ID.
Tại sao JOIN quan trọng
Hầu hết cơ sở dữ liệu thực tế được tách thành nhiều bảng để tránh lặp lại thông tin. Ví dụ, tên khách hàng nằm trong bảng customers, trong khi đơn hàng của họ nằm trong bảng orders. JOIN là cách bạn nối lại những phần đó khi cần câu trả lời.
Đó là lý do JOIN xuất hiện khắp nơi trong báo cáo và phân tích:
- Xây báo cáo bán hàng bao gồm tên khách hàng, tổng tiền đơn và trạng thái thanh toán
- Tìm khách hàng chưa từng đặt hàng
- Kiểm tra sự không khớp, như đơn hàng không có thanh toán
- Tạo tóm tắt “một hàng cho mỗi khách hàng” từ nhiều hàng liên quan
Không có JOIN, bạn sẽ phải chạy nhiều truy vấn riêng biệt và ghép kết quả bằng tay — chậm, dễ sai và khó lặp lại.
Nếu bạn xây sản phẩm trên cơ sở dữ liệu quan hệ (dashboard, admin panel, công cụ nội bộ, portal khách hàng), JOIN cũng là thứ biến “bảng thô” thành các view cho người dùng. Những nền tảng như Koder.ai (tự động tạo ứng dụng React + Go + PostgreSQL từ chat) vẫn dựa vào các nguyên tắc JOIN vững chắc khi bạn cần các trang danh sách chính xác, báo cáo và màn đối chiếu — vì logic cơ sở dữ liệu không biến mất, ngay cả khi phát triển nhanh hơn.
6 loại JOIN bạn sẽ dùng nhiều nhất
Hướng dẫn này tập trung vào sáu JOIN bao quát phần lớn công việc SQL hàng ngày:
- INNER JOIN: trả về chỉ những hàng khớp ở cả hai bảng (tốt cho “hiện mối quan hệ đã xác nhận”).
- LEFT JOIN: giữ mọi hàng từ bảng trái, và khớp những gì có thể từ bảng phải (tốt cho “bao gồm dữ liệu liên quan bị thiếu”).
- RIGHT JOIN: phản chiếu của LEFT JOIN (ít dùng hơn, nhưng khi hữu ích tùy phong cách truy vấn).\
- FULL OUTER JOIN: giữ mọi hàng từ cả hai bảng, ghép khi có thể (tuyệt vời cho đối chiếu và tìm khoảng trống).
- CROSS JOIN: tạo mọi kết hợp của hàng (hữu ích để tạo lịch, kịch bản, dữ liệu thử nghiệm — cũng dễ dùng sai).
- SELF JOIN: join một bảng với chính nó (tiện cho các cây phân cấp như nhân viên / quản lý).
Ghi chú ngắn về cú pháp
Cú pháp JOIN rất giống nhau ở hầu hết cơ sở dữ liệu (PostgreSQL, MySQL, SQL Server, SQLite). Có vài khác biệt — đặc biệt quanh hỗ trợ FULL OUTER JOIN và một số hành vi cạnh — nhưng khái niệm và mẫu cơ bản chuyển giao tốt.
Các bảng ví dụ chúng ta dùng (Customers, Orders, Payments)
Để giữ ví dụ JOIN đơn giản, chúng ta sẽ dùng ba bảng nhỏ phản ánh setup thực tế: customers đặt orders, và orders có thể (hoặc không) có payments.
Một lưu ý nhỏ trước khi bắt đầu: các bảng mẫu dưới đây chỉ hiển thị vài cột, nhưng một số truy vấn sau dùng thêm trường khác (như order_date, created_at, status, hoặc paid_at) để minh họa các mẫu phổ biến. Xem những cột đó như các trường “điển hình” bạn thường có trong schema production.
1) customers
Primary key: customer_id
| customer_id | name |
|---|---|
| 1 | Ava |
| 2 | Ben |
| 3 | Chen |
| 4 | Dia |
2) orders
Primary key: order_id
Foreign key: customer_id → customers.customer_id
| order_id | customer_id | order_total |
|---|---|---|
| 101 | 1 | 50 |
| 102 | 1 | 120 |
| 103 | 2 | 35 |
| 104 | 5 | 70 |
Lưu ý order_id = 104 tham chiếu customer_id = 5, không tồn tại trong customers. Sự “khớp thiếu” này hữu ích để thấy cách LEFT JOIN, RIGHT JOIN và FULL OUTER JOIN hành xử.
3) payments
Primary key: payment_id
Foreign key: order_id → orders.order_id
| payment_id | order_id | amount |
|---|---|---|
| 9001 | 101 | 50 |
| 9002 | 102 | 60 |
| 9003 | 102 | 60 |
| 9004 | 999 | 25 |
Hai chi tiết quan trọng để dạy:
order_id = 102có hai hàng payment (chia thanh toán). Khi bạn joinordersvớipayments, order đó sẽ xuất hiện hai lần — đây là nơi các bản sao thường làm mọi người bất ngờ.payment_id = 9004tham chiếuorder_id = 999, không tồn tại trongorders. Đó lại là một trường hợp “không khớp”.
Mong đợi gì khi JOIN các bảng này
- Hàng khớp: ví dụ, customer 1 ↔ orders 101/102; order 101 ↔ payment 9001.
- Hàng không khớp: ví dụ, customer 3 và 4 không có orders; order 104 không có customer; payment 9004 không có order.
- Bản sao: join
ordersvớipaymentssẽ nhân bản order 102 vì có hai payment liên quan.
INNER JOIN: Chỉ giữ hàng khớp
INNER JOIN trả về chỉ những hàng có khớp ở cả hai bảng. Nếu một khách hàng không có đơn, họ sẽ không xuất hiện. Nếu một đơn tham chiếu khách hàng không tồn tại (dữ liệu xấu), đơn đó cũng sẽ không xuất hiện.
Mô hình cơ bản
Bạn chọn bảng “trái”, join bảng “phải”, và nối bằng điều kiện trong ON.
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date
FROM customers c
INNER JOIN orders o
ON o.customer_id = c.customer_id;
Ý chính là dòng ON o.customer_id = c.customer_id: nó nói cho SQL biết các hàng liên quan như thế nào.
Trường hợp thực tế: khách hàng đã đặt hàng
Nếu bạn muốn danh sách chỉ những khách hàng đã thực sự đặt ít nhất một đơn (và chi tiết đơn), INNER JOIN là lựa chọn tự nhiên:
SELECT
c.name,
o.order_id,
o.total_amount
FROM customers c
INNER JOIN orders o
ON o.customer_id = c.customer_id
ORDER BY o.order_id;
Điều này hữu dụng cho việc như “gửi email theo dõi đơn” hoặc “tính doanh thu theo khách hàng” (khi bạn chỉ quan tâm khách hàng có mua hàng).
Sai lầm phổ biến: thiếu hoặc điều kiện join không rõ ràng
Nếu bạn viết join mà quên ON (hoặc join sai cột), bạn có thể vô tình tạo tích Descartes (mọi customer kết hợp với mọi order) hoặc tạo khớp sai.
Xấu (đừng làm):
SELECT c.name, o.order_id
FROM customers c
JOIN orders o;
Luôn đảm bảo có điều kiện join rõ ràng trong ON (hoặc USING khi phù hợp).
LEFT JOIN: Giữ mọi hàng từ bảng trái
LEFT JOIN trả về tất cả hàng từ bảng trái, và thêm dữ liệu khớp từ bảng phải khi có. Nếu không có khớp, các cột bên phải sẽ là NULL.
Khi dùng
Dùng LEFT JOIN khi bạn muốn danh sách đầy đủ từ bảng chính, cộng dữ liệu liên quan nếu có.
Ví dụ: “Hiển thị tất cả khách hàng, và bao gồm đơn hàng của họ nếu có.”
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
ORDER BY c.customer_id;
- Khách hàng có đơn sẽ xuất hiện kèm chi tiết.
- Khách hàng không có đơn vẫn xuất hiện, nhưng
o.order_id(và các cộtorders) sẽ làNULL.
Tìm các hàng “không khớp” (mẫu cổ điển)
Một lý do rất phổ biến để dùng LEFT JOIN là tìm những mục không có bản ghi liên quan.
Ví dụ: “Khách hàng nào chưa bao giờ đặt hàng?”
SELECT
c.customer_id,
c.name
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
Câu điều kiện WHERE ... IS NULL giữ chỉ những hàng bên trái mà join không tìm được khớp.
Chú ý: nhiều bản khớp sẽ nhân hàng
LEFT JOIN có thể “nhân” hàng bên trái khi có nhiều hàng khớp ở bên phải.
Nếu một khách hàng có 3 đơn, khách hàng đó sẽ xuất hiện 3 lần — một lần cho mỗi đơn. Điều này đúng, nhưng có thể làm bạn ngạc nhiên khi cố đếm số khách hàng.
Ví dụ, truy vấn sau đếm đơn (không phải khách hàng):
SELECT COUNT(*)
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id;
Nếu mục tiêu là đếm khách hàng, bạn thường sẽ đếm khóa khách hàng thay vào đó (thường với COUNT(DISTINCT c.customer_id)), tùy vào cách bạn đo.
RIGHT JOIN: Giữ mọi hàng từ bảng phải
RIGHT JOIN giữ mọi hàng từ bảng phải, và chỉ các hàng khớp từ bảng trái. Nếu không khớp, các cột bảng trái là NULL. Về bản chất nó là ảnh phản chiếu của LEFT JOIN.
Ví dụ đơn giản
Dùng bảng ví dụ, giả sử bạn muốn liệt kê mọi payment, ngay cả khi không thể liên kết với order (có thể order đã bị xóa hoặc dữ liệu payment lộn xộn).
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount,
p.paid_at
FROM orders o
RIGHT JOIN payments p
ON o.order_id = p.order_id;
Bạn sẽ nhận được:
- Tất cả payments được bao gồm (vì
paymentsở bên phải). - Nếu một payment không có order khớp,
o.order_idvào.customer_idsẽ làNULL.
Cùng truy vấn nhưng viết lại bằng LEFT JOIN (thường dễ đọc hơn)
Hầu hết thời gian, bạn có thể viết lại RIGHT JOIN thành LEFT JOIN bằng cách đổi thứ tự bảng:
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount,
p.paid_at
FROM payments p
LEFT JOIN orders o
ON o.order_id = p.order_id;
Kết quả giống nhau, nhưng nhiều người thấy dễ đọc hơn vì bắt đầu với bảng “chính” họ quan tâm (ở đây là payments) rồi kéo dữ liệu liên quan là tùy chọn.
Tính dễ đọc: lý do nhiều nhóm tránh RIGHT JOIN
Nhiều style guide SQL khuyên tránh RIGHT JOIN vì buộc người đọc phải đảo ngược mô hình thông thường:
- “Bắt đầu với bảng chính”
- “LEFT JOIN các bảng bổ sung”
Khi các quan hệ tùy chọn luôn viết thành LEFT JOIN, truy vấn dễ quét hơn.
Khi RIGHT JOIN vẫn tiện dụng
RIGHT JOIN hữu ích khi bạn chỉnh sửa truy vấn có sẵn và nhận ra bảng “phải giữ” đang ở bên phải. Thay vì viết lại toàn bộ truy vấn (đặc biệt khi nó dài và có nhiều join), đổi một join thành RIGHT JOIN có thể là thay đổi nhanh và ít rủi ro.
FULL OUTER JOIN: Giữ mọi hàng từ cả hai bảng
FULL OUTER JOIN trả về mọi hàng từ cả hai bảng.
- Nếu hàng khớp trên khóa, bạn có một hàng kết hợp (như
INNER JOIN). - Nếu hàng chỉ nằm ở trái, nó vẫn xuất hiện — với
NULLcho cột bên phải. - Nếu hàng chỉ nằm ở phải, nó vẫn xuất hiện — với
NULLcho cột bên trái.
Khi nó hữu dụng
Một trường hợp kinh điển là đối chiếu orders vs. payments:
- Bạn muốn xem các đơn đã thanh toán (khớp)
- đơn chưa thanh toán (order có, payment thiếu)
- payment lẻ (payment có nhưng không có order — lỗi dữ liệu, hoàn tiền, tham chiếu sai...)
Ví dụ:
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount
FROM orders o
FULL OUTER JOIN payments p
ON p.order_id = o.order_id;
Hỗ trợ cơ sở dữ liệu (ai chạy trực tiếp được)
FULL OUTER JOIN được hỗ trợ trong PostgreSQL, SQL Server, và Oracle.
Nó không có trong MySQL và SQLite (bạn cần thủ thuật thay thế).
Thay thế di động: UNION LEFT JOIN và RIGHT JOIN
Nếu DB không hỗ trợ FULL OUTER JOIN, bạn có thể mô phỏng bằng cách kết hợp:
- mọi hàng từ
orders(với payments khớp khi có), và - mọi hàng từ
paymentsmà không khớp vớiorders.
Một mẫu phổ biến:
SELECT o.order_id, o.customer_id, p.payment_id, p.amount
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.order_id
UNION
SELECT o.order_id, o.customer_id, p.payment_id, p.amount
FROM orders o
RIGHT JOIN payments p
ON p.order_id = o.order_id;
Mẹo: khi bạn thấy NULL ở một phía, đó là tín hiệu hàng đó “thiếu” ở bảng kia — chính xác điều bạn muốn cho kiểm toán và đối chiếu.
CROSS JOIN: Tạo mọi kết hợp (dùng cẩn thận)
CROSS JOIN trả về mọi cặp hàng giữa hai bảng. Nếu bảng A có 3 hàng và bảng B có 4 hàng, kết quả sẽ có 3 × 4 = 12 hàng. Đây còn gọi là tích Descartes.
Nghe có vẻ nguy hiểm — và đúng là vậy — nhưng nó thực sự hữu ích khi bạn muốn tạo các kết hợp.
Ví dụ nhỏ, an toàn: sizes × colors (tạo SKU)
Giả sử bạn quản lý các tuỳ chọn sản phẩm riêng:
sizes: S, M, Lcolors: Red, Blue
Một CROSS JOIN có thể sinh tất cả biến thể:
SELECT
s.size,
c.color
FROM sizes AS s
CROSS JOIN colors AS c;
Kết quả (3 × 2 = 6 hàng):
- S / Red
- S / Blue
- M / Red
- M / Blue
- L / Red
- L / Blue
Cảnh báo lớn: kết quả tăng nhanh
Vì số hàng nhân lên, CROSS JOIN có thể bùng nổ nhanh:
- 10,000 customers × 50 products = 500,000 hàng
- 100,000 × 100,000 = 10,000,000,000 hàng
Điều này có thể làm chậm truy vấn, quá tải bộ nhớ, và tạo ra đầu ra không sử dụng được. Nếu cần kết hợp, giữ các bảng đầu vào nhỏ và cân nhắc thêm giới hạn hoặc bộ lọc.
SELF JOIN: Join bảng với chính nó
SELF JOIN là join một bảng với chính nó. Điều này hữu ích khi một hàng trong bảng liên quan tới một hàng khác trong cùng bảng — thường là quan hệ cha/con như nhân viên và quản lý.
Tại sao cần bí danh (và nó giúp gì)
Vì dùng cùng một bảng hai lần, bạn phải đặt bí danh khác nhau cho mỗi “bản sao”. Bí danh làm truy vấn dễ đọc và cho SQL biết bạn đang tham chiếu bên nào.
Mẫu phổ biến:
echo employeemcho manager
Ví dụ thực tế: nhân viên và quản lý
Giả sử bảng employees có:
idnamemanager_id(trỏ tớiidcủa nhân viên khác)
Để liệt kê mỗi nhân viên cùng tên quản lý:
SELECT
e.id,
e.name AS employee_name,
m.name AS manager_name
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id;
Xử lý nhân viên cấp cao (manager_id là NULL)
Lưu ý truy vấn dùng LEFT JOIN, không phải INNER JOIN. Điều này quan trọng vì một số nhân viên có thể không có quản lý (ví dụ CEO). Trong trường hợp đó, manager_id thường là NULL, và LEFT JOIN giữ hàng nhân viên trong khi hiển thị manager_name là NULL.
Nếu dùng INNER JOIN thay vì, những nhân viên cấp cao đó sẽ biến mất khỏi kết quả vì không có hàng quản lý để join.
Điều kiện JOIN: ON vs USING (và vì sao quan trọng)
Một JOIN không “tự biết” hai bảng liên quan thế nào — bạn phải chỉ định. Mối quan hệ đó được định nghĩa trong điều kiện JOIN, và nó nên đứng ngay cạnh từ khóa JOIN vì nó giải thích cách các bảng khớp, không phải cách bạn lọc kết quả cuối.
ON: linh hoạt nhất (và phổ biến nhất)
Dùng ON khi bạn muốn toàn quyền kiểm soát logic khớp — tên cột khác nhau, nhiều điều kiện, hoặc quy tắc thêm.
SELECT
c.customer_id,
c.name,
o.order_id,
o.created_at
FROM customers AS c
INNER JOIN orders AS o
ON o.customer_id = c.customer_id;
ON cũng là nơi bạn định nghĩa những khớp phức tạp hơn (ví dụ khớp trên hai cột) mà không làm truy vấn khó hiểu.
USING: ngắn gọn nhưng chỉ cho cột cùng tên
Một số DB (như PostgreSQL và MySQL) hỗ trợ USING. Nó là viết tắt tiện khi cả hai bảng có một cột cùng tên và bạn muốn join theo cột đó.
SELECT
customer_id,
name,
order_id
FROM customers
JOIN orders
USING (customer_id);
Một lợi ích: USING thường trả về chỉ một cột customer_id trong output (thay vì hai cột giống nhau).
Tránh tên cột mơ hồ: luôn gắn tiền tố khi join
Sau khi join, tên cột thường trùng nhau (id, created_at, status). Nếu bạn viết SELECT id, DB có thể báo lỗi “ambiguous column” — hoặc tệ hơn, bạn có thể đọc nhầm id.
Ưu tiên tiền tố bảng (hoặc bí danh) cho rõ ràng:
SELECT c.customer_id, o.order_id
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id;
Tránh SELECT * trong truy vấn có join
SELECT * nhanh chóng trở nên lộn xộn với joins: bạn kéo vào các cột không cần, gặp rủi ro trùng tên, và làm khó hiểu mục đích truy vấn.
Thay vào đó, chọn đúng cột bạn cần. Kết quả rõ ràng hơn, dễ bảo trì hơn, và thường hiệu quả hơn — đặc biệt khi bảng rộng.
Lọc dữ liệu đã JOIN: WHERE vs ON
Khi bạn join bảng, WHERE và ON đều “lọc”, nhưng ở những thời điểm khác nhau.
- ON quyết định những hàng nào khớp trong lúc join.
- WHERE lọc kết quả cuối sau khi join hoàn thành.
Sự khác về thời gian này là lý do nhiều người vô tình biến LEFT JOIN thành INNER JOIN.
Khi WHERE làm hỏng LEFT JOIN
Giả sử bạn muốn tất cả khách hàng, kể cả những người không có đơn trả tiền gần đây.
SELECT c.customer_id, c.name, o.order_id, o.status, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
AND o.order_date >= DATE '2025-01-01';
Vấn đề: với khách hàng không có đơn, o.status và o.order_date là NULL. Điều kiện WHERE loại những hàng đó, nên khách hàng không khớp biến mất — LEFT JOIN hoạt động như INNER JOIN.
Di chuyển điều kiện liên quan vào ON để giữ hàng không khớp
SELECT c.customer_id, c.name, o.order_id, o.status, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
AND o.status = 'PAID'
AND o.order_date >= DATE '2025-01-01';
Bây giờ khách hàng không có đơn phù hợp vẫn hiển thị (với các cột order là NULL), thường là mục đích khi dùng LEFT JOIN.
Checklist nhanh: điều gì thuộc ON, điều gì thuộc WHERE?
- Đặt điều kiện vào ON khi chúng mô tả những hàng bên phải được phép khớp (lọc trạng thái/ngày/loại trên bảng join).
- Đặt điều kiện vào WHERE khi chúng mô tả những hàng cuối bạn muốn giữ (lọc trên bảng trái, hoặc khi bạn thực sự yêu cầu có khớp).
- Nếu muốn “giữ mọi hàng bên trái nhưng giới hạn các hàng bên phải”, ưu tiên LEFT JOIN + điều kiện trong ON.
- Nếu bạn thực sự muốn “chỉ hàng có khớp”, dùng INNER JOIN (hoặc
WHERE o.order_id IS NOT NULLrõ ràng).
Tránh trùng lặp và bất ngờ trong mối quan hệ nhiều-nhiều
Joins không chỉ “thêm cột” — chúng còn có thể nhân hàng. Điều này thường đúng nhưng thường làm mọi người ngạc nhiên khi tổng đột nhiên tăng gấp đôi (hoặc tệ hơn).
Tại sao hàng nhân lên
Một join trả về một hàng kết quả cho mỗi cặp hàng khớp.
- One-to-many: Một khách hàng có nhiều đơn. Nếu bạn join
customersvớiorders, mỗi khách hàng có thể xuất hiện nhiều lần — một lần cho mỗi đơn. - Many-to-many (cái bẫy thực sự): Nếu bạn join
ordersvớipaymentsvà mỗi order có thể có nhiều payment, bạn có thể có nhiều hàng cho mỗi order. Nếu bạn cũng join đến một bảng “many” khác (nhưorder_items), bạn có thể tạo hiệu ứng nhân:payments × itemscho mỗi order.
Tổng hợp trước khi join
Nếu mục tiêu là “một hàng cho mỗi khách hàng” hoặc “một hàng cho mỗi order”, hãy tóm tắt phía “many” trước, rồi mới join.
-- Một hàng cho mỗi order từ payments
WITH payment_totals AS (
SELECT
order_id,
SUM(amount) AS total_paid,
COUNT(*) AS payment_count
FROM payments
GROUP BY order_id
)
SELECT
o.order_id,
o.customer_id,
COALESCE(pt.total_paid, 0) AS total_paid,
COALESCE(pt.payment_count, 0) AS payment_count
FROM orders o
LEFT JOIN payment_totals pt
ON pt.order_id = o.order_id;
Điều này giữ hình dạng join dự đoán được: một hàng order vẫn là một hàng order.
DISTINCT là phương án cuối cùng
SELECT DISTINCT có thể làm vẻ như loại bỏ trùng lặp, nhưng nó có thể che dấu vấn đề thực sự:
- Nó có thể im lặng loại bỏ các hàng hợp lệ.
- Nó có thể che dấu điều kiện join sai (như thiếu một phần của khóa ghép).
- Nó có thể làm sai tổng (đặc biệt là sum và count).
Chỉ dùng khi bạn chắc chắn các bản sao là vô ý và hiểu vì sao chúng xảy ra.
Kiểm tra an toàn nhanh: xác thực số lượng
Trước khi tin tưởng kết quả, so sánh số lượng hàng:
- Đếm hàng trong bảng chính (ví dụ orders).
- Đếm hàng sau join.
- Nếu số tăng đột ngột, kiểm tra khóa nào gây nhiều khớp và quyết xem có cần tổng hợp trước hay đường join khác.
Cơ bản về hiệu năng và bảng cheat-sheet JOIN
JOINs thường bị đổ lỗi cho “truy vấn chậm”, nhưng nguyên nhân thực sự thường là bạn yêu cầu DB kết hợp bao nhiêu dữ liệu, và DB có thể tìm các hàng khớp nhanh thế nào.
Chỉ số (khái niệm) và vì sao nó giúp JOINs
Hãy tưởng tượng index như mục lục của một cuốn sách. Không có index, DB có thể phải quét nhiều hàng để tìm khớp cho điều kiện JOIN của bạn. Có index trên khóa join (ví dụ customers.customer_id và orders.customer_id) thì DB có thể nhảy tới hàng liên quan nhanh hơn.
Bạn không cần hiểu chi tiết nội bộ để dùng tốt: nếu một cột thường dùng để khớp hàng (ON a.id = b.a_id), đó là ứng viên tốt để được index.
Join trên khóa ổn định (không phải tên hay email)
Khi có thể, join trên các định danh ổn định, duy nhất:
- Tốt:
customers.customer_id = orders.customer_id - Rủi ro:
customers.email = orders.emailhoặccustomers.name = orders.name
Tên có thể thay đổi và trùng nhau. Email có thể thay đổi, thiếu hoặc khác kiểu chữ. ID được thiết kế để khớp nhất quán và thường đã được index.
Giảm khối lượng công việc sớm
Hai thói quen làm JOIN nhanh hơn rõ rệt:
- Chọn ít cột hơn. Tránh
SELECT *khi join nhiều bảng — cột thừa tăng dùng bộ nhớ và băng thông. - Hạn chế hàng trước hoặc trong JOIN. Lọc sớm khi hợp lý.
Ví dụ: giới hạn orders trước, rồi join:
SELECT c.customer_id, c.name, o.order_id, o.created_at
FROM customers c
JOIN (
SELECT order_id, customer_id, created_at
FROM orders
WHERE created_at >= DATE '2025-01-01'
) o
ON o.customer_id = c.customer_id;
Nếu bạn lặp trên các truy vấn này trong khi xây app (ví dụ tạo trang báo cáo dùng PostgreSQL), các công cụ như Koder.ai có thể tăng tốc phần scaffold — schema, endpoints, UI — trong khi bạn vẫn kiểm soát logic JOIN quyết định độ chính xác.
Bảng tóm tắt JOIN nhanh
- INNER JOIN → chỉ các hàng khớp ở cả hai bảng
- LEFT JOIN → tất cả hàng từ bảng trái, cộng các khớp từ phải (không khớp sẽ là
NULL) - RIGHT JOIN → tất cả hàng từ bảng phải, cộng các khớp từ trái (
NULLkhi thiếu) - FULL OUTER JOIN → tất cả hàng từ cả hai bảng; hàng khớp ghép lại, không khớp hiện
NULL - CROSS JOIN → mọi kết hợp của hàng (số lượng nhân lên; dùng cẩn thận)
- SELF JOIN → một bảng join với chính nó (hữu ích cho phân cấp và so sánh)
Câu hỏi thường gặp
SQL JOIN là gì, nói đơn giản?
Một JOIN trong SQL kết hợp các hàng từ hai (hoặc nhiều) bảng thành một tập kết quả bằng cách khớp các cột có liên quan — thường là một khóa chính với khóa ngoại (ví dụ: customers.customer_id = orders.customer_id). Đây là cách bạn “nối lại” các bảng đã chuẩn hóa khi cần báo cáo, đối chiếu hoặc phân tích.
Khi nào tôi nên dùng INNER JOIN?
Dùng INNER JOIN khi bạn chỉ muốn các hàng có mối quan hệ tồn tại ở cả hai bảng.
- Những khách hàng không có đơn sẽ không xuất hiện.
- Những đơn tham chiếu đến khách hàng bị thiếu cũng sẽ không xuất hiện.
Nó lý tưởng cho những mối quan hệ “đã xác nhận”, như liệt kê chỉ những khách hàng thực sự đã đặt hàng.
Làm sao để tìm các hàng không có khớp trong bảng khác?
Dùng LEFT JOIN khi bạn cần tất cả các hàng từ bảng chính (bên trái), cộng thêm dữ liệu khớp từ bên phải nếu có.
Để tìm “không có khớp”, JOIN rồi lọc phần bên phải là NULL:
SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
Tôi có thật sự cần RIGHT JOIN không, và làm sao thay thế bằng LEFT JOIN?
RIGHT JOIN giữ mọi hàng từ bảng bên phải và điền NULL cho các cột bên trái khi không có khớp. Nhiều đội tránh dùng nó vì đọc “ngược”.
Trong hầu hết trường hợp, bạn có thể viết lại bằng LEFT JOIN bằng cách đổi thứ tự bảng:
FROM payments p
LEFT JOIN orders o ON o.order_id = p.order_id
FULL OUTER JOIN dùng tốt nhất cho việc gì?
Dùng FULL OUTER JOIN để đối chiếu: bạn muốn xuất cả các bản khớp, các hàng chỉ bên trái và các hàng chỉ bên phải trong cùng một kết quả.
Nó rất hữu ích cho kiểm toán như “orders không có payments” và “payments không có orders”, vì các bên không khớp sẽ hiện dưới dạng cột NULL.
Nếu cơ sở dữ liệu không hỗ trợ FULL OUTER JOIN thì sao?
Một số cơ sở dữ liệu (ví dụ MySQL và SQLite) không hỗ trợ FULL OUTER JOIN trực tiếp. Cách làm phổ biến là kết hợp hai truy vấn:
orders LEFT JOIN payments- cộng các hàng chỉ nằm ở phía payments
Thường thực hiện bằng UNION (hoặc UNION ALL kèm lọc cẩn thận) để giữ cả các bản “left-only” và “right-only”.
CROSS JOIN là gì, và khi nào thực sự hữu dụng?
CROSS JOIN trả về mọi kết hợp giữa các hàng của hai bảng (tích Descartes). Nó hữu ích để tạo kịch bản (ví dụ sizes × colors) hoặc xây lưới lịch.
Cẩn thận: số hàng nhân lên rất nhanh, nên đảm bảo các bảng đầu vào nhỏ và được kiểm soát.
SELF JOIN là gì, và tại sao cần dùng bí danh bảng?
Self join là việc JOIN một bảng với chính nó để liên kết các hàng trong cùng một bảng (thường cho cấu trúc phân cấp như employee → manager).
Bạn phải dùng bí danh để phân biệt hai “bản sao”:
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id
Phân biệt lọc trong ON so với WHERE cho các JOIN?
ON định nghĩa cách các hàng khớp trong lúc JOIN; WHERE lọc sau khi kết quả JOIN đã được hình thành. Với LEFT JOIN, điều kiện WHERE trên bảng bên phải có thể vô tình loại bỏ các hàng NULL và biến nó thành INNER JOIN.
Nếu bạn muốn giữ tất cả hàng bên trái nhưng giới hạn các hàng bên phải được phép khớp, đặt điều kiện liên quan đến bảng bên phải trong ON thay vì WHERE.
Tại sao JOIN tạo ra bản sao, và làm sao tránh đếm nhầm?
Joins nhân hàng khi mối quan hệ là một-nhiều hoặc nhiều-nhiều. Ví dụ, một order có hai payment sẽ xuất hiện hai lần khi JOIN orders với payments.
Để tránh đếm nhầm, hãy tổng hợp phía “many” trước (ví dụ SUM(amount) nhóm theo order_id) rồi mới JOIN. Dùng DISTINCT chỉ khi thực sự cần và bạn hiểu tại sao các bản sao xảy ra, vì DISTINCT có thể che dấu vấn đề thực sự và làm sai tổng cộng.