Indexing
Making Use of Indexes – Poin-Poin Penting
Pentingnya Indexing
Indexing adalah inti dari performance database.
Berdasarkan pengalaman lebih dari 25 tahun dalam PostgreSQL consulting dan PostgreSQL 24/7 support, penyebab utama bad performance adalah bad indexing.
Pengaturan memory parameter penting, tetapi semua itu sia-sia jika index tidak digunakan dengan benar.
Tidak ada pengganti untuk missing index.
Rekomendasi Praktis
Jika performance buruk, hal pertama yang harus dicek adalah indexing, khususnya apakah ada index yang hilang (missing indexes).
Tidak mungkin mencapai good performance tanpa indexing yang tepat.
Kesalahpahaman Umum
Contoh skenario beban query:
1000 queries x 1 ms = 1 detik
1 query x 1000 ms = 1 detik
Hasilnya sama-sama 1 detik total, tetapi 1 query lambat saja bisa menyebabkan 50% load.
Jika query lambat itu dihilangkan (misalnya dengan indexing tepat), performa bisa meningkat 2x lipat.
Alasan Fokus pada Indexing
Satu bab penuh didedikasikan untuk membahas indexing karena dampaknya yang sangat besar.
Tujuannya memberi banyak insight tentang bagaimana indexing bekerja dan bisa dimanfaatkan di PostgreSQL.
Topik yang Dibahas dalam Chapter Ini
Understanding simple queries and the cost model
Improving speed using clustered tables
Understanding additional B-tree features
Introducing operator classes
Understanding PostgreSQL index types
Achieving better answers with fuzzy searching
Understanding full-text searches
Outcome yang Diharapkan
Setelah mempelajari chapter ini, pembaca akan memahami bagaimana index bisa digunakan secara optimal di PostgreSQL untuk mendapatkan performance yang jauh lebih baik.
Understanding simple queries and the cost model
Persiapan Data Uji
Buat tabel
t_test
dengan 2 kolom:id
→ auto-increment (serial)name
→ text (berisi nilai statis)
Isi data dengan fungsi generate_series:
2,000,000
baris dengan nilaihans
2,000,000
baris dengan nilaipaul
Total data = 4 juta rows.
DROP TABLE IF EXISTS t_test; CREATE TABLE t_test (id serial, name text); INSERT INTO t_test (name) SELECT 'hans' FROM generate_series(1, 2000000); INSERT INTO t_test (name) SELECT 'paul' FROM generate_series(1, 2000000);
Verifikasi jumlah data:
SELECT name, count(*) FROM t_test GROUP BY 1;
Hasil:
hans → 2,000,000
paul → 2,000,000
Karakteristik Data
Total 4 juta rows.
id
selalu berurutan naik (ascending order).name
hanya punya 2 nilai unik (distinct values).
→ Dataset ini sederhana tapi cocok untuk eksperimen indexing sepanjang bab.
Menjalankan Query Sederhana
Contoh query:
\timing SELECT * FROM t_test WHERE id = 432332;
Output hasil:
id | name 432332 | hans (1 row) Time: 87.967 ms
Peran
\timing
Perintah
\timing
di psql → menampilkan runtime query.Penting dicatat: ini bukan waktu eksekusi real di server, melainkan waktu yang diukur dari sisi psql client.
Untuk query yang sangat singkat, network latency bisa memberi pengaruh besar terhadap total waktu.
➡️ Bagian ini menekankan dasar eksperimen indexing:
Kita siapkan data besar (4 juta rows).
Jalankan query sederhana dengan filter
WHERE id = ...
.Mulai memahami cost model PostgreSQL dengan melihat waktu query (walaupun belum pure server-side).
Making use of EXPLAIN
Permasalahan Awal
Membaca 4 juta rows dengan query sederhana memakan waktu ±100 ms.
Dari sisi performance, ini dianggap buruk (total disaster).
Dibutuhkan alat untuk memahami apa yang salah.
EXPLAIN Command
Fungsi: menampilkan execution plan dari sebuah statement SQL.
Sintaks umum:
EXPLAIN [ ( option [, ...] ) ] statement
Options yang bisa digunakan (boolean / pilihan):
ANALYZE
VERBOSE
COSTS
SETTINGS
GENERIC_PLAN
BUFFERS
SERIALIZE
(NONE | TEXT | BINARY)WAL
TIMING
SUMMARY
MEMORY
FORMAT
(TEXT | XML | JSON | YAML)
Dokumentasi resmi: PostgreSQL EXPLAIN.
➝ Jika ada query terasa lambat, gunakan EXPLAIN untuk menemukan akar masalahnya.
Contoh Penggunaan
EXPLAIN SELECT * FROM t_test WHERE id = 432332;
Hasil (ringkas):
Gather (cost=1000.00..43455.43 rows=1 width=9) Workers Planned: 2 -> Parallel Seq Scan on t_test (cost=0.00..42455.33 rows=1 width=9) Filter: (id = 432332)
Empat Tahap Eksekusi Query di PostgreSQL
Parser → cek syntax error & masalah dasar.
Rewrite system → tangani rules (misalnya views).
Optimizer / Planner → hitung cara paling efisien untuk menjalankan query, buat plan.
Executor → jalankan plan, hasilkan output.
➝ EXPLAIN memperlihatkan hasil yang dibuat oleh planner (optimizer).
Interpretasi Hasil Contoh
PostgreSQL menggunakan parallel sequential scan.
2 workers dipakai untuk mengevaluasi kondisi
id = 432332
.Hasil parsial digabungkan melalui gather node (fitur sejak PostgreSQL 9.6 dalam parallel query infrastructure).
rows=1
artinya PostgreSQL memperkirakan hanya 1 row yang dihasilkan dari filter ini.
Parallel Workers
Jumlah parallel workers tergantung ukuran tabel.
Tabel besar → PostgreSQL otomatis menggunakan lebih banyak parallel workers.
Tabel kecil → parallelism tidak dipakai karena overhead justru lebih besar daripada manfaat.
Mengatur Parallelism
Bisa mengurangi jumlah workers (misalnya menonaktifkan parallelism) dengan:
SET max_parallel_workers_per_gather TO 0;
Catatan:
Setting ini hanya berlaku di session saat ini.
Bisa juga diubah di
postgresql.conf
, tetapi tidak disarankan karena akan menghilangkan optimisasi parallel query yang biasanya meningkatkan performa.
➡️ Bagian ini menjelaskan bahwa EXPLAIN adalah pintu masuk utama untuk menganalisis query plan, dan contoh pertama memperlihatkan bahwa query WHERE id = ...
tanpa index menyebabkan sequential scan (meskipun dipercepat dengan parallelism).
Digging into the PostgreSQL cost model
Contoh Execution Plan tanpa Parallelism (1 CPU)
EXPLAIN SELECT * FROM t_test WHERE id = 432332;
Hasil:
QUERY PLAN ---------------------------------------------------------- Seq Scan on t_test (cost=0.00..71622.00 rows=1 width=9) Filter: (id = 432332) (2 rows)
PostgreSQL melakukan sequential scan → baca seluruh tabel baris demi baris lalu terapkan filter.
Estimasi cost = 71622 penalty points.
Apa itu Cost (Penalty Points)?
Cost adalah konsep abstrak yang digunakan PostgreSQL untuk membandingkan berbagai cara eksekusi query.
Planner memilih execution plan dengan cost terendah.
Cost bukan waktu real (tidak bisa diterjemahkan langsung ke ms).
Menghitung Cost
Hitung jumlah blok tabel:
SELECT pg_relation_size('t_test') / 8192.0;
Hasil:
?column? -------------------- 21622.000000 (1 row)
➝ Tabel
t_test
terdiri dari 21622 blok (1 blok = 8192 bytes).Setiap blok dibaca sequential → cost =
1
per blok.SHOW seq_page_cost;
seq_page_cost --------------- 1 (1 row)
Selain membaca blok, ada cost untuk CPU:
SHOW cpu_tuple_cost;
cpu_tuple_cost ---------------- 0.01 (1 row)
SHOW cpu_operator_cost;
cpu_operator_cost ------------------- 0.0025 (1 row)
Perhitungan total:
SELECT 21622*1 + 4000000*0.01 + 4000000*0.0025;
Hasil:
?column? ------------ 71622.0000 (1 row)
➝ Cocok dengan angka di EXPLAIN plan.
Jadi cost terdiri dari:
I/O cost (membaca blok)
CPU cost (memproses tuple dan operator) → dijadikan satu angka abstrak.
Parameter Lain dalam Cost Model
Index-related cost parameters:
random_page_cost = 4
Jika PostgreSQL menggunakan index, akan ada banyak random I/O.
Pada spinning disks (HDD), random read jauh lebih mahal daripada sequential.
Pada SSD / NVMe, perbedaan random vs sequential hampir hilang → bisa diatur ke
1
.
cpu_index_tuple_cost = 0.005
CPU cost saat menggunakan index.
Default 0.005, biasanya tidak perlu diubah.
Parallel query cost parameters:
parallel_tuple_cost = 0.1
Cost untuk mentransfer satu tuple dari worker ke proses lain.
Default = 0.1 → jarang diubah.
parallel_setup_cost = 1000.0
Cost untuk memulai parallel worker.
Karena membuat proses tambahan itu ada overhead, parameter ini memodelkan biaya itu.
min_parallel_table_scan_size = 8MB
Ukuran minimum tabel agar dipertimbangkan untuk parallel scan.
Semakin besar tabel, semakin banyak CPU digunakan.
Tabel harus 3x lebih besar agar PostgreSQL menambah 1 worker baru.
min_parallel_index_scan_size = 512kB
Ukuran minimum index agar dipertimbangkan untuk parallel index scan.
📌 Kesimpulan Bagian Ini:
PostgreSQL planner menghitung cost query berdasarkan I/O + CPU.
Angka cost tidak mewakili waktu eksekusi real (ms), hanya untuk perbandingan antar strategi eksekusi.
Parameter seperti
seq_page_cost
,cpu_tuple_cost
,cpu_operator_cost
,random_page_cost
, hingga parallel-related cost (parallel_tuple_cost
, dll.) mempengaruhi cara PostgreSQL memilih execution plan.
Deploying simple indexes
Masalah Sequential Scan
Menambah parallel workers untuk scan tabel besar memang membantu, tapi membaca seluruh tabel hanya untuk menemukan 1 row tetap tidak efisien.
Solusi: gunakan index.
Membuat Index Sederhana
CREATE INDEX idx_id ON t_test (id);
Index
idx_id
dibuat pada kolomid
.Coba query ulang:
SELECT * FROM t_test WHERE id = 43242;
Output:
id | name -------+------ 43242 | hans (1 row) Time: 0.259 ms
🔑 Hasilnya: dari ±100 ms (sequential scan) → turun drastis jadi 0.259 ms dengan index.
Tipe Index Default PostgreSQL
PostgreSQL menggunakan Lehman-Yao’s high-concurrency B-tree untuk index standar. 📖 Paper asli Lehman-Yao B-tree
PostgreSQL menambahkan berbagai optimisasi spesifik.
Kelebihan utama Lehman-Yao B-tree:
Mendukung high concurrency → banyak operasi read dan write bisa berjalan bersamaan pada index yang sama.
Hasil: throughput meningkat drastis.
Biaya Index (Tidak Gratis)
Index memakan ruang disk dan membuat write lebih lambat, karena index harus selalu sinkron dengan tabel.
Contoh cek ukuran index:
\x -- aktifkan expanded display \di+ idx_id
Hasil:
List of relations -[ RECORD 1 ]-+---------- Schema | public Name | idx_id Type | index Owner | hs Table | t_test Persistence | permanent Access method | btree Size | 86 MB Description |
Index
idx_id
dengan 4 juta rows = 86 MB di disk.Setiap
INSERT
,UPDATE
,DELETE
harus update index juga. ➝ Jika ada 20 indexes, maka setiapINSERT
harus tulis ke 20 index → proses jadi jauh lebih lambat.
Parallel Index Creation
Sejak PostgreSQL 11 → mendukung parallel index creation untuk B-tree indexes.
Sejak PostgreSQL 17 → juga mendukung parallel build untuk BRIN indexes.
Artinya: PostgreSQL bisa pakai banyak CPU core untuk membangun index, membuat proses jauh lebih cepat.
Saat ini, parallel build hanya berlaku untuk B-tree dan BRIN → belum untuk index tipe lain.
Parameter kontrol:
max_parallel_maintenance_workers
Menentukan berapa banyak proses yang boleh digunakan sebagai batas atas saat membangun index.
📌 Kesimpulan Bagian Ini:
Index membuat query super cepat (100 ms → 0.259 ms).
PostgreSQL default = B-tree index dengan Lehman-Yao high-concurrency.
Index punya trade-off:
+ Membuat SELECT jauh lebih cepat.
– Membutuhkan ruang disk & memperlambat write.
Versi modern PostgreSQL mendukung parallel index build untuk mempercepat proses pembuatan index di tabel besar.
Making use of sorted output
Seperti yang sudah ditunjukkan, B-tree index tidak hanya berguna untuk menemukan baris tertentu, tetapi juga bisa digunakan untuk menyediakan data yang sudah tersortir ke tahap query berikutnya.
🔹 Contoh 1: ORDER BY + LIMIT
EXPLAIN SELECT *
FROM t_test
ORDER BY id DESC
LIMIT 10;
Hasil query plan:
QUERY PLAN
---------------------------------------------------------
Limit (cost=0.43..0.74 rows=10 width=9)
-> Index Scan Backward using idx_id on t_test
(cost=0.43..125505.43 rows=4000000 width=9)
👉 PostgreSQL cukup membaca 10 baris terakhir dari index (karena index idx_id
sudah tersortir ASC secara default, maka untuk DESC
cukup dibaca mundur).
➡️ Tidak ada full sort di seluruh tabel → sangat cepat!
✅ Praktisnya, kita bisa dapatkan Top-N query (misalnya "10 data terbaru") dalam <1 ms, bahkan di tabel dengan jutaan rows.
🔹 Contoh 2: MIN() dan MAX()
Fungsi min
dan max
juga sangat terbantu dengan index.
EXPLAIN SELECT min(id), max(id) FROM t_test;
Hasil query plan:
QUERY PLAN
--------------------------------------------------------
Result (cost=0.91..0.92 rows=1 width=8)
InitPlan 1
-> Limit (cost=0.43..0.46 rows=1 width=4)
-> Index Only Scan using idx_id on t_test
InitPlan 2
-> Limit (cost=0.43..0.46 rows=1 width=4)
-> Index Only Scan Backward using idx_id on t_test t_test_1
➡️ PostgreSQL cukup melakukan Index Only Scan sekali ke awal dan sekali ke akhir index:
Awal index → dapatkan
min(id)
Akhir index → dapatkan
max(id)
Tidak perlu baca semua data di tabel (t_test
), hanya cukup baca metadata index.
🔑 Insight penting
B-tree index = sorted structure
Bisa dibaca dari awal (ASC) maupun dari akhir (DESC).
Cocok untuk query yang membutuhkan data terurut.
Top-N query optimization
ORDER BY ... LIMIT N
akan sangat cepat dengan index yang sesuai.Biasanya dipakai untuk "recent data", "latest log", "top ranking".
Aggregate optimization (min/max)
Tidak perlu scan seluruh tabel.
Hanya perlu baca ujung index.
Index Only Scan
Kalau semua kolom yang dibutuhkan ada di index (atau bisa diambil dari index tanpa lihat tabel), maka query bisa dijawab tanpa menyentuh tabel utama.
Ini mempercepat query lebih jauh.
📘 Jadi, indeks B-tree bukan cuma soal WHERE filtering, tapi juga ORDER BY, LIMIT, MIN/MAX, dan aggregate tertentu. Nanti ada pembahasan lebih lanjut tentang bagaimana PostgreSQL memilih index untuk grouping, distinct, join, dll.
Using more than one index at a time
Sejauh ini, kita sudah lihat bahwa query biasanya menggunakan satu index pada satu waktu. Namun, di dunia nyata sering kali query membutuhkan lebih dari satu kondisi, sehingga PostgreSQL bisa menggunakan lebih dari satu index dalam satu query.
🔹 Contoh: OR condition
EXPLAIN SELECT * FROM t_test WHERE id = 30 OR id = 50;
Query Plan:
Bitmap Heap Scan on t_test (cost=8.88..16.85 rows=2 width=9)
Recheck Cond: ((id = 30) OR (id = 50))
-> BitmapOr (cost=8.88..8.88 rows=2 width=0)
-> Bitmap Index Scan on idx_id (cost=0.00..4.44 rows=1 width=0)
Index Cond: (id = 30)
-> Bitmap Index Scan on idx_id (cost=0.00..4.44 rows=1 width=0)
Index Cond: (id = 50)
👉 Hal penting di sini:
PostgreSQL menggunakan index yang sama (
idx_id
) dua kali untuk dua kondisi berbeda (id = 30
danid = 50
).Hasilnya digabung menggunakan
BitmapOr
.
🔹 Apa itu Bitmap Scan?
⚠️ Perlu dibedakan:
Bitmap index (Oracle) → jenis index itu sendiri.
Bitmap scan (PostgreSQL) → metode eksekusi query, bukan tipe index.
Cara kerjanya di PostgreSQL:
Index pertama discan → menghasilkan daftar halaman (pages/blocks) yang mungkin berisi hasil.
Index kedua discan → menghasilkan daftar halaman lagi.
Untuk operasi
OR
, daftar halaman ini digabung (union).PostgreSQL kemudian membaca halaman-halaman tersebut dari tabel.
Karena bisa saja ada “kelebihan” data (false positives), PostgreSQL melakukan recheck untuk memastikan baris benar-benar sesuai dengan kondisi
WHERE
.
🔹 Bitmap Scan untuk AND
Bitmap scan juga bisa digunakan untuk AND
atau kombinasi AND
+ OR
.
Namun PostgreSQL tidak selalu memilih bitmap scan pada AND
.
Contoh: Misalnya ada query mencari:
Semua orang dengan
country = 'Austria'
(8 juta baris)DAN
id = 12345
(hanya 1 baris)
👉 Kalau PostgreSQL scan index country
dulu, hasilnya jutaan rows, padahal kita hanya butuh 1 row dari kondisi id
.
Lebih murah kalau langsung pakai index id
.
➡️ Optimizer PostgreSQL akan menghitung biaya (cost) dari berbagai opsi, lalu memilih rute paling efisien.
🔹 Catatan penting
OR bisa mahal → karena harus scan lebih banyak halaman dari tabel.
Kadang lebih cepat diubah ke UNION:
SELECT * FROM t_test WHERE id = 30 UNION SELECT * FROM t_test WHERE id = 50;
→ PostgreSQL bisa eksekusi dua query kecil lalu gabungkan hasilnya, seringkali lebih efisien daripada satu query dengan
OR
.
🔑 Ringkasan
PostgreSQL bisa menggunakan lebih dari satu index dalam satu query.
Bitmap scan memungkinkan penggabungan hasil index (baik
OR
,AND
, atau kombinasi).PostgreSQL akan selalu mempertimbangkan cost untuk memutuskan strategi terbaik.
Untuk query dengan banyak
OR
, coba pertimbangkan alternatif seperti UNION.
Using Bitmap Scans Effectively
Bitmap scan bukan sekadar fitur tambahan — ada situasi spesifik di mana PostgreSQL memilihnya karena lebih efisien daripada Index Scan biasa.
Ada dua use case utama:
1. Menghindari fetching block yang sama berulang-ulang
Misalnya:
Kita mencari semua orang yang berbicara bahasa tertentu.
Katakanlah 10% dari semua data punya bahasa itu.
Kalau PostgreSQL pakai Index Scan biasa:
Index akan menunjuk ke banyak baris.
Tetapi baris-baris itu mungkin ada di block tabel yang sama.
Artinya, PostgreSQL harus baca block yang sama berkali-kali → overhead I/O lebih besar.
Dengan Bitmap Scan:
PostgreSQL mengumpulkan list block yang berisi data yang relevan.
Lalu membaca setiap block sekali saja.
Ini mengurangi I/O dan meningkatkan performa.
2. Menggabungkan kondisi yang relatif lemah
Misalnya query:
SELECT *
FROM people
WHERE age BETWEEN 20 AND 30
AND shirt_color = 'yellow';
Katakanlah 15% orang berusia 20–30.
Dan 15% orang memakai baju kuning.
Tetapi hasil gabungannya hanya sekitar 1% dari data total.
👉 Dalam kasus ini:
Sequential Scan terlalu mahal (baca semua tabel).
PostgreSQL bisa ambil index
age
dan indexshirt_color
, lalu gabungkan dengan bitmap.Hasilnya jauh lebih sedikit, sehingga lebih murah daripada scan penuh.
3. Parallel Bitmap Heap Scan (sejak PostgreSQL 10)
Bitmap scan biasanya dipakai untuk query mahal → data besar, kondisi kompleks.
Mulai PostgreSQL 10, PostgreSQL bisa menjalankan Parallel Bitmap Heap Scan.
Artinya, beberapa worker bisa membaca block secara paralel berdasarkan bitmap hasil index.
Ini mempercepat query skala besar dengan kondisi gabungan.
🔑 Ringkasan
Bitmap Scan efektif ketika:
Banyak baris target ada dalam block yang sama (hindari block dibaca berulang).
Kondisi filter masing-masing lemah, tapi hasil gabungannya kecil.
PostgreSQL pintar memilih: kalau pakai Index Scan atau Seq Scan lebih murah, dia akan hindari Bitmap Scan.
Parallel Bitmap Heap Scan (Postgres 10+) menambah power untuk dataset besar.
Using Indexes Intelligently
Selama ini, kita melihat index seperti alat sakti yang selalu membuat query lebih cepat. Tapi kenyataannya tidak selalu begitu — ada kondisi di mana index tidak berguna atau bahkan memperlambat query.
📍 Struktur tabel untuk contoh
\d t_test
Output:
Table "public.t_test"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+-------------------------
id | integer | | not null | nextval('t_test_id_seq')
name | text | | |
Indexes:
"idx_id" btree (id)
Sudah ada index
idx_id
di kolomid
.Lalu kita buat index tambahan di kolom
name
:
CREATE INDEX idx_name ON t_test (name);
📍 Query dengan nilai yang tidak ada
EXPLAIN SELECT * FROM t_test WHERE name = 'hans2';
Output:
Index Scan using idx_name on t_test
(cost=0.43..4.45 rows=1 width=9)
Index Cond: (name = 'hans2'::text)
✅ PostgreSQL pakai Index Scan, meskipun nilai 'hans2'
tidak ada di tabel.
Kenapa? Karena planner tetap asumsikan ada kemungkinan ada baris (tidak akan pernah estimasi rows=0
).
Kalau planner mengestimasi nol, perhitungan biaya untuk node lain di execution plan jadi sulit.
📍 Query dengan nilai yang mencakup seluruh tabel
EXPLAIN SELECT *
FROM t_test
WHERE name = 'hans'
OR name = 'paul';
Output:
Seq Scan on t_test
(cost=0.00..81622.00 rows=3000005 width=9)
Filter: ((name = 'hans'::text) OR (name = 'paul'::text))
❌ PostgreSQL abaikan index, langsung pakai Sequential Scan.
Kenapa? Karena 'hans'
dan 'paul'
mencakup seluruh dataset.
Membaca index + tabel → lebih mahal.
Lebih efisien langsung baca tabel penuh sekali jalan.
📍 Query dengan nilai yang jarang muncul
EXPLAIN SELECT *
FROM t_test
WHERE name = 'hans2'
OR name = 'paul2';
Output:
Bitmap Heap Scan on t_test (cost=8.88..12.89 rows=1 width=9)
Recheck Cond: ((name = 'hans2'::text) OR (name = 'paul2'::text))
-> BitmapOr (cost=8.88..8.88 rows=1 width=0)
-> Bitmap Index Scan on idx_name
(cost=0.00..4.44 rows=1 width=0)
Index Cond: (name = 'hans2'::text)
-> Bitmap Index Scan on idx_name
(cost=0.00..4.44 rows=1 width=0)
Index Cond: (name = 'paul2'::text)
✅ PostgreSQL pakai Bitmap Index Scan karena hasilnya diperkirakan sangat kecil → lebih hemat dibanding sequential scan.
🔑 Hal penting yang harus dipahami
Execution plan tergantung data & input value PostgreSQL tidak hanya melihat query, tapi juga data distribution di tabel.
Index tidak selalu digunakan Kalau kondisi WHERE mencakup hampir semua data, index dilewati → langsung Seq Scan.
Planner tidak pernah estimasi nol baris Bahkan jika nilai jelas tidak ada, PostgreSQL tetap kasih estimasi minimal (misalnya
rows=1
).Rencana eksekusi bisa berubah Query yang sama bisa punya execution plan berbeda tergantung datanya. Inilah sebabnya runtime bisa terasa tidak konsisten di dunia nyata.
⚡ Jadi, index harus digunakan dengan bijak. Tidak semua index akan mempercepat query — dalam beberapa kasus, index justru menambah overhead.
Understanding index de-duplication (PostgreSQL 13+)
Mulai PostgreSQL 13, ada fitur baru yang sangat signifikan: Index De-duplication. Fitur ini khusus berlaku untuk B-tree indexes.
🔹 Masalah sebelum PostgreSQL 13
Jika kita punya tabel dengan banyak nilai yang sama (high cardinality rendah, banyak duplikat), maka index B-tree akan menyimpan entry untuk setiap row.
Contoh: kolom
gender
dengan nilaiM
/F
.Jika ada 10 juta baris, setengah
M
, setengahF
, maka index tetap menyimpan jutaan entry dengan nilai yang sama.
Akibatnya:
Index jadi besar → misalnya ratusan MB.
Cache hit ratio rendah → lebih banyak I/O.
Query jadi lambat karena traversal lebih panjang.
🔹 Solusi PostgreSQL 13: Index Entry De-duplication
PostgreSQL sekarang bisa mengompres duplikat di index.
Jadi untuk banyak baris dengan nilai sama, index tidak menyimpan setiap baris, melainkan menyimpan satu entry nilai lalu daftar pointer ke block/row.
Hasilnya:
Index jadi jauh lebih kecil.
Lebih efisien di RAM (buffer cache).
Faster index scans karena traversal lebih singkat.
🔹 Contoh nyata
Misalnya:
CREATE TABLE t_test (
id serial primary key,
name text
);
-- Isi dengan banyak duplikat
INSERT INTO t_test (name)
SELECT CASE WHEN i % 2 = 0 THEN 'Alice' ELSE 'Bob' END
FROM generate_series(1, 5000000) i;
-- Buat index
CREATE INDEX idx_name ON t_test(name);
Sebelum PostgreSQL 13:
Index
idx_name
bisa mencapai puluhan MB karena setiap "Alice"/"Bob" direkam jutaan kali.
Sesudah PostgreSQL 13 (dengan dedup):
Index bisa lebih kecil drastis (misalnya 26 MB vs 86 MB seperti di contoh).
🔹 Dampak positif utama
Smaller Index Size → hemat disk dan RAM.
Better Cache Hit Rate → index bisa lebih sering disimpan penuh di memori.
Faster Queries → lebih sedikit disk access, traversal lebih cepat.
Efisiensi Maintenance → VACUUM, REINDEX, dan Index Scans lebih cepat.
🔹 Batasan
Hanya berlaku untuk B-tree indexes.
Efektif jika data punya banyak nilai yang sama (low cardinality).
Kalau datanya high cardinality (unik hampir semua), dedup tidak membawa banyak manfaat.
⚡ Kesimpulan:
Sejak PostgreSQL 13, index dengan banyak nilai duplikat (misalnya kolom status
, gender
, country_code
, dll) akan jauh lebih hemat dan lebih cepat, berkat index de-duplication.
Clustered Tables dan Correlation
Ketika PostgreSQL membaca data, letak fisik di disk berpengaruh besar terhadap performa. Meskipun jumlah row yang diambil sama, runtime bisa berbeda drastis tergantung apakah data tersusun rapi (clustered) atau acak (randomized).
📌 Contoh kasus:
Tabel
t_test
(ordered byid
) → Data di-insert pakaiSERIAL
/ auto-increment, sehingga baris tersusun berurutan. → QueryWHERE id < 10000
bisa diambil dengan sedikit block access → hanya 85 blocks disentuh. → Waktu eksekusi: ~4 ms.Tabel
t_random
(shuffled byORDER BY random()
) → Data diacak di disk. → QueryWHERE id < 10000
harus baca 8034 blocks (!). → PostgreSQL sampai ubah rencana eksekusi → pakai Bitmap Heap Scan (lebih hemat daripada Index Scan biasa). → Waktu eksekusi: ~28 ms.
🔹 Correlation di pg_stats
pg_stats
PostgreSQL menyimpan informasi statistik di pg_stats
, termasuk correlation untuk setiap kolom.
SELECT tablename, attname, correlation
FROM pg_stats
WHERE tablename IN ('t_test', 't_random')
ORDER BY 1, 2;
📌 Hasil:
t_test
id
1.0
t_test
name
1.0
t_random
id
-0.006
t_random
name
0.49
👉 Interpretasi:
Correlation = 1 → Data tersusun rapi (monoton meningkat).
Correlation mendekati 0 → Data acak.
Correlation negatif → Data benar-benar tidak berhubungan (shuffled).
Efek correlation ini:
Jika correlation tinggi (≈1) → query range scan (
BETWEEN
,<
,>
, dll) jadi cepat.Jika correlation rendah → PostgreSQL harus lompat ke banyak block berbeda → performa turun.
🔹 Clustered Tables
Untuk mengatasi masalah acak ini, PostgreSQL menyediakan perintah CLUSTER:
CLUSTER t_random USING idx_random;
👉 Hasilnya:
PostgreSQL akan menyusun ulang baris di disk sesuai dengan urutan index (
idx_random
).Setelah clustering, correlation akan naik mendekati 1 lagi.
Query range (
WHERE id < 10000
,BETWEEN
,ORDER BY
) jadi jauh lebih cepat.
🔹 Pentingnya ANALYZE
PostgreSQL menggunakan ANALYZE untuk meng-update statistik (
pg_stats
).Autovacuum biasanya sudah otomatis menjalankannya.
Bisa juga manual:
ANALYZE t_random;
🔹 Kesimpulan
Layout fisik data di disk memengaruhi kecepatan query range.
Correlation di pg_stats memberi gambaran apakah kolom “berurutan” di disk atau tidak.
Gunakan CLUSTER untuk menata ulang tabel berdasarkan index → meningkatkan correlation.
Hati-hati: CLUSTER reorganizes seluruh tabel (bisa lama & blocking). Biasanya dipakai untuk batch maintenance, bukan rutin harian.
CLUSTER di PostgreSQL
Perintah CLUSTER digunakan untuk menyusun ulang data fisik dalam tabel sesuai urutan index tertentu. Setelah clustering, baris di disk akan tersimpan dalam urutan yang sama dengan index pilihan.
📌 Sintaks:
CLUSTER [ ( option [, ...] ) ] [ table_name [ USING index_name ] ];
Contoh:
CLUSTER t_random USING idx_random;
🔹 Hal-hal penting tentang CLUSTER
Walaupun powerful, ada beberapa hal yang harus dipertimbangkan sebelum digunakan, terutama di production:
Locking 🔒
Selama proses CLUSTER, tabel akan di-lock eksklusif.
Artinya: INSERT/UPDATE/DELETE tidak bisa dilakukan hingga proses selesai.
Jadi, di production harus dijalankan saat maintenance window atau saat sistem idle.
Hanya 1 index 📌
CLUSTER hanya bisa menggunakan satu index untuk mengatur urutan data.
Jadi, kamu harus memilih index yang paling sering dipakai.
Tidak bisa sekaligus urut berdasarkan
id
,name
,created_at
misalnya.
Efeknya relatif terhadap workload ⚡
Seperti contoh di bab sebelumnya, CLUSTER bisa membuat query range jadi jauh lebih cepat.
Tapi kalau workload lebih banyak random access / OLTP, efeknya mungkin kecil.
Efektivitas juga dipengaruhi cache hit rate & pola query.
Tidak bertahan selamanya 🕒
Setelah clustering, data memang rapi.
Tapi begitu ada INSERT/UPDATE/DELETE, urutan fisik pelan-pelan rusak lagi (correlation turun).
Artinya: CLUSTER bukan proses sekali untuk selamanya, biasanya perlu dijalankan ulang berkala.
🔹 Kapan pakai CLUSTER?
Cocok untuk tabel besar yang sering di-scan berdasarkan range (misalnya data time-series: log, transaksi harian).
Tidak cocok untuk tabel dengan banyak update/insert random, karena clustering cepat rusak.
Biasanya dijalankan sebagai bagian dari batch maintenance (mirip VACUUM FULL atau REINDEX).
🔹 Perbandingan CLUSTER vs lainnya
VACUUM FULL → rewrite tabel juga, tapi hanya untuk menghapus bloat, tidak menjamin urutan.
REINDEX → hanya rebuild index, tidak mengubah urutan fisik tabel.
CLUSTER → rewrite tabel dengan urutan sesuai index → meningkatkan correlation.
⏩ Jadi intinya, CLUSTER bagus untuk kasus spesifik, terutama saat ada range query dominan. Tapi harus dipakai hati-hati karena locking & maintenance overhead.
Index Scan vs Index-Only Scan
Sejauh ini kita sudah melihat Index Scan biasa. Tapi PostgreSQL bisa lebih pintar dengan Index-Only Scan.
Contoh perbedaan:
1. Query ambil semua kolom (butuh akses tabel):
EXPLAIN SELECT * FROM t_test WHERE id = 34234;
Hasil:
Index Scan using idx_id on t_test
(cost=0.43..8.45 rows=1 width=9)
Index Cond: (id = 34234)
👉 PostgreSQL pakai index untuk mencari row, tapi tetap akses tabel (heap) karena butuh baca semua kolom.
2. Query ambil kolom yang ada di index saja:
EXPLAIN SELECT id FROM t_test WHERE id = 34234;
Hasil:
Index Only Scan using idx_id on t_test
(cost=0.43..8.45 rows=1 width=4)
Index Cond: (id = 34234)
👉 PostgreSQL bisa langsung jawab query hanya dari index, tanpa perlu baca tabel. Ini disebut Index-Only Scan → jauh lebih cepat karena menghindari akses ke heap (table data).
🔹 Kapan Index-Only Scan bisa dipakai?
Semua kolom yang diminta query ada di index
Misalnya index
(id)
→ query hanya SELECTid
bisa pakai index-only scan.Kalau query
SELECT id, name
, maka tetap harus ke tabel (heap).
Visibility map bersih (VACUUM/Autovacuum berjalan baik)
PostgreSQL tetap perlu tahu apakah row "valid" (tidak dihapus).
Kalau visibility map sudah update (misalnya habis vacuum), maka index-only scan bisa berjalan 100%.
Kalau tidak, PostgreSQL mungkin masih harus cek heap → menurunkan benefit.
🔹 Covering Indexes (INCLUDE)
Kadang query butuh lebih dari 1 kolom. Untuk menghindari heap lookup, PostgreSQL mendukung covering indexes (sejak PostgreSQL 11).
📌 Contoh:
CREATE INDEX idx_name_id ON t_test (name) INCLUDE (id);
name
→ kolom utama index (dipakai untuk pencarian/filtering/sorting).id
→ kolom tambahan, tidak ikut urutan index, tapi disimpan di index agar query bisa dijawab dari index saja.
Dengan begitu query seperti:
SELECT id FROM t_test WHERE name = 'hans';
👉 Bisa dijawab dengan Index-Only Scan tanpa akses ke tabel.
🔹 Manfaat Index-Only Scan
✅ Lebih cepat → karena tidak perlu baca blok tabel.
✅ Lebih sedikit I/O.
✅ Sangat berguna untuk query yang hanya ambil beberapa kolom yang sudah ada di index.
✅ Cocok untuk workload read-heavy (misalnya laporan, dashboard).
⚡ Singkatnya: Index Scan → baca index lalu ke tabel. Index-Only Scan → cukup baca index (lebih hemat). Covering Index (INCLUDE) → cara praktis bikin index-only scan jalan di lebih banyak query.
Combined Indexes (Multi-Column Indexes)
Sebuah combined index (atau multi-column index) adalah indeks yang mencakup lebih dari satu kolom. PostgreSQL menyimpan data dalam urutan berdasarkan kolom-kolom yang didefinisikan pada indeks tersebut.
Misalnya, kita punya tabel dengan kolom:
CREATE TABLE phonebook (
postal_code TEXT,
last_name TEXT,
first_name TEXT
);
Jika kita buat indeks seperti ini:
CREATE INDEX idx_phonebook
ON phonebook (postal_code, last_name, first_name);
Maka data di indeks tersebut akan diurutkan berdasarkan:
postal_code terlebih dahulu,
lalu di dalam setiap
postal_code
diurutkan berdasarkan last_name,dan di dalam kombinasi
postal_code + last_name
, diurutkan lagi berdasarkan first_name.
Kapan indeks gabungan bisa digunakan?
PostgreSQL hanya bisa menggunakan bagian prefix kiri dari indeks gabungan secara efisien. Artinya:
Query dengan
WHERE postal_code = '12345'
→ bisa pakai indeks.Query dengan
WHERE postal_code = '12345' AND last_name = 'Doe'
→ bisa pakai indeks.Query dengan
WHERE postal_code = '12345' AND last_name = 'Doe' AND first_name = 'John'
→ full usage dari indeks.Query dengan
WHERE last_name = 'Doe'
saja → tidak bisa langsung pakai indeks ini, karenapostal_code
adalah kolom pertama di indeks.Query dengan
WHERE last_name = 'Doe' AND first_name = 'John'
→ juga tidak bisa langsung pakai, karenapostal_code
dilewati. Biasanya PostgreSQL akan jatuh ke bitmap scan atau full scan.
Contoh penggunaan
EXPLAIN SELECT * FROM phonebook
WHERE postal_code = '12345'
AND last_name = 'Doe';
Akan menggunakan Index Scan
dengan baik, karena query memanfaatkan prefix postal_code
lalu last_name
.
Tapi:
EXPLAIN SELECT * FROM phonebook
WHERE last_name = 'Doe';
Biasanya tidak bisa memanfaatkan indeks (postal_code, last_name, first_name)
secara langsung. PostgreSQL mungkin akan tetap mempertimbangkan bitmap index scan (menggabungkan beberapa indeks jika tersedia), tapi performanya lebih buruk dibanding indeks yang memang dibuat khusus untuk last_name
.
Best Practice untuk Combined Indexes
Urutan kolom penting
Kolom dengan selectivity tinggi (membatasi hasil query lebih spesifik) biasanya ditempatkan di awal.
Contoh:
postal_code
punya ribuan kemungkinan, tapigender
cuma ada 2 (M/F). Lebih baik taruhpostal_code
dulu.
Gunakan secara selektif Jangan buat semua kombinasi kolom jadi indeks, karena akan boros storage & memperlambat operasi
INSERT
/UPDATE
/DELETE
.Pertimbangkan kebutuhan query Lihat pola query yang sering digunakan, lalu buat indeks gabungan sesuai kebutuhan.
📌 Jadi ringkasannya:
Indeks gabungan (multi-column) berguna kalau query sering memakai kombinasi kolom dengan urutan tertentu.
PostgreSQL hanya bisa memanfaatkan prefix kiri dari indeks secara optimal.
Jika pola query sangat beragam, kadang lebih baik menggunakan indeks terpisah + biarkan PostgreSQL menggunakan bitmap scan.
1. Functional Indexes
Biasanya kita membuat indeks langsung pada kolom. Namun, PostgreSQL juga mendukung indeks fungsional, yaitu indeks yang menyimpan hasil dari suatu ekspresi atau fungsi terhadap kolom.
Contoh:
CREATE INDEX idx_cos ON t_random (cos(id));
ANALYZE;
Sekarang query seperti ini:
EXPLAIN SELECT * FROM t_random WHERE cos(id) = 10;
Akan menggunakan Index Scan pada idx_cos
.
Syarat penting: fungsi yang dipakai harus immutable → hasilnya tidak berubah untuk input yang sama.
✅
cos(x)
,lower(text)
→ aman.❌
age(timestamp)
,random()
→ tidak bisa, karena hasilnya berubah tergantung waktu/kondisi.
👉 Functional indexes sangat berguna untuk query pencarian case-insensitive (LOWER(name)
), pencarian substring (substr(field,1,3)
), atau normalisasi data sebelum pencarian.
2. Space Consumption of Indexes
Indeks itu tidak gratis. Untuk setiap baris, PostgreSQL harus menyimpan pointer + nilai indeks.
Contoh kasus di buku:
Tabel total data: 338 MB
Indeks total: 284 MB
Artinya, indeks bisa hampir sebesar (atau bahkan lebih besar) dari tabel itu sendiri.
Masalahnya:
Over-indexing → membuat DB membengkak, memperlambat
INSERT
,UPDATE
,DELETE
karena indeks juga harus di-maintain.Kadang 50%+ ukuran DB adalah indeks, dan sering ada indeks yang tidak pernah dipakai.
👉 Untuk menganalisis:
\di+ -- lihat ukuran indeks
Atau dengan query:
SELECT relname AS index_name,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
JOIN pg_index USING (indexrelid)
WHERE schemaname = 'public';
3. Partial Indexes
Jika data punya distribusi yang sangat tidak seimbang, kita bisa bikin partial index → hanya untuk subset data tertentu.
Contoh:
CREATE INDEX idx_name ON t_test (name)
WHERE name NOT IN ('hans', 'paul');
Artinya indeks hanya mencakup baris di mana name ≠ 'hans' AND name ≠ 'paul'
.
Jika 80% data = "hans" atau "paul", maka indeks jadi sangat kecil → lebih hemat storage & lebih cepat.
Ukuran indeks bisa turun drastis:
Full index: puluhan MB
Partial index: hanya beberapa KB
👉 Cocok untuk kolom dengan nilai dominan, misalnya:
gender
(jika mayoritas male/female)status
(jika mayoritas "active")nationality
(jika mayoritas warga negara sama)
⚠️ Syarat: PostgreSQL hanya bisa gunakan partial index jika kondisi query persis sama dengan definisi indeks.
🔑 Ringkasan
Functional Index: mempercepat pencarian berbasis fungsi (harus immutable).
Indeks mahal dalam storage → bisa lebih besar dari tabel.
Partial Index: solusi hemat ruang & lebih cepat jika mayoritas data punya nilai dominan yang tidak perlu diindeks.
Adding Data While Indexing
Membuat indeks memang mudah, tapi ada konsekuensi locking yang perlu dipahami.
1. Normal CREATE INDEX
CREATE INDEX
Saat kita menjalankan:
CREATE INDEX idx_name2 ON t_test (name);
PostgreSQL akan memberikan SHARE lock pada tabel.
Artinya:
SELECT
masih bisa berjalan,tapi
INSERT
,UPDATE
, danDELETE
akan diblokir sampai indeks selesai dibuat.
Ini tidak masalah untuk tabel kecil, tapi untuk tabel besar (misalnya 1 TB) → bisa jadi downtime panjang di sistem produksi.
2. CREATE INDEX CONCURRENTLY
CREATE INDEX CONCURRENTLY
Untuk mengatasi masalah itu, PostgreSQL menyediakan mode concurrent:
CREATE INDEX CONCURRENTLY idx_name2 ON t_test (name);
Perbedaan penting:
Tabel tetap bisa digunakan normal (dapat
SELECT
,INSERT
,UPDATE
,DELETE
) selama proses pembuatan indeks.Proses pembuatan indeks berlangsung lebih lama (bisa 2x lipat atau lebih).
Karena berjalan paralel dengan transaksi lain, ada risiko gagal → indeks bisa ditandai sebagai invalid.
3. Mengecek Index Invalid
Kalau indeks gagal dibuat dengan CONCURRENTLY
, PostgreSQL akan tetap mencatatnya, tapi dengan status invalid.
Cara cek:
\d t_test
Indeks yang invalid akan muncul dengan tanda seperti:
"idx_name2" btree (name) INVALID
Atau dengan query:
SELECT indexrelid::regclass AS index_name, indisvalid
FROM pg_index
WHERE indrelid = 't_test'::regclass;
4. Menghapus Index Invalid
Jika ketemu indeks invalid, hapus lalu buat ulang:
DROP INDEX CONCURRENTLY idx_name2;
CREATE INDEX CONCURRENTLY idx_name2 ON t_test (name);
⚖️ Trade-offs
CREATE INDEX
✅ Lebih cepat
❌ Lock menulis (tidak cocok untuk tabel besar di production)
CREATE INDEX CONCURRENTLY
✅ Non-blocking, tetap bisa read/write
❌ Lebih lama, risiko gagal (invalid index)
👉 Jadi, untuk production dengan tabel besar, selalu gunakan CONCURRENTLY
.
Untuk dev/testing atau tabel kecil, cukup pakai CREATE INDEX
biasa.
Understanding PostgreSQL Index Types
Sejauh ini kita sudah banyak membahas B-tree, tapi PostgreSQL menyediakan beberapa jenis indeks lain, masing-masing dengan tujuan dan keunggulannya sendiri. Tidak semua tipe data cocok dengan B-tree, terutama jika data tidak bisa diurutkan secara alami (misalnya geometri, teks, atau range).
PostgreSQL (hingga versi 17) menyediakan 6 jenis indeks utama:
postgres=# SELECT * FROM pg_am;
oid | amname | amhandler | amtype
------+---------+---------------+-------
403 | btree | bthandler | i
405 | hash | hashhandler | i
783 | gist | gisthandler | i
2742 | gin | ginhandler | i
4000 | spgist | spghandler | i
3580 | brin | brinhandler | i
Selain itu, ada ekstensi tambahan seperti rum, vodka, cognac, dan bloom filter.
Mari kita bahas satu per satu:
1. B-tree
Default index di PostgreSQL.
Mendukung operator:
<
,<=
,=
,>=
,>
.Cocok untuk:
Pencarian exact match (
id = 100
)Pencarian range (
date BETWEEN '2025-01-01' AND '2025-02-01'
)Sorting (
ORDER BY
)
Tidak cocok untuk: full-text search, data geometri, data dengan banyak nilai duplikat.
2. Hash Indexes
Menyimpan hash dari nilai, bukan nilainya langsung.
Ideal untuk equality lookup (
WHERE col = 'value'
).Dulu (sebelum PostgreSQL 10) tidak WAL-logged, jadi tidak crash-safe → sekarang sudah aman.
Kekurangan:
Lebih besar di disk dibanding B-tree.
Tidak bisa dipakai untuk range query (
>
,<
).
Contoh:
CREATE INDEX idx_hash_name ON users USING hash(name);
3. GiST (Generalized Search Tree)
Paling fleksibel karena bisa di-customize dengan operator class.
Cocok untuk:
Range types (
int4range
,daterange
, dll.)Fuzzy searching (misalnya similarity search)
Geometri (PostGIS →
ST_Contains
,ST_Within
, dll.)
Bisa mendukung operator: overlap, contains, left-of, right-of, dsb.
Contoh:
CREATE INDEX idx_range ON sales USING gist(sale_period); CREATE INDEX idx_geom ON locations USING gist(geom);
4. GIN (Generalized Inverted Index)
Dirancang untuk full-text search dan array.
Menyimpan inverted map: setiap key disimpan sekali → punya daftar dokumen (posting list) tempat key muncul.
Sangat hemat ruang untuk teks besar.
Cocok untuk:
Full-text search (
to_tsvector
,to_tsquery
)JSONB (
jsonb_path_ops
)Array columns
Kekurangan: tidak bisa digunakan untuk sorting.
Contoh:
CREATE INDEX idx_doc_text ON documents USING gin(to_tsvector('english', content)); CREATE INDEX idx_json ON orders USING gin(details jsonb_path_ops);
5. SP-GiST (Space-Partitioned GiST)
Dirancang untuk struktur data hierarkis / partisi ruang.
Cocok untuk:
Quadtrees, k-d trees, radix trees (tries)
Biasanya dipakai untuk data in-memory atau data yang sangat terstruktur.
Contoh:
CREATE INDEX idx_ip ON users USING spgist(ip inet);
6. BRIN (Block Range Index)
Sangat kecil dibanding B-tree, karena hanya menyimpan min/max per blok (default 128 pages ≈ 1MB).
Cocok untuk data berurutan / terurut (correlated), misalnya:
Timestamp (
created_at
)ID autoincrement
Tidak efisien jika datanya acak.
Contoh:
CREATE INDEX idx_brin_created ON logs USING brin(created_at);
Ukuran bisa ribuan kali lebih kecil:
B-tree index: 96 MB
BRIN index : 48 kB
7. Bloom Filters (Extension)
Tersedia lewat
pg_trgm
/bloom
extension.Probabilistic index: tidak pernah false negative, tapi bisa false positive.
Cocok untuk mengindeks banyak kolom sekaligus.
Contoh:
CREATE EXTENSION bloom; CREATE INDEX idx_bloom ON big_table USING bloom(col1, col2, col3, col4);
📌 Ringkasan Kapan Memakai Index Type
B-tree
Equality, range, sorting
Full-text, JSONB, spatial
Hash
Equality only (=
)
Range queries
GiST
Range, fuzzy, spatial, PostGIS
Sorting biasa
GIN
Full-text search, JSONB, array
Range, sorting
SP-GiST
Trees (quad, k-d, radix), IP search
Generic workloads
BRIN
Large sequential data (timestamps, IDs)
Random/unordered data
Bloom
Many-column filtering
Large random workloads
⚡ Jadi, di PostgreSQL kita bisa mix index types sesuai kebutuhan query. Tidak ada “satu indeks terbaik untuk semua kasus” — justru kombinasi yang tepat bisa menyelamatkan kinerja query di production.
Last updated