← Tất cả bài viết

SQL JOIN cheat sheet: INNER, LEFT, RIGHT, FULL và CROSS JOIN

19/07/2026

Khi làm việc với cơ sở dữ liệu quan hệ như PostgreSQL, dữ liệu hiếm khi nằm gọn trong một bảng duy nhất. Thông tin người dùng ở một bảng, đơn hàng ở một bảng khác, sản phẩm ở bảng thứ ba. Để trả lời một câu hỏi thực tế như "mỗi người dùng đã đặt bao nhiêu đơn và tổng tiền là bao nhiêu", bạn cần ghép (JOIN) các bảng lại với nhau qua khoá liên kết. Bài viết này là một bản tra cứu nhanh (cheat sheet) cho 5 loại JOIN quan trọng nhất, kèm ví dụ chạy được với PostgreSQL.

JOIN là gì và vì sao cần?

Trong mô hình quan hệ, dữ liệu được chuẩn hoá và tách thành nhiều bảng để tránh trùng lặp. Mối quan hệ giữa các bảng được biểu diễn bằng khoá ngoại (foreign key): một cột ở bảng con trỏ tới khoá chính của bảng cha. JOIN là câu lệnh SQL cho phép bạn kết hợp các dòng từ hai (hoặc nhiều) bảng dựa trên điều kiện khớp giữa các cột đó — thường là bảng_a.khoa = bảng_b.khoa_ngoai.

Xuyên suốt bài viết, chúng ta dùng hai bảng ví dụ đơn giản:

CREATE TABLE users (
    id   INTEGER PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE orders (
    id      INTEGER PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    total   NUMERIC(10, 2) NOT NULL
);

Và dữ liệu mẫu — lưu ý có chủ đích một user không có đơn nào, và một đơn "mồ côi" không thuộc user nào (giả sử dữ liệu chưa ràng buộc chặt) để thấy rõ khác biệt giữa các loại JOIN:

INSERT INTO users (id, name) VALUES
    (1, 'An'),
    (2, 'Bình'),
    (3, 'Chi');   -- Chi chưa đặt đơn nào

INSERT INTO orders (id, user_id, total) VALUES
    (100, 1, 250.00),
    (101, 1, 80.00),
    (102, 2, 120.00),
    (103, NULL, 45.00);  -- đơn không gắn user

INNER JOIN — chỉ giữ dòng khớp cả hai bên

Định nghĩa: INNER JOIN chỉ trả về những dòng mà điều kiện ON khớp ở cả hai bảng. Dòng nào không tìm được cặp tương ứng ở bảng kia sẽ bị loại.

Khi nào dùng: khi bạn chỉ quan tâm tới các bản ghi có quan hệ đầy đủ — ví dụ "liệt kê các đơn hàng cùng tên người đặt". Đây là loại JOIN dùng nhiều nhất.

SELECT u.name, o.id AS order_id, o.total
FROM users u
INNER JOIN orders o ON o.user_id = u.id;

Kết quả: An (2 đơn), Bình (1 đơn). Chi bị loại vì chưa có đơn, và đơn 103 (user_id NULL) cũng bị loại vì không khớp user nào. Từ khoá INNER có thể lược bỏ — viết JOIN trơn cũng chính là inner join.

LEFT (OUTER) JOIN — giữ mọi dòng bảng trái

Định nghĩa: LEFT JOIN giữ tất cả dòng của bảng bên trái, bổ sung dữ liệu từ bảng phải khi khớp. Nếu không khớp, các cột bên phải trả về NULL.

Khi nào dùng: khi bạn muốn giữ toàn bộ danh sách gốc dù có dữ liệu liên quan hay không — ví dụ "liệt kê tất cả user, kèm đơn hàng nếu có". Cực kỳ hữu ích để tìm bản ghi thiếu quan hệ.

SELECT u.name, o.id AS order_id, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

Kết quả có thêm dòng Chi với order_id = NULLtotal = NULL. Muốn tìm những user chưa từng đặt đơn, chỉ cần lọc phần NULL đó:

SELECT u.name
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;   -- các user không có đơn

RIGHT (OUTER) JOIN — giữ mọi dòng bảng phải

Định nghĩa: RIGHT JOIN là bản đối xứng của LEFT JOIN — giữ tất cả dòng của bảng bên phải, điền NULL cho các cột bên trái khi không khớp.

Khi nào dùng: trên thực tế ít khi dùng, vì bạn luôn có thể đảo thứ tự bảng và viết bằng LEFT JOIN cho dễ đọc. Nó tiện khi bạn thêm một bảng vào cuối câu query dài và không muốn viết lại thứ tự.

SELECT u.name, o.id AS order_id, o.total
FROM users u
RIGHT JOIN orders o ON o.user_id = u.id;

Kết quả giữ mọi đơn hàng, kể cả đơn 103 (user_id NULL) — dòng đó có name = NULL. Còn Chi bị loại vì không có đơn. Câu này tương đương với việc đổi chỗ hai bảng rồi dùng LEFT JOIN:

SELECT u.name, o.id AS order_id, o.total
FROM orders o
LEFT JOIN users u ON o.user_id = u.id;

FULL (OUTER) JOIN — giữ dòng không khớp ở cả hai bên

Định nghĩa: FULL JOIN kết hợp cả LEFT và RIGHT — giữ tất cả dòng của cả hai bảng, điền NULL ở bên nào không có cặp khớp.

Khi nào dùng: khi bạn cần bức tranh trọn vẹn từ hai phía — ví dụ đối soát dữ liệu, tìm cả user không đơn lẫn đơn không user trong một lần chạy.

SELECT u.name, o.id AS order_id, o.total
FROM users u
FULL JOIN orders o ON o.user_id = u.id;

Kết quả gồm: An, Bình (khớp), Chi (user không đơn, phần order NULL) và đơn 103 (đơn không user, phần name NULL). Đây là cách nhanh để soi mọi bản ghi lệch:

SELECT u.id AS user_id, o.id AS order_id
FROM users u
FULL JOIN orders o ON o.user_id = u.id
WHERE u.id IS NULL OR o.id IS NULL;  -- mọi dòng "mồ côi" hai phía

CROSS JOIN — tích Descartes

Định nghĩa: CROSS JOIN ghép mọi dòng bảng trái với mọi dòng bảng phải, không cần điều kiện ON. Kết quả có số dòng = số dòng bảng trái × số dòng bảng phải.

Khi nào dùng: khi cần sinh mọi tổ hợp có thể — ví dụ ghép mọi kích cỡ với mọi màu sắc, tạo lịch trống cho mọi nhân viên × mọi ngày. Hãy cẩn thận: 1000 × 1000 dòng = 1 triệu dòng.

SELECT u.name, o.id AS order_id
FROM users u
CROSS JOIN orders o;   -- 3 user × 4 đơn = 12 dòng

Lưu ý: một JOIN mà quên mất mệnh đề ON (hoặc điều kiện sai) sẽ vô tình biến thành tích Descartes khổng lồ — nguyên nhân kinh điển của query "chạy mãi không xong".

Bảng so sánh nhanh

Loại JOIN Giữ dòng không khớp của bảng trái? Giữ dòng không khớp của bảng phải?
INNER JOIN Không Không
LEFT JOIN Không
RIGHT JOIN Không
FULL JOIN
CROSS JOIN Không có điều kiện — ghép mọi tổ hợp Không có điều kiện — ghép mọi tổ hợp

Khi câu query JOIN nhiều bảng bắt đầu dài ra, việc đọc và soi lỗi trở nên khó. Dán query vào SQL Formatter để căn lề, thụt dòng và xuống hàng gọn gàng sẽ giúp bạn nhìn rõ từng mệnh đề JOIN ... ON ... đang khớp cột nào.

Những cái bẫy thường gặp

1. Điều kiện WHERE làm mất tính "outer" của LEFT JOIN

Đây là lỗi phổ biến nhất. Bạn viết LEFT JOIN để giữ mọi user, nhưng lại đặt điều kiện lọc trên bảng phải trong mệnh đề WHERE:

-- SAI: WHERE trên bảng phải loại luôn các dòng NULL
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.total > 100;

Vấn đề: với user không có đơn, o.totalNULL, mà NULL > 100 cho kết quả không đúng (unknown), nên dòng đó bị loại — LEFT JOIN thoái hoá thành INNER JOIN. Nếu bạn muốn điều kiện chỉ áp dụng khi khớp, hãy đưa nó vào mệnh đề ON:

-- ĐÚNG: điều kiện lọc bảng phải đặt trong ON
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.total > 100;

Quy tắc ghi nhớ: điều kiện lọc bảng phải (bên được OUTER) thuộc về ON; điều kiện lọc bảng trái thuộc về WHERE.

2. Nhân bản dòng khi khoá không unique

Nếu cột join ở bảng phải không duy nhất, mỗi dòng bảng trái sẽ được nhân lên theo số dòng khớp. An có 2 đơn, nên câu query dưới đây trả về 2 dòng cho An — nếu bạn quên và đi SUM một giá trị của bảng users, con số sẽ bị đếm trùng:

-- Cẩn thận: users bị nhân đôi theo số đơn
SELECT u.name, o.total
FROM users u
JOIN orders o ON o.user_id = u.id;
-- An xuất hiện 2 lần

Khi cần số liệu tổng hợp, hãy gộp bằng GROUP BY thay vì join thô rồi cộng ở tầng ứng dụng:

SELECT u.name, COUNT(o.id) AS so_don, COALESCE(SUM(o.total), 0) AS tong_tien
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY tong_tien DESC;

3. Xử lý NULL khi không khớp

Với các OUTER JOIN, cột bên không khớp luôn là NULL. Dùng COALESCE để thay giá trị mặc định (như ví dụ SUM ở trên trả về 0 thay vì NULL), và nhớ rằng so sánh = NULL luôn sai — phải dùng IS NULL / IS NOT NULL.

Vài mẹo giúp query JOIN dễ đọc và ít lỗi

  • Luôn đặt alias ngắn cho bảng (users u, orders o) và prefix mọi cột bằng alias — tránh mơ hồ khi hai bảng có cột trùng tên như id.
  • Phân biệt rõ ON và WHERE: ON quyết định cách ghép dòng; WHERE lọc kết quả sau khi ghép. Đặt sai chỗ là nguồn gốc của bẫy số 1.
  • Nêu rõ loại JOIN: viết INNER JOIN / LEFT JOIN đầy đủ thay vì JOIN trơn để người đọc sau này hiểu ngay ý định.
  • Format query gọn gàng: mỗi JOIN một dòng, thụt lề mệnh đề ON — query 5-6 bảng sẽ dễ soi hơn rất nhiều.
  • Kiểm tra tính duy nhất của khoá join trước khi tổng hợp số liệu, để không bị đếm trùng.

Tổng kết

Năm loại JOIN phủ gần như mọi nhu cầu ghép bảng: INNER giữ phần giao, LEFT/RIGHT giữ trọn một phía, FULL giữ cả hai, còn CROSS sinh mọi tổ hợp. Nắm chắc bảng so sánh "giữ dòng không khớp bên nào", tránh bẫy WHERE-trên-bảng-phải và cẩn thận với khoá không unique, bạn đã xử lý được đại đa số truy vấn thực tế trong PostgreSQL.

Lần tới khi phải đọc một câu JOIN nhiều tầng rối rắm, hãy để công cụ giúp bạn: dán vào SQL Formatter của DevToolHub để format tức thì ngay trên trình duyệt — miễn phí, không gửi dữ liệu lên server. Chúc bạn viết query gọn và đúng!