Index dan rencana eksekusi
Aturan prefiks kiri, kenapa clustered index InnoDB mengubah cara kamu memilih primary key, apa yang membuat predikat tak terpakai oleh index, dan cara membaca EXPLAIN di kedua mesin.
Index adalah salinan terurut dari beberapa kolom, ditambah penunjuk kembali ke barisnya. Kalimat itu menjelaskan hampir semua yang bisa dan tidak bisa dilakukan index: dia bisa menemukan nilai dengan cepat, dia bisa mengembalikan baris yang sudah terurut, dan dia berbiaya waktu pada setiap penulisan karena salinannya harus dijaga.
Kedua mesin memakai B-tree secara bawaan.
Aturan prefiks kiri
Index gabungan diurutkan berdasar kolom pertama, lalu kolom kedua di antara yang pertamanya sama, dan seterusnya. Seperti buku telepon yang diurutkan berdasar nama keluarga lalu nama depan.
CREATE INDEX ix_order_customer_placed ON "order" (customer_id, placed_at);| Query | Bisa memakai index-nya? |
|---|---|
WHERE customer_id = 7 |
ya |
WHERE customer_id = 7 AND placed_at > '2026-01-01' |
ya, kedua kolom |
WHERE customer_id = 7 ORDER BY placed_at |
ya, dan tidak perlu langkah pengurutan |
WHERE placed_at > '2026-01-01' |
tidak — melewati kolom pertama berarti nilai yang dicarinya tersebar |
WHERE customer_id IN (1,2,3) AND placed_at > ... |
ya, satu rentang per nilai |
Karena itulah urutan kolom di index gabungan adalah keputusan, bukan formalitas. Patokannya: kolom kesamaan dulu, lalu kolom yang kamu pindai sebagai rentang atau kamu pakai untuk mengurutkan.
Akibat yang sering terlewat: (a, b) membuat index terpisah pada (a) jadi berlebihan,
tapi tidak pada (b). Dua index (a, b) dan (a) biasanya satu index terlalu banyak.
InnoDB itu clustered, PostgreSQL tidak
Ini perbedaan struktural terbesar antara keduanya, dan dia mengubah keputusan desain.
| PostgreSQL | MySQL / InnoDB | |
|---|---|---|
| Penyimpanan tabel | sebuah heap; setiap index menunjuk lokasi fisik | tabelnya adalah B-tree primary key |
| Isi entri secondary index | key + lokasi baris | key + nilai primary key |
| Akibat | ukuran index tidak bergantung PK | PK yang gemuk menggelembungkan setiap secondary index |
| Pencarian via secondary index | satu langkah ke heap | dua langkah: secondary index, lalu pohon PK |
Jadi di MySQL: jaga primary key tetap kecil, dan utamakan key yang berurutan. Primary
key UUID CHAR(36) disalin ke setiap secondary index dan menyebarkan penyisipan ke
seluruh file; BIGINT AUTO_INCREMENT menambahkan di ujung. Di PostgreSQL pilihan yang
sama jauh lebih murah.
Ganti rugi PostgreSQL adalah index-only scan: kalau setiap kolom yang dibutuhkan query ada di dalam index-nya, dia tidak menyentuh tabelnya sama sekali.
-- Tercakup oleh ix_order_customer_placed: tidak perlu akses heapSELECT customer_id, placed_at FROM "order" WHERE customer_id = 7;
-- PostgreSQL juga bisa membawa kolom tambahan tanpa menjadikannya bagian keyCREATE INDEX ix_order_cover ON "order" (customer_id) INCLUDE (status, placed_at);MySQL mendapat efek yang sama dengan mencantumkan kolomnya di dalam index; dia tidak
punya INCLUDE.
Apa yang menghentikan index dipakai
Sebuah predikat itu sargable ketika mesinnya bisa mengubahnya jadi rentang atas nilai yang di-index. Bungkus kolomnya dengan fungsi dan dia tidak bisa.
-- Tidak sargable: index-nya menyimpan placed_at, bukan YEAR(placed_at)WHERE YEAR(placed_at) = 2026WHERE DATE(placed_at) = '2026-01-01'WHERE lower(email) = 'a@b.com'
-- Sargable: arti yang sama, ramah indexWHERE placed_at >= '2026-01-01' AND placed_at < '2027-01-01'WHERE email = 'a@b.com' -- dengan collation tak peka huruf, atau:Kalau fungsinya memang dibutuhkan, index-kan ekspresinya:
-- PostgreSQL: expression indexCREATE INDEX ix_customer_lower_email ON customer (lower(email));
-- MySQL: functional index (8.0.13+), atau generated column plus indexCREATE INDEX ix_customer_lower_email ON customer ((lower(email)));Hal lain yang mengalahkan index:
LIKE '%abc'— wildcard di depan tidak punya prefiks untuk dituju.LIKE 'abc%'aman.ORmelintasi kolom berbeda — kadang ditangani bitmap atau index merge, sering lebih baik ditulis ulang sebagaiUNION ALL.- Ketidakcocokan tipe. Membandingkan kolom
varchardengan angka membuat MySQL mengonversi kolomnya, bukan literalnya, dan index-nya tak terpakai. Yang ini tidak terlihat dari teks query-nya. - Selektivitas rendah. Index pada boolean yang cocok dengan 60% tabel lebih banyak kerja daripada memindai, dan planner benar mengabaikannya.
Partial index, dan penggantinya di MySQL
PostgreSQL bisa meng-index sebagian baris, yang sekaligus lebih kecil dan lebih selektif:
-- Hanya order yang belum dikirim — yang ditanyakan antrean pengirimanCREATE INDEX ix_order_unshipped ON "order" (placed_at) WHERE shipped_at IS NULL;
-- Dan aturan keunikan dari materi 2 yang tidak bisa diungkapkan UNIQUE biasaCREATE UNIQUE INDEX ux_one_open_coupon ON "order" (customer_id) WHERE coupon_code IS NULL;MySQL tidak punya partial index. Jalan pintasnya adalah generated column yang bernilai
NULL untuk baris yang ingin kamu kecualikan — karena UNIQUE mengabaikan NULL —
ditambah index padanya.
Membaca EXPLAIN
-- PostgreSQL: hanya perkiraanEXPLAIN SELECT ...;-- PostgreSQL: benar-benar dijalankan, lalu melaporkan waktu dan I/O sesungguhnyaEXPLAIN (ANALYZE, BUFFERS) SELECT ...;
-- MySQL: perkiraanEXPLAIN SELECT ...;EXPLAIN FORMAT=JSON SELECT ...;-- MySQL 8.0.18+: benar-benar dijalankanEXPLAIN ANALYZE SELECT ...;EXPLAIN ANALYZE mengeksekusi pernyataannya. Pada SELECT itu aman; pada UPDATE
atau DELETE dia benar-benar mengerjakannya. Di PostgreSQL, bungkus dalam transaksi
lalu rollback.
Yang perlu dicari, berurutan:
- Perkiraan baris versus baris sesungguhnya. Di PostgreSQL,
rows=1000di sebelahactual rows=250000berarti statistiknya salah, dan setiap keputusan di atas node itu dibuat berdasar angka yang buruk. JalankanANALYZE namatabel(PostgreSQL) atauANALYZE TABLE namatabel(MySQL). - Metode aksesnya.
Seq Scan/ALLpada tabel besar denganWHEREyang selektif adalah temuan yang paling lazim.Index Scan,Index Only Scan,ref,range,eq_refadalah pemakaian index;constdaneq_refkasus terbaiknya. - Pengurutan yang tidak kamu inginkan.
Sortdi PostgreSQL atauUsing filesortdi MySQL berarti urutannya harus diproduksi. Index yang cocok denganORDER BYmenghapusnya — dan bersamaLIMIT, itu sering jadi seluruh perbedaannya. - Ke mana waktunya sebenarnya pergi. Di PostgreSQL,
actual timeitu kumulatif dan per-putaran: node yang menunjukkan 0,5ms denganloops=20000berbiaya sepuluh detik. Baca jumlah putarannya sebelum menyimpulkan sebuah node itu murah.
Peta kosakata:
| Arti | PostgreSQL | MySQL |
|---|---|---|
| Membaca seluruh tabel | Seq Scan |
type: ALL |
| Membaca rentang index | Index Scan, Bitmap Index Scan |
type: range / ref |
| Index menjawab sepenuhnya | Index Only Scan |
Extra: Using index |
| Pengurutan diperlukan | Sort |
Extra: Using filesort |
| Tabel sementara dibangun | HashAggregate / Materialize |
Extra: Using temporary |
Biaya index
Setiap index dijaga pada setiap INSERT, setiap UPDATE kolom yang di-index, dan
setiap DELETE. Sepuluh index di tabel yang sibuk membuat penulisan beberapa kali lebih
mahal, dan mereka memakan cache yang diinginkan tabelnya sendiri.
Kedua mesin memaparkan pemakaiannya, jadi ini bisa diukur alih-alih jadi soal selera:
-- PostgreSQL: index yang tidak pernah dibaca apa punSELECT relname, indexrelname, idx_scanFROM pg_stat_user_indexes WHERE idx_scan = 0 ORDER BY relname;
-- MySQL: pertanyaan yang samaSELECT object_name, index_name, count_starFROM performance_schema.table_io_waits_summary_by_index_usageWHERE index_name IS NOT NULL AND count_star = 0;Index dengan nol pemindaian sejak restart terakhir adalah biaya tulis murni. Periksa lama server hidup sebelum menghapusnya — index untuk laporan bulanan memang wajar menunjukkan nol di hari kedua.
Yang perlu dibawa pulang
- Index gabungan bekerja dari kiri ke kanan. Kolom kesamaan dulu, rentang atau pengurutan di belakang.
- InnoDB menyimpan tabel dalam urutan PK dan menyalin PK ke setiap secondary index: jaga dia kecil dan berurutan.
- Fungsi yang membungkus kolom mematikan index-nya. Index-kan ekspresinya.
LIKE 'abc%'memakai index;LIKE '%abc'tidak bisa.- PostgreSQL punya partial dan expression index; MySQL menggantinya dengan generated column.
- Di
EXPLAIN, bandingkan perkiraan dengan baris sesungguhnya lebih dulu, dan baca jumlah putaran sebelum memercayai sebuah node itu murah.
Diskusi
Memuat komentar…