Quay lại
Công Nghệ

SQL Index Under the Hood (Part 3)

5 phút đọc7 thg 7, 2026
H

hoanggg2110

Tác giả

SQL Index Under the Hood (Part 3)

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 ANALYZE vẫn hiện Seq 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 — 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?

SQL Index Under the Hood (Part 3)

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.

SQL Index Under the Hood (Part 3)

→ 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.

SQL Index Under the Hood (Part 3)

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

SQL Index Under the Hood (Part 3)

Production Tips — 5 câu hỏi trước khi tạo Index

  1. Query có thực sự chậm không?

  2. Đang dùng Seq Scan hay Index Scan?

  3. Query trả về bao nhiêu records?

  4. Cột trong WHERE có High Cardinality không?

  5. 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.