โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 4 ยท Database & Persistensi

Index, EXPLAIN & kueri yang lambat

Sebagian besar masalah performa aplikasi web bukan di kode Go, melainkan di satu kueri yang tidak memakai index. Materi ini tentang membuktikannya, bukan menebaknya.

Intisari

  • EXPLAIN (ANALYZE, BUFFERS) menjalankan kueri sungguhan dan menunjukkan waktu aktual, bukan perkiraan.
  • Seq Scan pada tabel besar hampir selalu index yang hilang; pada tabel kecil ia justru wajar.
  • Pada index gabungan, urutan kolom menentukan apakah ia terpakai sama sekali.
  • Fungsi pada kolom (lower(nama) = ...) mematikan index โ€” kecuali kamu membuat index ekspresi.
  • Paginasi OFFSET makin lambat makin dalam; keyset pagination berbiaya tetap.

Membaca rencana kueri

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, nama, harga FROM produk
WHERE kategori_id = 3 AND harga < 50000
ORDER BY harga LIMIT 20;
Limit  (cost=0.43..8.91 rows=20 width=32)
       (actual time=0.031..0.089 rows=20 loops=1)
  Buffers: shared hit=23
  ->  Index Scan using idx_produk_kategori_harga on produk
        (cost=0.43..421.55 rows=994 width=32)
        (actual time=0.029..0.082 rows=20 loops=1)
        Index Cond: ((kategori_id = 3) AND (harga < 50000))
Planning Time: 0.187 ms
Execution Time: 0.118 ms
Yang dibacaArtinya
actual timeWaktu sungguhan. cost cuma perkiraan relatif โ€” abaikan angkanya
rows perkiraan vs aktualSelisih besar = statistik basi. Jalankan ANALYZE produk
Buffers: shared hitBlok dari cache. read berarti dari disk โ€” jauh lebih mahal
loops=1Kalau > 1, waktunya dikali jumlah itu
Index Cond vs FilterFilter berarti baris dibaca dulu lalu dibuang โ€” index tidak menyaringnya

Jenis pemindaian

NodeArtinyaBermasalah kalau
Seq ScanBaca seluruh tabelTabelnya besar dan ada WHERE selektif
Index ScanLewat index, lalu ambil barisnyaBiasanya baik
Index Only ScanCukup dari index sajaTerbaik
Bitmap Heap ScanBanyak baris cocok; dikumpulkan duluWajar untuk hasil menengah
Nested LoopUntuk tiap baris kiri, cari di kananBuruk kalau sisi kiri banyak baris
Hash JoinBangun tabel hash lalu cocokkanBaik untuk join besar
Sort dengan external mergePengurutan tumpah ke diskNaikkan work_mem, atau beri index yang sudah urut

Urutan kolom pada index gabungan

CREATE INDEX idx_produk_kategori_harga ON produk (kategori_id, harga);
KueriIndex terpakai?
WHERE kategori_id = 3โœ… ya
WHERE kategori_id = 3 AND harga < 50000โœ… ya, keduanya
WHERE kategori_id = 3 ORDER BY hargaโœ… ya, tanpa langkah sort
WHERE harga < 50000โŒ tidak โ€” kolom pertama dilewati
ORDER BY harga sajaโŒ tidak

Aturannya: kolom yang dibandingkan dengan = lebih dulu, lalu rentang, lalu yang dipakai ORDER BY. Index gabungan hanya bisa dipakai dari kiri โ€” persis seperti mencari nama di buku telepon yang diurutkan nama belakang lalu nama depan: tanpa nama belakang, urutannya tidak membantu sama sekali.

Yang mematikan index

-- โŒ fungsi pada kolom: index pada nama tidak terpakai
WHERE lower(nama) = 'kopi'
-- โœ… buat index ekspresinya
CREATE INDEX idx_produk_nama_lower ON produk (lower(nama));

-- โŒ wildcard di depan: tidak bisa dicari lewat B-tree
WHERE nama LIKE '%kopi%'
-- โœ… trigram, atau pencarian teks penuh
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_produk_nama_trgm ON produk USING gin (nama gin_trgm_ops);

-- โŒ tipe tidak cocok memaksa konversi
WHERE id = '42'
-- โœ… kirim tipe yang benar dari Go (int64, bukan string)

-- โŒ OR antar kolom berbeda sering menggagalkan index
WHERE nama = $1 OR email = $1
-- โœ… dua kueri digabung UNION, atau index terpisah + bitmap OR

Index yang biasanya dibutuhkan

PolaIndex
Foreign keySelalu โ€” Postgres tidak membuatnya otomatis
Kolom di WHERE yang selektifB-tree biasa
Filter + urutan bersamaGabungan, urutan sesuai aturan di atas
Soft deleteIndex parsial: WHERE dihapus_pada IS NULL
Kolom jsonbGIN
Pencarian teksGIN dengan tsvector atau trigram
Multi-tenanttenant_id sebagai kolom pertama di hampir semua index
-- Index parsial: jauh lebih kecil, dan cocok persis dengan kueri aplikasi
CREATE INDEX idx_produk_aktif ON produk (kategori_id, harga)
WHERE dihapus_pada IS NULL;

Index bukan gratis. Setiap index memperlambat INSERT, UPDATE, dan DELETE, serta memakan disk dan memori cache. Tabel dengan 15 index yang setengahnya tidak pernah dipakai adalah pola yang umum โ€” dan ia membuat semua tulisan lebih lambat tanpa mempercepat satu bacaan pun.

-- Index yang tidak pernah dipakai sejak statistik terakhir direset
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

Paginasi: OFFSET versus keyset

-- โŒ OFFSET 100000 berarti database membaca lalu MEMBUANG 100.000 baris
SELECT * FROM produk ORDER BY id LIMIT 20 OFFSET 100000;

-- โœ… keyset: biaya tetap, sedalam apa pun halamannya
SELECT * FROM produk WHERE id > $1 ORDER BY id LIMIT 20;
OFFSETKeyset
Biaya di halaman jauhNaik terusTetap
Lompat ke halaman 500BisaTidak
Tahan data baru masukTidak โ€” baris bisa terlihat dua kaliYa
Cocok untukTabel admin kecilAPI, umpan tak terbatas

Menemukan kueri lambat di produksi

-- Aktifkan pg_stat_statements (RDS: lewat parameter group)
SELECT
    calls,
    round(mean_exec_time::numeric, 2) AS rata_ms,
    round(total_exec_time::numeric, 2) AS total_ms,
    rows,
    query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

Urutkan berdasarkan total_exec_time, bukan mean_exec_time. Kueri 2 ms yang dipanggil sejuta kali membebani database jauh lebih besar daripada laporan 5 detik yang dijalankan sekali sehari โ€” dan yang pertama itulah yang biasanya bisa diperbaiki dengan satu index atau satu cache. Di AWS, Performance Insights menampilkan hal yang sama secara grafis (Fase 10).

Latihan: isi tabel produk dengan satu juta baris (INSERT ... SELECT generate_series), jalankan kueri filter + urut dan catat Execution Time-nya. Tambahkan index gabungan dengan urutan yang benar dan ulangi. Lalu buat index dengan urutan kolom terbalik dan buktikan ia tidak terpakai โ€” jalankan EXPLAIN untuk ketiganya dan bandingkan.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.