Chuyển tới nội dung chính

SQL Joins (Các Phép Nối SQL)

Bạn sẽ Học được gì

Kết thúc trang này, bạn sẽ có thể:

  • Giải thích tại sao phép nối (JOIN) cần thiết trong cơ sở dữ liệu quan hệ (Relational Database)
  • Viết các truy vấn INNER JOIN, LEFT JOIN, RIGHT JOINFULL OUTER JOIN
  • Hiểu sự khác biệt giữa từng loại nối bằng sơ đồ Venn (Venn Diagrams)bảng kết quả
  • Sử dụng CROSS JOINSELF JOIN cho các trường hợp đặc biệt
  • Tránh các cạm bẫy hiệu năng phổ biến khi viết truy vấn nối
  • Chọn loại nối phù hợp cho từng câu hỏi kinh doanh

Vấn đề: Dữ liệu Nằm ở Nhiều Bảng

Cơ sở dữ liệu quan hệ chia dữ liệu thành nhiều bảng để tránh trùng lặp và duy trì tính toàn vẹn (Chuẩn hóa - Normalization). Nhưng để trả lời các câu hỏi thực tế, bạn thường cần kết hợp dữ liệu từ nhiều bảng.

employees table:          departments table:
+----+--------+--------+ +--------+------------------+
| id | name | dept_id| | dept_id| dept_name |
+----+--------+--------+ +--------+------------------+
| 1 | Alice | 10 | | 10 | Engineering |
| 2 | Bob | 20 | | 20 | Marketing |
| 3 | Carol | 30 | | 30 | Sales |
| 4 | Dave | NULL | | 40 | HR |
+----+--------+--------+ +--------+------------------+

Các câu hỏi bạn không thể trả lời chỉ từ một bảng:

  • "Alice làm ở phòng ban nào?" — cần cả hai bảng
  • "Phòng ban nào không có nhân viên?" — cần cả hai bảng
  • "Ai không có phòng ban?" — cần cả hai bảng

Phép nối (JOIN) là cơ chế SQL kết hợp các dòng từ hai bảng dựa trên cột liên kết giữa chúng.

Các Bảng Mẫu

Tất cả ví dụ sử dụng hai bảng sau:

CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50)
);

INSERT INTO departments VALUES
(10, 'Engineering'),
(20, 'Marketing'),
(30, 'Sales'),
(40, 'HR');

CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
dept_id INT REFERENCES departments(dept_id)
);

INSERT INTO employees VALUES
(1, 'Alice', 10),
(2, 'Bob', 20),
(3, 'Carol', 30),
(4, 'Dave', NULL),
(5, 'Eve', 10);

Điểm quan trọng cần lưu ý:

  • Phòng ban 40 (HR) không có nhân viên
  • Nhân viên 4 (Dave) không có phòng ban (dept_id là NULL)
  • Sự bất đối xứng này làm nổi bật sự khác biệt giữa các loại nối

Tổng quan Các loại Nối

INNER JOIN

Trả về chỉ các dòng có sự khớp ở cả hai bảng. Các dòng không khớp từ bất kỳ bảng nào đều bị loại trừ.

SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;

Kết quả:

namedept_name
AliceEngineering
BobMarketing
CarolSales
EveEngineering
  • 4 dòng được trả về — chỉ nhân viên có phòng ban hợp lệ
  • Dave bị loại trừ (không có phòng ban)
  • HR bị loại trừ (không có nhân viên)
mẹo

INNER JOIN là mặc định — viết chỉ JOIN tương đương với INNER JOIN.

LEFT JOIN (Left Outer Join)

Trả về tất cả dòng từ bảng bên tráicác dòng khớp từ bảng bên phải. Nếu không có sự khớp, các cột bên phải được điền NULL.

SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;

Kết quả:

namedept_name
AliceEngineering
BobMarketing
CarolSales
DaveNULL
EveEngineering
  • 5 dòng được trả về — tất cả nhân viên đều được bao gồm
  • dept_name của Dave là NULL vì anh ấy không có phòng ban
  • HR vẫn bị loại trừ (không có nhân viên)

Tìm các dòng không có sự khớp

Một mẫu phổ biến là sử dụng LEFT JOIN ... WHERE right.id IS NULL để tìm các dòng tồn tại ở bảng trái nhưng không có sự khớp ở bảng phải:

-- Tìm nhân viên không có phòng ban
SELECT e.name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_id IS NULL;

Kết quả:

name
Dave

RIGHT JOIN (Right Outer Join)

Trả về tất cả dòng từ bảng bên phảicác dòng khớp từ bảng bên trái. Nếu không có sự khớp, các cột bên trái được điền NULL.

SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id;

Kết quả:

namedept_name
AliceEngineering
BobMarketing
CarolSales
NULLHR
EveEngineering
  • 5 dòng được trả về — tất cả phòng ban đều được bao gồm
  • HR có NULL cho name vì không có nhân viên
  • Dave lúc này bị loại trừ (không có phòng ban)
mẹo

RIGHT JOIN về mặt chức năng tương đương với LEFT JOIN khi đổi thứ tự bảng. Trong thực tế, hầu hết lập trình viên thích LEFT JOIN hơn vì dễ đọc — giữ bảng "chính" ở trước.

FULL OUTER JOIN

Trả về tất cả dòng từ cả hai bảng. Các dòng khớp được kết hợp; các dòng không khớp từ bất kỳ bên nào nhận NULL cho các cột của bảng kia.

SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;

Kết quả:

namedept_name
AliceEngineering
BobMarketing
CarolSales
DaveNULL
EveEngineering
NULLHR
  • 6 dòng được trả về — mọi nhân viên và mọi phòng ban
  • Dave có NULL cho phòng ban (không có phòng ban)
  • HR có NULL cho tên (không có nhân viên)

Tìm các dòng không khớp từ cả hai bên

-- Tìm nhân viên không có phòng ban VÀ phòng ban không có nhân viên
SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id
WHERE e.dept_id IS NULL OR d.dept_id IS NULL;

Kết quả:

namedept_name
DaveNULL
NULLHR

CROSS JOIN

Trả về tích Descartes (Cartesian Product) — mọi dòng từ bảng trái kết hợp với mọi dòng từ bảng phải. Không cần điều kiện ON.

SELECT e.name, d.dept_name
FROM employees e
CROSS JOIN departments d;

Kết quả: 5 nhân viên x 4 phòng ban = 20 dòng

namedept_name
AliceEngineering
AliceMarketing
AliceSales
AliceHR
BobEngineering
......
cảnh báo

CROSS JOIN có thể tạo tập kết quả rất lớn. Cross join hai bảng mỗi bảng 10,000 dòng sẽ tạo ra 100,000,000 dòng. Hãy sử dụng có chủ đích, không phải vô tình.

Khi nào sử dụng CROSS JOIN

  • Tạo tất cả tổ hợp (ví dụ: tất cả kích thước x tất cả màu sắc)
  • Tạo ma trận hoặc lưới giá trị
  • Xây dựng kỳ báo cáo bằng cách kết hợp phạm vi ngày với các thực thể

SELF JOIN

Bảng tự nối với chính nó. Hữu ích khi các dòng trong cùng bảng có mối quan hệ phân cấp (Hierarchical Relationship) hoặc quan hệ ngang hàng.

Ví dụ: Nhân viên và Quản lý

ALTER TABLE employees ADD COLUMN manager_id INT REFERENCES employees(id);

-- Alice và Bob báo cáo cho Eve (manager_id = 5)
-- Carol báo cáo cho Alice (manager_id = 1)
-- Dave và Eve không có quản lý
UPDATE employees SET manager_id = 5 WHERE id IN (1, 2);
UPDATE employees SET manager_id = 1 WHERE id = 3;
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Kết quả:

employeemanager
AliceEve
BobEve
CarolAlice
DaveNULL
EveNULL

Các trường hợp sử dụng SELF JOIN phổ biến

Trường hợpBảng tự tham chiếu để tìm...
Sơ đồ tổ chức (Org Chart)Mối quan hệ Nhân viên → Quản lý
Cấu trúc vật tư (Bill of Materials)Mối quan hệ Lắp ráp → Linh kiện
Cây danh mục (Category Tree)Mối quan hệ Danh mục → Danh mục cha
Đồ thị bạn bè/người theo dõiMối quan hệ Người dùng → Người dùng liên quan

So sánh Các loại Nối

INNERLEFTRIGHTFULL OUTERCROSS
Trả vềChỉ các dòng khớpTất cả trái + khớp phảiTất cả phải + khớp tráiTất cả dòng từ cả haiMọi tổ hợp
Dòng trái không khớpLoại trừBao gồm (NULL bên phải)Loại trừBao gồm (NULL bên phải)N/A
Dòng phải không khớpLoại trừLoại trừBao gồm (NULL bên trái)Bao gồm (NULL bên trái)N/A
Kích thước kết quảNhỏ nhấtPhụ thuộcPhụ thuộcLớn nhất (trừ CROSS)Trái x Phải
Trường hợp sử dụngNối mặc địnhHầu hết truy vấn kinh doanhHiếm (đổi thành LEFT)Kiểm toán dữ liệuTổ hợp

Nối Nhiều Bảng

Các truy vấn thực tế thường nối nhiều hơn hai bảng. Các phép nối được nối tiếp từ trái sang phải:

SELECT
e.name AS employee,
d.dept_name,
p.project_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
LEFT JOIN employee_projects ep ON e.id = ep.employee_id
LEFT JOIN projects p ON ep.project_id = p.id;
mẹo

Kết hợp các loại nối một cách có chủ đích. Sử dụng INNER JOIN cho các mối quan hệ bắt buộc và LEFT JOIN cho các mối quan hệ tùy chọn.

Hiệu năng Phép nối

Cơ sở dữ liệu thực thi nối như thế nào

Cơ sở dữ liệu sử dụng các thuật toán nối (Join Algorithms) khác nhau tùy thuộc vào kích thước bảng, chỉ mục (Index) và độ phức tạp truy vấn:

Thuật toánPhù hợp nhấtĐộ phức tạpBộ nhớ
Nested LoopNối có chỉ mục, bảng ngoài nhỏO(M x log N) với chỉ mụcThấp
Hash JoinBảng lớn chưa sắp xếp, phép nối bằng (Equijoin)O(M + N)O(bảng nhỏ hơn)
Merge JoinDữ liệu đã sắp xếp sẵn, bảng lớnO(M + N)Thấp

Mẹo tối ưu hiệu năng

  1. Đánh chỉ mục cột nối — tối ưu hóa có tác động lớn nhất. Các cột khóa ngoại (Foreign Key) nên được đánh chỉ mục.
-- Điều này làm cho phép nối trên dept_id nhanh hơn
CREATE INDEX idx_employees_dept_id ON employees(dept_id);
  1. Lọc sớm — áp dụng điều kiện WHERE trước khi nối để giảm số lượng dòng:
-- Tốt: lọc trước khi nối
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_name = 'Engineering';

-- Tệ: nối tất cả, rồi mới lọc
-- (trình tối ưu có thể sửa, nhưng hãy viết tường minh)
  1. Chỉ chọn các cột cần thiết — tránh SELECT * trong truy vấn nối:
-- Tốt
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;

-- Tránh
SELECT *
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
  1. Cẩn thận với nhiều phép nối — mỗi phép nối có thể nhân số dòng kết quả trung gian. Kiểm tra số dòng với EXPLAIN.

  2. Sử dụng điều kiện nối tường minh — tránh nối kiểu dấu phẩy (cross join với bộ lọc WHERE) vì khó đọc và dễ lỗi hơn.

-- Tốt: cú pháp JOIN tường minh
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;

-- Tránh: nối ngầm (cú pháp cũ)
SELECT e.name, d.dept_name
FROM employees e, departments d
WHERE e.dept_id = d.dept_id;

Cạm bẫy Phổ biến

  • Cross join vô tình — quên mệnh đề ON tạo ra tích Descartes. Luôn bao gồm điều kiện nối.
  • NULL trong cột nốiNULL = NULL đánh giá là UNKNOWN (không phải TRUE), nên các dòng có khóa nối NULL bị loại trừ khỏi INNER JOIN một cách im lặng.
  • Dòng trùng lặp từ nhiều sự khớp — nếu cột nối có giá trị trùng lặp, bạn nhận được nhiều dòng hơn dự kiến. Kiểm tra trùng lặp bằng COUNT(*).
  • Sai loại nối — sử dụng INNER JOIN khi cần LEFT JOIN sẽ loại trừ im lặng các dòng không khớp. Hãy nghĩ xem các dòng không khớp có quan trọng không.
  • Nối trên cột sai — luôn xác minh điều kiện nối tham chiếu đúng mối quan hệ khóa ngoại.
  • Lọc trong WHERE vs ON — đối với LEFT JOIN, đặt điều kiện bảng phải trong WHERE biến nó thành INNER JOIN hiệu quả:
-- Điều này LOẠI TRỪ phòng ban không có nhân viên (WHERE lọc sau khi nối)
SELECT d.dept_name, e.name
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
WHERE e.name LIKE 'A%';

-- Điều này GIỮ tất cả phòng ban (ON lọc trước khi nối)
SELECT d.dept_name, e.name
FROM departments d
LEFT JOIN employees e ON d.dept_id = d.dept_id AND e.name LIKE 'A%';

Best Practices

  1. Sử dụng cú pháp JOIN tường minh — không phải bảng phân cách bằng dấu phẩy với bộ lọc WHERE
  2. Luôn định danh tên cột — sử dụng alias bảng (e.name, d.dept_name) để tránh nhầm lẫn
  3. Đánh chỉ mục cột nối — khóa ngoại và vị từ nối nên được đánh chỉ mục
  4. Chọn loại nối đúng — INNER cho các khớp bắt buộc, LEFT cho dữ liệu tùy chọn
  5. Ưu tiên LEFT hơn RIGHT — đổi thứ tự bảng và sử dụng LEFT để dễ đọc hơn
  6. Sử dụng alias bảng — giữ truy vấn ngắn gọn, nhưng dùng alias có ý nghĩa (e cho employees, d cho departments)
  7. Kiểm tra với EXPLAIN — xác minh kế hoạch truy vấn sử dụng đúng thuật toán nối và chỉ mục

Câu hỏi Phỏng vấn

1. Sự khác biệt giữa INNER JOIN và LEFT JOIN là gì?

INNER JOINLEFT JOIN
Trả vềChỉ các dòng khớp từ cả hai bảngTất cả dòng từ bảng trái + các dòng khớp bên phải
Dòng trái không khớpLoại trừBao gồm (cột bên phải là NULL)
Trường hợp sử dụng"Cho tôi xem nhân viên và phòng ban của họ""Cho tôi xem tất cả nhân viên, kể cả những người chưa có phòng ban"
Kích thước kết quảNhỏ hơn hoặc bằng số dòng bảng tráiBằng số dòng bảng trái

2. Sự khác biệt giữa LEFT JOIN và RIGHT JOIN là gì?

Chúng là hình ảnh phản chiếu của nhau:

  • A LEFT JOIN B = tất cả dòng từ A + các dòng khớp từ B
  • A RIGHT JOIN B = tất cả dòng từ B + các dòng khớp từ A

Bất kỳ RIGHT JOIN nào cũng có thể viết lại thành LEFT JOIN bằng cách đổi thứ tự bảng:

-- Hai truy vấn này tương đương:
SELECT * FROM A RIGHT JOIN B ON A.id = B.a_id;
SELECT * FROM B LEFT JOIN A ON B.a_id = A.id;

Trong thực tế, LEFT JOIN được ưa chuộng hơn vì dễ đọc — giữ bảng "chính" ở trước.

3. FULL OUTER JOIN là gì và khi nào sử dụng?

FULL OUTER JOIN trả về tất cả dòng từ cả hai bảng, với NULL ở những nơi không có sự khớp. Sử dụng khi bạn cần:

  • Đối chiếu dữ liệu (Data Reconciliation) — so sánh hai tập dữ liệu để tìm sự không khớp
  • Tìm bản ghi mồ côi (Orphaned Records) — các dòng ở bất kỳ bảng nào không có sự khớp ở bảng kia
  • Gộp tập dữ liệu (Merging Datasets) — kết hợp hai nguồn trong khi giữ lại tất cả bản ghi
-- Tìm tất cả các dòng không khớp ở bất kỳ bảng nào
SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id
WHERE e.id IS NULL OR d.dept_id IS NULL;

4. Sự khác biệt giữa ON và WHERE trong LEFT JOIN là gì?

Đây là một cạm bẫy phổ biến:

-- Mệnh đề WHERE: lọc SAU khi nối → biến LEFT thành INNER hiệu quả
SELECT d.dept_name, e.name
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
WHERE e.name IS NOT NULL;

-- Mệnh đề ON: lọc TRONG khi nối → giữ nguyên hành vi LEFT
SELECT d.dept_name, e.name
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id AND e.name IS NOT NULL;
  • ON — xác định các dòng để nối. Các dòng trái không khớp vẫn xuất hiện với NULL.
  • WHERE — lọc kết quả cuối cùng. WHERE e.name IS NOT NULL loại bỏ các dòng có e.name là NULL, biến LEFT JOIN thành INNER JOIN hiệu quả.

5. Làm thế nào để tìm các dòng trong một bảng không có sự khớp trong bảng khác?

Sử dụng LEFT JOIN ... WHERE right.key IS NULL:

-- Nhân viên không có phòng ban
SELECT e.name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_id IS NULL;

-- Phòng ban không có nhân viên
SELECT d.dept_name
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
WHERE e.id IS NULL;

Hoặc sử dụng NOT EXISTS (thường được ưa chuộng vì dễ đọc):

SELECT e.name
FROM employees e
WHERE NOT EXISTS (
SELECT 1 FROM departments d WHERE d.dept_id = e.dept_id
);

6. Self join là gì và khi nào sử dụng?

Self join là phép nối thông thường mà cả hai bên đều tham chiếu cùng một bảng. Nó yêu cầu alias bảng để phân biệt hai "bản sao":

-- Tìm nhân viên và quản lý của họ
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Trường hợp sử dụng phổ biến: phân cấp tổ chức, cây danh mục, danh sách kề (Adjacency List) và so sánh các dòng trong cùng bảng.

Tìm hiểu thêm