SQL Index Under the Hood (Part 3)
hoanggg2110
Tác giả

SQL Index Under the Hood (Part 3): Vì sao PostgreSQL đôi khi bỏ qua Index
"Bạn tạo Index với hy vọng query sẽ nhanh hơn. Nhưng
EXPLAIN ANALYZEvẫn hiệnSeq Scan."
Nếu từng gặp tình huống này — bạn không cô đơn. Câu trả lời ngắn gọn:
PostgreSQL không quan tâm bạn đã tạo Index hay chưa. Nó chỉ quan tâm cách nào nhanh nhất để lấy dữ liệu.
Sau bài viết này bạn sẽ hiểu
Query Planner là gì
Vì sao PostgreSQL bỏ qua Index
Khi nào Index thực sự hiệu quả
Case nào nên/không nên tạo Index — kèm giải pháp cụ thể
Query Planner — người ra quyết định
Trước khi chạy bất kỳ câu SQL nào, PostgreSQL đánh giá nhiều phương án và chọn phương án chi phí thấp nhất. Bộ phận này gọi là Query Planner.
Mental model: Google Maps không chọn đường ngắn nhất, nó chọn đường nhanh nhất (tránh kẹt xe, tránh trạm thu phí). PostgreSQL không hỏi "có Index không?" — nó hỏi "Execution Plan nào nhanh nhất?"
Ví dụ thực tế: Index bị bỏ qua
CREATE INDEX idx_status ON users(status);
SELECT * FROM users WHERE status = 'ACTIVE';
Kỳ vọng Index Scan, nhưng thực tế là Seq Scan. Vì sao?

Giả sử bảng users có 10 triệu dòng, chia đều 5 triệu ACTIVE / 5 triệu INACTIVE. Nếu dùng Index, Database phải: tìm trong Index → nhảy sang bảng thật → lặp lại 5 triệu lần. Đó là 5 triệu lần Random I/O. Trong khi Seq Scan chỉ đọc tuần tự từ đầu đến cuối — ít di chuyển hơn, nhanh hơn.
Random I/O — kẻ giết hiệu năng
Kiểu đọc Ví dụ Đặc điểm Random I/O Lấy hàng ở A1, K9, D3, Z7... Phải chạy khắp kho — chậm Sequential Read Lấy hàng ở A1, A2, A3, A4... Đi thẳng một đường — nhanh
Ngay cả SSD hiện đại cũng đọc tuần tự nhanh hơn đọc ngẫu nhiên. Đó là lý do Full Table Scan đôi khi thắng Index Scan.
Nguyên tắc cốt lõi
Index chỉ hiệu quả khi giúp Database loại bỏ dữ liệu càng sớm càng tốt. Query trả về càng ít dữ liệu → Index càng có giá trị.
Các case thường gặp — kèm giải pháp
Case 1: Tìm theo ID ⭐⭐⭐⭐⭐
SELECT * FROM users WHERE id = 1000000;
Chỉ trả về 1 bản ghi → cardinality cực cao. → Giải pháp: Index mặc định (Primary Key) đã đủ dùng, PostgreSQL gần như luôn chọn Index Scan.
Case 2: Login bằng Email ⭐⭐⭐⭐⭐
SELECT * FROM users WHERE email = 'john@gmail.com';
Email gần như duy nhất → cardinality cao. → Giải pháp:
CREATE UNIQUE INDEX idx_email ON users(email);
Áp dụng tương tự cho username, phone.
Case 3: Lịch sử đơn hàng (lọc + sắp xếp) ⭐⭐⭐⭐⭐
SELECT * FROM users
WHERE id = 1001
ORDER BY created_at DESC
LIMIT 20;
Nếu chỉ index id, Database vẫn phải Sort thủ công sau khi lọc.

→ Giải pháp: Composite Index theo đúng thứ tự lọc → sắp xếp:
CREATE INDEX idx_users_created
ON users(id, created_at DESC);
Database vừa lọc vừa lấy đúng thứ tự — không cần Sort.

Case 4: Cột Low Cardinality ❌
SELECT * FROM users WHERE status = 'ACTIVE';
-- hoặc: WHERE gender = 'MALE';
Chỉ có vài giá trị khả dĩ → Index không giúp loại bỏ được nhiều dữ liệu. → Giải pháp: Không tạo Index riêng cho cột này. Nếu vẫn cần tối ưu, cân nhắc Partial Index:
CREATE INDEX idx_users_inactive
ON users(id) WHERE status = 'INACTIVE';
(hiệu quả nếu INACTIVE chỉ là số nhỏ trong tổng dữ liệu).
Case 5: Composite Index cho nhiều điều kiện ⭐⭐⭐⭐⭐
SELECT * FROM orders
WHERE status = 'PAID'
AND created_at >= NOW() - INTERVAL '1 day';
Kết hợp 2 điều kiện giúp lượng dữ liệu còn lại rất nhỏ. → Giải pháp:
CREATE INDEX idx_status_created
ON orders(status, created_at);
Case 6: Chỉ SELECT vài cột — Covering Index ⭐⭐⭐⭐⭐
SELECT email, name FROM users WHERE email = 'abc@gmail.com';
Bình thường: Index → nhảy sang Table → trả về. Có thể bỏ qua bước nhảy bảng. → Giải pháp:
CREATE INDEX idx_email_covering
ON users(email) INCLUDE(name);
PostgreSQL dùng Index Only Scan — không cần đọc bảng vật lý.
Bảng tóm tắt

Production Tips — 5 câu hỏi trước khi tạo Index
Query có thực sự chậm không?
Đang dùng Seq Scan hay Index Scan?
Query trả về bao nhiêu records?
Cột trong
WHEREcó High Cardinality không?Composite Index hoặc Covering Index có phù hợp hơn không?
Chưa trả lời được → chưa nên tạo Index.
Những hiểu lầm phổ biến
Hiểu lầm Thực tế Có Index thì chắc chắn PostgreSQL sẽ dùng ❌ Planner tự quyết định dựa trên chi phí Càng nhiều Index càng tốt ❌ Mỗi Index làm chậm INSERT/UPDATE/DELETE Query chậm → cứ tạo thêm Index ❌ Cần xác định nguyên nhân trước Index chỉ giúp tăng tốc SELECT ❌ Index cũng cần được cập nhật khi ghi dữ liệu
Tổng kết
Index không phải là mục tiêu. Execution Plan mới là mục tiêu.
Một Index chỉ có giá trị khi giúp Query Planner đọc ít dữ liệu hơn Full Table Scan. Nếu không, PostgreSQL sẽ bỏ qua nó — và đó là quyết định hợp lý.
Hẹn gặp ở Part 4
Phần thực chiến nhất của series: đóng vai Backend Engineer xử lý production, benchmark 100K/1M/10M records, đọc EXPLAIN ANALYZE, áp dụng Composite Index, Covering Index, INCLUDE, Bitmap Scan — so sánh trước/sau tối ưu.
Thích bài viết này?
Nội dung trên Vết Mực luôn được chia sẻ miễn phí. Nếu bài viết mang lại giá trị cho bạn, hãy cân nhắc ủng hộ để chúng mình có thể duy trì máy chủ, phát triển thêm tính năng mới và tiếp tục xây dựng một không gian dành cho những người yêu viết lách. ✨
Các cách ủng hộ:
- •Viết và đăng bài trên Vết Mực
- •Chia sẻ bài viết với bạn bè
- •Góp ý để chúng mình cải thiện sản phẩm qua email: nsikhoa@gmail.com
Dù bạn chọn ủng hộ hay chỉ đơn giản là tiếp tục đọc và chia sẻ bài viết, đó đều là nguồn động lực rất lớn với chúng mình. ❤️
Bình luận
Đăng nhập để để lại bình luận.