SQL JOIN dễ hiểu: INNER, LEFT, RIGHT và những lỗi thường gặp

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Dữ liệu trong cơ sở dữ liệu quan hệ được chia thành nhiều bảng để tránh lặp lại. JOIN là cách ghép chúng lại khi truy vấn. Nắm chắc các loại JOIN là điều bắt buộc với bất kỳ ai làm backend, và cũng là câu hỏi phỏng vấn rất quen thuộc.

Dữ liệu mẫu

Giả sử có hai bảng: customers (khách hàng) và orders (đơn hàng, có cột customer_id).

customers:  (1, 'An'), (2, 'Bình'), (3, 'Chi')orders:     (101, customer_id=1, total=200),            (102, customer_id=1, total=150),            (103, customer_id=2, total=90),            (104, customer_id=NULL, total=50)

Chi chưa có đơn nào, và đơn 104 không gắn khách hàng nào. Hai trường hợp này sẽ giúp bạn thấy rõ khác biệt giữa các loại JOIN.

INNER JOIN: chỉ lấy phần khớp

SELECT c.name, o.id, o.totalFROM customers cINNER JOIN orders o ON o.customer_id = c.id;

Kết quả chỉ gồm các cặp có khớp ở cả hai bên: An (2 đơn) và Bình (1 đơn). Chi và đơn 104 đều không xuất hiện. Đây là loại JOIN thông dụng nhất và là mặc định khi bạn chỉ viết JOIN.

LEFT JOIN: giữ nguyên bảng bên trái

SELECT c.name, o.id, o.totalFROM customers cLEFT JOIN orders o ON o.customer_id = c.id;

Mọi khách hàng đều xuất hiện. Với Chi, các cột của orders sẽ là NULL vì không có đơn tương ứng. LEFT JOIN rất hữu ích để tìm "những thứ chưa có":

-- Khách hàng chưa từng muaSELECT c.nameFROM customers cLEFT JOIN orders o ON o.customer_id = c.idWHERE o.id IS NULL;

RIGHT JOIN và FULL JOIN

RIGHT JOIN là bản đối xứng, giữ nguyên bảng bên phải; trong thực tế người ta thường hoán đổi vị trí bảng và dùng LEFT JOIN cho dễ đọc. FULL OUTER JOIN giữ cả hai phía, có trong PostgreSQL nhưng MySQL không hỗ trợ trực tiếp; bạn có thể mô phỏng bằng UNION của hai phép LEFT JOIN và RIGHT JOIN.

Gom nhóm sau khi JOIN

SELECT c.name,       COUNT(o.id) AS so_don,       COALESCE(SUM(o.total), 0) AS tong_tienFROM customers cLEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id, c.name;

Chú ý dùng COUNT(o.id) thay vì COUNT(*): với Chi, COUNT(*) trả 1 (vì dòng vẫn tồn tại), còn COUNT(o.id) trả 0 như mong muốn. COALESCE thay NULL bằng 0 cho tổng tiền.

Những lỗi thường gặp

  • Điều kiện lọc đặt sai chỗ khi dùng LEFT JOIN: viết WHERE o.total > 100 sẽ loại các dòng NULL và biến LEFT JOIN thành INNER JOIN. Nếu muốn giữ khách hàng, đặt điều kiện vào ON: LEFT JOIN orders o ON o.customer_id = c.id AND o.total > 100.
  • Quên điều kiện JOIN: tạo ra tích Descartes, mọi dòng ghép với mọi dòng, số kết quả bùng nổ.
  • Nhân đôi dữ liệu: JOIN với bảng quan hệ một-nhiều làm dòng phía "một" lặp lại, khiến tổng bị cộng nhiều lần. Hãy cẩn thận khi cộng dồn sau nhiều JOIN.
  • Không đánh chỉ mục cột JOIN: khiến truy vấn chậm trên bảng lớn.
  • Cột mơ hồ: hai bảng cùng có cột id; hãy dùng bí danh bảng (alias) để chỉ rõ.

Mẹo để tư duy JOIN

Hãy hình dung bảng bên trái là danh sách chính bạn muốn giữ, rồi tự hỏi: nếu dòng bên trái không có bạn bè bên phải thì có muốn giữ không? Nếu có, dùng LEFT JOIN. Nếu không, dùng INNER JOIN. Cách tự hỏi này giải quyết được hầu hết tình huống mà không cần nhớ sơ đồ hình tròn giao nhau.

JOIN so với truy vấn con và nhiều truy vấn

Có khi bạn cân nhắc giữa JOIN, truy vấn con (subquery) và việc chạy hai truy vấn riêng rồi ghép trong code. Không có đáp án tuyệt đối. JOIN thường rõ ràng và được tối ưu tốt khi có chỉ mục. Truy vấn con tiện cho các điều kiện kiểu EXISTS, ví dụ kiểm tra khách có ít nhất một đơn. Còn tách thành hai truy vấn đôi khi lại đơn giản và hiệu quả hơn khi dữ liệu quan hệ một-nhiều lớn, vì tránh được việc lặp lại cột của bảng cha cho mỗi dòng con. Hãy chọn cách dễ đọc nhất trước, rồi đo hiệu năng bằng EXPLAIN khi dữ liệu đủ lớn để quan trọng.

Bài tập tự luyện

Hãy thử viết các truy vấn sau trên dữ liệu mẫu: liệt kê mỗi khách hàng kèm tổng tiền đã mua; tìm khách hàng có nhiều hơn một đơn; tìm đơn hàng không gắn khách hàng; tìm sản phẩm chưa từng được bán. Hãy luôn kiểm tra lại bằng thực hành thay vì chỉ đọc lý thuyết: dựng một dự án nhỏ, cố tình tạo lỗi, quan sát log và công cụ gỡ lỗi, rồi sửa.

Kết luận

JOIN không khó khi bạn thử trên dữ liệu nhỏ như ví dụ. Hãy tự tạo hai bảng, chèn vài dòng gây khó chịu (thiếu khóa, giá trị NULL) và chạy từng loại JOIN để xem kết quả. Vài phút thực hành đáng giá hơn nhiều lần đọc lý thuyết.

Muốn học bài bản và có lộ trình rõ ràng? Bạn có thể xem các khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment