Membuat index hanyalah separuh pekerjaan — separuh sisanya adalah memastikan index itu benar-benar dipakai. Optimizer MySQL memilih jalur eksekusi sendiri setiap kali query berjalan: kadang index dilibatkan penuh, kadang sebagian, kadang diabaikan karena memang lebih murah membaca tabelnya. Kita sebagai perancang tidak menebak-nebak keputusan itu — kita membacanya lewat EXPLAIN, perintah yang menampilkan rencana eksekusi query tanpa menjalankannya. Pada chapter ini kita memetakan pola-pola sehat yang paling sering muncul: lookup equality, range scan, ORDER BY yang dilayani index, dan GROUP BY yang berjalan lewat index terurut. Pola-pola yang justru mematikan index menjadi materi chapter berikutnya.
Membaca EXPLAIN Sekilas #
EXPLAIN diletakkan di depan statement SELECT; outputnya satu tabel kecil berisi rencana per tabel yang disentuh query. Untuk kebutuhan section ini, lima kolom sudah menutup sebagian besar pembacaan:
| Kolom | Arti singkat |
|---|---|
type | Cara tabel diakses — dari terbaik ke terburuk: const, ref, range, index, ALL |
possible_keys | Index mana saja yang layak dipakai |
key | Index yang benar-benar dipilih — NULL berarti index tidak terpakai |
rows | Perkiraan jumlah baris yang akan dibaca |
Extra | Catatan tambahan — Using where, Using index, Using filesort, Using index condition |
Tiga tipe akses yang akan sering kita temui di chapter ini: ref berarti index dipakai untuk mencari nilai yang cocok (biasanya satu hingga beberapa baris), range berarti index dipakai untuk membaca rentang nilai, dan ALL berarti full table scan. Detail mendalam tiap kolom dan tipe akses — lengkap dengan EXPLAIN ANALYZE — menjadi materi khusus di section query-optimization; panduan singkat ini sudah lebih dari cukup untuk membaca semua eksperimen kita.
Tabel latihan kita kembali berperan, kali ini dengan empat index yang mewakili empat pola penggunaan berbeda:
DROP DATABASE IF EXISTS belajar_index;
CREATE DATABASE belajar_index;
USE belajar_index;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE karyawan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_depan VARCHAR(50) NOT NULL,
nama_belakang VARCHAR(50) NOT NULL,
email VARCHAR(120) NOT NULL,
kota VARCHAR(40) NOT NULL,
gaji DECIMAL(10,2) NOT NULL,
tanggal_masuk DATE NOT NULL
);
INSERT INTO karyawan (nama_depan, nama_belakang, email, kota, gaji, tanggal_masuk) VALUES
('Budi', 'Santoso', '[email protected]', 'Jakarta', 8500000.00, '2026-01-05'),
('Siti', 'Rahayu', '[email protected]', 'Bandung', 7200000.00, '2026-01-12'),
('Andi', 'Wijaya', '[email protected]', 'Surabaya', 9100000.00, '2026-02-01'),
('Rina', 'Kartika', '[email protected]', 'Jakarta', 6800000.00, '2026-02-10'),
('Dewi', 'Lestari', '[email protected]', 'Yogyakarta', 7500000.00, '2026-03-03'),
('Tono', 'Sutrisno', '[email protected]', 'Bandung', 6000000.00, '2026-03-15'),
('Sari', 'Wulandari','[email protected]','Medan', 7300000.00, '2026-04-01'),
('Joko', 'Pratama', '[email protected]', 'Jakarta', 8900000.00, '2026-04-20'),
('Nurul', 'Hidayah', '[email protected]', 'Surabaya', 7000000.00, '2026-05-05'),
('Rizky', 'Maulana', '[email protected]', 'Medan', 7700000.00, '2026-05-18');
CREATE INDEX idx_kota ON karyawan(kota);
CREATE INDEX idx_gaji ON karyawan(gaji);
CREATE INDEX idx_nama ON karyawan(nama_depan, nama_belakang);
CREATE INDEX idx_tanggal ON karyawan(tanggal_masuk);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 10 rows affected (0.00 sec)
Records: 10 Duplicates: 0 Warnings: 0
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0Equality Lookup: ref dan const #
Pola paling dasar: mencocokkan nilai tepat lewat =. Index single kolom melayaninya dengan tipe akses ref:
EXPLAIN SELECT * FROM karyawan WHERE kota = 'Jakarta';Output yang Dihasilkan:
+----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------+
| 1 | SIMPLE | karyawan | NULL | ref | idx_kota | idx_kota | 162 | const | 3 | 100.00 | NULL |
+----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)Kolom possible_keys dan key sama-sama idx_kota — index tersedia dan memang dipilih. type = ref dengan ref = const berarti server melompat ke posisi nilai 'Jakarta' di index lalu membaca 3 baris (perkiraan rows) yang terkumpul di sana. Bandingkan dengan lookup pada kunci unik yang kita lihat di chapter UNIQUE INDEX: tipe aksesnya naik kelas menjadi const karena optimizer tahu hasilnya pasti maksimal satu baris. Pada tabel kecil, rows = 3 tidak tampak istimewa; pada tabel sejuta baris dengan 300 ribu baris Jakarta, pola yang sama memangkas pembacaan menjadi sepertiga saja.
Range Scan: BETWEEN, Perbandingan, dan LIKE Berawalan #
Rentang nilai adalah keluarga pola kedua. Index B-tree yang terurut membuat rentang bisa dilayani dengan menemukan titik awal lalu membaca searah — dua pernyataan range yang paling umum:
EXPLAIN SELECT * FROM karyawan WHERE tanggal_masuk BETWEEN '2026-01-01' AND '2026-03-31';Output yang Dihasilkan:
+----+-------------+----------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | karyawan | NULL | range | idx_tanggal | idx_tanggal | 3 | NULL | 6 | 100.00 | Using index condition |
+----+-------------+----------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)EXPLAIN SELECT * FROM karyawan WHERE gaji > 8000000;Output yang Dihasilkan:
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | karyawan | NULL | range | idx_gaji | idx_gaji | 5 | NULL | 3 | 100.00 | Using index condition |
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)Kedua rencana menampilkan type = range: idx_tanggal membaca rentang 6 baris kuartal pertama, idx_gaji membaca 3 baris di atas delapan juta. Catatan Using index condition pada Extra menandakan optimasi tambahan bernama index condition pushdown — kondisi WHERE diperiksa sedini mungkin di dalam index sebelum baris tabel disentuh. Pola wildcard berawalan termasuk keluarga range karena awalan tetap menentukan posisi awal di urutan index:
EXPLAIN SELECT * FROM karyawan WHERE kota LIKE 'J%';Output yang Dihasilkan:
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | karyawan | NULL | range | idx_kota | idx_kota | 162 | NULL | 3 | 100.00 | Using index condition |
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)LIKE 'J%' dilayani idx_kota sebagai range — semua nilai yang berawalan J berdampingan di urutan index. Perhatikan pembagian perannya: wildcard di depan ('J%') baik-baik saja, karena bagian yang pasti (J) tetap menjadi titik masuk; wildcard di awalan ('%x') tidak meninggalkan apa pun untuk dipandu — materi yang kita bedah tuntas di chapter berikutnya.
ORDER BY Dilayani Index: Tanpa Filesort #
Index terurut punya manfaat kedua yang sering luput: hasil yang diambil searah index otomatis sudah terurut. Query berikut meminta kolom-kolom yang seluruhnya tersedia di dalam idx_nama:
EXPLAIN SELECT nama_depan, nama_belakang FROM karyawan
WHERE nama_depan LIKE 'B%'
ORDER BY nama_depan, nama_belakang;Output yang Dihasilkan:
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | karyawan | NULL | range | idx_nama | idx_nama | 202 | NULL | 1 | 100.00 | Using where; Using index |
+----+-------------+----------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)Rencananya ramping: type = range dengan key = idx_nama pada key_len = 202 — rentang nilai nama_depan berawalan B dibaca searah index, dan karena index mengurutkan nama_depan lalu nama_belakang, klausa ORDER BY yang sama persis urutannya terpenuhi tanpa langkah sortir. Label Using index pada Extra menegaskan query ini sekaligus covering: kedua kolom yang dibutuhkan hidup di dalam index, tabel tidak disentuh sama sekali; label Using where yang mendampinginya hanya mencatat pemeriksaan kondisi LIKE pada baris kandidat. Kebalikannya terjadi bila pengurutan menuntut kolom yang tak terindeks:
EXPLAIN SELECT * FROM karyawan ORDER BY email;Output yang Dihasilkan:
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | karyawan | NULL | ALL | NULL | NULL | NULL | NULL | 10 | 100.00 | Using filesort |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)type = ALL disertai Using filesort — tanpa index pada email, MySQL membaca seluruh tabel lalu menjalankan langkah sortir terpisah untuk memenuhi ORDER BY. Kata filesort memang terdengar mengerikan; artinya hanya “proses sortir di luar index”, meskipun untuk dataset besar inilah salah satu penyebab query lambat yang paling sering dijumpai. Aturannya bisa dirangkum satu kalimat: ORDER BY dilayani index bila daftar kolomnya membentuk awalan berurutan dari urutan kolom suatu index yang juga terpakai query.
GROUP BY Berjalan di Atas Index #
Pengelompokan memanen manfaat yang sama dengan ORDER BY: baris-baris yang terkumpul di satu area index sudah bersebelahan, sehingga agregasi per kelompok tidak perlu tahap sortir atau tabel sementara. Kita pakai tabel produk dengan index pada kategorinya:
CREATE TABLE produk (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(60) NOT NULL,
kategori VARCHAR(30) NOT NULL,
harga DECIMAL(10,2) NOT NULL
);
INSERT INTO produk (nama, kategori, harga) VALUES
('Kopi Gayo', 'kopi', 55000.00),
('Teh Melati', 'teh', 25000.00),
('Kopi Toraja','kopi', 62000.00),
('Gula Aren', 'pemanis', 15000.00),
('Teh Hijau', 'teh', 27000.00),
('Susu Kental','pemanis', 18000.00);
CREATE INDEX idx_kategori ON produk(kategori);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0SELECT kategori, COUNT(*) AS jumlah_produk
FROM produk
GROUP BY kategori
ORDER BY kategori;Output yang Dihasilkan:
+----------+---------------+
| kategori | jumlah_produk |
+----------+---------------+
| kopi | 2 |
| pemanis | 2 |
| teh | 2 |
+----------+---------------+
3 rows in set (0.00 sec)EXPLAIN SELECT kategori, COUNT(*) AS jumlah_produk
FROM produk
GROUP BY kategori
ORDER BY kategori;Output yang Dihasilkan:
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | produk | NULL | index | idx_kategori | idx_kategori | 122 | NULL | 6 | 100.00 | Using index |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)Hasilnya tiga kelompok — kopi, pemanis, teh, masing-masing dua produk — dan rencana di baliknya membaca type = index dengan key = idx_kategori berlabel Using index: seluruh index dibaca berurutan, kelompok demi kelompok terbentuk saat entri-entri sejenis bersebelahan lewat, dan tak ada tahap sortir maupun tabel sementara di Extra. Catatan kecil yang layak diingat: di MySQL 8, GROUP BY tidak lagi mengurutkan hasilnya secara implisit — justru karena alasan inilah ORDER BY kategori tetap kita tulis eksplisit, dan index yang sama kebetulan melayani keduanya.
Membersihkan Database Latihan #
Database belajar_index kita jatuhkan kembali:
DROP DATABASE belajar_index;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)Referensi #
- EXPLAIN Statement — MySQL 8.4 Reference Manual
- EXPLAIN Output Format — MySQL 8.4 Reference Manual
- How MySQL Uses Indexes — MySQL 8.4 Reference Manual
Pola sehat kini punya nama dan wajah: ref untuk equality, range untuk rentang dan awalan LIKE, index yang melayani ORDER BY tanpa filesort, dan GROUP BY yang berjalan di atas index terurut. Sayangnya, sejumlah pola query yang tampak wajar justru mematikan index — membungkus kolom dalam fungsi, wildcard di awal, hingga pencarian angka pada kolom teks. Chapter berikutnya mengumpulkan jebakan-jebakan itu satu per satu beserta bukti EXPLAIN-nya.