Anatomi Index PostgreSQL: B-Tree, GIN, dan GiST — Kapan Pakai yang Mana?
Mengapa Index Itu Penting?
Tanpa index, PostgreSQL harus melakukan sequential scan—membaca setiap baris di tabel satu per satu—setiap kali Anda menjalankan query dengan klausa WHERE, JOIN, atau ORDER BY. Untuk tabel dengan jutaan baris, ini bisa berarti perbedaan antara query yang selesai dalam beberapa milidetik versus beberapa detik atau bahkan menit.
Index adalah struktur data tambahan yang memungkinkan database "melompat" langsung ke baris yang relevan, mirip seperti daftar isi di sebuah buku. Tapi tidak semua index diciptakan sama—PostgreSQL menyediakan beberapa jenis index dengan struktur data dan use case yang sangat berbeda.
B-Tree: Index Default yang Serbaguna
Ketika Anda menjalankan CREATE INDEX tanpa menentukan jenisnya, PostgreSQL otomatis memakai B-Tree (singkatan dari Balanced Tree). Ini adalah pilihan yang tepat untuk sebagian besar kasus.
CREATE INDEX idx_posts_published_at ON blog.posts (published_at);
B-Tree unggul untuk operator perbandingan standar: =, <, <=, >, >=, dan BETWEEN. Strukturnya menjaga data tetap terurut, sehingga sangat efisien untuk:
- Pencarian kesetaraan (
WHERE status = 'published') - Range query (
WHERE created_at BETWEEN '2026-01-01' AND '2026-12-31') - Sorting (
ORDER BY published_at DESC)—bahkan bisa menghindari operasi sort terpisah sama sekali jika index-nya cocok dengan urutan yang diminta
Batasan: B-Tree tidak membantu untuk pencarian substring (LIKE '%keyword%'), pencarian di dalam array atau JSON, atau full-text search berbasis kata.
GIN: Untuk Data Multi-Nilai (Full-Text, Array, JSONB)
GIN (Generalized Inverted Index) dirancang untuk kolom yang menyimpan banyak nilai per baris—array, JSONB, atau tsvector untuk full-text search. Alih-alih memetakan satu baris ke satu nilai (seperti B-Tree), GIN memetakan setiap nilai individual kembali ke baris-baris yang mengandungnya—mirip seperti indeks kata di belakang buku.
-- Full-text search: setiap kata di dalam dokumen diindeks secara terpisahCREATE INDEX idx_posts_search ON blog.posts USING gin (search_vector);-- JSONB: memungkinkan query seperti WHERE metadata @> '{"featured": true}'CREATE INDEX idx_posts_metadata ON blog.posts USING gin (metadata);
Trade-off utamanya: GIN lebih lambat untuk operasi INSERT/UPDATE dibanding B-Tree (karena setiap perubahan berarti memperbarui banyak entri index, bukan satu), tapi jauh lebih cepat untuk query pencarian pada data multi-nilai. Untuk workload yang jarang menulis tapi sering membaca (seperti blog atau katalog produk), ini adalah trade-off yang sepadan.
Sebagai catatan tambahan, pg_trgm (ekstensi trigram) juga memakai GIN untuk mendukung pencarian substring dan kemiripan kata (word_similarity) yang tidak bisa dilakukan B-Tree biasa.
GiST: Untuk Data Geometris dan Range
GiST (Generalized Search Tree) adalah index serbaguna yang mendukung tipe data yang tidak punya urutan linear yang jelas—seperti tipe geometris (point, polygon), range (daterange, int4range), atau pencarian nearest-neighbor.
CREATE INDEX idx_bookings_period ON bookings USING gist (during);-- Mencegah dua booking yang tumpang tindih pada periode yang samaALTER TABLE bookings ADD CONSTRAINT no_overlapEXCLUDE USING gist (room_id WITH =, during WITH &&);
Contoh di atas menunjukkan kekuatan unik GiST: exclusion constraint yang mencegah dua baris memiliki range waktu yang tumpang tindih untuk ruangan yang sama—sesuatu yang mustahil dilakukan secara efisien dengan B-Tree.
Tabel Perbandingan Cepat
| Jenis Index | Cocok Untuk | Contoh Kasus |
|---|---|---|
| B-Tree | Kesetaraan, range, sorting | WHERE status = 'published', ORDER BY created_at |
| GIN | Data multi-nilai, full-text search | tsvector, jsonb, array, pg_trgm |
| GiST | Data geometris, range, nearest-neighbor | Booking system, radius search, exclusion constraint |
| Hash | Kesetaraan murni (jarang dipakai) | WHERE id = 123 (B-Tree biasanya tetap lebih fleksibel) |
Jangan Over-Index
Index bukan solusi gratis. Setiap index menambah beban pada setiap INSERT/UPDATE/DELETE, memakan storage tambahan, dan perlu di-maintain oleh VACUUM. Aturan praktisnya:
- Index kolom yang sering muncul di
WHERE,JOIN, atauORDER BY—bukan kolom yang jarang di-query. - Gunakan
EXPLAIN ANALYZEsebelum menambah index untuk memastikan bottleneck-nya memang di situ, bukan di tempat lain (misalnya N+1 query dari aplikasi). - Pertimbangkan partial index untuk kolom dengan distribusi nilai yang sangat miring—misalnya
CREATE INDEX ON orders (id) WHERE status = 'pending'jika 99% order sudah selesai dan Anda hanya sering query yang pending. - Composite index urutan kolom itu penting—index pada
(status, created_at)bisa dipakai untuk queryWHERE status = 'x'saja, tapi index pada(created_at, status)tidak bisa dipakai secara efisien untuk query yang sama.
Kesimpulan
Memilih jenis index yang tepat bukan soal menghafal, tapi memahami bentuk data dan pola akses Anda. B-Tree menangani mayoritas kasus sehari-hari, GIN unggul ketika satu baris menyimpan banyak nilai yang perlu dicari, dan GiST hadir untuk kasus data spasial atau range yang lebih eksotis. Kombinasikan dengan EXPLAIN ANALYZE untuk memvalidasi asumsi Anda—index yang tidak pernah dipakai query planner hanyalah beban tanpa manfaat.
Diskusi Pembaca (1)
Penjelasan GIN vs GiST paling jelas yang pernah saya baca. Langsung paham kapan harus pakai yang mana.
