PostgreSQL Index Types — B-tree, Hash, GiST, GIN và cách chọn
Hồi mới làm backend, mình cứ nghĩ index trong PostgreSQL chỉ có mỗi B-tree. Gặp query nào chậm là "thêm index B-tree", may thì chạy nhanh, không may thì... vẫn chậm như cũ. Mãi tới khi một anh senior chỉ cho mình câu EXPLAIN ANALYZE và hỏi "mày biết Postgres có 6 loại index không?", mình mới vỡ ra nhiều thứ.
Bài này mình sẽ điểm qua các loại index chính trong PostgreSQL — B-tree, Hash, GiST, GIN, BRIN — và quan trọng nhất là khi nào nên xài loại nào.
B-tree — Index mặc định và đa năng nhất
B-tree là index mặc định khi bạn CREATE INDEX không ghi gì thêm. Nó phù hợp cho:
- So sánh
=,>,<,>=,<= BETWEEN,IN,LIKE 'prefix%'ORDER BYvàGROUP BY- Cột có nhiều giá trị phân biệt (high cardinality)
CREATE INDEX idx_users_email ON users(email);
-- Tương đương với:
CREATE INDEX idx_users_email ON users USING btree (email);
Kinh nghiệm: B-tree rất mạnh nhưng không phải lúc nào cũng là best choice. Với cột có ít giá trị (low cardinality) như status (chỉ 3-5 giá trị), B-tree vẫn chạy nhưng bitmap scan của Postgres đôi khi còn nhanh hơn.
Hash — Chỉ so sánh bằng
Hash index chỉ hỗ trợ toán tử =, không support range query hay sorting. Trước PostgreSQL 10 nó không crash-safe, nên ít ai dùng. Từ bản 10+ nó đã được viết lại an toàn hơn.
CREATE INDEX idx_users_phone ON users USING hash (phone_hash);
Dùng khi: Bạn chỉ cần tìm exact match, không cần range query. Trong thực tế mình thấy B-tree cũng xử lý = rất nhanh nên Hash hiếm khi cần thiết, trừ những cột cực kỳ lớn (JSON hash, checksum).
GiST — Cho dữ liệu đặc biệt
GiST (Generalized Search Tree) cho phép bạn index các kiểu dữ liệu "không xếp thứ tự được" kiểu như:
- Geometric data (point, polygon, circle)
- Full-text search (kết hợp với tsvector)
- Range types (daterange, int4range)
- KNN search (
ORDER BY col <-> point)
CREATE INDEX idx_locations ON places USING gist (coord);
-- Tìm 10 quán cà phê gần nhất (KNN search)
SELECT name, coord <-> point(106.7, 10.8) AS dist
FROM places
ORDER BY coord <-> point(106.7, 10.8)
LIMIT 10;
Kinh nghiệm: GiST rất ngon cho geo queries — mình từng viết API tìm cửa hàng gần nhất, query B-tree không chạy nổi, chuyển qua GiST giảm từ 800ms xuống 5ms.
GIN — Cho full-text search và JSONB
GIN (Generalized Inverted Index) dùng cho dữ liệu "nhiều giá trị trong một ô" như:
- JSONB (
@>,?,?|operators) - Full-text search (tsvector @@ tsquery)
- Arrays (
@>,&&operators)
-- Index cho JSONB
CREATE INDEX idx_users_prefs ON users USING gin (preferences);
-- Index cho full-text search
CREATE INDEX idx_posts_content ON posts USING gin (to_tsvector('english', content));
-- Query full-text search
SELECT * FROM posts
WHERE to_tsvector('english', content) @@ to_tsquery('english', 'postgresql & index');
Kinh nghiệm: GIN build chậm hơn B-tree, update/insert cũng nặng hơn. Nhưng trade-off là search siêu nhanh. Với JSONB columns mà bạn hay query filter, GIN là chân ái.
BRIN — Index cho dữ liệu tuần tự cực lớn
BRIN (Block Range Index) hoạt động bằng cách ghi nhớ "khoảng giá trị" trên từng block range. Nó rất nhẹ (có thể nhỏ hơn B-tree 100 lần), nhưng chỉ hiệu quả khi dữ liệu có tính tuần tự.
-- Log table 50 triệu dòng, có tính tuần tự theo thời gian
CREATE INDEX idx_logs_created ON logs USING brin (created_at);
-- Chỉ vài MB thay vì hàng GB như B-tree
Dùng khi: Table cực kỳ lớn (hàng trăm triệu dòng), dữ liệu có correlation cao với physical order — log timestamp, auto-increment ID, sensor readings.
Kinh nghiệm: Mình từng optimize một cái log table 200M rows. B-tree index trên created_at tốn 4GB, BRIN chỉ 50MB. Query khoảng thời gian 1 tuần chạy nhanh tương đương, vì BRIN scan rất ít blocks.
Chọn index nào cho đúng?
| Loại | Khi nào dùng | Query chính | Kích thước |
|---|---|---|---|
| B-tree | Đa năng, high cardinality | =, <, >, ORDER BY | Trung bình |
| Hash | Chỉ exact match | = | Nhỏ |
| GiST | Geo, range types, KNN | <<, <@, - | - |
| GIN | JSONB, full-text, arrays | @>, @@, ? | Lớn (build chậm) |
| BRIN | Dữ liệu tuần tự, table lớn | Khoảng thời gian | Rất nhỏ |
Mẹo cuối
Luôn chạy EXPLAIN ANALYZE trước khi tạo index — đừng chạy theo kiểu "thêm index cho chắc". Có khi Postgres dùng bitmap scan + seq scan còn nhanh hơn index scan nếu table nhỏ.
Và nhớ: index có cost. Mỗi index mới làm INSERT/UPDATE/DELETE chậm hơn. Đừng tạo index cho mọi cột — chọn đúng cột, đúng loại index cho query pattern thực tế.
Có thắc mắc gì về index PostgreSQL để lại comment nha. Hẹn anh em bài sau!