Menu

LEFT JOIN trong SQLite: giữ mọi hàng của bảng bên trái

Cách LEFT JOIN hoạt động trong SQLite: giữ các hàng không khớp, đọc giá trị NULL, lọc an toàn và nối nhiều hơn hai bảng.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

LEFT JOIN giữ lại mọi thứ ở bên trái

INNER JOIN chỉ trả về các hàng khớp ở cả hai phía. Thường thì đó là điều bạn muốn, nhưng không phải lúc nào cũng vậy. Đôi khi "không khớp" lại chính là câu trả lời bạn cần tìm: người dùng chưa đặt đơn nào, sản phẩm chưa bán được lần nào, bài viết chưa có bình luận. Với những trường hợp đó, bạn cần LEFT JOIN.

LEFT JOIN trả về mọi hàng của bảng bên trái. Nếu bảng bên phải có hàng khớp, bạn nhận được các cột đã khớp. Nếu không, bạn vẫn nhận được hàng bên trái, và các cột bên phải trả về NULL.

Cleo chưa có đơn hàng nào nhưng vẫn xuất hiện, với NULL ở cột total. Đổi LEFT JOIN thành INNER JOIN thì Cleo biến mất hoàn toàn.

Cách hình dung

Đọc truy vấn từ trên xuống và coi bảng bên trái là điểm neo. Mọi hàng trong users đều sẽ xuất hiện trong kết quả, dù thế nào đi nữa. Sau đó LEFT JOIN hỏi, với từng người dùng: "có hàng nào khớp trong orders không?"

  • Có hàng khớp → gắn các cột khớp vào hàng của người dùng.
  • Nhiều hàng khớp → tạo mỗi hàng khớp một hàng kết quả (Ada có hai đơn hàng nên xuất hiện hai lần).
  • Không có hàng khớp → tạo một hàng với NULL cho mọi cột của bảng bên phải.

Trường hợp cuối chính là lý do LEFT JOIN tồn tại. Ở đây NULL không có nghĩa là "chúng ta không biết", mà là "bên phải không có gì để gắn vào".

LEFT OUTER JOIN là cùng một phép toán. Từ khóa OUTER là tùy chọn trong SQLite, và hầu hết mọi người bỏ nó đi.

Tìm các hàng không có cặp khớp

Trường hợp dùng LEFT JOIN kinh điển: tìm những hàng của bảng bên trái không có cặp khớp bên phải. Mẹo là lọc theo một cột của bảng bên phải mà trong dữ liệu thật luôn NOT NULL (thường là khóa chính của nó) và kiểm tra NULL sau khi join:

Chỉ có Cleo được trả về. Phép join gắn dữ liệu đơn hàng vào những chỗ có; sau đó WHERE o.id IS NULL chỉ giữ các hàng mà việc gắn thất bại. Kỹ thuật này đôi khi được gọi là "anti-join".

ON và WHERE: cái bẫy khó thấy

Đây là bug phổ biến nhất với LEFT JOIN, và rất đáng dừng lại một chút. Điều kiện có thể đặt trong mệnh đề ON hoặc mệnh đề WHERE, nhưng với outer join, chúng hoạt động rất khác nhau.

  • ON chạy trong khi phép join đang diễn ra. Điều kiện ở đây quyết định hàng nào bên phải được tính là khớp.
  • WHERE chạy sau khi phép join đã tạo ra các hàng. Nó lọc kết quả đã ghép.

Hãy xem điều gì xảy ra khi bạn đặt điều kiện trên bảng bên phải vào WHERE:

Cleo không có đơn hàng nên o.status của cô là NULL, và NULL = 'shipped' không đúng, nên cô bị lọc mất. Trạng thái của Boris là 'pending', cũng bị loại. LEFT JOIN đã lặng lẽ hoạt động như INNER JOIN.

Cách sửa: chuyển điều kiện vào ON, để nó lọc các cặp khớp chứ không lọc các hàng kết quả:

Giờ mọi người dùng đều xuất hiện. Ada có đơn hàng đã giao; Boris nhận NULL (đơn đang chờ của anh không được tính là khớp); Cleo nhận NULL (không có đơn hàng nào). Đó là câu trả lời đúng khi câu hỏi là "hiển thị mọi người dùng, kèm các đơn đã giao nếu có".

Quy tắc chung: điều kiện trên bảng bên trái có thể đặt trong WHERE. Điều kiện trên bảng bên phải gần như luôn nên nằm trong ON, trừ khi bạn chủ động muốn tìm các hàng không khớp bằng IS NULL.

Đếm với LEFT JOIN

Một việc rất hay gặp: đếm số hàng liên quan cho từng bản ghi cha, kể cả bản ghi có số lượng bằng 0. INNER JOIN sẽ loại mất các số 0. LEFT JOIN kết hợp COUNT trên một cột bên phải cho kết quả đúng:

Có hai điều đáng chú ý:

  • COUNT(o.id) đếm các hàng bên phải khác null. Cleo nhận 0 chứ không phải 1, vì COUNT bỏ qua NULL. Nếu bạn viết COUNT(*), Cleo sẽ nhận 1 (hàng vẫn tồn tại, chỉ là chứa NULL). Gần như luôn luôn, COUNT(right.id) mới là thứ bạn cần.
  • COALESCE(SUM(o.total), 0) biến tổng NULL của Cleo thành 0. Nếu không có nó, doanh thu của cô sẽ hiện NULL, đúng về mặt kỹ thuật nhưng hiển thị không đẹp.

Nối nhiều bảng

LEFT JOIN có thể nối tiếp nhau. Mỗi phép join lấy kết quả hiện có và nối thêm một bảng vào. Khi một cột đã có thể mang NULL do LEFT JOIN, hãy tiếp tục dùng LEFT JOIN cho mọi bảng nối từ nó; nếu không, INNER JOIN tiếp theo sẽ lặng lẽ loại những hàng bạn muốn giữ.

Ba người dùng được trả về. Ada có đơn hàng và có lô giao. Boris có đơn hàng nhưng chưa có lô giao (carrier là NULL). Cleo không có đơn hàng, nên cả o.total và s.carrier đều là NULL. Chuỗi LEFT JOIN giữ lại mọi người dùng, bất kể dữ liệu dừng lại ở bước nào trong chuỗi quan hệ.

Khi nào nên dùng LEFT JOIN

Chọn LEFT JOIN khi câu hỏi về bản chất xoay quanh bảng bên trái, còn bảng bên phải chỉ là thông tin bổ sung. Những câu như "mọi người dùng, kèm đơn hàng nếu có" hay "tất cả sản phẩm và đánh giá mới nhất của chúng" đều tương ứng trực tiếp với LEFT JOIN.

Chọn INNER JOIN khi cả hai phía đều bắt buộc như nhau: "đơn hàng kèm thông tin người dùng" không có ý nghĩa với một đơn hàng không có người dùng, nên việc lọc của inner join chính là điều bạn muốn.

Nếu thấy mình đang viết LEFT JOIN ... WHERE right.col IS NOT NULL, thứ bạn cần là INNER JOIN. Nếu đang viết LEFT JOIN ... WHERE right.col IS NULL, thứ bạn cần là anti-join, và bạn đã viết đúng.

Tiếp theo: self-join

Đôi khi bảng bạn muốn join tới lại chính là bảng bạn đang truy vấn: nhân viên và người quản lý của họ, danh mục và danh mục cha, các cặp người dùng ở cùng thành phố. Đó là self-join, nội dung của trang tiếp theo.

Câu hỏi thường gặp

LEFT JOIN trong SQLite dùng để làm gì?

LEFT JOIN trả về mọi hàng của bảng bên trái, cùng các hàng khớp của bảng bên phải nếu có. Nếu bảng bên phải không có hàng khớp, bạn vẫn nhận được hàng bên trái, và các cột bên phải trả về NULL. LEFT OUTER JOIN cũng là một: trong SQLite, OUTER là tùy chọn.

LEFT JOIN và INNER JOIN trong SQLite khác nhau thế nào?

INNER JOIN chỉ trả về các hàng mà điều kiện join khớp ở cả hai bảng. LEFT JOIN luôn trả về mọi hàng của bảng bên trái, điền NULL vào các cột bên phải không khớp. Dùng LEFT JOIN khi việc 'không khớp' tự nó đã là một câu trả lời có ý nghĩa, ví dụ người dùng chưa có đơn hàng nào.

Vì sao LEFT JOIN trong SQLite lại hoạt động như INNER JOIN?

Gần như luôn là do một mệnh đề WHERE lọc theo cột của bảng bên phải mà không tính đến NULL. Điều kiện trên bảng bên phải nên đặt trong mệnh đề ON, không phải WHERE, hoặc bạn phải viết WHERE right.col IS NULL để tìm các hàng không khớp. WHERE right.col = 'x' sẽ lặng lẽ loại mọi hàng không khớp.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu