TL;DR
Optimasi query SQL yang paling besar dampaknya biasanya cuma tiga hal: kasih index di kolom yang sering kamu filter, hindari SELECT bintang, dan filter data sebelum JOIN, bukan sesudah. Sebelum ngubah apa pun, jalanin EXPLAIN ANALYZE dulu buat lihat di mana waktunya kebuang. Ngubah query berdasarkan tebakan sering bikin lebih lambat, bukan lebih cepat.
Optimasi query SQL yang paling besar dampaknya biasanya cuma tiga hal: index di kolom yang sering difilter, berhenti pakai SELECT *, dan filter data sebelum JOIN.
Sisanya pelengkap. Tapi pelengkap yang kadang nyelametin kamu waktu tiga teknik pertama udah kepakai dan query-nya masih lambat.
Satu aturan sebelum mulai: jangan ubah apa pun sebelum kamu jalanin EXPLAIN ANALYZE. Ngoptimasi berdasarkan tebakan sering bikin lebih lambat, bukan lebih cepat.
SELECT * narik semua kolom, termasuk kolom deskripsi yang panjangnya 2 KB dan gak kamu pakai.
-- Lambat
SELECT * FROM transaksi WHERE tanggal >= '2026-01-01';
-- Cepat
SELECT id, kota, total FROM transaksi WHERE tanggal >= '2026-01-01';
Efeknya dua lapis. Pertama, data yang dibaca dari disk lebih sedikit. Kedua, dan ini yang lebih penting: SELECT * bikin Index Only Scan gak mungkin kepakai. Database terpaksa balik ke tabel buat ngambil kolom yang gak ada di index.
Di tabel transaksi toko_berkah yang punya 14 kolom, ganti dari SELECT * jadi 3 kolom bikin query turun dari 2,1 detik ke 780 ms. Cuma dari ini.
Ini teknik dengan rasio dampak-per-usaha paling tinggi. Satu baris SQL, efeknya bisa ratusan kali lipat.
CREATE INDEX idx_transaksi_tanggal ON transaksi (tanggal);
Kolom yang layak dikasih index: yang muncul di WHERE, JOIN, atau ORDER BY, dan sering dipakai.
Buat query yang sering nyaring dua kolom sekaligus, bikin composite index:
CREATE INDEX idx_transaksi_kota_tanggal ON transaksi (kota, tanggal);
Urutan kolom di composite index itu penting. Index (kota, tanggal) kepakai buat filter kota doang, dan buat filter kota + tanggal. Tapi gak kepakai buat filter tanggal doang.
Aturannya: kolom yang paling sering difilter, taruh di depan.
Ini pembunuh index yang paling senyap.
-- Index mati
SELECT id, total FROM transaksi WHERE YEAR(tanggal) = 2026;
-- Index hidup
SELECT id, total FROM transaksi
WHERE tanggal >= '2026-01-01' AND tanggal < '2027-01-01';
Kenapa? Index nyimpen nilai tanggal, bukan nilai YEAR(tanggal). Begitu kamu bungkus kolomnya dengan fungsi, database gak bisa lagi nyari lewat index. Dia harus hitung fungsi itu di tiap baris.
Sama juga buat teks:
-- Index mati
WHERE UPPER(kota) = 'JAKARTA'
-- Index hidup
WHERE kota = 'Jakarta'
Kalau kamu emang butuh pencarian tanpa peduli huruf besar-kecil, bikin index-nya di ekspresi itu:
CREATE INDEX idx_kota_upper ON transaksi (UPPER(kota));
Kalau kamu JOIN dua tabel besar lalu baru filter, database sering ngerjain gabungan yang jauh lebih besar dari yang kamu butuh.
WITH transaksi_2026 AS (
SELECT produk_id, kota, total
FROM transaksi
WHERE tanggal >= '2026-01-01'
)
SELECT p.kategori, t.kota, SUM(t.total) AS omzet
FROM transaksi_2026 t
JOIN produk p ON p.id = t.produk_id
GROUP BY p.kategori, t.kota;
Query planner yang bagus sering udah ngelakuin ini sendiri. Tapi "sering" bukan "selalu". Apalagi kalau statistik tabelnya basi.
Nulis eksplisit kayak gini juga bikin query kamu lebih gampang dibaca orang lain. Itu bonus yang gak keitung di benchmark.
Correlated subquery itu subquery yang jalan sekali per baris hasil. Kalau hasilnya 100 ribu baris, subquery itu jalan 100 ribu kali.
-- Lambat: subquery jalan tiap baris
SELECT
kota,
total,
(SELECT AVG(total) FROM transaksi t2 WHERE t2.kota = t1.kota) AS rata_kota
FROM transaksi t1
WHERE tanggal >= '2026-01-01';
-- Cepat: sekali baca, semua dihitung
SELECT
kota,
total,
AVG(total) OVER (PARTITION BY kota) AS rata_kota
FROM transaksi
WHERE tanggal >= '2026-01-01';
Di data toko_berkah dengan 2,4 juta baris, versi pertama makan 38 detik. Versi window function: 1,2 detik.
Kalau kamu belum familiar sama window function, mulai dari glosarium window function.
-- Lambat kalau subquery-nya ngembaliin jutaan baris
SELECT id, nama FROM produk
WHERE id IN (SELECT produk_id FROM transaksi);
-- Cepat: berhenti begitu ketemu satu
SELECT p.id, p.nama
FROM produk p
WHERE EXISTS (
SELECT 1 FROM transaksi t WHERE t.produk_id = p.id
);
EXISTS berhenti nyari begitu nemu satu baris yang cocok. IN sering ngumpulin dulu semua hasil subquery-nya.
Bonus: kalau kolom di subquery bisa berisi NULL, NOT IN bakal ngasih hasil kosong yang bikin kamu bingung berjam-jam. NOT EXISTS gak punya masalah itu.
Waktu kamu lagi ngintip data buat pertama kali, kamu gak butuh 2 juta baris.
SELECT id, kota, total FROM transaksi LIMIT 100;
Kedengeran sepele, tapi ini nyelametin kamu dari nge-hang bikin editor SQL kamu, dan nyelametin database production dari beban yang gak perlu.
Hati-hati satu hal: LIMIT tanpa ORDER BY gak ngasih jaminan baris mana yang keluar. Buat eksplorasi, gak masalah. Buat laporan, selalu pasangin sama ORDER BY.
Sorting itu mahal. Kalau data yang di-sort gak muat di memori, database numpahin ke disk, dan itu lambat banget.
Tandanya kelihatan di query plan:
Sort Method: external merge Disk: 84200kB
Kata Disk di situ artinya sorting kamu tumpah. Dua cara benerinnya:
ORDER BY, biar data yang di-sort lebih sedikit.ORDER BY, biar database bisa baca data yang udah terurut dan skip sorting-nya sama sekali.Banyak orang nambahin DISTINCT waktu hasil query-nya ada duplikat. Padahal duplikat itu biasanya gejala dari JOIN yang salah.
-- Nambal gejala
SELECT DISTINCT p.nama FROM produk p
JOIN transaksi t ON t.produk_id = p.id;
-- Benerin akarnya
SELECT p.nama FROM produk p
WHERE EXISTS (SELECT 1 FROM transaksi t WHERE t.produk_id = p.id);
DISTINCT maksa database ngurutin atau bikin hash dari seluruh hasil buat buang duplikat. Itu kerjaan mahal yang sebenernya bisa dihindari.
Kalau kamu nemu DISTINCT di query kamu, tanya dulu: kenapa ada duplikat? Biasanya jawabannya ada di JOIN yang kena baris ganda.
Query yang sama dijalanin 200 orang tiap pagi, hasilnya gak harus real-time, dan hitungnya mahal? Simpan hasilnya.
CREATE MATERIALIZED VIEW omzet_bulanan AS
SELECT
DATE_TRUNC('month', tanggal) AS bulan,
kota,
SUM(total) AS omzet
FROM transaksi
GROUP BY 1, 2;
Lalu refresh sekali sehari:
REFRESH MATERIALIZED VIEW omzet_bulanan;
200 orang itu sekarang baca hasil yang udah jadi. Query yang tadinya 12 detik jadi 30 ms.
Jangan pakai ini buat data yang harus akurat detik itu juga: stok barang, saldo, atau apa pun yang orang bakal ambil keputusan langsung dari situ.
Ini query laporan bulanan toko_berkah yang aslinya begini:
SELECT *
FROM transaksi t
JOIN produk p ON p.id = t.produk_id
JOIN kategori k ON k.id = p.kategori_id
WHERE YEAR(t.tanggal) = 2026
AND UPPER(t.kota) = 'JAKARTA'
ORDER BY t.total DESC;
Waktu eksekusi: 41,3 detik di tabel 2,4 juta baris.
EXPLAIN ANALYZE nunjukin Seq Scan di transaksi, dengan 2,3 juta baris dibuang oleh filter.
Perbaikannya, satu per satu:
SELECT * jadi 5 kolom yang beneran dipakai. → 28,7 detikYEAR(tanggal) jadi range tanggal. → 26,1 detik (belum ada index, jadi belum kerasa)UPPER(kota) jadi kota = 'Jakarta'. → 25,8 detik(kota, tanggal). → 0,89 detikproduk_id buat JOIN-nya. → 0,21 detikTotal: dari 41,3 detik jadi 210 ms. 197 kali lebih cepat.
Perhatiin langkah 4. Itu yang paling besar dampaknya. Turun dari 25,8 detik ke 0,89 detik dalam satu langkah.
Tapi index itu cuma kepakai karena langkah 2 dan 3 udah ngelepas fungsi dari kolom WHERE. Kalau YEAR(tanggal) masih ada, index-nya gak bakal kesentuh sama sekali.
Itu poinnya. Teknik-teknik ini saling numpuk.
Kamu ubah 4 hal, query jadi cepat, tapi kamu gak tau yang mana yang berhasil. Ubah satu, ukur, ubah lagi. Lebih lama sedikit, tapi kamu jadi ngerti.
Index bikin SELECT cepat, tapi INSERT dan UPDATE lambat. Tiap tulis data, semua index-nya ikut diperbarui. Di tabel yang sering ditulis, kebanyakan index bisa lebih merugikan dari yang kamu kira.
Query plan di tabel 1.000 baris beda total sama di 2 juta baris. Database bakal pilih Seq Scan di tabel kecil dan kamu gak lihat masalahnya sama sekali.
Habis import jutaan baris, statistik tabel kamu masih ngira tabelnya kecil. Query planner bakal salah pilih rencana.
ANALYZE transaksi;
Query kedua sering lebih cepat dari yang pertama gara-gara data udah masuk cache. Jalanin 3 kali, ambil yang tengah.
EXPLAIN ANALYZE. Cari langkah dengan actual time paling besar.Seq Scan di tabel besar dengan Rows Removed by Filter yang gede? Kolom itu butuh index.WHERE? Lepas dulu, baru index-nya kepakai.SELECT * jadi kolom yang beneran dipakai.Referensi lengkap soal cara kerja query planner ada di dokumentasi resmi PostgreSQL.
Biasanya ada fungsi di kolom WHERE yang matiin index, atau ada correlated subquery yang jalan berulang tiap baris. Jalanin EXPLAIN ANALYZE dan cari langkah paling lama.
Iya. Selain narik kolom yang gak kepakai, dia bikin Index Only Scan gak mungkin dipakai.
Cuma di kolom yang sering muncul di WHERE, JOIN, atau ORDER BY. Tiap index bikin tulis data jadi lebih lambat.
Mirip, kecuali correlated subquery, itu yang beneran lambat. Ganti pakai JOIN atau window function.
Waktu query yang sama dipakai berulang, mahal, dan hasilnya gak harus real-time.
Tiga hal yang paling sering nyelesaiin masalah:
WHERE: kalau gak, index kamu gak kepakai.EXPLAIN ANALYZE sebelum ngubah apa pun: biar kamu gak ngoptimasi bagian yang salah.Ambil query paling lambat di kerjaan kamu minggu ini. Jalanin EXPLAIN ANALYZE, cari Seq Scan-nya, dan bikin satu index. Sepuluh menit, dan kemungkinan besar query itu jadi puluhan kali lebih cepat.
Mau latihan nulis query dan langsung lihat waktu eksekusinya? Coba NgulikSQL, gratis, langsung di browser, gak perlu install database.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.