Tuần 1

Mô hình quan hệ và truy vấn nền tảng

SQL cho Interview — Lộ trình 10 tuần

Nhấn phím → để bắt đầu · T để xem mục lục

1. Tổng quan và mục tiêu

1.1

1.1 Vì sao học SQL nghiêm túc?

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ò:

Các nền tảng luyện đề chính:

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:

Lưu ý
Mục tiêu KHÔNG phải nhớ cú pháp. Mục tiêu là nhận ra patternphâ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

  1. Đọ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).
  2. 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.
  3. 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?
  4. Đọ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
Ví dụ — bảng customers
customer_idnamecountrysignup_date
1An NguyễnVietnam2023-03-12
2Bình TrầnAustralia2024-01-05
3Chi LêVietnamNULL

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:

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

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ệ:

customers PK customer_id name country signup_date orders PK order_id FK customer_id order_date total order_items PK order_id PK product_id quantity unit_price 1 N 1 N Một khách hàng có nhiều đơn; mỗi đơn có nhiều mặt hàng.
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:

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.
customers (đầu vào) id name country 1AnVietnam 2BìnhAustralia 3ChiVietnam 4DũngUK 5EnaVietnam σ country='Vietnam' đầu ra id name country 1AnVietnam 3ChiVietnam 5EnaVietnam

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.
id name country date 1AnVietnam23-03 2BìnhAustralia24-01 3ChiVietnam23-07 π name, country name country AnVietnam BìnhAustralia ChiVietnam

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):

A ∪ B (Union) tất cả phần tử ở A hoặc B A ∩ B (Intersection) phần tử ở cả A và B A − B (Difference) ở A nhưng không ở B

SQL tương đương:

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.

Yêu cầu
Lấy tên khách hàng Việt Nam đăng ký sau 2023.
Đại số quan hệ
$$\pi_{\text{name}}\Big(\sigma_{\text{country}=\text{'Vietnam'} \,\wedge\, \text{signup\_date} > \text{'2023-01-01'}}(\text{customers})\Big)$$

Đọc từ trong ra: lọc → chiếu.

SQL tương đương
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)
  1. FROM — lấy bảng nguồn
  2. WHERE — lọc dòng
  3. SELECT — chọn cột
  4. ORDER BY — sắp xếp
  5. 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ĩaVí dụ
=bằngcountry = 'Vietnam'
<> hoặc !=khácstatus <> 'closed'
<, >, <=, >=so sánh thứ tựprice >= 100
BETWEEN ... ANDtrong khoảng (bao gồm)price BETWEEN 10 AND 50
INthuộc tậpcountry IN ('VN','AU','UK')
LIKEkhớp patternname LIKE 'A%'
IS NULL / IS NOT NULLkiểm tra rỗngsignup_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.

ABA AND BA OR B
TRUENULLNULLTRUE
FALSENULLFALSENULL
NULLNULLNULLNULL

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
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:

  1. Đọc đề 2 lần. Lần 1 hiểu yêu cầu, lần 2 chú ý edge case (NULL, ties, duplicates).
  2. Đọc schema. Xác định PK, FK, kiểu dữ liệu, xem có cột NULLABLE không.
  3. Phân rã đại số quan hệ. Trên giấy: \(\sigma\)? \(\pi\)? \(\bowtie\)?
  4. Code. Viết stub trước (SELECT ... FROM ... WHERE ...), điền chi tiết.
  5. Test mental. Chạy query qua sample data trong đầu. Có dòng nào sai không?
  6. 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'.

Phân rã

Phép biến đổi: \(\pi_{\text{product\_id}}(\sigma_{\text{low\_fats}=\text{'Y'} \wedge \text{recyclable}=\text{'Y'}}(\text{Products}))\)

Lời giải

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.

Phân rã

$$\pi_{\text{name}, \text{population}, \text{area}}\Big(\sigma_{\text{area} \ge 3{,}000{,}000 \,\vee\, \text{population} \ge 25{,}000{,}000}(\text{World})\Big)$$

Lời giải

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)

  1. 1757. Recyclable and Low Fat Products — đã giải tại lớp, viết lại không nhìn.
  2. 584. Find Customer Referee — chú ý NULL.
  3. 595. Big Countries — đơn giản, viết dưới 2 phút.
  4. 183. Customers Who Never Order — gợi ý: NOT IN hoặc LEFT JOIN ... WHERE NULL. Sẽ học chi tiết tuần 4.
  5. 1873. Calculate Special Bonus — gợi ý: dùng CASE WHEN. Sẽ học tuần 2.

Yêu cầu nộp bài

7.3

7.3 Đọc thêm và ôn tập

Đọc

Tự kiểm tra

Tuần sau
Tuần 2: Lọc nâng cao và xử lý NULL. CASE WHEN, string functions, date arithmetic, COALESCE/IFNULL — chuẩn bị cho Section 7 question 5 ở trên.

Mục lục

Nhấn T hoặc Escape để đóng