TL;DR
Query SQL lambat biasanya gara-gara database harus baca seluruh isi tabel (full table scan) padahal cuma butuh sebagian kecil baris. Penyebab paling sering: kolom filter gak punya index, kolom di WHERE dibungkus fungsi sehingga index-nya mati, SELECT * yang narik semua kolom, dan JOIN tanpa kunci yang bener. Jalanin EXPLAIN dulu sebelum nebak-nebak.
Query SQL lambat hampir selalu punya sebab yang sama: database kepaksa baca seluruh isi tabel padahal kamu cuma butuh 200 baris.
Bukan gara-gara servernya lemot. Bukan gara-gara datanya kegedean. Data 50 juta baris tetap bisa dijawab dalam 2 detik kalau query-nya bener.
Ini 8 penyebab yang paling sering aku temuin, diurut dari yang paling sering, plus cara benerinnya.
Jangan nebak. Jalanin EXPLAIN dulu.
EXPLAIN nunjukin rencana eksekusi: langkah apa aja yang bakal ditempuh database buat jawab query kamu. Tabel mana yang dibaca, index mana yang dipakai, berapa baris yang diproses.
EXPLAIN ANALYZE
SELECT kota, SUM(total_harga) AS omzet
FROM transaksi
WHERE tanggal >= '2026-03-01'
GROUP BY kota;
Yang kamu cari di hasilnya: kata Seq Scan (di PostgreSQL) atau full table scan (di MySQL) di tabel yang gede.
Itu artinya database baca semua baris satu per satu. Kalau tabelnya 30 juta baris, ya lama.
Lawannya: Index Scan. Itu artinya database langsung loncat ke baris yang relevan.
Angka lain yang penting: rows (perkiraan baris yang diproses) dan actual time. Kalau perkiraan bilang 100 tapi aktualnya 2 juta, statistik tabelnya basi. Jalanin ANALYZE.
Penyebab nomor satu. Gak ada saingan.
Index adalah struktur bantu yang bikin database bisa nemu baris tanpa baca semuanya. Mirip daftar isi di buku.
Tanpa index di kolom tanggal, query ini baca semua 30 juta baris:
SELECT * FROM transaksi
WHERE tanggal >= '2026-03-01';
Benerinnya:
CREATE INDEX idx_transaksi_tanggal ON transaksi(tanggal);
Aturan praktis: kolom yang sering muncul di WHERE, JOIN, atau ORDER BY layak dikasih index.
Tapi jangan asal index semua kolom. Tiap index bikin operasi INSERT dan UPDATE jadi lebih lambat, soalnya index-nya harus ikut diperbarui.
Ini jebakan yang bikin banyak orang frustasi. Index udah dibikin, tapi query tetap lambat.
Penyebabnya: kolom filter dibungkus fungsi.
-- Index di kolom tanggal jadi gak kepakai
SELECT * FROM transaksi
WHERE YEAR(tanggal) = 2026;
Begitu tanggal dibungkus YEAR(), database gak bisa lagi pakai index-nya. Dia harus hitung YEAR() buat tiap baris dulu, baru bandingin.
Tulis ulang jadi rentang:
SELECT id_transaksi, kota, total_harga
FROM transaksi
WHERE tanggal >= '2026-01-01' AND tanggal < '2027-01-01';
Sekarang index-nya kepakai. Hasilnya sama persis, tapi kecepatannya beda jauh.
Pola yang sama berlaku buat UPPER(kota) = 'BANDUNG', SUBSTRING(kode, 1, 3) = 'JKT', dan semua fungsi lain yang nempel di kolom.
SELECT * maksa database baca dan kirim semua kolom, termasuk kolom teks panjang yang gak kamu lihat.
Efek yang lebih parah: dia matiin covering index.
Covering index itu index yang udah nyimpen semua kolom yang kamu minta. Database bisa jawab langsung dari index, gak perlu balik ke tabel aslinya.
Begitu kamu minta 20 kolom padahal index-nya nyimpen 3, database kepaksa balik ke tabel buat tiap baris. Lambat.
Sebut kolomnya. Ribet dikit di awal, hemat banyak di akhir.
JOIN antara tabel 100.000 baris dan tabel 50.000 baris, tanpa kondisi JOIN yang bener, ngasilin 5 miliar baris perantara.
Database bakal ngitungnya. Terus buang hampir semuanya. Ini yang namanya cartesian product.
-- Bahaya: kondisi JOIN-nya kelewat
SELECT t.id_transaksi, p.nama_produk
FROM transaksi t, produk p
WHERE t.total_harga > 100000;
Selalu tulis JOIN eksplisit dengan ON:
SELECT t.id_transaksi, p.nama_produk
FROM transaksi t
JOIN produk p ON t.id_produk = p.id_produk
WHERE t.total_harga > 100000;
Dan pastiin kolom kunci di kedua sisi punya index. Kolom id_produk di tabel transaksi sering kelupaan diindex, padahal dia yang dipakai buat nyocokin.
Kalau kamu masih ragu bedanya, baca dulu cara kerja JOIN.
Urutan eksekusi SQL: FROM → WHERE → GROUP BY → HAVING → SELECT.
WHERE nyaring baris sebelum agregasi. HAVING nyaring setelah agregasi.
-- Buruk: ngitung semua kota dulu, baru buang 11 dari 12
SELECT kota, SUM(total_harga) AS omzet
FROM transaksi
GROUP BY kota
HAVING kota = 'Bandung';
Query di atas ngitung SUM buat 12 kota, terus buang 11 hasilnya. Boros.
-- Bagus: saring dulu, baru agregasi
SELECT kota, SUM(total_harga) AS omzet
FROM transaksi
WHERE kota = 'Bandung'
GROUP BY kota;
Aturannya: pakai HAVING cuma kalau filternya nyentuh hasil agregasi, kayak HAVING SUM(total_harga) > 10000000. Filter yang lain taruh di WHERE.
Ini namanya correlated subquery. Subquery-nya nyebut kolom dari query luar, jadi dia kepaksa jalan ulang buat tiap baris.
-- Subquery jalan 30 juta kali
SELECT t.id_transaksi, t.total_harga,
(SELECT nama_kota FROM cabang c WHERE c.id_cabang = t.id_cabang) AS kota
FROM transaksi t;
Ganti pakai JOIN. Database bisa proses sekali jalan:
SELECT t.id_transaksi, t.total_harga, c.nama_kota AS kota
FROM transaksi t
JOIN cabang c ON c.id_cabang = t.id_cabang;
Optimizer modern kadang bisa nerjemahin subquery jadi JOIN sendiri. Tapi jangan diandelin. Tulis JOIN dari awal.
-- Index mati
SELECT * FROM produk WHERE nama_produk LIKE '%indomie%';
Wildcard % di depan bikin index gak kepakai. Index itu diurut dari huruf pertama. Kalau huruf pertamanya bebas, gak ada yang bisa dilompati.
-- Index kepakai
SELECT * FROM produk WHERE nama_produk LIKE 'indomie%';
Kalau kamu beneran butuh cari di tengah teks, pakai full-text search. Bukan LIKE.
Ngurutin 5 juta baris tanpa index = database bikin file sortir sementara di disk. Lambat banget.
Kalau kamu sering nulis ORDER BY tanggal DESC LIMIT 100, kasih index ke kolom tanggal. Database bisa langsung ambil 100 baris teratas dari index tanpa nyortir apa-apa.
Ini kejadian nyata di dataset toko_berkah, tabel transaksi 4,2 juta baris dari 12 cabang.
Query awalnya begini, dipakai buat laporan bulanan:
SELECT *
FROM transaksi t
WHERE YEAR(t.tanggal) = 2026
AND MONTH(t.tanggal) = 3
AND UPPER(t.kota) = 'BANDUNG';
Waktu jalan: 4 menit 12 detik.
Hasil EXPLAIN: Seq Scan di tabel transaksi. 4,2 juta baris dibaca semua. Index di kolom tanggal ada, tapi mati gara-gara dibungkus YEAR() dan MONTH().
Perbaikannya cuma 2 hal:
SELECT t.id_transaksi, t.tanggal, t.kota, t.total_harga
FROM transaksi t
WHERE t.tanggal >= '2026-03-01'
AND t.tanggal < '2026-04-01'
AND t.kota = 'Bandung';
Waktu jalan: 3,1 detik. Dari 252 detik jadi 3 detik. 80 kali lebih cepat.
Baris yang dibaca database turun dari 4.200.000 jadi 31.480.
Gak ada server baru. Gak ada tuning database. Cuma nulis ulang WHERE-nya.
| Langkah | Yang dicek | Tanda masalah |
|---|---|---|
| 1 | Jalanin EXPLAIN ANALYZE | Ada Seq Scan di tabel gede |
| 2 | Lihat klausa WHERE | Ada fungsi nempel di kolom |
| 3 | Cek index kolom filter | Kolom filter gak punya index |
| 4 | Cek kondisi JOIN | Kolom kunci gak diindex |
| 5 | Cek daftar kolom SELECT | Masih pakai SELECT * |
| 6 | Cek HAVING | Ada filter yang gak nyentuh agregasi |
Enam langkah ini nutup sekitar 90% kasus query lambat yang pernah aku pegang.
Jalanin EXPLAIN atau EXPLAIN ANALYZE di depan query kamu. Perintah itu nunjukin rencana eksekusi database: tabel mana yang dibaca, index mana yang kepakai, dan berapa baris yang diperkirakan diproses. Yang kamu cari kata Seq Scan atau full table scan di tabel gede. Itu tanda database baca semua baris padahal cuma butuh sebagian.
Iya, terutama di tabel yang punya banyak kolom atau kolom teks panjang. SELECT * maksa database baca dan kirim semua kolom, termasuk yang gak kamu pakai. Efek lain yang lebih parah: dia matiin covering index. Kalau index kamu cuma nyimpen 3 kolom dan kamu minta 20 kolom, database tetap harus balik ke tabel aslinya.
Biasanya index-nya ada tapi gak kepakai. Penyebab paling sering: kolom filter dibungkus fungsi, kayak WHERE YEAR(tanggal) = 2026 atau WHERE UPPER(kota) = 'BANDUNG'. Begitu kolom dibungkus fungsi, database gak bisa pakai index-nya. Tulis ulang filternya jadi rentang: WHERE tanggal >= '2026-01-01' AND tanggal < '2027-01-01'.
WHERE nyaring baris sebelum agregasi, HAVING nyaring hasil setelah agregasi. Kalau kamu naruh filter yang gak butuh hasil agregasi di HAVING, database bakal ngitung semua grup dulu baru buang sebagian besar. Buang-buang kerja. Taruh filter di WHERE selama filter itu gak nyentuh hasil SUM, COUNT, atau AVG.
Hampir selalu bisa. Data 50 juta baris tetap bisa dijawab dalam hitungan detik kalau index-nya bener dan filternya nyaring lebih dulu. Yang bikin lambat bukan ukuran tabel, tapi berapa baris yang kepaksa dibaca database. Kalau tetap berat setelah index dibenerin, pertimbangkan tabel ringkasan yang di-refresh tiap malam.
Dua hal yang paling sering nolong:
Ambil query paling lambat di kerjaan kamu sekarang, tempelin EXPLAIN ANALYZE di depannya, dan lihat ada Seq Scan atau nggak.
Lanjut ke panduan JOIN lengkap kalau query kamu banyak gabung tabel, atau pahami dulu apa itu index database dan cara kerja GROUP BY.
Buat detail cara baca hasil EXPLAIN, dokumentasi PostgreSQL soal EXPLAIN ini yang paling jelas penjelasannya.
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.