Bạn đã học hai trục lớn: filter (tuần 1–2) và aggregate (tuần 3). Tuần này thêm trục thứ ba — JOIN: ghép dữ liệu từ nhiều bảng theo cầu nối khóa.
Sau tuần 4 bạn cần làm được
Phân biệt rõ INNER, LEFT, RIGHT, FULL OUTER, CROSS qua hành vi cụ thể, không chỉ định nghĩa.
Giải thích vì sao đặt predicate trong WHERE phá vỡ semantics của OUTER JOIN.
Viết self-join cho bài toán "so sánh dòng với dòng cùng bảng" (employee/manager, consecutive rows).
Implement bài "không có dữ liệu" (anti-join) bằng ba cách: NOT EXISTS, LEFT JOIN ... IS NULL, NOT IN, và biết cái nào an toàn nhất.
Nhận ra "Cartesian explosion" và sửa được trong 30 giây.
Insight
Sau tuần 4, bạn sẽ giải được hầu hết bài Easy LeetCode SQL. Tuần 5 (subquery) sẽ unlock thêm khoảng 50% Medium.
1.2
1.2 Mental model — JOIN là Cartesian + Filter
Mọi dạng JOIN đều có thể nghĩ qua hai bước:
Cartesian product — ghép mọi cặp (dòng A, dòng B).
Filter — giữ lại các cặp thỏa predicate (thường là A.key = B.key).
(Với OUTER JOIN) Bổ sung dòng "không match" với NULL.
Đây là cách bạn nên tư duy khi gỡ bug. Engine không thực sự làm cross product (quá đắt) — nó dùng hash join hoặc merge join — nhưng kết quả tương đương.
Hệ quả: Cartesian explosion
Nếu bạn quên ON, hoặc dùng FROM A, B mà không có WHERE, kết quả là cross product đầy đủ:
A 1M dòng × B 10K dòng = 10 tỷ dòng. Server treo.
Đã xuất hiện trong quiz tuần 1. Tuần này sẽ học cách phát hiện sớm bằng kiểm tra predicate ON.
2. INNER JOIN
2.1
2.1 INNER JOIN — chỉ giữ cặp match
Định nghĩa
A INNER JOIN B ON A.k = B.k trả về các cặp (a, b) sao cho a.k = b.k. Dòng không có match ở bên kia bị loại bỏ.
-- Lấy đơn hàng kèm tên khách
SELECT o.order_id, o.total, c.name
FROM orders o
INNER JOIN customers c ON c.customer_id = o.customer_id;
Đơn không có khách hàng tương ứng (FK rỗng) bị loại. Khách không có đơn cũng bị loại.
Cú pháp tắt
JOIN không có chữ INNER ⇒ mặc định là INNER. FROM A, B WHERE A.k = B.k cũng tương đương (cú pháp cũ, không khuyến khích).
2.2
2.2 ON vs USING vs NATURAL JOIN
-- ON: predicate tự do, rõ ràng nhất, dùng phổ biến
SELECT o.order_id, c.name
FROM orders o JOIN customers c ON c.customer_id = o.customer_id;
-- USING: khi tên cột giống hệt, gộp thành 1 cột trong kết quả
SELECT order_id, name
FROM orders JOIN customers USING (customer_id);
-- (customer_id chỉ xuất hiện 1 lần, không phải c.customer_id và o.customer_id)
-- NATURAL JOIN: tự động JOIN trên mọi cột cùng tên — TRÁNH DÙNG
SELECT * FROM orders NATURAL JOIN customers;
Vì sao tránh NATURAL JOIN?
NATURAL JOIN dùng mọi cột cùng tên. Một ngày DBA thêm cột created_at vào cả hai bảng → query đột ngột thay đổi semantic và có thể trả 0 dòng. Bug ngầm khó phát hiện.
Khuyên: luôn ON, hoặc USING khi tên cột chắc chắn ổn định.
Quy tắc nhỏ — đặt alias bảng
Mọi query có JOIN nên dùng alias bảng (orders o, customers c) và luôn qualify cột (o.order_id, không chỉ order_id). Code dễ đọc hơn và phòng bug khi cùng tên cột ở nhiều bảng.
2.3
2.3 JOIN nhiều bảng (chain JOIN)
Một query có thể JOIN nhiều bảng liên tiếp. Engine xử lý trái-sang-phải theo mặc định.
-- Lấy chi tiết: ai đặt cái gì, bao nhiêu tiền
SELECT
c.name AS customer,
o.order_date,
p.name AS product,
oi.quantity,
oi.unit_price
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE c.country = 'Vietnam'
ORDER BY c.name, o.order_date;
Đọc query chain JOIN
Bắt đầu từ "anchor table" — bảng FROM.
Mỗi JOIN là một bước mở rộng dữ liệu, ON là cầu nối.
Vẽ ERD trong đầu và highlight cột nào nối đến cột nào.
Trong mock interview, narrate mỗi bước: "tôi bắt đầu từ customers, ghép orders qua customer_id, sau đó ghép order_items qua order_id..." — interviewer thấy bạn có cấu trúc tư duy.
2.4
2.4 INNER JOIN và NULL trong khóa
Predicate A.k = B.k là phép so sánh thông thường — và bạn đã biết: NULL = NULL trả UNKNOWN, không phải TRUE.
Hệ quả
Nếu orders.customer_id có thể NULL (đơn guest checkout), những đơn này không bao giờ match dòng nào trong customers qua INNER JOIN — kể cả khi customers cũng có dòng NULL.
-- Đơn guest sẽ bị bỏ qua
SELECT o.order_id, c.name
FROM orders o
INNER JOIN customers c ON c.customer_id = o.customer_id;
Khi nào điều này quan trọng
Yêu cầu interview: "đếm tổng đơn hàng, kèm tên khách nếu có" — INNER JOIN sai vì bỏ guest. Phải LEFT JOIN (Section 3). Đây là dấu hiệu đầu tiên cần OUTER JOIN.
3. OUTER JOIN
3.1
3.1 LEFT JOIN — giữ tất cả dòng từ bảng trái
Định nghĩa
A LEFT JOIN B ON A.k = B.k: giữ tất cả dòng từ A. Dòng A nào không match thì cột B = NULL.
Ví dụ — đếm số đơn mỗi khách (kể cả khách 0 đơn)
SELECT
c.customer_id,
c.name,
COUNT(o.order_id) AS n_orders -- không phải COUNT(*)!
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.name;
Bẫy COUNT(*) vs COUNT(col)
Khách không có đơn ⇒ o.order_id NULL ⇒ COUNT(o.order_id) = 0 (đúng). COUNT(*) đếm cả dòng có NULL → trả 1 cho khách 0 đơn (sai). Đây là pattern interview kinh điển.
3.2
3.2 RIGHT JOIN và FULL OUTER JOIN
RIGHT JOIN — đối xứng của LEFT
A RIGHT JOIN B = B LEFT JOIN A. Hiếm khi dùng trong thực tế — quy ước team thường viết LEFT JOIN và đảo thứ tự bảng để đọc tự nhiên.
FULL OUTER JOIN — giữ cả hai bên
A FULL OUTER JOIN B: dòng A không match → B NULL; dòng B không match → A NULL; dòng match → bình thường.
Phương ngữ
PostgreSQL, SQL Server, Oracle: hỗ trợ FULL OUTER JOIN.
MySQL: không hỗ trợ. Mô phỏng bằng LEFT JOIN ... UNION ... RIGHT JOIN.
SQLite: hỗ trợ từ 3.39.
LeetCode (MySQL) ít khi yêu cầu FULL OUTER. Biết tồn tại là đủ.
3.3
3.3 WHERE vs ON trong OUTER JOIN — bug đẳng cấp
Đây là bug subtle nhất trong cả tuần. Đặt predicate trên cột bảng phải sai chỗ sẽ biến LEFT JOIN thành INNER JOIN.
Yêu cầu
Liệt kê mọi customer kèm số đơn paid (kể cả khách 0 đơn paid).
SAI — predicate ở WHERE
SELECT c.name, COUNT(o.order_id) AS n_paid
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.status = 'paid'
GROUP BY c.name;
Khách 0 đơn paid bị loại! Vì o.status NULL không thỏa o.status = 'paid'.
ĐÚNG — predicate ở ON
SELECT c.name, COUNT(o.order_id) AS n_paid
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
AND o.status = 'paid'
GROUP BY c.name;
Filter chạy trong khi join. Khách không có đơn paid vẫn xuất hiện với count = 0.
Quy tắc xương sống
Predicate trên bảng trái (anchor) ⇒ đặt ở WHERE.
Predicate trên bảng phải (LEFT JOIN-ed) ⇒ đặt ở ON.
Predicate so sánh IS NULL trên bảng phải ⇒ ở WHERE (đây là pattern anti-join, Section 6).
4. Self-join
4.1
4.1 Self-join — bảng tham chiếu chính nó
Self-join là kỹ thuật join một bảng với chính nó dưới hai alias khác nhau. Dùng khi:
Bảng có FK trỏ về PK của chính nó (hierarchy).
Cần so sánh dòng với dòng khác cùng bảng (consecutive, pair).
Pattern kinh điển — Employee/Manager
Schema: Employee(id, name, manager_id). Cột manager_id trỏ về id trong cùng bảng.
-- Liệt kê (nhân viên, quản lý) cho mọi nhân viên có quản lý
SELECT e.name AS employee, m.name AS manager
FROM Employee e
JOIN Employee m ON m.id = e.manager_id;
Hai alias e và m tham chiếu cùng bảng nhưng đóng vai trò khác nhau — engine coi như hai bảng riêng biệt.
CEO không có manager?
INNER JOIN bỏ CEO (manager_id NULL). Để giữ CEO, dùng LEFT JOIN — manager sẽ là NULL.
4.2
4.2 So sánh dòng với dòng — pair queries
Self-join với predicate khác equality trên khóa sẽ tạo các cặp dòng cần so sánh.
LeetCode 181 — Employee Earning More Than Their Managers
Schema: Employee(id, name, salary, manager_id).
SELECT e.name AS Employee
FROM Employee e
JOIN Employee m
ON m.id = e.manager_id
AND e.salary > m.salary;
-- Hoặc tách điều kiện:
-- ON m.id = e.manager_id WHERE e.salary > m.salary;
LeetCode 197 — Rising Temperature
Schema: Weather(id, recordDate, temperature). Tìm các ngày có nhiệt độ cao hơn ngày trước đó.
SELECT today.id
FROM Weather today
JOIN Weather yesterday
ON yesterday.recordDate = today.recordDate - INTERVAL 1 DAY -- MySQL
AND today.temperature > yesterday.temperature;
Đây cũng có thể giải bằng window function LAG (tuần 6) — gọn hơn. Self-join là cách "cổ điển".
4.3
4.3 Pair generation và bẫy duplicate
Khi tạo cặp (a, b) từ cùng bảng, cần cẩn thận về thứ tự và trùng lặp.
Bài toán
Cho bảng friends(user_a, user_b) (mỗi cặp xuất hiện 1 lần). Tìm các cặp bạn của bạn — tức cặp (X, Z) sao cho có Y mà (X, Y) và (Y, Z) đều là bạn.
-- Naive
SELECT f1.user_a, f2.user_b
FROM friends f1
JOIN friends f2 ON f1.user_b = f2.user_a;
Bug
Có thể cho ra (X, X) — X là bạn của chính X qua Y.
Có thể cho cả (X, Z) và (Z, X) — duplicate logic.
Nếu (X, Z) đã là bạn trực tiếp, vẫn xuất hiện trong kết quả.
-- Sạch hơn
SELECT DISTINCT
LEAST(f1.user_a, f2.user_b) AS u1,
GREATEST(f1.user_a, f2.user_b) AS u2
FROM friends f1
JOIN friends f2 ON f1.user_b = f2.user_a
WHERE f1.user_a <> f2.user_b
AND NOT EXISTS (
SELECT 1 FROM friends ff
WHERE (ff.user_a = f1.user_a AND ff.user_b = f2.user_b)
OR (ff.user_a = f2.user_b AND ff.user_b = f1.user_a)
);
Pattern: dùng LEAST/GREATEST để chuẩn hóa thứ tự cặp, NOT EXISTS để loại friend trực tiếp.
5. CROSS JOIN
5.1
5.1 CROSS JOIN — Cartesian có chủ đích
Định nghĩa
A CROSS JOIN B: ghép mọi cặp (a, b). Không có ON. Kết quả có \(|A| \times |B|\) dòng.
Ngược lại với "Cartesian explosion" tai nạn, CROSS JOIN cố ý dùng cho các pattern hữu ích.
Use case 1 — Dense reporting
Yêu cầu: bảng "doanh thu mỗi (region × month) năm 2024" cho mọi tổ hợp, kể cả region không bán gì trong tháng đó (cần hiện 0).
-- Tạo 4 region × 12 tháng = 48 dòng baseline, sau đó LEFT JOIN sales
SELECT r.region, m.month, COALESCE(SUM(s.amount), 0) AS revenue
FROM regions r
CROSS JOIN months m
LEFT JOIN sales s
ON s.region = r.region
AND s.month = m.month
AND s.year = 2024
GROUP BY r.region, m.month
ORDER BY r.region, m.month;
Khi nào cần
Khi user yêu cầu "đầy đủ tổ hợp" — không phải "chỉ những tổ hợp có data". Pivot reports, cohort tables, calendar fills. Đây cũng là kỹ thuật cho LeetCode 1280 (sẽ giải ở Section 7).
5.2
5.2 Generate sequences với CROSS JOIN
Khi không có bảng calendar/numbers sẵn, có thể tạo ad-hoc bằng CROSS JOIN.
-- PostgreSQL có generate_series, đơn giản nhất:
SELECT day::date FROM generate_series('2024-01-01'::date, '2024-12-31', '1 day') day;
-- MySQL phải tự sinh (ad-hoc, dùng để demo):
WITH RECURSIVE days AS (
SELECT DATE '2024-01-01' AS day
UNION ALL
SELECT day + INTERVAL 1 DAY FROM days WHERE day < '2024-12-31'
)
SELECT * FROM days;
-- Recursive CTE chính thức học tuần 7
Trick CROSS JOIN — sinh chuỗi 0–99
SELECT a.n + b.n*10 AS num
FROM (SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a
CROSS JOIN
(SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b
ORDER BY num;
Generation bằng recursive CTE sẽ học chính thức tuần 7. Tạm biết CROSS JOIN có thể dùng cho mục đích này.
6. Anti-join — bài toán "không có"
6.1
6.1 Ba cách viết anti-join
Yêu cầu (LeetCode 183)
Lấy customer chưa từng đặt đơn nào.
Cách 1 — NOT EXISTS (khuyên dùng)
SELECT c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
);
Cách 2 — LEFT JOIN ... IS NULL
SELECT c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
Cách 3 — NOT IN (CẨN THẬN)
SELECT c.name
FROM customers c
WHERE c.customer_id NOT IN (
SELECT customer_id FROM orders WHERE customer_id IS NOT NULL
-- IS NOT NULL bắt buộc — nếu không sẽ trả tập rỗng (bug tuần 2)
);
Ba query trên cùng kết quả (giả sử dữ liệu sạch). Khác biệt nằm ở an toàn NULL và performance — slide tiếp theo.
6.2
6.2 So sánh: an toàn NULL và performance
Approach
An toàn NULL
Đọc
Performance
NOT EXISTS
An toàn tự nhiên
Tốt
Tốt — optimizer thường tối ưu thành anti-join
LEFT JOIN ... IS NULL
An toàn
Khá
Tốt — tương đương NOT EXISTS
NOT IN
NGUY HIỂM — bug tập rỗng nếu subquery có NULL
Ngắn
Khá đến chậm — cần materialize subquery
Khuyên
NOT EXISTS là default. Lý do:
Tự nhiên semantic "không có dòng nào thỏa".
Không bug NULL.
Optimizer hiện đại (PostgreSQL 9+, SQL Server, MySQL 8+) chuyển thành anti-join hiệu quả.
Code di chuyển dialect dễ.
LEFT JOIN ... IS NULL dùng khi: bạn đã LEFT JOIN sẵn cho lý do khác và chỉ thêm filter NULL — dễ đọc. NOT IN: tránh trừ khi subquery chắc chắn không NULL.
Trong mock interview, viết NOT EXISTS và giải thích "tôi chọn vì an toàn NULL" — interviewer sẽ thấy bạn hiểu sâu.
7. Worked Examples
7.1
7.1 LeetCode 175 — Combine Two Tables
Schema
Person(personId INT PK, firstName, lastName)
Address(addressId INT PK, personId INT, city, state)
Yêu cầu
Trả về (firstName, lastName, city, state) cho mọi Person — kể cả Person không có Address.
Phân rã
"Mọi Person" ⇒ Person là bảng anchor ⇒ LEFT JOIN Address.
Lời giải
SELECT p.firstName, p.lastName, a.city, a.state
FROM Person p
LEFT JOIN Address a ON a.personId = p.personId;
Pattern
Bất cứ khi đề bài có cụm "kể cả ... không có ...", ngay lập tức nghĩ LEFT JOIN. Dấu hiệu rõ nhất:
"return all customers, even those without orders", "list all employees, including those who haven't logged in".
7.2
7.2 LeetCode 181 — Employees Earning More Than Their Managers
Schema
Employee(id INT PK, name, salary INT, managerId INT NULL)
Yêu cầu
Trả về tên các nhân viên có salary > salary của manager họ.
Phân rã
Mỗi nhân viên (e) có một manager (m) qua e.managerId = m.id → self-join.
Filter e.salary > m.salary.
Lời giải
SELECT e.name AS Employee
FROM Employee e
JOIN Employee m
ON m.id = e.managerId
WHERE e.salary > m.salary;
Vì sao INNER JOIN, không LEFT?
Đề chỉ quan tâm nhân viên CÓ manager (vì cần so sánh). CEO (managerId NULL) không có gì để so → loại tự nhiên qua INNER JOIN. Dùng LEFT JOIN cũng được nhưng filter m.salary IS NOT NULL sẽ tự lọc CEO — tương đương.
7.3
7.3 LeetCode 183 — Customers Who Never Order
Schema
Customers(id INT PK, name)
Orders(id INT PK, customerId INT)
Ba cách giải — tất cả đều pass
-- Cách 1: NOT EXISTS (khuyên)
SELECT name AS Customers
FROM Customers c
WHERE NOT EXISTS (SELECT 1 FROM Orders o WHERE o.customerId = c.id);
-- Cách 2: LEFT JOIN ... IS NULL
SELECT c.name AS Customers
FROM Customers c
LEFT JOIN Orders o ON o.customerId = c.id
WHERE o.id IS NULL;
-- Cách 3: NOT IN (chỉ an toàn nếu Orders.customerId NOT NULL)
SELECT name AS Customers
FROM Customers
WHERE id NOT IN (SELECT customerId FROM Orders);
Trong mock interview
Viết Cách 1, sau đó nói: "có hai cách khác là Cách 2 và Cách 3. Tôi chọn NOT EXISTS vì an toàn với NULL — Cách 3 sẽ trả tập rỗng nếu subquery có một dòng NULL." Interviewer sẽ gật đầu.
7.4
7.4 LeetCode 1280 — Students and Examinations (Medium)
Schema
Students(student_id INT PK, student_name)
Subjects(subject_name VARCHAR PK)
Examinations(student_id, subject_name) -- nhiều dòng/sinh viên/môn
Yêu cầu
Với mỗi (student, subject) — kể cả khi sinh viên chưa thi môn đó — trả về số lần thi.
Phân rã — kết hợp 3 kỹ thuật tuần này
"Mỗi (student, subject)" ⇒ CROSS JOIN Students với Subjects để tạo grid đầy đủ.
Đếm số lần thi ⇒ LEFT JOIN Examinations (kể cả 0 lần).
GROUP BY và COUNT đúng cột (không phải COUNT(*)!).
Lời giải
SELECT
s.student_id,
s.student_name,
sub.subject_name,
COUNT(e.subject_name) AS attended_exams
FROM Students s
CROSS JOIN Subjects sub
LEFT JOIN Examinations e
ON e.student_id = s.student_id
AND e.subject_name = sub.subject_name
GROUP BY s.student_id, s.student_name, sub.subject_name
ORDER BY s.student_id, sub.subject_name;
Đây là bài Medium chỉ với kỹ thuật tuần 4. Cảm giác "all click together" này là dấu hiệu bạn đã thấm.
8. Knowledge Check
8.Q
Knowledge Check — Tuần 4
Q1: Yêu cầu "đếm tổng đơn paid của mỗi customer, kể cả khách 0 đơn paid". Query nào ĐÚNG?
A sai: WHERE chạy sau LEFT JOIN, nên khách 0 đơn paid (o.status NULL) bị WHERE loại — biến LEFT thành INNER. C sai: INNER JOIN bỏ luôn khách 0 đơn. B đúng: predicate trên bảng phải đặt trong ON, đảm bảo OUTER semantic.
Q2: Bảng orders.customer_id NULLABLE và có dòng NULL. Query nào trả khách chưa từng đặt đơn?
A trả tập rỗng do bug NOT IN với NULL (đã học tuần 2). B đúng — NOT EXISTS an toàn. C sai logic — IS NOT NULL trả khách CÓ đơn. D phụ thuộc dialect (MySQL không có EXCEPT) và không xử lý NULL chuẩn.
Q3: A có 1000 dòng, B có 50 dòng. SELECT * FROM A CROSS JOIN B trả bao nhiêu dòng?
CROSS JOIN là Cartesian product có chủ đích — không cần ON. 1000 × 50 = 50,000 dòng. Đây là feature, không phải bug; dùng cho dense reporting (như LeetCode 1280) và sinh chuỗi.
9. Bài tập về nhà
9.1
9.1 Bài tập LeetCode (5 problem)
175. Combine Two Tables (Easy) — đã giải tại lớp.
181. Employees Earning More Than Their Managers (Easy) — đã giải tại lớp.
183. Customers Who Never Order (Easy) — viết bằng cả 3 cách, đo thời gian execution trên LeetCode.
1280. Students and Examinations (Easy/Medium) — đã giải tại lớp.
1731. The Number of Employees Which Report to Each Employee (Easy) — self-join + GROUP BY + AVG. Bẫy: chỉ tính manager có ≥ 1 báo cáo trực tiếp.
Bonus — viết 3 cách cho bài 183 và so sánh EXPLAIN
Trên LeetCode, sau khi submit, click "Compile/Run" để xem thời gian. Trên dialect bạn dùng (MySQL/PostgreSQL local), chạy EXPLAIN trước query và so sánh execution plan của ba cách. Note lại: cách nào dùng "anti-join" hash, cách nào hiện "Nested Loop".
Tránh các bài này tuần này
LeetCode 196 (DELETE), 1158, 1532 — sẽ học sau khi có window function (tuần 6) và CTE (tuần 7). Ép giải tuần này sẽ ra code dài 30 dòng không cần thiết.
9.2
9.2 Đọc thêm và tự kiểm tra
Đọc
SQL Cookbook (Anthony Molinaro) — Chương 4 (Inserting, Updating, Deleting) §4.7–4.9 và Chương 6 nâng cao về JOIN.
SQL Antipatterns (Bill Karwin) — Chương "Implicit Columns" và "Diplomatic Immunity" về bẫy NATURAL JOIN.
Bạn vẽ được Venn diagram cho INNER, LEFT, RIGHT, FULL OUTER không cần nhìn?
Bạn giải thích được vì sao đặt o.status = 'paid' trong WHERE phá LEFT JOIN?
Bạn viết được self-join cho "tìm cặp employee cùng phòng có salary chênh lệch > 1000" trong dưới 2 phút?
Bạn nhớ ba cách viết anti-join và lý do chọn NOT EXISTS làm default?
Tuần sau — Subquery và Set Operations
Tuần 5: scalar/row/table subquery, correlated subquery, EXISTS sâu hơn, UNION/INTERSECT/EXCEPT. Bạn đã thấy NOT EXISTS tuần này — tuần sau khám phá toàn bộ "thế giới subquery" và unlock thêm khoảng 50% bài Medium LeetCode.