Junior dùng SQL để lấy data.
Senior dùng SQL để chữa lành nỗi đau truy vấn chậm.
Principal dùng SQL để “vẽ ra cái business model từ mấy cái bảng”.
Học xong bài này bạn có thể:
- Viết subquery lồng như búp bê Nga
- Dùng CTE đệ quy tìm cây phân cấp
- Viết window function đẹp như thơ
- Hiểu tại sao
index
không luôn giúp nhanh - Biết cách bắt bệnh query bằng
EXPLAIN
🧬 Subquery – Cái gì trong cái gì?
👉 Subquery trong SELECT
SELECT
name,
(SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) AS total_orders
FROM users;
Tính số order mỗi user ngay trong dòng, thay vì JOIN + GROUP BY
.
👉 Subquery trong WHERE
SELECT *
FROM users
WHERE id IN (
SELECT user_id
FROM orders
WHERE amount > 1000
);
Ai từng “vung tiền” là hiện!
🤯 Subquery trong FROM (hay dùng để build bảng tạm)
SELECT country, AVG(age)
FROM (
SELECT * FROM users WHERE active = true
) AS active_users
GROUP BY country;
🔁 CTE – Common Table Expression
Dễ đọc hơn subquery, support đệ quy, cực kỳ mạnh!
🧹 CTE cơ bản
WITH active_users AS (
SELECT * FROM users WHERE active = true
)
SELECT COUNT(*) FROM active_users;
🚀 CTE đệ quy – Truy vấn dạng cây (như comments, categories)
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 1 AS level
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree;
Truy xuất cây phân cấp không cần vòng lặp trong app! Chỉ cần 1 query!
🔭 Window Function – Mỗi dòng đều có quyền được tổng hợp
📌 RANK, ROW_NUMBER, DENSE_RANK
SELECT
name,
age,
RANK() OVER (ORDER BY age DESC) AS age_rank
FROM users;
🧮 SUM chạy theo cửa sổ
SELECT
user_id,
amount,
SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS running_total
FROM orders;
Phù hợp cho dashboard, báo cáo thời gian, vẽ biểu đồ etc.
🧠 INDEX – Không phải lúc nào cũng nhanh
- Tăng tốc độ truy vấn ở
WHERE
,JOIN
,ORDER BY
- Nhưng ghi dữ liệu sẽ chậm hơn
- Có nhiều loại:
B-tree
,GIN
,GiST
,BRIN
…
CREATE INDEX idx_users_email ON users(email);
⚠️ Những case không nên index:
- Trường có quá ít giá trị (
gender
,status
) → selectivity thấp - Dữ liệu nhỏ quá → overhead tạo index lớn hơn lợi ích
🩺 EXPLAIN – Bắt bệnh query
EXPLAIN ANALYZE
SELECT * FROM orders WHERE amount > 100;
Cho bạn biết:
- PostgreSQL dùng index hay full scan
- Dự đoán số row
- Thời gian thực tế
Ví dụ output (rút gọn):
Seq Scan on orders (cost=0.00..12.25 rows=5 width=...)
Filter: (amount > 100)
Nếu thấy “Seq Scan” mà bảng lớn → Cần index đó bro.
🏆 Chiêu cuối: Materialized View
Cache kết quả query phức tạp, dùng cho dashboard, thống kê.
CREATE MATERIALIZED VIEW top_users AS
SELECT user_id, COUNT(*) AS total_orders
FROM orders
GROUP BY user_id;
-- Cập nhật view khi có data mới
REFRESH MATERIALIZED VIEW top_users;
🧩 Một số pattern “xịn” cần học
Kỹ thuật | Dùng khi nào |
---|---|
EXISTS vs IN | Kiểm tra tồn tại, tối ưu hơn với bảng lớn |
LEFT JOIN + IS NULL | Tìm dữ liệu "không có liên kết" |
COALESCE() | Gán giá trị mặc định nếu NULL |
FILTER() WITH AGG | Tổng hợp có điều kiện trong 1 dòng |
CASE WHEN | If/else trong SQL |
🧠 Tổng kết
Mức độ | Kỹ năng cần biết |
---|---|
Basic | SELECT, INSERT, JOIN, WHERE, GROUP BY |
Trung | Subquery, CTE, Aggregate, HAVING, Index |
Nâng | Window function, CTE đệ quy, Performance tuning |
Pro | Materialized view, Partitioning, Explain plan |
SQL không phải để học thuộc – mà là để xài đúng trong đúng thời điểm!
💬 Tấu hài kết bài
Dev 1: “Tao viết query nhanh cực. Chạy trong 0.002 giây.”
Dev 2: “Ờ nhưng trả về sai hết.”
Dev 1: “Vấn đề ở logic, không phải tốc độ.”
🤣🤣🤣
📦 Bonus tài nguyên học nâng cao
-- Và như thường lệ: kết thúc bằng 1 query "truyền cảm hứng"
SELECT 'SQL giỏi là backend có cơm ăn!' AS truth;
🫰