Query yang mulus pada sembilan baris latihan bisa memakan waktu menit pada tabel produksi berisi jutaan baris. Perbedaannya bukan kebenaran query, melainkan cara database mengeksekusinya: apakah engine membaca seluruh tabel, atau melompat langsung ke baris yang dibutuhkan. Kuncinya adalah membaca execution plan — peta rencana eksekusi yang disusun database sebelum query berjalan. Chapter penutup ini berlatih membaca plan di SQLite, membangun index pertama, dan menulis WHERE yang tidak meniadakan index — semua dengan hasil eksekusi nyata.
Membaca Plan dengan EXPLAIN QUERY PLAN #
Prefiks EXPLAIN QUERY PLAN tidak menjalankan query — database hanya menampilkan rencana eksekusinya. Dua kata kunci penting pada hasilnya: SCAN berarti engine membaca seluruh tabel baris demi baris, sedangkan SEARCH berarti engine melompat ke baris tertentu dengan bantuan index. Karena kita akan sering membandingkan plan, berikut fungsinya:
from setup_toko import buat_database
def tampilkan_plan(koneksi, query):
for baris in koneksi.execute("EXPLAIN QUERY PLAN " + query):
print(baris[-1])
koneksi = buat_database()
tampilkan_plan(koneksi, "SELECT * FROM pesanan WHERE pelanggan_id = 2")Output yang Dihasilkan:
SCAN pesananTabel pesanan belum memiliki index pada pelanggan_id, jadi meski hanya dua baris memenuhi kondisi, engine memeriksa kesembilan barisnya. Pada sembilan baris ini tidak terasa; pada sembilan juta baris, SCAN berarti sembilan juta pemeriksaan untuk hasil yang mungkin hanya ratusan baris.
CREATE INDEX: Dari SCAN ke SEARCH #
Index adalah struktur terurut yang memetakan nilai kolom ke lokasi barisnya — seperti indeks buku memetakan kata ke halaman. Index pada kolom yang sering difilter mengubah rencana eksekusi:
CREATE INDEX idx_pesanan_pelanggan ON pesanan(pelanggan_id);Setelah index dibuat, plan query yang sama dibaca kembali:
koneksi.execute("CREATE INDEX idx_pesanan_pelanggan ON pesanan(pelanggan_id)")
tampilkan_plan(koneksi, "SELECT * FROM pesanan WHERE pelanggan_id = 2")Output yang Dihasilkan:
SEARCH pesanan USING INDEX idx_pesanan_pelanggan (pelanggan_id=?)Rencana berubah dari SCAN menjadi SEARCH ... USING INDEX idx_pesanan_pelanggan (pelanggan_id=?) — engine melompat langsung ke entri pelanggan_id = 2 di dalam index; tanda tanya menandai posisi nilai yang diisi saat eksekusi.
Mengapa SELECT * Merugikan #
Tiga alasan menjadikan SELECT * kebiasaan yang buruk pada pipeline produksi:
- Transfer berlebih: query menarik semua kolom meski transformasi hanya butuh tiga — boros jaringan dan memori pada tabel lebar.
- Kontrak rapuh: pipeline berhenti benar atau berubah diam-diam ketika kolom sumber bertambah, berkurang, atau berganti urutan.
- Peluang index tertutup: kolom eksplisit memungkinkan covering index — index yang sudah memuat semua kolom yang dibutuhkan sehingga tabel tidak perlu disentuh.
Bentuk penggantinya sederhana: sebutkan kolom yang dipakai downstream.
SELECT id, tanggal, status
FROM pesanan
WHERE pelanggan_id = 2;Output yang Dihasilkan:
id | tanggal | status
2 | 2024-06-03 | selesai
6 | 2024-07-01 | diprosesWHERE yang Ramah Index #
Index hanya terpakai bila kondisi WHERE cocok langsung dengan isi index. Menyelubungi kolom dengan fungsi memaksa engine menghitung fungsi itu untuk tiap baris — index menjadi tidak terpakai. Bandingkan dua query pencari pesanan Juni pada kolom tanggal yang sudah terindeks:
koneksi = buat_database()
koneksi.execute("CREATE INDEX idx_pesanan_tanggal ON pesanan(tanggal)")
tampilkan_plan(koneksi, "SELECT status FROM pesanan WHERE DATE(tanggal) = '2024-06-01'")Output yang Dihasilkan:
SCAN pesanantampilkan_plan(koneksi, """
SELECT status FROM pesanan
WHERE tanggal >= '2024-06-01' AND tanggal < '2024-07-01'
""")Output yang Dihasilkan:
SEARCH pesanan USING INDEX idx_pesanan_tanggal (tanggal>? AND tanggal<?)Query pertama memakai DATE(tanggal), sehingga engine menghitung fungsi itu pada tiap baris — plan kembali SCAN. Query kedua membandingkan kolom langsung dengan rentang nilai, dan engine memakai urutan index untuk melompat ke rentang tersebut. Prinsip ini disebut sargable (Search ARGument ABLE): letakkan fungsi pada nilai pembanding, bukan pada kolom yang diindeks — tanggal >= '2024-06-01' alih-alih strftime('%Y', tanggal) = '2024'.
LIMIT dan Pola Top-N #
LIMIT membatasi jumlah baris hasil dan paling pas dipasangkan ORDER BY untuk pola “N teratas”:
SELECT id, pelanggan_id, tanggal, status
FROM pesanan
ORDER BY tanggal DESC
LIMIT 3;Output yang Dihasilkan:
id | pelanggan_id | tanggal | status
9 | 1 | 2024-07-20 | selesai
8 | 3 | 2024-07-12 | diproses
7 | 5 | 2024-07-05 | selesaiTiga pesanan terbaru tampil tanpa memuat seluruh tabel. Namun LIMIT bukan obat query lambat — pada pengujian nyata, ORDER BY tanggal DESC LIMIT 3 tanpa index tetap memicu SCAN plus pengurutan sementara; dengan index tanggal, engine membaca index terurut dan berhenti setelah tiga baris tanpa pengurutan. Kombinasi index + ORDER BY + LIMIT membuat laporan top-N cepat pada tabel besar; rujukan lengkapnya ada di dokumentasi resmi SQLite
.
Section Dasar SQL tertutup dengan perangkat lengkap: membaca data, membentuk hasil (CTE, window function, ekspresi kondisional), menulis data beserta transaksinya, dan menjaga performa (index, plan, WHERE ramah index). SQL hanyalah bahasanya — rumahnya adalah sistem penyimpanan itu sendiri. Section berikutnya, Database dan Penyimpanan Data, membedah database relasional lebih dalam: pemodelan data, normalisasi, hingga pemilihan sistem penyimpanan untuk tiap kebutuhan pipeline.