DHDataHọc
Lý thuyếtBài 23/307 phút đọc+10 XP

Vì sao cần JOIN

Bạn sẽ học được

  • Hiểu vì sao dữ liệu thật được chia thành nhiều bảng thay vì một bảng lớn.
  • Đọc được mối quan hệ khóa chính — khóa ngoại giữa hai bảng.
  • Biết JOIN giải quyết việc gì mà truy vấn một bảng không làm được.
  • Nhận ra các câu hỏi bắt buộc phải nối bảng.
  • Chuẩn bị nền tảng cho INNER JOIN, LEFT JOIN và nối nhiều bảng.

Khái niệm cốt lõi

Mọi bài trước chỉ dùng một bảng. Dữ liệu thật hiếm khi đơn giản như vậy. Bảng orders chỉ lưu customer_id, không lưu tên khách:

code
SELECT id, customer_id, order_date, status
FROM orders
LIMIT 3;

Ba dòng đầu: đơn 1 của khách 1, đơn 2 của khách 2, đơn 3 của khách 3. Nếu chỉ nhìn bảng này, bạn biết đơn thuộc khách số mấy, nhưng không biết khách đó tên gì. Tên nằm ở bảng customers. Ghép hai mảnh thông tin lại chính là việc của JOIN.

Vì sao không lưu luôn tên khách vào bảng orders? Vì một khách có nhiều đơn. Nếu mỗi đơn lưu tên, số điện thoại, thành phố của khách, thì khi khách đổi địa chỉ bạn phải sửa nhiều dòng và rất dễ sót, gây ra dữ liệu mâu thuẫn: cùng một người, hai địa chỉ khác nhau tùy đơn. Nguyên tắc chuẩn hóa dữ liệu: mỗi sự thật lưu một chỗ. Tên khách thuộc về khách, nằm ở customers. Đơn hàng chỉ tham chiếu đến khách bằng customer_id.

Mối liên kết đó dựa trên hai loại khóa:

KhóaThuộc bảngÝ nghĩa
Khóa chính (id)customersĐịnh danh duy nhất mỗi khách
Khóa ngoại (customer_id)ordersTrỏ đến customers.id

Với cặp khóa này, JOIN ghép dòng đơn hàng với dòng khách có cùng id:

code
SELECT o.id AS don, o.order_date, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
ORDER BY o.id
LIMIT 5;

Kết quả năm dòng: đơn 1 của An Nguyễn, đơn 2 của Bình Trần, đơn 3 của Chi Lê, đơn 4 của An Nguyễn, đơn 5 của Em Võ. Mỗi dòng đơn hàng được "dán" tên khách tương ứng. ON o.customer_id = c.id là điều kiện ghép — nó nói rằng dòng nào của hai bảng được coi là cùng một thực thể.

Ba điểm cần nắm trước khi đi vào cú pháp:

Một, JOIN không làm mất dữ liệu gốc. Nó tạo ra một kết quả mới; bảng trong cơ sở dữ liệu không thay đổi. Bạn có thể chạy bao nhiêu lần tùy thích.

Hai, JOIN có thể nhân dòng. Một đơn có nhiều món trong order_items, nên khi nối orders với order_items, mỗi đơn xuất hiện nhiều lần — một lần cho mỗi món. Đây là hành vi đúng, nhưng nếu bạn đếm sau khi nối mà quên điều này, con số sẽ bị thổi phồng. Luôn tự hỏi: "kết quả nối đại diện cho cái gì — đơn, món, hay khách?"

Ba, loại JOIN quyết định chuyện gì xảy ra với dòng không có cặp. JOIN (hay INNER JOIN) chỉ giữ dòng khớp ở cả hai bên. LEFT JOIN giữ mọi dòng bên trái, kể cả khi không tìm được cặp. Đây là khác biệt quan trọng nhất trong chương, học ở bài thứ ba.

Có nhiều loại JOIN trong SQL, nhưng thực tế bạn dùng chủ yếu ba loại: INNER, LEFT, và các biến thể như FULL/CROSS ít gặp hơn. RIGHT JOIN về bản chất là LEFT JOIN đảo chiều — viết lại được bằng cách đổi thứ tự bảng, nên nhiều nhóm kỹ thuật quy ước chỉ dùng LEFT cho dễ đọc.

Khi làm việc với nhiều bảng, hãy luôn dùng bí danh bảng: orders o, customers c. Nó ngắn hơn, và quan trọng hơn, nó làm rõ cột nào thuộc bảng nào. Khi hai bảng cùng có cột id, viết id trần sẽ báo lỗi mơ hồ (column reference "id" is ambiguous). Viết o.id hoặc c.id để nói chính xác.

Ví dụ chạy được

Câu hỏi không trả lời được bằng một bảng: "Đơn hàng của khách ở Hà Nội". Điều kiện lọc nằm ở customers, dữ liệu đơn nằm ở orders:

code
SELECT o.id, c.name, o.order_date
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.city = 'Hà Nội'
ORDER BY o.id;

Kết quả: đơn 1 (An Nguyễn), 4 (An Nguyễn), 9 (Dũng Phạm). Ba đơn. Nếu thử lọc chỉ trên orders với WHERE customer_id IN (1, 4, 9) thì phải tra bảng khách bằng tay trước — chính là việc JOIN làm thay bạn. Và nếu ngày mai có khách mới ở Hà Nội, câu lệnh JOIN vẫn đúng mà không cần sửa.

Nối order_items với products để xem tên sản phẩm thay vì id:

code
SELECT oi.order_id, p.name, oi.quantity, oi.unit_price
FROM order_items oi
JOIN products p ON p.id = oi.product_id
ORDER BY oi.order_id
LIMIT 6;

Sáu dòng đầu: đơn 1 có Tai nghe Bluetooth (2 chiếc, 890.000) và Bình giữ nhiệt (1, 320.000); đơn 2 có Bàn phím cơ (1, 1.250.000) và Áo thun cotton (2, 250.000). Bảng order_items chỉ lưu product_id; products cung cấp tên. Ghép lại, bạn đọc được hoá đơn.

Đếm số dòng sau khi nối để thấy hiệu ứng nhân dòng:

code
SELECT COUNT(*) AS so_dong_sau_noi
FROM orders o
JOIN order_items oi ON oi.order_id = o.id;

Kết quả: 29 — bằng số dòng trong order_items, vì mọi dòng hàng đều thuộc một đơn tồn tại. Trong khi đó:

code
SELECT COUNT(*) AS so_don FROM orders;

Kết quả: 16. Cùng là "đếm đơn" theo cách nói thông thường, nhưng câu thứ nhất đếm món hàng. Đây là lý do phải xác định rõ hạt của kết quả trước khi chọn hàm.

Đếm khách có đơn — chú ý khách chưa mua bị loại:

code
SELECT COUNT(DISTINCT c.id) AS so_khach_co_don
FROM customers c
JOIN orders o ON o.customer_id = c.id;

Kết quả: 11, trong khi bảng customers có 12. Hà Bùi chưa có đơn nào, nên JOIN không tìm được cặp và loại cô ấy. Nếu câu hỏi là "mọi khách, kèm số đơn", bạn cần LEFT JOIN — bài tiếp theo.

Một ví dụ cho thấy bí danh bắt buộc khi tên cột trùng:

code
SELECT o.id, o.customer_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
ORDER BY o.id
LIMIT 2;

Cả hai bảng đều có id, nên phải viết rõ o.id. Thử SELECT id sẽ nhận lỗi column reference "id" is ambiguous — một thông báo dễ hiểu và dễ sửa.

Thực hành

  1. Viết truy vấn liệt kê mã đơn và tên khách cho mọi đơn. Kết quả phải có 16 dòng. Kiểm tra dòng đầu là đơn 1 — An Nguyễn.
  2. Liệt kê tên sản phẩm và số lượng cho đơn 13. Ba dòng: Bàn phím cơ 1, Tai nghe Bluetooth 2, Áo thun cotton 4.
  3. Đếm số sản phẩm khác nhau đã từng được bán. Kết quả 8 — một sản phẩm chưa bán lần nào.
  4. Viết truy vấn lấy đơn của khách vip bằng cách nối orders với customers và lọc segment. Kết quả: 6 đơn (Bình 3, Giang 2, Minh 1).

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
column reference "id" is ambiguousCột trùng tên ở hai bảngThêm bí danh: o.id, c.id
Kết quả nhiều dòng hơn mong đợiNối bảng một-nhiềuXác định hạt, dùng COUNT(DISTINCT ...)
Mất khách chưa có đơnDùng JOIN thay LEFT JOINĐổi sang LEFT JOIN
JOIN ... ON sai cộtGhép nhầm khóaKiểm tra customer_id = c.id, không phải = o.id
Câu lệnh chạy rất chậmNối bảng lớn thiếu chỉ mục trên khóa ngoạiThêm chỉ mục cho cột dùng trong ON

Lỗi "mất khách chưa có đơn" đặc biệt khó phát hiện vì kết quả trông hoàn toàn hợp lý — 11 dòng, đúng tên, đúng số đơn. Chỉ khi đối chiếu với tổng số khách bạn mới thấy thiếu. Hãy tạo thói quen so sánh tổng sau khi nối với tổng trước khi nối.

Tóm tắt

  • Dữ liệu chuẩn hóa nằm ở nhiều bảng, liên kết bằng khóa chính — khóa ngoại.
  • JOIN ... ON ghép dòng từ hai bảng theo điều kiện khóa.
  • JOIN có thể nhân dòng; luôn xác định hạt của kết quả.
  • JOIN mặc định (INNER) loại dòng không có cặp; LEFT JOIN giữ chúng.
  • Dùng bí danh bảng và viết rõ cột khi hai bảng trùng tên.

Dùng ← → để chuyển bài