JOIN
Xem thửTình huống mở đầu
Cuối tháng, chị kế toán nhắn cho bạn: "Cho chị danh sách mọi khách hàng kèm các đơn họ đã mua, để chị gọi hỏi thăm." Cửa hàng chỉ có 3 khách và 3 đơn, nhưng thông tin nằm ở hai bảng khác nhau: bảng orders chỉ ghi customer_id (một con số), còn tên khách nằm ở bảng customers. Bạn viết một câu INNER JOIN, nhận về 3 dòng, và gửi đi. Năm phút sau chị hỏi lại: "Carol đâu rồi em?"
Hãy đoán trước: danh sách mà chị cần có bao nhiêu dòng, và vì sao Carol không có mặt trong kết quả của bạn? Cuối bài bạn sẽ có cả con số lẫn cách sửa.
Ôn nhanh bài trước: bí danh (alias) trong câu SELECT dùng để làm gì?
Đáp án: đặt một tên ngắn cho bảng hoặc cột, ví dụ orders AS o, để viết o.id thay cho orders.id. Bài này dùng bí danh liên tục vì câu lệnh có hai bảng.
Mục tiêu bài học
Sau bài này bạn có thể:
- Viết
INNER JOINnốiordersvớicustomersbằng điều kiệnON, và đếm đúng số dòng kết quả. - Viết
LEFT JOINđể giữ mọi khách kể cả khách chưa mua, và nhận raNULLở cột của bảng bên phải. - Dự đoán số dòng khi một khách có nhiều đơn (quan hệ một nhiều).
- Tránh ba bẫy hay gặp: cột mơ hồ, điều kiện đặt sai chỗ giữa
ONvàWHERE, quênON.
Bức tranh lớn
Xuyên suốt bài, mỗi bảng là một cuốn sổ: sổ khách customers và sổ đơn orders. Trên mỗi đơn có ghi số thẻ khách (customer_id). Việc của JOIN là đặt hai cuốn sổ cạnh nhau rồi ghép từng đơn với đúng thẻ khách của nó. Dự án xuyên suốt của chương này là cửa hàng shop, và mọi bảng đều thuộc database shop.
Hình dưới trả lời một câu hỏi: với mỗi dòng của bảng bên trái, máy làm gì khi tìm không thấy dòng khớp bên phải?
Đang tải sơ đồ…
Cách đọc hình: đọc từ ô xanh dương ở trên xuống. Ô cam đầu tiên là phép thử của ON: có dòng bên phải nào thỏa điều kiện không. Nếu có, ô xanh lá cho biết mỗi dòng khớp sinh ra một dòng kết quả. Nếu không, ô cam thứ hai hỏi loại JOIN: INNER JOIN bỏ dòng (ô đỏ), LEFT JOIN giữ dòng và điền NULL (ô tím).
Chỗ ẩn dụ không còn đúng: nhân viên thật nhìn hai cuốn sổ cùng lúc. MySQL thì tự chọn đọc bảng nào trước tùy kế hoạch thực thi, và kết quả không được phép phụ thuộc vào lựa chọn đó. Vì vậy nếu cần thứ tự dòng thì bạn phải tự viết ORDER BY.
Khái niệm cốt lõi
Bài này có ba khái niệm.
1. Khóa ngoại và điều kiện ON
Bảng orders không lặp lại tên khách trên mỗi đơn. Nó chỉ giữ customer_id, là khóa ngoại (foreign key) trỏ tới customers.id. Điều kiện ON o.customer_id = c.id nói với máy cách ghép: dòng đơn nào và dòng khách nào được xem là một cặp.
2. INNER JOIN
INNER JOIN chỉ giữ những cặp dòng thỏa ON. Dòng nào của một trong hai bảng không tìm được cặp thì không xuất hiện trong kết quả.
3. LEFT JOIN
LEFT JOIN giữ mọi dòng của bảng đứng bên trái từ khóa JOIN. Dòng nào không có cặp thì cột của bảng bên phải được điền NULL. Bảng bên trái là bảng được viết ngay sau FROM.
Ví dụ đầu tiên
Dựng hai bảng và nạp dữ liệu. Đoạn dưới tạo bảng khách, bảng đơn có khóa ngoại, rồi thêm 3 khách và 3 đơn:
CREATE TABLE customers (
id INT PRIMARY KEY,
full_name VARCHAR(50) NOT NULL
);
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT, -- số thẻ khách, trỏ tới customers.id
total INT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers (id)
);
INSERT INTO customers VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Carol');
INSERT INTO orders VALUES (101, 1, 450000), (102, 2, 120000), (103, 1, 80000);
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
customer_id INT, | Tạo cột số nguyên cho phép NULL, dùng để lưu số thẻ khách | Cho phép NULL để về sau biểu diễn được đơn chưa gắn khách |
FOREIGN KEY (customer_id) REFERENCES customers (id) | Bắt mọi giá trị customer_id phải có sẵn trong customers.id | Khóa ngoại ngăn đơn trỏ tới một khách không tồn tại |
INSERT INTO orders VALUES (101, 1, 450000), ... | Thêm ba đơn: hai đơn của Alice (101, 103) và một đơn của Bob (102) | Carol không có đơn nào, cố ý để làm bẫy |
Xem dữ liệu hiện có [Đã chạy: MySQL 8.0.44]:
+----+-----------+
| id | full_name |
+----+-----------+
| 1 | Alice |
| 2 | Bob |
| 3 | Carol |
+----+-----------+
+-----+-------------+--------+
| id | customer_id | total |
+-----+-------------+--------+
| 101 | 1 | 450000 |
| 102 | 2 | 120000 |
| 103 | 1 | 80000 |
+-----+-------------+--------+
Giờ ghép đơn với khách bằng INNER JOIN:
SELECT o.id AS order_id, c.full_name, o.total
FROM orders AS o
INNER JOIN customers AS c
ON c.id = o.customer_id
ORDER BY o.id;
Dự đoán trước khi xem kết quả: có mấy dòng, và Alice xuất hiện mấy lần?
Kết quả [Đã chạy: MySQL 8.0.44]:
+----------+-----------+--------+
| order_id | full_name | total |
+----------+-----------+--------+
| 101 | Alice | 450000 |
| 102 | Bob | 120000 |
| 103 | Alice | 80000 |
+----------+-----------+--------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
FROM orders AS o | Lấy bảng đơn làm bảng bên trái, đặt bí danh o | Bí danh ngắn giúp viết o.id thay cho orders.id |
INNER JOIN customers AS c | Nối thêm bảng khách, bí danh c | Câu lệnh có hai bảng cùng có cột id, nên bí danh là bắt buộc để phân biệt |
ON c.id = o.customer_id | Với mỗi đơn, tìm khách có id bằng customer_id của đơn | Đây là điều kiện ghép: khóa ngoại bên đơn khớp khóa chính bên khách |
ORDER BY o.id | Sắp kết quả theo mã đơn | Không có ORDER BY thì thứ tự dòng không được bảo đảm |
Carol không có trong kết quả. Bây giờ đổi sang LEFT JOIN và đổi vai: lấy bảng khách làm bảng bên trái.
SELECT c.full_name, o.id AS order_id, o.total
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id -- vế phải là bảng được phép thiếu dòng
ORDER BY c.id, o.id;
Dự đoán trước khi xem: có mấy dòng, và dòng của Carol có giá trị gì ở hai cột bên phải?
Kết quả [Đã chạy: MySQL 8.0.44]:
+-----------+----------+--------+
| full_name | order_id | total |
+-----------+----------+--------+
| Alice | 101 | 450000 |
| Alice | 103 | 80000 |
| Bob | 102 | 120000 |
| Carol | NULL | NULL |
+-----------+----------+--------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
FROM customers AS c | Lấy bảng khách làm bảng bên trái | LEFT JOIN bảo vệ bảng bên trái, nên bảng cần giữ đủ dòng phải đứng ở đây |
LEFT JOIN orders AS o | Nối bảng đơn, giữ khách dù không có đơn | Báo cáo của chị kế toán cần mọi khách, không chỉ khách đã mua |
ON o.customer_id = c.id | Cùng điều kiện ghép như trước, chỉ viết ngược vế | Thứ tự hai vế của dấu bằng không ảnh hưởng kết quả |
dòng Carol NULL NULL trong kết quả | Máy điền NULL vào mọi cột của bảng orders | Không có đơn nào khớp, nên không có giá trị thật để điền |
Xem từng bước
Animation dưới đây ghép từng đơn với khách bằng chính điều kiện ON ở trên. Hãy trả lời câu dự đoán trước khi bấm bước đầu tiên.
Đang tải Animation…
Bảng trace bằng chữ [Đã chạy: MySQL 8.0.44, dữ liệu 3 khách và 3 đơn]:
| Khách | Số đơn khớp | Dòng trong INNER JOIN | Dòng trong LEFT JOIN từ bảng khách |
|---|---|---|---|
| Alice | 2 | 2 | 2 |
| Bob | 1 | 1 | 1 |
| Carol | 0 | 0 | 1 (đơn là NULL) |
Bạn vừa làm được: dự đoán chính xác số dòng của hai loại JOIN chỉ bằng cách đếm số đơn của từng khách.
Concept Check
Dùng lại bảng customers (Alice, Bob, Carol) và orders (101 của Alice, 102 của Bob, 103 của Alice).
Câu 1. FROM orders o INNER JOIN customers c ON c.id = o.customer_id trả về bao nhiêu dòng?
- A. 3, vì mỗi đơn khớp đúng một khách và Carol không có đơn.
- B. 2, vì mỗi khách chỉ được xuất hiện đúng một lần trong kết quả.
- C. 4, vì mọi khách đều được giữ lại dù không có đơn nào.
- D. 6, vì mỗi đơn được ghép với cả ba khách rồi mới lọc lại.
Câu 2. Vì sao Alice xuất hiện hai lần trong kết quả?
- A. Vì Alice có hai đơn, và mỗi đơn khớp tạo một dòng kết quả.
- B. Vì
INNER JOINluôn lặp lại dòng của bảng đứng bên trái. - C. Vì bảng
customerscó hai dòng giống nhau cho Alice. - D. Vì thiếu
ORDER BYnên máy in trùng một số dòng.
Câu 3. Muốn danh sách có cả Carol, cách sửa nào đúng?
- A. Thêm
WHERE o.id IS NOT NULLvào câuINNER JOINhiện tại. - B. Viết
FROM customers c LEFT JOIN orders o ON o.customer_id = c.id. - C. Đổi
ONthànhON c.id <> o.customer_idđể lấy thêm dòng. - D. Đổi thứ tự hai vế dấu bằng trong điều kiện
ONcủa câu cũ.
Đáp án và giải thích:
- Câu 1: A. B sai vì một khách có hai đơn thì xuất hiện hai dòng. C mô tả
LEFT JOIN. D mô tả tích Descartes, thứ chỉ xảy ra khi bỏ điều kiện ghép. - Câu 2: A. B sai vì
INNER JOINkhông lặp dòng tùy tiện, nó sinh một dòng cho mỗi cặp khớp. C sai vìidlà khóa chính nên không thể có hai Alice. D sai vìORDER BYchỉ sắp xếp, không thêm hay bớt dòng. - Câu 3: B. A sai vì
INNER JOINđã bỏ Carol từ trước khiWHEREchạy. C sai vì điều kiện khác nhau sẽ ghép sai khách với đơn. D sai vì đổi vế của dấu bằng không đổi ý nghĩa.
Làm cùng mình
Chị kế toán hỏi tiếp: "Cho chị riêng những khách chưa mua gì để chị gửi mã giảm giá." Đây là dạng truy vấn tìm khách không có đơn nào, dựa đúng vào dòng NULL mà LEFT JOIN vừa tạo ra.
Bước 1. Vẽ ra giấy
Trước khi gõ lệnh, hãy vẽ bảng kết quả của customers LEFT JOIN orders với 4 dòng, rồi gạch chân dòng nào có cột bên phải là NULL. Ghi dự đoán: kết quả cuối có mấy dòng, là ai?
Bước 2. Điền chỗ trống
Dùng lại dữ liệu ở trên. Điền hai chỗ trống:
SELECT c.full_name
FROM customers AS c
-- TODO 1: nối bảng orders với bí danh o bằng LEFT JOIN, điều kiện o.customer_id = c.id
-- TODO 2: chỉ giữ những dòng mà o.id là NULL
ORDER BY c.id;
Gợi ý cho chỗ trống thứ hai: so sánh với NULL phải dùng IS NULL, không dùng dấu bằng. Nên kiểm tra cột nào của o? Hãy chọn cột chắc chắn không bao giờ NULL khi dòng có thật, ví dụ khóa chính o.id.
Bước 3. Thử đổi sang INNER JOIN
Giữ nguyên câu của bạn nhưng đổi LEFT JOIN thành INNER JOIN. Dự đoán trước: kết quả có mấy dòng? Sau đó so với đáp án ở mục Lời giải và đáp án.
Bước 4. Vẽ lại ra giấy
Ghi một câu: vì sao điều kiện o.id IS NULL chỉ có ý nghĩa khi đi cùng LEFT JOIN.
Thử phá nó đi
Bốn lỗi người mới hay gặp với JOIN [Đã chạy: MySQL 8.0.44]:
| Code lỗi | Thông báo hoặc hiện tượng | Nguyên nhân | Cách sửa |
|---|---|---|---|
SELECT id FROM orders o JOIN customers c ON ... | ERROR 1052: Column 'id' in field list is ambiguous | Cả hai bảng đều có cột id, máy không biết lấy cột của bảng nào | Ghi rõ bí danh: o.id hoặc c.id |
LEFT JOIN ... WHERE o.total > 100000 | Carol biến mất khỏi kết quả mà không có lỗi nào | WHERE chạy sau JOIN, và NULL > 100000 không đúng | Đưa điều kiện vào ON: ON ... AND o.total > 100000 |
FROM customers c JOIN orders o (thiếu ON) | Không báo lỗi, ra 9 dòng | MySQL chấp nhận và ghép mỗi khách với mọi đơn (3 nhân 3) | Luôn viết ON cho mọi JOIN |
FULL OUTER JOIN | ERROR 1064: You have an error in your SQL syntax | MySQL 8.0 không có FULL OUTER JOIN | Dùng UNION của LEFT JOIN và RIGHT JOIN (phần Đào sâu) |
Lỗi đầu tiên:
SELECT id
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id;
ERROR 1052 (23000) at line 16: Column 'id' in field list is ambiguous
Con số line trong thông báo lỗi phụ thuộc vị trí câu lệnh trong script bạn chạy, nên trên máy bạn nó sẽ khác. Phần quan trọng là mã lỗi 1052 và câu mô tả.
Lỗi thứ hai nguy hiểm nhất vì máy không phàn nàn gì. Cùng điều kiện total nhưng đặt ở hai chỗ khác nhau sẽ cho hai kết quả khác nhau:
SELECT c.full_name, o.id AS order_id, o.total
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.total > 100000
ORDER BY c.id;
+-----------+----------+--------+
| full_name | order_id | total |
+-----------+----------+--------+
| Alice | 101 | 450000 |
| Bob | 102 | 120000 |
+-----------+----------+--------+
SELECT c.full_name, o.id AS order_id, o.total
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id AND o.total > 100000
ORDER BY c.id;
+-----------+----------+--------+
| full_name | order_id | total |
+-----------+----------+--------+
| Alice | 101 | 450000 |
| Bob | 102 | 120000 |
| Carol | NULL | NULL |
+-----------+----------+--------+
Hình dưới giải thích vì sao.
Đang tải sơ đồ…
Cách đọc hình: ô xanh dương là trạng thái sau khi LEFT JOIN ghép xong, lúc này Carol đã có dòng với NULL. Ô cam hỏi điều kiện phụ nằm ở đâu. Nhánh ON (xanh lá) lọc đơn trong lúc ghép, nên Carol vẫn còn. Nhánh WHERE (đỏ) lọc sau khi ghép, và dòng NULL không thỏa điều kiện nên bị loại, khiến LEFT JOIN trở nên giống INNER JOIN.
Tự tay làm
Dùng chung hai bảng customers và orders của bài. Mỗi bài nêu đầu vào, kết quả mong đợi và tiêu chí đạt.
Cơ bản
Bài 1. Điền bảng trace. Đầu vào: customers có Alice (1), Bob (2), Carol (3). orders có 4 đơn với customer_id lần lượt là 1, 1, 1, 2. Điền số dòng:
| Câu lệnh | Số dòng |
|---|---|
customers INNER JOIN orders | ? |
customers LEFT JOIN orders | ? |
orders LEFT JOIN customers | ? |
Tiêu chí đạt: điền đúng cả ba ô, sau đó chạy để kiểm tra. Gợi ý: LEFT JOIN giữ đủ dòng của bảng đứng bên trái, nên hãy xác định bảng nào đứng bên trái trước.
Vận dụng
Bài 2. Chỉ đơn lớn, vẫn đủ khách. Đầu vào: dữ liệu ban đầu của bài (3 khách, 3 đơn). Viết câu liệt kê mọi khách kèm các đơn có total từ 100000 trở lên, khách nào không có đơn như vậy vẫn xuất hiện một dòng với NULL. Tiêu chí đạt: kết quả có đúng 3 dòng gồm Alice với đơn 101, Bob với đơn 102, và Carol với NULL. Gợi ý: điều kiện về total phải nằm ở ON, không phải WHERE.
Thử thách (tùy chọn)
Bài 3. Đơn mồ côi. Đầu vào: thêm đơn (104, NULL, 5000) vào orders, đơn này chưa gắn khách nào. Viết câu tìm những đơn không khớp khách nào, và giải thích vì sao INNER JOIN che mất chúng. Tiêu chí đạt: kết quả chỉ có đơn 104, và bạn nêu được con số dòng của INNER JOIN so với tổng số đơn. Gợi ý: đổi vai, lấy orders làm bảng bên trái.
Tự kiểm tra
Trả lời bằng lời của bạn, không nhìn lại bài. Mỗi câu ghi mục tiêu được kiểm tra.
- (Mục tiêu 1, nhớ lại)
ONtrongJOINquy định điều gì? Cho ví dụ vớiordersvàcustomers. - (Mục tiêu 2, dự đoán)
customers LEFT JOIN ordersvới 3 khách, trong đó Carol không có đơn: dòng của Carol có giá trị gì ở cộto.id? - (Mục tiêu 3, dự đoán số dòng) Alice có 2 đơn, Bob có 1, Carol có 0.
INNER JOINtrả về mấy dòng vàLEFT JOINtừ bảng khách trả về mấy dòng? - (Mục tiêu 4, tìm lỗi)
SELECT id FROM orders o JOIN customers c ON c.id = o.customer_idbáo lỗi gì, và sửa thế nào? - (Mục tiêu 4, giải thích) Vì sao
LEFT JOIN ... WHERE o.total > 100000làm mất khách chưa có đơn?
Điều cần nhớ
JOINghép dòng của hai bảng theo điều kiệnON, thường là khóa ngoại bằng khóa chính.INNER JOINchỉ giữ cặp khớp.LEFT JOINgiữ đủ dòng bảng bên trái và điềnNULLở bên phải khi không khớp.- Một khách có nhiều đơn thì xuất hiện nhiều dòng: số dòng đếm theo cặp khớp, không đếm theo khách.
- Với
LEFT JOIN, điều kiện lọc bảng bên phải nên nằm ởON. Đặt ởWHEREsẽ loại luôn dòngNULL.
Cheat sheet:
| Muốn | Viết | Kết quả |
|---|---|---|
| Chỉ cặp khớp | FROM a INNER JOIN b ON a.x = b.y | Dòng không khớp bị bỏ |
| Giữ mọi dòng của a | FROM a LEFT JOIN b ON a.x = b.y | Dòng không khớp có NULL bên b |
| Tìm dòng của a không có cặp | ... LEFT JOIN b ON ... WHERE b.id IS NULL | Chỉ các dòng a chưa có cặp |
| Lọc bảng b mà giữ đủ a | ... LEFT JOIN b ON ... AND b.cot > 100 | Dòng a vẫn còn, cột b là NULL |
| Tránh cột mơ hồ | o.id, c.id | Hết lỗi 1052 |
Hình tóm tắt của bài là sơ đồ ở mục Bức tranh lớn.
Câu hỏi để dành cho bài sau: bảng sizes có 3 dòng, bảng colors có 2 dòng. Nếu ghép mọi size với mọi màu mà không có điều kiện nào, kết quả có bao nhiêu dòng?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. Mô tả ở bài là logic của kết quả. Cách MySQL thực sự chạy (đọc bảng nào trước, dùng chỉ mục hay bộ đệm băm) do trình tối ưu chọn [Có thể khác nhau: tùy kích thước bảng và chỉ mục]. Từ MySQL 8.0.18 có thêm thuật toán hash join cho các phép nối không dùng chỉ mục.
- So sánh với lựa chọn khác.
RIGHT JOINlàLEFT JOINđổi vai hai bảng. Câu dưới cho đúng kết quả như ở Ví dụ đầu tiên:
SELECT c.full_name, o.id AS order_id
FROM orders AS o
RIGHT JOIN customers AS c
ON o.customer_id = c.id
ORDER BY c.id, o.id;
+-----------+----------+
| full_name | order_id |
+-----------+----------+
| Alice | 101 |
| Alice | 103 |
| Bob | 102 |
| Carol | NULL |
+-----------+----------+
Vì vậy nhiều đội chỉ dùng LEFT JOIN cho dễ đọc, bảng cần giữ đủ dòng luôn đứng trước.
- Nhiều bảng. Nối tiếp nhiều
JOINtrong một câu lệnh. MỗiJOINcóONriêng, và kết quả của phép nối trước là bảng bên trái của phép nối sau [Đã chạy: MySQL 8.0.44]:
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT NOT NULL,
product VARCHAR(20) NOT NULL
);
INSERT INTO order_items VALUES (1, 101, 'keyboard'), (2, 101, 'cable'), (3, 102, 'mouse');
SELECT c.full_name, o.id AS order_id, i.product
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
JOIN order_items AS i ON i.order_id = o.id
ORDER BY o.id, i.id;
+-----------+----------+----------+
| full_name | order_id | product |
+-----------+----------+----------+
| Alice | 101 | keyboard |
| Alice | 101 | cable |
| Bob | 102 | mouse |
+-----------+----------+----------+
Đơn 101 có hai dòng hàng nên xuất hiện hai lần. Đơn 103 không có dòng hàng nào nên bị INNER JOIN bỏ. Đây là mầm của lỗi cộng trùng tiền mà bạn sẽ gặp ở bài SUM().
- Hiểu lầm nâng cao. MySQL không có
FULL OUTER JOIN. Có thể mô phỏng bằngUNIONcủaLEFT JOINvàRIGHT JOIN[Đã chạy: MySQL 8.0.44, có thêm đơn 104 chưa gắn khách]:
SELECT c.full_name, o.id AS order_id
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
UNION
SELECT c.full_name, o.id
FROM customers AS c
RIGHT JOIN orders AS o ON o.customer_id = c.id
ORDER BY order_id;
+-----------+----------+
| full_name | order_id |
+-----------+----------+
| Carol | NULL |
| Alice | 101 |
| Bob | 102 |
| Alice | 103 |
| NULL | 104 |
+-----------+----------+
Dòng đầu tiên có order_id là NULL vì Carol không có đơn. Dòng có NULL ở cột tên là đơn 104. UNION tự loại các dòng trùng.
- Khác biệt với PostgreSQL. PostgreSQL có
FULL OUTER JOINvà từ chốiJOINthiếuONthay vì ngầm hiểu là tích Descartes [Đã chạy: PostgreSQL 16.15]:
SELECT c.full_name
FROM customers AS c
JOIN orders AS o;
ERROR: syntax error at or near ";"
LINE 3: JOIN orders AS o;
^
SELECT c.full_name, o.id AS order_id
FROM customers AS c
FULL OUTER JOIN orders AS o ON o.customer_id = c.id
ORDER BY o.id;
full_name | order_id
-----------+----------
Alice | 101
Bob | 102
Alice | 103
| 104
Carol |
(5 rows)
- Giới hạn của bài. Bài này chưa nói đến
USING,NATURAL JOIN, nối bảng với chính nó (self join) và cách đọc kế hoạch thực thi. - Đọc thêm. MySQL 8.0 Reference Manual, trang "JOIN Clause": https://dev.mysql.com/doc/refman/8.0/en/join.html
Bài tiếp theo
Bài sau là CROSS JOIN: một phép nối không có điều kiện, ghép mọi dòng của bảng này với mọi dòng của bảng kia. Mini-dự án nối tiếp: sinh danh sách mọi cặp size và màu cho cửa hàng shop.
Lời giải và đáp án
Làm cùng mình, Bước 2. Hai dòng cần điền:
LEFT JOIN orders AS o
ON o.customer_id = c.id
WHERE o.id IS NULL
SELECT c.full_name
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.id;
+-----------+
| full_name |
+-----------+
| Carol |
+-----------+
Carol là khách duy nhất không có đơn nào, nên chỉ có một dòng. Kiểm tra o.id vì khóa chính không bao giờ NULL ở dòng có thật, nên o.id IS NULL chỉ đúng với dòng do LEFT JOIN tự điền.
Làm cùng mình, Bước 3. Với INNER JOIN, mọi dòng đều có o.id thật, nên điều kiện o.id IS NULL không bao giờ đúng:
Kết quả rỗng, không có dòng nào. Bước 4: o.id IS NULL chỉ có nghĩa khi LEFT JOIN điền NULL cho dòng không khớp. Với INNER JOIN, dòng không khớp đã bị bỏ từ trước.
Bài 1. Đáp án: INNER JOIN có 4 dòng, customers LEFT JOIN orders có 5 dòng (Alice 3, Bob 1, Carol 1 dòng NULL), orders LEFT JOIN customers có 4 dòng (mỗi đơn đều có khách). Kiểm tra bằng cách chạy:
CREATE TABLE customers (id INT PRIMARY KEY, full_name VARCHAR(50) NOT NULL);
CREATE TABLE orders (id INT PRIMARY KEY, customer_id INT, total INT NOT NULL);
INSERT INTO customers VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Carol');
INSERT INTO orders VALUES (1, 1, 10), (2, 1, 20), (3, 1, 30), (4, 2, 40);
SELECT 'inner' AS join_type, COUNT(*) AS row_count
FROM customers c INNER JOIN orders o ON o.customer_id = c.id
UNION ALL
SELECT 'customers LEFT orders', COUNT(*)
FROM customers c LEFT JOIN orders o ON o.customer_id = c.id
UNION ALL
SELECT 'orders LEFT customers', COUNT(*)
FROM orders o LEFT JOIN customers c ON o.customer_id = c.id;
+-----------------------+-----------+
| join_type | row_count |
+-----------------------+-----------+
| inner | 4 |
| customers LEFT orders | 5 |
| orders LEFT customers | 4 |
+-----------------------+-----------+
Bài 2.
SELECT c.full_name, o.id AS order_id, o.total
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id AND o.total >= 100000 -- lọc đơn ngay trong ON
ORDER BY c.id, o.id;
+-----------+----------+--------+
| full_name | order_id | total |
+-----------+----------+--------+
| Alice | 101 | 450000 |
| Bob | 102 | 120000 |
| Carol | NULL | NULL |
+-----------+----------+--------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
LEFT JOIN orders AS o | Giữ mọi khách | Yêu cầu là khách nào cũng phải có mặt |
ON o.customer_id = c.id AND o.total >= 100000 | Chỉ ghép những đơn của khách có total từ 100000 | Điều kiện trong ON lọc đơn trước khi điền NULL, nên khách không có đơn lớn vẫn còn |
dòng Carol NULL NULL | Carol không có đơn nào đủ điều kiện nên nhận NULL | Đơn 103 của Alice (80000) bị loại, Alice vẫn còn nhờ đơn 101 |
Bài 3.
SELECT o.id AS order_id, o.total
FROM orders AS o
LEFT JOIN customers AS c
ON c.id = o.customer_id
WHERE c.id IS NULL;
+----------+-------+
| order_id | total |
+----------+-------+
| 104 | 5000 |
+----------+-------+
Để so sánh, đếm số dòng khi dùng INNER JOIN:
SELECT COUNT(*) AS rows_with_customer
FROM orders AS o
INNER JOIN customers AS c
ON c.id = o.customer_id;
+--------------------+
| rows_with_customer |
+--------------------+
| 3 |
+--------------------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
FROM orders AS o LEFT JOIN customers AS c | Lấy đơn làm bảng bên trái, giữ mọi đơn | Cần nhìn thấy cả đơn không có khách |
WHERE c.id IS NULL | Chỉ giữ dòng mà phía khách không có cặp | Đây là cách tìm dòng mồ côi |
INNER JOIN chỉ cho 3 dòng, trong khi bảng có 4 đơn. Đơn 104 bị bỏ im lặng, không báo lỗi, nên báo cáo doanh thu nối bằng INNER JOIN có thể thiếu tiền mà không ai hay.
Tự kiểm tra.
ONquy định điều kiện để hai dòng của hai bảng được xem là một cặp. Ví dụON o.customer_id = c.idghép đơn với khách cóidbằngcustomer_idcủa đơn.NULL, vì không có đơn nào khớp nên máy điềnNULLvào mọi cột của bảngorders.INNER JOINtrả về 3 dòng (Alice 2, Bob 1).LEFT JOINtừ bảng khách trả về 4 dòng, thêm dòng của Carol.- Lỗi
ERROR 1052: Column 'id' in field list is ambiguous, vì cả hai bảng có cộtid. Sửa bằng cách ghi rõo.idhoặcc.id. WHEREchạy sau khiJOINxong. Dòng của Carol cóo.totallàNULL, vàNULL > 100000không đúng, nên dòng bị loại. Muốn giữ Carol, đưa điều kiện vàoON.