Bài 1 dừng lại ở việc đọc dữ liệu từ 1 bảng duy nhất. Nhưng dữ liệu thật gần như luôn nằm rải rác trên nhiều bảng có liên hệ với nhau — đúng như 4 bảng customers/products/orders/ order_items của TechMart. JOIN là công cụ ghép các bảng đó lại theo 1 điều kiện khớp nào đó, và nắm chắc 5 loại JOIN — cùng cách chọn đúng loại cho đúng bài toán — là kỹ năng SQL quan trọng nhất sau SELECT/WHERE. Bài này đi qua từng loại bằng nhiều tình huống kinh doanh thật khác nhau, không dừng ở 1 ví dụ minh hoạ duy nhất, và có 1 Venn diagram tương tác chạy trực tiếp trên engine SQLite thật để bạn nhìn thấy chính xác JOIN nào giữ lại vùng dữ liệu nào.

1. Khoá Chính & Khoá Ngoại: Nền Tảng Của JOIN

Một khoá chính (primary key) là cột (hoặc tổ hợp cột) định danh duy nhất 1 hàng trong bảng — TechMart dùng customer_id, product_id, order_id, order_item_id làm khoá chính cho 4 bảng tương ứng. Một khoá ngoại (foreign key) là cột ở 1 bảng tham chiếu tới khoá chính của bảng khác — orders.customer_id trỏ tới customers.customer_id, order_items.order_id trỏ tới orders.order_id, order_items.product_id trỏ tới products.product_id. JOIN chính là thao tác nối 2 bảng dựa trên đúng mối quan hệ khoá chính↔khoá ngoại này.

🕳️ Cạm bẫy thường gặp: SQLite không tự ép ràng buộc khoá ngoại
Nhìn lại file sql-techmart-seed.sql ở tab "SQL" bên dưới, bạn sẽ thấy orders.customer_id không hề khai báo REFERENCES customers(customer_id) — SQLite mặc định không kiểm tra ràng buộc khoá ngoại trừ khi bật rõ ràng bằng PRAGMA foreign_keys = ON; (và phải bật lại mỗi kết nối mới, không phải cấu hình toàn cục). Ngược lại, bản seed PostgreSQL bạn dùng trong lab Docker ở Bài 2 khai báo REFERENCES tường minh và luôn ép buộc — cố INSERT 1 đơn hàng với customer_id không tồn tại sẽ bị PostgreSQL từ chối ngay, trong khi SQLite mặc định cho qua lặng lẽ. Đây là 1 khác biệt dialect quan trọng khác cần nhớ ngoài type affinity đã học ở Bài 1.

2. INNER JOIN: Chỉ Giữ Lại Cặp Khớp Nhau

INNER JOIN (hoặc viết tắt JOIN) chỉ trả về những hàng có khớp ở cả 2 bảng theo điều kiện ON — hàng không tìm được cặp tương ứng ở bảng còn lại sẽ bị loại hoàn toàn khỏi kết quả. Dưới đây là 4 tình huống kinh doanh thật minh hoạ INNER JOIN ở các quy mô khác nhau (2 bảng, 3 bảng, kết hợp GROUP BY, lọc theo tên):

Tình huống 1 — Danh sách đơn hàng kèm tên khách: ghép 2 bảng đơn giản nhất, chỉ muốn xem đơn hàng của khách đã đăng ký tài khoản (bỏ qua đơn khách vãng lai).

SELECT o.order_id, c.full_name, o.order_date, o.total_amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
ORDER BY o.order_date;

orders có 4 đơn với customer_id IS NULL (đơn khách vãng lai, xem lại Bài 1), INNER JOIN loại thẳng 4 đơn này khỏi kết quả — chỉ còn 24/28 đơn. Đây chính là hành vi cốt lõi của INNER JOIN: không khớp = không xuất hiện, dù hàng đó hoàn toàn hợp lệ ở bảng gốc.

Tình huống 2 — Hoá đơn chi tiết (invoice): nối 3 bảng liên tiếp để hiển thị từng dòng sản phẩm cụ thể trong 1 đơn hàng, kèm tên sản phẩm thay vì chỉ product_id.

SELECT o.order_id, p.product_name, oi.quantity, oi.unit_price,
       oi.quantity * oi.unit_price AS line_total
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE o.order_id = 12
ORDER BY p.product_name;

Nối 3 bảng chỉ là 2 lần INNER JOIN liên tiếp — không có cú pháp đặc biệt nào khác. SQLite xử lý từng cặp JOIN theo thứ tự viết (dù optimizer có thể chọn thứ tự thực thi khác bên trong — xem Bài 9), miễn kết quả cuối cùng đúng theo logic quan hệ.

Tình huống 3 — Doanh thu theo danh mục sản phẩm: kết hợp INNER JOIN với GROUP BY để trả lời câu hỏi kinh doanh "danh mục nào bán chạy nhất".

SELECT p.category, SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category
ORDER BY revenue DESC;

Đây là khuôn mẫu cực kỳ phổ biến trong báo cáo phân tích: JOIN để lấy thuộc tính mô tả (danh mục) rồi GROUP BY/SUM để tổng hợp. Bài 4 sẽ đào sâu GROUP BY/HAVING, nhưng việc JOIN trước khi tổng hợp là kỹ năng của bài này.

Tình huống 4 — Khách đã từng mua 1 sản phẩm cụ thể: lọc theo tên sản phẩm ở bảng thứ 3 để tìm khách hàng liên quan, phục vụ chiến dịch marketing "ai đã mua Laptop Stand".

SELECT DISTINCT c.full_name, c.email
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE p.product_name = 'Laptop Stand';

DISTINCT ở đây cần thiết vì 1 khách có thể mua cùng 1 sản phẩm nhiều lần qua nhiều đơn khác nhau — không có nó, tên khách sẽ lặp lại nhiều lần trong kết quả.

3. LEFT JOIN (LEFT OUTER JOIN): Giữ Toàn Bộ Bên Trái

LEFT JOIN giữ lại toàn bộ hàng của bảng bên trái (bảng viết ngay sau FROM), bất kể có khớp được với bảng bên phải hay không — nếu không khớp, các cột của bảng phải sẽ là NULL. Đây là loại JOIN được dùng nhiều thứ nhì sau INNER JOIN, đặc biệt khi câu hỏi kinh doanh có dạng "toàn bộ X, kèm thông tin Y nếu có".

Tình huống 1 — Toàn bộ đơn hàng, kể cả đơn khách vãng lai: đảo ngược Tình huống 1 của INNER JOIN — lần này không được bỏ sót đơn nào.

SELECT o.order_id, c.full_name, o.order_date, o.total_amount
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id
ORDER BY o.order_date;

Kết quả có đủ 28/28 đơn hàng — 4 đơn khách vãng lai vẫn xuất hiện, chỉ là cột full_name hiện NULL thay vì bị loại hẳn như INNER JOIN. Đây chính xác là cơ chế đứng sau truy vấn WHERE customer_id IS NULL ở Bài 1 — giờ bạn thấy toàn cảnh cả 24 đơn khớp lẫn 4 đơn không khớp trong cùng 1 kết quả.

Tình huống 2 — Mọi khách hàng kèm tổng chi tiêu: kết hợp LEFT JOIN + GROUP BY để không bỏ sót khách chưa từng mua hàng (nếu có) trong báo cáo.

SELECT c.full_name, COALESCE(SUM(o.total_amount), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.full_name
ORDER BY total_spent DESC;

Nếu dùng INNER JOIN ở đây, bất kỳ khách nào chưa từng đặt đơn nào sẽ biến mất khỏi báo cáo hoàn toàn thay vì hiện với tổng chi tiêu 0 — sai lệch nghiêm trọng cho mục đích "toàn bộ khách hàng". COALESCE(..., 0) thay NULL (do không có đơn nào để SUM) bằng số 0 để báo cáo dễ đọc hơn.

Tình huống 3 — Sản phẩm bán chạy vs sản phẩm ế: mọi sản phẩm kèm số lượng đã bán, kể cả sản phẩm chưa từng có ai mua.

SELECT p.product_name, p.category, COALESCE(SUM(oi.quantity), 0) AS units_sold
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
GROUP BY p.product_id, p.product_name, p.category
ORDER BY units_sold ASC;

Chạy thử trong sân chơi bên dưới: với dataset TechMart hiện tại, mọi sản phẩm đều đã bán được ít nhất 1 lần nên không có hàng nào units_sold = 0 — nhưng cấu trúc truy vấn này vẫn đúng và cần thiết, vì dữ liệu thật sẽ liên tục thay đổi (thêm sản phẩm mới chưa bán được ngày nào là chuyện bình thường). Muốn tự kiểm chứng, gõ thêm 1 sản phẩm mới vào bảng products ngay trong workbench (INSERT INTO products (product_id, product_name, category, unit_price, stock_quantity) VALUES (15, 'Test Sản Phẩm Mới', 'Accessories', 9.99, 10);) rồi chạy lại — sản phẩm mới sẽ hiện với units_sold = 0 đúng như kỳ vọng.

🕳️ Cạm bẫy thường gặp: lọc cột bảng phải trong WHERE vô tình biến LEFT JOIN thành INNER JOIN
-- SAI: ý định "toàn bộ đơn hàng, ưu tiên đơn delivered của khách có tài khoản"
-- nhưng thực chất lại loại hết đơn khách vãng lai!
SELECT o.order_id, c.full_name, o.status
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id
WHERE c.country = 'Vietnam';
Với đơn khách vãng lai, c.countryNULL (vì không khớp được hàng customers nào) — và theo logic 3 trị đã học ở Bài 1, NULL = 'Vietnam' luôn cho UNKNOWN, bị WHERE loại bỏ. Kết quả: toàn bộ đơn khách vãng lai biến mất, dù LEFT JOIN đã cố tình giữ chúng lại ở bước JOIN. Muốn lọc điều kiện theo bảng phải mà vẫn giữ hàng không khớp, đưa điều kiện đó vào mệnh đề ON thay vì WHERE: LEFT JOIN customers c ON o.customer_id = c.customer_id AND c.country = 'Vietnam'.

4. RIGHT JOIN & FULL OUTER JOIN: Khi Cả 2 Hướng Đều Quan Trọng

RIGHT JOIN làm ngược lại LEFT JOIN: giữ toàn bộ bảng bên phải, điền NULL cho bên trái không khớp. FULL OUTER JOIN (hay FULL JOIN) giữ cả 2 bên — vừa không bỏ sót hàng trái không khớp, vừa không bỏ sót hàng phải không khớp.

Tình huống 1 — RIGHT JOIN là LEFT JOIN đổi hướng nhìn: viết lại đúng Tình huống 1 của LEFT JOIN ở trên, chỉ đổi thứ tự bảng và loại JOIN.

SELECT o.order_id, c.full_name, o.order_date
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id
ORDER BY o.order_date;

Chạy thử: kết quả giống hệt ví dụ LEFT JOIN orders → customers ở mục 3, chỉ khác thứ tự cột trả về (do thứ tự bảng trong SELECT/FROM đổi chỗ). Đây là lý do trong thực tế, lập trình viên hiếm khi dùng RIGHT JOIN — họ quen viết lại thành LEFT JOIN bằng cách đổi chỗ 2 bảng, vì tư duy "giữ bảng chính ở FROM, JOIN thêm bảng phụ vào" dễ đọc hơn "giữ bảng phụ ở phía RIGHT". Một số hệ quản trị cũ hơn (SQLite trước phiên bản 3.39.0, phát hành tháng 6/2022) thậm chí không hỗ trợ RIGHT JOIN hay FULL OUTER JOIN native — mọi engine SQLite trước đó buộc phải viết lại thành LEFT JOIN.

Tình huống 2 — FULL OUTER JOIN: không bỏ sót cả 2 hướng bất thường: ghép khách hàng với đơn hàng theo cách không bỏ sót bất kỳ bất thường nào ở cả 2 phía.

SELECT c.full_name, o.order_id, o.order_date
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY c.customer_id;

Chạy thử trong sân chơi: bạn sẽ thấy 4 hàng với full_name IS NULL (4 đơn khách vãng lai — phía "orders không khớp customers"), nhưng không có hàng nào với order_id IS NULL (phía "customers không khớp orders") — vì trong dataset TechMart hiện tại, cả 12 khách hàng đăng ký đều đã đặt ít nhất 1 đơn. Đây là điểm khác biệt cốt lõi giữa FULL OUTER JOINLEFT/RIGHT JOIN: nó sẵn sàng hiện cả 2 loại bất thường cùng lúc, dù dataset hiện tại chỉ đang thể hiện 1 loại. Muốn tự mắt thấy loại còn lại, thử thêm 1 khách hàng mới chưa từng mua gì ngay trong workbench (INSERT INTO customers (customer_id, full_name, email, country, signup_date, is_active) VALUES (13, 'Khách Test Chưa Mua', '[email protected]', 'Vietnam', '2026-04-01', 1);) rồi chạy lại — bạn sẽ thấy hàng mới xuất hiện với order_idNULL, đúng như dự đoán.

🔬 Đào sâu: giả lập FULL OUTER JOIN trên engine không hỗ trợ (MySQL, SQLite cũ)
Tính đến hiện tại, MySQL (mọi phiên bản, kể cả 8.0) vẫn không có FULL OUTER JOIN native. Kỹ thuật giả lập kinh điển: ghép LEFT JOINRIGHT JOIN bằng UNION (không phải UNION ALL, để tự loại trùng phần giao nhau xuất hiện ở cả 2 vế):
SELECT c.full_name, o.order_id FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id
UNION
SELECT c.full_name, o.order_id FROM customers c RIGHT JOIN orders o ON c.customer_id = o.customer_id;
Vế đầu bắt hết "trái + phần giao", vế sau bắt hết "phải + phần giao" — UNION hợp nhất và loại bản sao trùng của phần giao, cho đúng kết quả FULL OUTER JOIN dù engine không hỗ trợ cú pháp đó trực tiếp. Đây là kiến thức đáng biết ngay cả khi dùng SQLite/PostgreSQL hiện đại — bạn sẽ gặp lại kỹ thuật UNION này khi làm việc với MySQL hoặc code base cũ.

5. CROSS JOIN & Tích Descartes: Cẩn Trọng Với Phép Nhân

CROSS JOIN không dựa vào điều kiện khớp nào cả — nó ghép mọi hàng của bảng trái với mọi hàng của bảng phải, tạo ra M × N hàng kết quả (tích Descartes/Cartesian product). Đây là loại JOIN duy nhất không có mệnh đề ON.

Tình huống 1 — Dùng đúng mục đích: tạo khung báo cáo đầy đủ tổ hợp: cần 1 bảng liệt kê mọi tổ hợp (danh mục sản phẩm × trạng thái đơn hàng) để làm khung pivot table, kể cả tổ hợp chưa từng có dữ liệu thực tế (hiện 0 thay vì thiếu hẳn dòng đó).

SELECT DISTINCT p.category, o.status
FROM (SELECT DISTINCT category FROM products) p
CROSS JOIN (SELECT DISTINCT status FROM orders) o
ORDER BY p.category, o.status;

Đây là kỹ thuật thật sự hữu ích trong báo cáo: đảm bảo mọi ô trong ma trận pivot đều tồn tại (kể cả ô sẽ hiện giá trị 0 sau khi LEFT JOIN thêm dữ liệu thực), thay vì thiếu hẳn hàng cho tổ hợp chưa phát sinh — rất quan trọng khi vẽ biểu đồ hoặc export báo cáo cố định số dòng.

Tình huống 2 — Tai nạn thường gặp: quên mệnh đề ON khi định viết INNER JOIN:

-- Gõ nhầm/quên ON — đây vô tình là CROSS JOIN, không phải INNER JOIN!
SELECT o.order_id, oi.product_id
FROM orders o, order_items oi;

Cú pháp FROM a, b (dùng dấu phẩy thay vì JOIN ... ON) là cách viết CROSS JOIN kiểu cũ (SQL-89) — rất dễ gõ nhầm khi định viết INNER JOIN nhưng quên mất điều kiện nối. Với 28 orders và 34 order_items, câu lệnh trên trả về 28 × 34 = 952 hàng hoàn toàn vô nghĩa (mọi đơn hàng ghép với mọi dòng sản phẩm, kể cả những cặp chẳng liên quan gì tới nhau) thay vì con số đúng là 34 hàng (mỗi order_item chỉ thuộc đúng 1 đơn hàng). Với bảng có hàng triệu dòng, lỗi này có thể tạo ra hàng tỷ dòng kết quả, làm treo cả server. Luôn ưu tiên cú pháp JOIN ... ON tường minh — không bao giờ dùng cú pháp phẩy ngăn cách bảng trong code sản xuất.

6. Self-Join: Một Bảng Tự Nối Với Chính Nó

Self-join là JOIN giữa 1 bảng với chính nó, dùng khi cần so sánh các hàng trong cùng 1 bảng với nhau. TechMart không có cột phân cấp kiểu quản lý/nhân viên, nhưng vẫn có 2 tình huống self-join rất thực tế không cần cấu trúc cây:

Tình huống 1 — Tìm khách hàng cùng quốc gia: gợi ý sự kiện offline theo khu vực, cần ghép mọi cặp khách hàng khác nhau nhưng cùng country.

SELECT c1.full_name AS khach_1, c2.full_name AS khach_2, c1.country
FROM customers c1
INNER JOIN customers c2
  ON c1.country = c2.country AND c1.customer_id < c2.customer_id
ORDER BY c1.country;

Vì đang nối bảng customers với chính nó, bắt buộc phải đặt 2 alias khác nhau (c1, c2) để SQL phân biệt được "bản sao nào của customers đang được nhắc tới" ở mỗi cột — không có alias, câu lệnh sẽ mơ hồ và báo lỗi. Điều kiện c1.customer_id < c2.customer_id (thay vì !=) là mẹo quan trọng để tránh 2 vấn đề cùng lúc: (1) khách tự ghép với chính mình (c1.customer_id = c2.customer_id), và (2) mỗi cặp bị đếm 2 lần theo 2 chiều (An-Bình và Bình-An).

Tình huống 2 — Phân tích khoảng cách giữa các đơn hàng liên tiếp: với mỗi khách có từ 2 đơn trở lên, tìm cặp đơn hàng gần nhau nhất để đo hành vi mua lặp lại (customer retention).

SELECT o1.customer_id, o1.order_id AS don_truoc, o2.order_id AS don_sau,
       o1.order_date AS ngay_truoc, o2.order_date AS ngay_sau,
       julianday(o2.order_date) - julianday(o1.order_date) AS so_ngay_cach
FROM orders o1
INNER JOIN orders o2
  ON o1.customer_id = o2.customer_id
  AND o1.order_id < o2.order_id
WHERE o1.customer_id IS NOT NULL
ORDER BY o1.customer_id, so_ngay_cach;

julianday() chuyển chuỗi ngày ISO-8601 (xem lại Bài 1) thành số ngày Julian, cho phép trừ trực tiếp để ra số ngày cách nhau. Lưu ý điều kiện o1.order_id < o2.order_id không đảm bảo o1 luôn là đơn gần nhất trước o2 theo thời gian nếu 1 khách có 3 đơn trở lên (sẽ sinh ra mọi cặp, không chỉ cặp liên tiếp) — đây là bài toán "tìm hàng kế tiếp" mà Bài 7 (Window Functions) sẽ giải gọn hơn nhiều bằng LAG()/LEAD(), thay vì self-join.

7. Alias: Tên Ngắn Gọn Cho Bảng & Cột

Mọi ví dụ ở trên đều dùng AS (hoặc bỏ luôn từ khoá AS, chỉ viết tên ngay sau bảng/cột — cả 2 cách đều hợp lệ trong SQLite) để đặt tên ngắn cho bảng (orders o) và cột tính toán (oi.quantity * oi.unit_price AS line_total). Alias bảng bắt buộc trong self-join (không thể phân biệt 2 bản sao cùng tên bảng nếu không đặt tên khác nhau), và nên dùng ở mọi JOIN nhiều bảng để tránh gõ lại tên bảng đầy đủ hàng chục lần, đồng thời giải quyết trường hợp 2 bảng có cột trùng tên (ví dụ cả ordersorder_items đều không có cột trùng tên trong TechMart, nhưng nếu có, không dùng alias sẽ gây lỗi "ambiguous column name").

Bảng Tổng Hợp 5 Loại JOIN

Loại JOIN Giữ lại hàng nào Dùng khi nào
INNER JOIN Chỉ hàng khớp ở cả 2 bảng Chỉ quan tâm dữ liệu có liên kết đầy đủ (hoá đơn, doanh thu theo danh mục)
LEFT JOIN Toàn bộ bảng trái + phần khớp bên phải "Toàn bộ X, kèm Y nếu có" — báo cáo không được bỏ sót X (mọi khách hàng, mọi sản phẩm)
RIGHT JOIN Toàn bộ bảng phải + phần khớp bên trái Tương đương LEFT JOIN đổi chỗ bảng — hiếm dùng trực tiếp trong thực tế
FULL OUTER JOIN Toàn bộ cả 2 bảng, khớp được thì ghép chung 1 hàng Đối chiếu dữ liệu 2 nguồn, tìm bất thường ở cả 2 hướng cùng lúc
CROSS JOIN Mọi tổ hợp (M × N), không cần điều kiện khớp Cố ý tạo khung tổ hợp đầy đủ (pivot); nếu vô tình xảy ra — luôn là lỗi

Sân chơi tương tác: Venn Diagram & SQL Workbench Trên Dataset TechMart

Bấm 1 loại JOIN bên trái để tô sáng đúng vùng Venn diagram tương ứng — đồng thời truy vấn thật sẽ tự chạy trên engine SQLite-WASM bên dưới với đúng 2 bảng orders (vòng tròn trái) và customers (vòng tròn phải), cho bạn thấy trực tiếp số dòng kết quả khớp với vùng được tô sáng. Sau đó có thể chỉnh sửa tự do trong ô soạn thảo, hoặc bấm 1 nút tình huống khác để nạp sẵn các ví dụ 3-4 bảng, self-join, hay CROSS JOIN ở trên.

🗄️ Sân chơi tương tác: JOIN Venn & SQL Workbench (TechMart)
orders customers
INNER JOIN: chỉ vùng giao nhau (tím) được giữ — đơn hàng có khách hàng khớp.

customers

Cột Kiểu
customer_id INTEGER PRIMARY KEY
full_name TEXT NOT NULL
email TEXT
country TEXT
signup_date TEXT
is_active INTEGER

products

Cột Kiểu
product_id INTEGER PRIMARY KEY
product_name TEXT NOT NULL
category TEXT
unit_price REAL
stock_quantity INTEGER

orders

Cột Kiểu
order_id INTEGER PRIMARY KEY
customer_id INTEGER (NULL = khách vãng lai)
order_date TEXT
status TEXT
total_amount REAL

order_items

Cột Kiểu
order_item_id INTEGER PRIMARY KEY
order_id INTEGER
product_id INTEGER
quantity INTEGER
unit_price REAL
Kết quả sẽ hiện ở đây sau khi chạy query...
Đang tải SQLite-WASM engine...
sql-techmart-seed.sql
Đang tải...
sql-join-complete-guide.js

⬇ Tải file schema + dữ liệu mẫu TechMart (.sql) — chạy được trực tiếp bằng sqlite3 CLI hoặc DB Browser for SQLite trên máy bạn.

Trắc nghiệm ôn tập

Câu 1: Khác biệt cốt lõi giữa INNER JOINLEFT JOIN là gì?

Trắc nghiệm ôn tập

Câu 2: Truy vấn ... FROM orders o LEFT JOIN customers c ON o.customer_id = c.customer_id WHERE c.country = 'Vietnam' có vấn đề gì?

Trắc nghiệm ôn tập

Câu 3: Vì sao trong thực tế lập trình viên hiếm khi dùng RIGHT JOIN trực tiếp?

Trắc nghiệm ôn tập

Câu 4: Engine không hỗ trợ FULL OUTER JOIN native (ví dụ MySQL) có thể giả lập nó bằng cách nào?

Trắc nghiệm ôn tập

Câu 5: Câu lệnh SELECT * FROM orders o, order_items oi; (cú pháp phẩy, không có WHERE/ON) với 28 hàng orders và 34 hàng order_items sẽ trả về bao nhiêu hàng?

Trắc nghiệm ôn tập

Câu 6: Vì sao self-join bắt buộc phải dùng 2 alias khác nhau cho cùng 1 bảng (ví dụ customers c1customers c2)?

📖 Tài liệu tham khảo / References

Bài viết liên quan trong series

Bài 4: Aggregate & GROUP BY Bài 2: Môi Trường Thực Hành Kép — Browser & Docker Quay lại Lộ trình Series SQL