SQL không phải "kỹ năng phụ" trong interview kỹ thuật hiện đại — nó là filter đầu tiên cho rất nhiều vai trò:
Data Engineer / Data Analyst / Data Scientist: SQL screening hầu như luôn xuất hiện vòng đầu.
Backend / Full-stack Engineer: nhiều công ty test SQL ở vòng technical, đặc biệt với schema design.
ML Engineer: feature engineering pipeline phần lớn là SQL.
Các nền tảng luyện đề chính:
LeetCode — hơn 80 SQL problem (Easy/Medium/Hard), dialect mặc định MySQL.
StrataScratch — đề mô phỏng FAANG (Facebook, Airbnb, Uber), PostgreSQL.
DataLemur — hướng analytics interview, có giải thích chi tiết.
HackerRank, Codility — dùng cho coding screen ở nhiều công ty Việt Nam và Úc.
1.2
1.2 Mục tiêu sau 10 tuần
Cuối lộ trình, bạn cần đạt được các mốc cụ thể sau:
Easy LeetCode SQL: giải dưới 5 phút, không cần tra cú pháp.
Medium LeetCode SQL: giải dưới 15 phút, viết clean với CTE.
Hard LeetCode SQL: đọc đề và đề xuất hướng tiếp cận trong 5 phút, dù chưa code xong.
Mock interview: think-aloud trong khi viết, đề xuất tối ưu, xử lý edge case chủ động.
Lưu ý
Mục tiêu KHÔNG phải nhớ cú pháp. Mục tiêu là nhận ra pattern và phân rã bài toán. Cú pháp tra được; tư duy thì phải luyện.
1.3
1.3 Bẫy lớn nhất: tư duy mệnh lệnh vs tư duy tập hợp
Đây là rào cản số một với sinh viên đến từ Python/Java. Cùng một yêu cầu — "đếm số đơn hàng mỗi khách hàng":
Tư duy mệnh lệnh (Python)
counts = {}
for order in orders:
cid = order["customer_id"]
counts[cid] = counts.get(cid, 0) + 1
"Với mỗi đơn, tăng counter cho khách hàng tương ứng."
Tư duy tập hợp (SQL)
SELECT customer_id, COUNT(*) AS n
FROM orders
GROUP BY customer_id;
"Phân hoạch tập đơn theo customer_id, áp aggregator COUNT lên mỗi nhóm."
Quy tắc vàng
Khi đối mặt một bài SQL, đừng nghĩ "tôi sẽ duyệt thế nào". Hãy nghĩ "tôi cần phép biến đổi gì trên tập hợp dữ liệu này".
1.4
1.4 Quy tắc làm việc cho tuần này
Đọc schema trước. Trước khi gõ phím, viết ra: bảng nào có những cột gì, cột nào là PK/FK, cardinality giữa các bảng (1-1, 1-N, N-N).
Vẽ phép biến đổi. Trên giấy nháp, ghi: bảng đầu vào → phép biến đổi → kết quả trung gian → phép biến đổi tiếp → output.
Test edge case. Sau khi viết, hỏi: NULL thì sao? Bảng rỗng thì sao? Có khả năng có dòng trùng không?
Đọc to query của mình. Mock interview yêu cầu think-aloud. Tập từ tuần đầu.
2. Mô hình quan hệ
2.1
2.1 Bảng, dòng, cột — terminology
SQL được xây trên mô hình quan hệ (Codd, 1970). Ba khái niệm cốt lõi:
Định nghĩa
Quan hệ (Relation) = bảng, ví dụ customers.
Tuple = dòng (một bản ghi cụ thể, ví dụ một khách hàng).
Attribute = cột (một thuộc tính, ví dụ name).
Domain = tập giá trị hợp lệ của một cột (kiểu dữ liệu + ràng buộc).
Ví dụ — bảng customers
customer_id
name
country
signup_date
1
An Nguyễn
Vietnam
2023-03-12
2
Bình Trần
Australia
2024-01-05
3
Chi Lê
Vietnam
NULL
3 dòng (tuples), 4 cột (attributes). Cột signup_date có thể NULL.
2.2
2.2 Khóa chính (Primary Key)
Định nghĩa
Khóa chính là một cột (hoặc tổ hợp cột) định danh duy nhất mỗi dòng trong bảng. Hai ràng buộc bắt buộc:
Unique — không có hai dòng cùng giá trị PK.
Not NULL — PK không bao giờ rỗng.
Trong bảng customers, customer_id là PK. Trong bảng order_items, PK thường là tổ hợp (order_id, product_id) — gọi là composite key.
Tại sao quan trọng cho interview?
Khi đọc đề, xác định ngay PK của mỗi bảng. Nó cho biết "1 dòng đại diện cho cái gì". Nhiều bài LeetCode bẫy ở chỗ assume PK sai.
2.3
2.3 Khóa ngoại (Foreign Key)
Định nghĩa
Khóa ngoại là cột tham chiếu đến PK của một bảng khác. Đây là cơ chế tạo quan hệ giữa các bảng.
Ví dụ
Bảng orders có cột customer_id — đây là FK trỏ đến PK của customers.
Mỗi đơn hàng phải thuộc về một khách hàng có tồn tại trong bảng customers (referential integrity).
FK có thể NULL (đơn hàng chưa gắn khách hàng), nhưng PK thì không.
Cardinality — quan hệ giữa bảng
1-1: một user — một profile.
1-N: một customer — nhiều order. (Phổ biến nhất.)
N-N: một order — nhiều product, một product trong nhiều order. Cần bảng trung gian (junction table).
2.4
2.4 Đọc Entity-Relationship Diagram (ERD)
Trước khi viết bất kỳ query nào, hãy hiểu schema. ERD là cách trình bày trực quan các bảng và quan hệ:
Hình 2.1 — ERD đơn giản với 3 bảng và quan hệ 1-N.
3. Đại số quan hệ trực quan
3.0
3.0 Vì sao học đại số quan hệ trước SQL?
Đại số quan hệ là ngôn ngữ tư duy đằng sau SQL. Mỗi câu lệnh SQL bạn viết, dù phức tạp tới đâu, đều có thể phân rã thành chuỗi các phép toán đại số quan hệ cơ bản.
Học các phép này trước giúp bạn:
Phân rã bài toán trước khi code — đây là kỹ năng số một trong mock interview.
Hiểu vì sao một query chạy đúng (hoặc sai), không chỉ "thuộc cú pháp".
Tối ưu — query optimizer trong RDBMS thực chất chuyển SQL về đại số quan hệ rồi sắp xếp lại.
Năm phép cốt lõi cần thuộc: \(\sigma\) (selection), \(\pi\) (projection), \(\times\) (cross product), \(\bowtie\) (join), và bộ ba tập hợp \(\cup, \cap, -\).
3.1
3.1 Selection \(\sigma\) — lọc dòng
Định nghĩa
\(\sigma_{\text{predicate}}(R)\) trả về tập con các dòng của \(R\) thỏa mãn predicate.
Ví dụ
\(\sigma_{\text{country}=\text{'Vietnam'}}(\text{customers})\) — giữ lại các khách hàng ở Việt Nam.
SQL tương đương: SELECT * FROM customers WHERE country = 'Vietnam';
3.2
3.2 Projection \(\pi\) — chọn cột
Định nghĩa
\(\pi_{a_1, a_2, \ldots}(R)\) trả về quan hệ chỉ chứa các cột được chọn (loại bỏ duplicate trong toán học thuần; SQL không tự loại trừ khi viết DISTINCT).
Ví dụ
\(\pi_{\text{name}, \text{country}}(\text{customers})\) — chỉ lấy hai cột name và country.
SQL tương đương: SELECT name, country FROM customers;
3.3
3.3 Cross Product \(\times\) và Join \(\bowtie\)
Cross Product
\(R \times S\) ghép mọi cặp (dòng từ R, dòng từ S). Nếu \(|R| = m, |S| = n\) thì kết quả có \(m \cdot n\) dòng.
Join (Inner Join)
\(R \bowtie_{\theta} S = \sigma_\theta(R \times S)\). Tức là cross product, sau đó lọc theo predicate \(\theta\) (thường là so khớp khóa).
Ví dụ
\(\text{customers} \bowtie_{c.id = o.customer\_id} \text{orders}\) — ghép mỗi đơn hàng với khách hàng tương ứng.
Tại sao quan trọng
Nắm được join = "cross product + filter" giúp bạn hiểu vì sao thiếu điều kiện join sẽ bùng nổ kết quả (Cartesian explosion). Đây là bug kinh điển trong interview.
3.4
3.4 Phép tập hợp: \(\cup, \cap, -\)
Áp dụng cho hai quan hệ cùng schema (cùng số cột, cùng kiểu):
SQL tương đương:
\(A \cup B\) → UNION (loại trùng) hoặc UNION ALL (giữ trùng)
\(A \cap B\) → INTERSECT
\(A - B\) → EXCEPT (PostgreSQL/SQL Server) hoặc MINUS (Oracle)
3.5
3.5 Composition — kết hợp các phép biến đổi
Mọi query SQL phức tạp đều là composition các phép cơ bản. Đọc query phức tạp = đọc chuỗi phép biến đổi từ trong ra ngoài.
SELECT name
FROM customers
WHERE country = 'Vietnam'
AND signup_date > '2023-01-01';
Bài tập tại lớp
Trên giấy, viết biểu thức đại số quan hệ cho yêu cầu: "Lấy customer_id và total của các đơn trên 1000 đặt bởi khách Australia". (Cần \(\sigma\), \(\bowtie\), \(\pi\).)
4. SQL nền tảng
4.1
4.1 Cấu trúc câu lệnh SELECT
Câu lệnh SQL cơ bản nhất có dạng:
SELECT <columns> -- chọn cột (Projection π)
FROM <table> -- nguồn dữ liệu
WHERE <predicate> -- lọc dòng (Selection σ)
ORDER BY <column> -- sắp xếp
LIMIT <n>; -- giới hạn số dòng
Thứ tự thực thi logic (KHÁC thứ tự viết)
FROM — lấy bảng nguồn
WHERE — lọc dòng
SELECT — chọn cột
ORDER BY — sắp xếp
LIMIT — cắt
Hiểu thứ tự này quan trọng vì giải thích tại sao bạn không dùng được alias từ SELECT trong WHERE (ở hầu hết dialect).
4.2
4.2 WHERE — toán tử so sánh
Toán tử
Ý nghĩa
Ví dụ
=
bằng
country = 'Vietnam'
<> hoặc !=
khác
status <> 'closed'
<, >, <=, >=
so sánh thứ tự
price >= 100
BETWEEN ... AND
trong khoảng (bao gồm)
price BETWEEN 10 AND 50
IN
thuộc tập
country IN ('VN','AU','UK')
LIKE
khớp pattern
name LIKE 'A%'
IS NULL / IS NOT NULL
kiểm tra rỗng
signup_date IS NULL
Cảnh báo NULL
signup_date = NULL luôn trả về UNKNOWN, không phải TRUE. Phải dùng IS NULL.
4.3
4.3 Logic ba giá trị: TRUE, FALSE, UNKNOWN
SQL không phải logic Boolean thông thường. Khi NULL tham gia, kết quả là UNKNOWN.
A
B
A AND B
A OR B
TRUE
NULL
NULL
TRUE
FALSE
NULL
FALSE
NULL
NULL
NULL
NULL
NULL
WHERE chỉ giữ dòng có predicate = TRUE. Dòng UNKNOWN bị loại.
Bẫy interview kinh điển
Yêu cầu: "Lấy khách hàng KHÔNG ở Vietnam".
-- SAI: bỏ sót khách có country = NULL
SELECT * FROM customers WHERE country <> 'Vietnam';
-- ĐÚNG: cần xử lý NULL rõ ràng
SELECT * FROM customers
WHERE country <> 'Vietnam' OR country IS NULL;
Đây là bài LeetCode 584 (sẽ làm trong Section 5).
4.4
4.4 ORDER BY và LIMIT
ORDER BY sắp xếp kết quả. Mặc định ASC (tăng dần). Có thể sắp xếp theo nhiều cột.
SELECT name, signup_date
FROM customers
ORDER BY signup_date DESC, name ASC;
LIMIT giới hạn số dòng (chỉ MySQL/PostgreSQL/SQLite):
SELECT * FROM customers ORDER BY signup_date DESC LIMIT 10;
-- Bỏ qua 10 dòng đầu, lấy 10 dòng tiếp:
SELECT * FROM customers ORDER BY signup_date DESC LIMIT 10 OFFSET 10;
Khác biệt phương ngữ — interview hay test
MySQL / PostgreSQL: LIMIT n OFFSET m
SQL Server: SELECT TOP n ... OFFSET m ROWS FETCH NEXT n ROWS ONLY
Oracle: FETCH FIRST n ROWS ONLY (12c+) hoặc ROWNUM
LeetCode mặc định MySQL — bám LIMIT.
4.5
4.5 DISTINCT — loại bỏ trùng lặp
SQL không tự loại bỏ dòng trùng (khác đại số quan hệ thuần). Phải dùng DISTINCT.
-- Có thể trả 1000 dòng vì nhiều khách cùng quốc gia
SELECT country FROM customers;
-- Trả các quốc gia duy nhất
SELECT DISTINCT country FROM customers;
-- DISTINCT áp dụng cho TỔ HỢP cột
SELECT DISTINCT country, signup_year FROM customers;
DISTINCT có chi phí
DISTINCT phải sort hoặc hash toàn bộ kết quả để loại trùng. Trên dữ liệu lớn, đắt. Khi có thể, dùng GROUP BY hoặc kiểm tra logic xem có thực sự cần DISTINCT không — nhiều bug "dòng trùng" thực ra là bug join, không phải thiếu DISTINCT.
5. Worked Examples — LeetCode Easy
5.0
5.0 Quy trình giải bài LeetCode
Áp dụng cho mọi problem, dù Easy hay Hard:
Đọc đề 2 lần. Lần 1 hiểu yêu cầu, lần 2 chú ý edge case (NULL, ties, duplicates).
Đọc schema. Xác định PK, FK, kiểu dữ liệu, xem có cột NULLABLE không.
Phân rã đại số quan hệ. Trên giấy: \(\sigma\)? \(\pi\)? \(\bowtie\)?
Code. Viết stub trước (SELECT ... FROM ... WHERE ...), điền chi tiết.
Test mental. Chạy query qua sample data trong đầu. Có dòng nào sai không?
Edge case. Bảng rỗng? Tất cả NULL? Có ties?
5.1
5.1 LeetCode 1757 — Recyclable and Low Fat Products
Schema
Products(product_id INT PK, low_fats ENUM('Y','N'), recyclable ENUM('Y','N'))
Yêu cầu
Trả về product_id của những sản phẩm vừa low_fat = 'Y' vừa recyclable = 'Y'.
SELECT product_id
FROM Products
WHERE low_fats = 'Y' AND recyclable = 'Y';
Take-away
Đây là dạng đơn giản nhất: chỉ cần \(\sigma + \pi\). Đừng overthink. Nhiều bài Easy chính là kiểu này — hãy giải dưới 2 phút.
5.2
5.2 LeetCode 584 — Find Customer Referee
Schema
Customer(id INT PK, name VARCHAR, referee_id INT NULL)
Yêu cầu
Trả về tên khách hàng không được giới thiệu bởi khách hàng có id = 2.
Bẫy
Cột referee_id NULLABLE. Predicate referee_id <> 2 sẽ trả NULL khi referee_id IS NULL → bị WHERE loại oan.
Lời giải sai vs đúng
-- SAI: bỏ sót khách có referee_id = NULL
SELECT name FROM Customer WHERE referee_id <> 2;
-- ĐÚNG
SELECT name
FROM Customer
WHERE referee_id <> 2 OR referee_id IS NULL;
Lý do
Logic ba giá trị: NULL <> 2 = UNKNOWN, không phải TRUE. WHERE loại UNKNOWN. Phải dùng IS NULL để bắt riêng.
5.3
5.3 LeetCode 595 — Big Countries
Schema
World(name VARCHAR PK, continent VARCHAR, area INT, population INT, gdp BIGINT)
Yêu cầu
Một quốc gia "lớn" nếu area ≥ 3,000,000 HOẶC population ≥ 25,000,000. Trả về name, population, area của các quốc gia lớn.
SELECT name, population, area
FROM World
WHERE area >= 3000000 OR population >= 25000000;
Lưu ý — câu hỏi follow-up cho mock interview
"Nếu bảng World có 100 triệu dòng, làm sao tối ưu query này?"
→ Câu trả lời tốt: index trên area và population (hai single-column index, optimizer sẽ chọn — composite không giúp với OR), hoặc rewrite thành UNION ALL hai SELECT.
6. Knowledge Check
6.Q
Knowledge Check — Tuần 1
Q1: Predicate salary <> 5000 sẽ giữ lại dòng nào trong các dòng sau?
Logic ba giá trị: NULL ≠ 5000 cho UNKNOWN, không phải TRUE. WHERE loại UNKNOWN. Phải OR salary IS NULL nếu muốn giữ NULL.
Q2: Trong đại số quan hệ, biểu thức nào tương đương với SELECT name FROM customers WHERE country = 'VN';
σ lọc dòng (WHERE), π chọn cột (SELECT). Lọc trước rồi chiếu — đọc từ trong ra ngoài.
Q3: Bảng orders có 1M dòng, customers có 10K dòng. SELECT * FROM orders, customers; trả về bao nhiêu dòng?
Dấu phẩy trong FROM = CROSS JOIN. Không có WHERE để lọc → cross product 1M × 10K = 10 tỷ dòng. Đây là Cartesian explosion — bug kinh điển khi quên ON.
7. Bài tập về nhà
7.1
7.1 Schema chuẩn cho cả khóa học
Mọi bài tập trong 10 tuần dùng schema e-commerce này. In ra hoặc lưu lại để tham khảo:
customers (
customer_id INT PRIMARY KEY,
name VARCHAR(100),
country VARCHAR(50),
signup_date DATE
)
products (
product_id INT PRIMARY KEY,
name VARCHAR(100),
category VARCHAR(50),
price DECIMAL(10,2),
low_fat CHAR(1),
recyclable CHAR(1)
)
orders (
order_id INT PRIMARY KEY,
customer_id INT REFERENCES customers,
order_date DATE,
status VARCHAR(20)
)
order_items (
order_id INT,
product_id INT,
quantity INT,
unit_price DECIMAL(10,2),
PRIMARY KEY (order_id, product_id)
)
7.2
7.2 Bài tập LeetCode (5 problem Easy)
1757. Recyclable and Low Fat Products — đã giải tại lớp, viết lại không nhìn.
584. Find Customer Referee — chú ý NULL.
595. Big Countries — đơn giản, viết dưới 2 phút.
183. Customers Who Never Order — gợi ý: NOT IN hoặc LEFT JOIN ... WHERE NULL. Sẽ học chi tiết tuần 4.
1873. Calculate Special Bonus — gợi ý: dùng CASE WHEN. Sẽ học tuần 2.
Yêu cầu nộp bài
Mỗi problem viết kèm 1-2 dòng comment ghi pattern bạn nhận ra (\(\sigma\)? \(\pi\)? \(\bowtie\)?).
Với problem 4 và 5, ghi rõ "chưa học pattern này, cách tiếp cận thử của tôi là...".