Kolom type di EXPLAIN adalah satu huruf diagnosis paling padat dalam seluruh output: satu kata merangkum bagaimana sebuah tabel diakses — apakah server melompat tepat ke baris yang dicari, menyapu rentang nilai, atau membaca seluruh tabel dari awal sampai akhir. Optimizer selalu memilih akses termurah yang tersedia, tetapi “yang tersedia” ditentukan oleh index, bentuk kondisi, dan tipe data kita. Karena itu membaca type bukan sekadar hafalan urutan; kita perlu melihat setiap jenis akses dalam bentuk query nyata supaya refleks “lihat type, tahu apa yang terjadi” terbentuk. Chapter ini menyusun ketujuh access type dari yang tercepat sampai yang paling sering menjadi biang keladi.

Menyiapkan Data Latihan #

Dua tabel berelasi dengan index campuran — cukup untuk melahirkan seluruh jenis akses yang ingin kita amati:

sql
DROP DATABASE IF EXISTS belajar_opt;
CREATE DATABASE belajar_opt;
USE belajar_opt;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  kota VARCHAR(40) NOT NULL,
  email VARCHAR(100) NOT NULL,
  tanggal_daftar DATE NOT NULL
);

INSERT INTO pelanggan (nama, kota, email, tanggal_daftar) VALUES
  ('Budi Santoso',   'Jakarta',    '[email protected]',  '2026-01-05'),
  ('Siti Rahayu',    'Bandung',    '[email protected]',  '2026-01-12'),
  ('Andi Wijaya',    'Surabaya',   '[email protected]',  '2026-02-01'),
  ('Rina Kartika',   'Jakarta',    '[email protected]',  '2026-02-10'),
  ('Dewi Lestari',   'Yogyakarta', '[email protected]',  '2026-03-03'),
  ('Tono Sutrisno',  'Bandung',    '[email protected]',  '2026-03-15'),
  ('Sari Wulandari', 'Medan',      '[email protected]',  '2026-04-01'),
  ('Joko Pratama',   'Jakarta',    '[email protected]',  '2026-04-20'),
  ('Nurul Hidayah',  'Surabaya',   '[email protected]', '2026-05-05'),
  ('Rizky Maulana',  'Medan',      '[email protected]', '2026-05-18');

CREATE INDEX idx_kota ON pelanggan(kota);
CREATE INDEX idx_daftar ON pelanggan(tanggal_daftar);

CREATE TABLE pesanan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  total DECIMAL(12,2) NOT NULL,
  status VARCHAR(20) NOT NULL
);

INSERT INTO pesanan (pelanggan_id, tanggal, total, status) VALUES
  (1,  '2026-01-10', 150000.00, 'selesai'),
  (2,  '2026-01-15', 250000.00, 'selesai'),
  (3,  '2026-01-20',  75000.00, 'batal'),
  (1,  '2026-02-05', 320000.00, 'selesai'),
  (4,  '2026-02-12', 125000.00, 'diproses'),
  (5,  '2026-02-18', 480000.00, 'selesai'),
  (2,  '2026-03-01', 210000.00, 'selesai'),
  (6,  '2026-03-08',  95000.00, 'batal'),
  (1,  '2026-03-15', 540000.00, 'diproses'),
  (7,  '2026-03-22', 175000.00, 'selesai'),
  (8,  '2026-04-02', 260000.00, 'selesai'),
  (3,  '2026-04-09', 310000.00, 'selesai'),
  (9,  '2026-04-16',  88000.00, 'batal'),
  (4,  '2026-04-25', 430000.00, 'selesai'),
  (10, '2026-05-01', 365000.00, 'selesai');

CREATE INDEX idx_tanggal ON pesanan(tanggal);

Output yang Dihasilkan:

text
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)

Query OK, 15 rows affected (0.00 sec)
Records: 15  Duplicates: 0  Warnings: 0

Query OK, 0 rows affected (0.00 sec)
Records: 0  Duplicates: 0  Warnings: 0

Hierarki dari Cepat ke Lambat #

Dokumentasi MySQL menetapkan urutan type dari yang terbaik ke terburuk. Urutan ini bukan skala waktu yang linear — lompatan dari ref ke range kecil dibanding lompatan dari index ke ALL pada tabel besar — tetapi arah peringkatnya konsisten:

PeringkattypeMaknaBiaya relatif
1systemTabel berisi tepat satu baris (kasus khusus const)Sangat murah
2constLookup PRIMARY/UNIQUE dengan paling banyak satu hasilSangat murah
3eq_refJoin lewat PRIMARY/UNIQUE, satu baris per kunci joinSangat murah
4refLookup index non-unique, bisa beberapa baris per kunciMurah
5rangeIndex membaca rentang nilaiSedang
6indexFull index scan — seluruh index dibaca berurutanLebih mahal
7ALLFull table scan — seluruh tabel dibacaPaling mahal

Kita bedah satu per satu dengan query nyata di atas data latihan.

system: Satu Baris Saja #

Kasus paling khusus: sumber data yang dijamin berisi tepat satu baris. Subquery turunan yang menghasilkan satu baris (misalnya agregat tanpa GROUP BY) menampilkan tipe ini:

sql
EXPLAIN SELECT s.jumlah_pelanggan
FROM (SELECT COUNT(*) AS jumlah_pelanggan FROM pelanggan) AS s;

Output yang Dihasilkan:

text
+----+-------------+------------+------------+--------+---------------+------------+---------+------+------+----------+-------------+
| id | select_type | table      | partitions | type   | possible_keys | key        | key_len | ref  | rows | filtered | Extra       |
+----+-------------+------------+------------+--------+---------------+------------+---------+------+------+----------+-------------+
|  1 | PRIMARY     | <derived2> | NULL       | system | NULL          | NULL       | NULL    | NULL |    1 |   100.00 | NULL        |
|  2 | DERIVED     | pelanggan  | NULL       | index  | NULL          | idx_daftar | 3       | NULL |   10 |   100.00 | Using index |
+----+-------------+------------+------------+--------+---------------+------------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

Baris <derived2> bertanda type = system — hasil subquery berupa tepat satu baris sehingga aksesnya nyaris gratis. Baris id = 2 dengan select_type = DERIVED adalah proses menghitung COUNT(*) itu sendiri: menariknya, perhitungan itu menyapu idx_daftar (type = index, Using index) — index mana pun cukup untuk menghitung baris, dan optimizer memilih menyapu index yang paling ramping.

const: Primary Key Lookup #

Akses klasik yang paling sering kita tulis: mencari satu baris lewat PRIMARY KEY (atau index UNIQUE) dengan nilai literal. Optimizer tahu hasilnya maksimal satu baris, sehingga nilai pencarian diperlakukan sebagai konstanta:

sql
EXPLAIN SELECT * FROM pelanggan WHERE id = 3;

Output yang Dihasilkan:

text
+----+-------------+-----------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | pelanggan | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

type = const dengan key = PRIMARY — satu lompatan langsung ke baris Andi Wijaya. Perhatikan ref = const: nilai 3 dianggap konstanta yang diikat sebelum eksekusi. Pola yang sama berlaku untuk lookup UNIQUE, misalnya kolom email bila kita pasangi UNIQUE index.

eq_ref: Join lewat Primary Key #

eq_ref adalah versi join dari const: tabel kedua dicari lewat PRIMARY/UNIQUE menggunakan nilai kolom dari tabel pertama. Setiap kunci menghasilkan paling banyak satu baris:

sql
EXPLAIN SELECT o.id, p.nama, o.total
FROM pesanan o
JOIN pelanggan p ON p.id = o.pelanggan_id
WHERE o.tanggal >= '2026-04-01';

Output yang Dihasilkan:

text
+----+-------------+-------+------------+--------+---------------+-------------+---------+----------------------------+------+----------+-----------------------+
| id | select_type | table | partitions | type   | possible_keys | key         | key_len | ref                        | rows | filtered | Extra                 |
+----+-------------+-------+------------+--------+---------------+-------------+---------+----------------------------+------+----------+-----------------------+
|  1 | SIMPLE      | o     | NULL       | range  | idx_tanggal   | idx_tanggal | 3       | NULL                       |    5 |   100.00 | Using index condition |
|  1 | SIMPLE      | p     | NULL       | eq_ref | PRIMARY       | PRIMARY     | 4       | belajar_opt.o.pelanggan_id |    1 |   100.00 | NULL                  |
+----+-------------+-------+------------+--------+---------------+-------------+---------+----------------------------+------+----------+-----------------------+
2 rows in set, 1 warning (0.00 sec)

Baris p menunjukkan type = eq_ref dengan key = PRIMARY dan ref = belajar_opt.o.pelanggan_id — untuk setiap pesanan sejak April, tepat satu pelanggan diambil lewat primary key. Ini bentuk join paling sehat yang bisa kita harapkan.

ref: Index Non-Unique #

ref memakai index biasa (non-unique) sebagai kamus: satu nilai kunci bisa menunjuk beberapa baris. Index idx_kota kita contohnya:

sql
EXPLAIN SELECT * FROM pelanggan WHERE kota = 'Jakarta';

Output yang Dihasilkan:

text
+----+-------------+-----------+------------+------+---------------+----------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys | key      | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------+----------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | pelanggan | NULL       | ref  | idx_kota      | idx_kota | 162     | const |    3 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------+----------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

type = ref dengan key = idx_kota dan rows kecil — server melompat ke kelompok entri Jakarta di index lalu membaca baris-barisnya. ref juga muncul di join ketika kolom penghubung hanya ber-index biasa, misalnya pesanan.pelanggan_id bila join dibalik arahnya.

range: Membaca Rentang #

Bila kondisi berbentuk rentang — BETWEEN, >, <, >=, IN — index dipakai untuk melompat ke titik awal lalu membaca searah sampai batas:

sql
EXPLAIN SELECT * FROM pelanggan
WHERE tanggal_daftar BETWEEN '2026-03-01' AND '2026-05-31';

Output yang Dihasilkan:

text
+----+-------------+-----------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table     | partitions | type  | possible_keys | key        | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-----------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | pelanggan | NULL       | range | idx_daftar    | idx_daftar | 3       | NULL |    6 |   100.00 | Using index condition |
+----+-------------+-----------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

type = range dengan key = idx_daftar, key_len = 3 (satu kolom DATE), dan Extra = Using index condition — filter rentang dievaluasi langsung di index. Biaya range sebanding lebar rentang: rentang yang menangkap hampir seluruh tabel membuat range nyaris semahal full scan, dan optimizer bisa saja memilih ALL sebagai jalan yang lebih jujur.

index: Menyapu Seluruh Index #

index adalah full scan versi index: seluruh entri index dibaca berurutan, tanpa lompatan. Terjadi ketika hasil query sepenuhnya dilayani index (kolom yang di-SELECT dan diurutkan ada di index) tetapi tidak ada kondisi yang bisa mempersempit:

sql
EXPLAIN SELECT id, kota FROM pelanggan;

Output yang Dihasilkan:

text
+----+-------------+-----------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys | key      | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | pelanggan | NULL       | index | NULL          | idx_kota | 162     | NULL |   10 |   100.00 | Using index |
+----+-------------+-----------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

type = index dengan key = idx_kota dan Extra = Using index — tidak ada WHERE, jadi index disapu penuh; namun karena kolom id dan kota (primary key selalu menempel di index InnoDB) cukup untuk menjawab query, baris tabel tak pernah disentuh. Menyapu index lebih murah daripada menyapu tabel karena index lebih ramping — tetapi tetaplah sapuan penuh: biayanya tumbuh mengikuti jumlah baris.

ALL: Menyapu Seluruh Tabel #

Penghuni paling bawah peringkat. Server membaca setiap baris tabel satu per satu dan menguji kondisinya:

sql
EXPLAIN SELECT * FROM pelanggan WHERE nama = 'Budi Santoso';

Output yang Dihasilkan:

text
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | pelanggan | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   10 |    10.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

type = ALL, key = NULL, rows sama dengan jumlah baris tabel — kolom nama memang tidak punya index, jadi tidak ada jalan lain. Extra = Using where menegaskan filter diterapkan selama penyapuan. Pada tabel besar, nilai ALL inilah yang menjelaskan query yang “tiba-tiba” lambat setelah data menumpuk.

Kapan ALL Masih Wajar #

Melihat ALL bukan otomatis alarm. Tabel latihan kita berisi 10–15 baris; menyapunya memakan waktu mikrodetik, dan memasang index tambahan hanya menambah beban tulis tanpa manfaat terasa. Full scan juga wajar untuk query laporan yang memang membaca mayoritas baris — agregat tahunan atas seluruh pesanan, misalnya — karena index tidak bisa memangkas banyak baris yang tetap harus dibaca.

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_opt;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Referensi #

Peta access type kini lengkap di tangan kita: dari system yang hampir gratis sampai ALL yang menyapu segalanya. Access type satu tabel hanyalah separuh cerita pada query nyata — kebanyakan query memuat beberapa tabel, dan urutan penyambungannya menentukan berapa kali tiap lookup diulang. Chapter berikutnya masuk ke wilayah itu: join optimization, dari pemilihan driving table sampai tanda bahaya Using join buffer.