- Pengenalan Query Optimization
- EXPLAIN β Membaca Query Plans
- Indexes β Mempercepat Pencarian Data
- Composite Indexes & Covering Indexes
- Membaca & Menganalisis Query Plans
- Teknik Optimasi Query Umum
- Subquery vs JOIN β Mana Lebih Cepat?
- Slow Query Log & Monitoring
- Best Practices & Anti-Patterns
- Quiz Pemahaman
1. Pengenalan Query Optimization
Query Optimization adalah proses membuat query SQL berjalan seefisien mungkin. Saat database Anda kecil (ribuan baris), hampir semua query terasa cepat. Tapi begitu data bertambah jutaan baris, perbedaan antara query yang dioptimalkan dan tidak bisa sangat dramatis β dari milidetik menjadi menit.
Database modern seperti MySQL, PostgreSQL, dan SQL Server memiliki komponen yang disebut Query Optimizer β sebuah mesin internal yang menentukan cara terbaik untuk mengeksekusi setiap query. Namun, optimizer hanya sebaik struktur data dan query yang Anda berikan.
Mengapa Query Optimization Penting?
- Kecepatan Response β Query yang dioptimalkan bisa 100-1000x lebih cepat
- Hemat Resource β CPU, RAM, dan I/O disk berkurang drastis
- Skalabilitas β Aplikasi tetap responsif saat data dan traffic meningkat
- Biaya Infrastruktur β Server yang lebih kecil bisa menangani beban yang sama
- User Experience β Response time di bawah 200ms membuat aplikasi terasa cepat
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β PROSES EKSEKUSI QUERY β β β β ββββββββββββ ββββββββββββββββ ββββββββββββββββββββββ β β β Query βββββΆβ Parser βββββΆβ Query Rewriter β β β β SQL β β (Syntax β β (Optimasi awal) β β β β String β β Check) β β β β β ββββββββββββ ββββββββββββββββ ββββββββββ¬ββββββββββββ β β β β β βΌ β β ββββββββββββ ββββββββββββββββ ββββββββββββββββββββββ β β β Hasil ββββββ Executor ββββββ Query Optimizer β β β β Data β β (Eksekusi β β (Pilih plan β β β β (Rows) β β fisik) β β terbaik) β β β ββββββββββββ ββββββββββββββββ ββββββββββββββββββββββ β β β β Optimizer memilih: β β β’ Jenis JOIN (Nested Loop, Hash, Merge) β β β’ Urutan tabel β β β’ Index yang digunakan β β β’ Strategi filter dan aggregasi β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Kapan Perlu Optimasi?
| Indikator | Threshold | Aksi |
|---|---|---|
| Response time query | > 1 detik | Perlu investigasi |
| Full table scan pada tabel besar | > 100K baris | Tambah index |
| CPU usage tinggi | > 80% | Optimasi query berat |
| Disk I/O tinggi | > 80% | Kurangi data yang dibaca |
| Slow query log | Banyak entry | Prioritas optimasi |
2. EXPLAIN β Membaca Query Plans
EXPLAIN adalah perintah paling penting dalam query optimization. Perintah ini menunjukkan bagaimana database akan mengeksekusi query Anda tanpa benar-benar menjalankannya. Dengan EXPLAIN, Anda bisa melihat apakah query menggunakan index, berapa baris yang dipindai, dan di mana bottleneck terjadi.
-- =============================================
-- Setup: Buat tabel untuk contoh
-- =============================================
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
customer_id INT NOT NULL,
product_id INT NOT NULL,
amount DECIMAL(12,2),
status VARCHAR(20),
order_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample data (1 juta baris)
-- ... bulk insert ...
-- =============================================
-- EXPLAIN Dasar: Lihat query plan
-- =============================================
EXPLAIN SELECT * FROM orders WHERE customer_id = 1234;
-- Hasil:
-- +----+------+------+------+----------+------+
-- | id | type | key | rows | filtered | Extra|
-- +----+------+------+------+----------+------+
-- | 1 | ALL | NULL | 1000000| 10.00 |Using where|
-- +----+------+------+------+----------+------+
-- type=ALL β Full table scan! Sangat lambat pada tabel besar.
-- =============================================
-- EXPLAIN ANALYZE (MySQL 8.0.18+)
-- =============================================
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 1234;
-- Menampilkan actual execution time, bukan hanya estimate
-- =============================================
-- EXPLAIN FORMAT=JSON
-- =============================================
EXPLAIN FORMAT=JSON
SELECT * FROM orders WHERE customer_id = 1234;
-- Output JSON detail β bisa di-parse untuk analisis otomatis
-- =============================================
-- EXPLAIN FORMAT=TREE (MySQL 8.0.16+)
-- =============================================
EXPLAIN FORMAT=TREE
SELECT o.*, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'shipped';
Kolom Penting dalam EXPLAIN
| Kolom | Deskripsi | Yang Dicari |
|---|---|---|
type | Jenis akses data | system > const > eq_ref > ref > range > index > ALL |
key | Index yang digunakan | NULL berarti tidak pakai index! |
rows | Estimasi baris yang dipindai | Semakin kecil semakin baik |
filtered | % baris setelah filter | Mendekati 100% lebih baik |
Extra | Info tambahan | Hindari "Using filesort", "Using temporary" |
Tipe Akses Data (type column)
| Type | Performa | Penjelasan |
|---|---|---|
system | βββββ | Tabel hanya punya 1 baris β paling cepat |
const | βββββ | Mencari PRIMARY KEY atau UNIQUE β 1 baris |
eq_ref | ββββ | JOIN menggunakan PRIMARY KEY β 1 baris per join |
ref | ββββ | Menggunakan non-unique index β beberapa baris |
range | βββ | Range scan (BETWEEN, >, <, IN) β cukup baik |
index | ββ | Full index scan β lebih baik dari ALL tapi masih lambat |
ALL | β | Full table scan β paling lambat, hindari pada tabel besar |
PostgreSQL menggunakan syntax berbeda: EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT). Kolom yang dihasilkan juga berbeda β gunakan seq_scan, idx_scan, dan n_live_tup dari pg_stat_user_tables untuk monitoring.
3. Indexes β Mempercepat Pencarian Data
Index adalah struktur data tambahan yang membantu database menemukan baris dengan cepat tanpa memindai seluruh tabel. Analoginya: index di buku β Anda langsung tahu halaman mana yang harus dibuka tanpa membaca dari halaman pertama.
Tanpa index, database melakukan full table scan β memeriksa setiap baris satu per satu. Dengan index, database langsung melompat ke baris yang tepat. Perbedaannya bisa sangat dramatis: dari 5 detik menjadi 5 milidetik.
-- ============================================= -- INDEX DASAR: Single Column Index -- ============================================= -- Buat index pada kolom customer_id CREATE INDEX idx_orders_customer_id ON orders(customer_id); -- Sekarang query ini jauh lebih cepat: EXPLAIN SELECT * FROM orders WHERE customer_id = 1234; -- type: ref, key: idx_orders_customer_id β Menggunakan index! -- ============================================= -- INDEX pada kolom yang sering di-SEARCH -- ============================================= -- Index untuk pencarian berdasarkan status CREATE INDEX idx_orders_status ON orders(status); -- Index untuk pencarian berdasarkan tanggal CREATE INDEX idx_orders_date ON orders(order_date); -- Index untuk pencarian berdasarkan product CREATE INDEX idx_orders_product ON orders(product_id); -- ============================================= -- UNIQUE INDEX: Menjamin keunikan -- ============================================= CREATE UNIQUE INDEX idx_customers_email ON customers(email); -- UNIQUE INDEX berfungsi ganda: -- 1. Mempercepat pencarian berdasarkan email -- 2. Mencegah duplikat email -- ============================================= -- MENGECEK INDEX yang ada pada tabel -- ============================================= -- MySQL: SHOW INDEX FROM orders; -- PostgreSQL: SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders'; -- ============================================= -- MENGHAPUS INDEX yang tidak diperlukan -- ============================================= DROP INDEX idx_orders_status ON orders; -- Atau dengan aman (MySQL): DROP INDEX IF EXISTS idx_orders_status ON orders; -- ============================================= -- INDEX pada kolom yang sering di-ORDER BY -- ============================================= -- Query ini akan benefit dari index: SELECT * FROM orders ORDER BY order_date DESC LIMIT 20; -- Buat index: CREATE INDEX idx_orders_date_desc ON orders(order_date DESC);
B-TREE INDEX (customer_id)
========================
[500]
/ \
[250] [750]
/ \ / \
[125] [375] [625] [875]
/ \ / \ / \ / \
[..] [..] [..][..][..][..][..] [..]
Pencarian customer_id = 375:
1. Root [500] β 375 < 500, go LEFT
2. Node [250] β 375 > 250, go RIGHT
3. Node [375] β FOUND! Langsung ke data row
Tanpa index: Pindai 1,000,000 baris (full scan)
Dengan index: Pindai 3-4 level β ~4 I/O saja!
B-Tree cocok untuk:
βββ = (equality)
βββ <, >, BETWEEN (range)
βββ LIKE 'abc%' (prefix)
βββ ORDER BY (jika match kolom index)
Jenis Index
| Jenis Index | Struktur | Cocok Untuk | Database |
|---|---|---|---|
| B-Tree | Tree balance | Pencarian umum, range, sorting | Semua RDBMS |
| Hash | Hash table | Pencarian exact (=) saja | PostgreSQL, Memory |
| GIN | Generalized Inverted | Full-text, array, JSON | PostgreSQL |
| GiST | Generalized Search Tree | Spatial, range types | PostgreSQL |
| Full-Text | Inverted index | Pencarian teks penuh | MySQL, PostgreSQL |
| Partial | B-Tree (filtered) | Subset data tertentu | PostgreSQL |
4. Composite Indexes & Covering Indexes
Composite Index (multi-column index) adalah index yang mencakup lebih dari satu kolom. Ini sangat penting untuk query yang memfilter berdasarkan beberapa kolom sekaligus. Urutan kolom dalam composite index sangat menentukan efektivitasnya β ini disebut Leftmost Prefix Rule.
-- ============================================= -- COMPOSITE INDEX: Multi-kolom -- ============================================= CREATE INDEX idx_orders_cust_status_date ON orders(customer_id, status, order_date); -- Query yang TERBANTU oleh index ini: -- β WHERE customer_id = 1234 -- β WHERE customer_id = 1234 AND status = 'shipped' -- β WHERE customer_id = 1234 AND status = 'shipped' AND order_date > '2026-01-01' -- β ORDER BY customer_id, status, order_date -- Query yang TIDAK terbantu: -- β WHERE status = 'shipped' (tidak pakai prefix customer_id) -- β WHERE order_date > '2026-01-01' (tidak pakai prefix) -- ============================================= -- LEFTMOST PREFIX RULE -- ============================================= -- Index: (A, B, C) bisa dipakai untuk: -- β (A) β prefix pertama -- β (A, B) β dua prefix pertama -- β (A, B, C) β ketiganya -- β (B, C) β skip A -- β (C) β skip A dan B -- ============================================= -- COVERING INDEX: Semua kolom ada di index -- ============================================= -- Query ini hanya butuh kolom: customer_id, status, amount SELECT customer_id, status, amount FROM orders WHERE customer_id = 1234 AND status = 'shipped'; -- Covering index yang ideal: CREATE INDEX idx_orders_covering ON orders(customer_id, status, amount); -- Dengan covering index, database TIDAK perlu mengakses -- data rows sama sekali (Index-Only Scan) -- EXPLAIN akan menampilkan: "Using index" -- ============================================= -- INDEX MERGE (MySQL) -- ============================================= -- Kadang MySQL bisa pakai 2 index sekaligus: -- Index: idx_customer, idx_status SELECT * FROM orders WHERE customer_id = 1234 OR status = 'shipped'; -- EXPLAIN mungkin menampilkan: "Using union(idx_customer, idx_status)" -- Tapi lebih efisien dengan composite index: CREATE INDEX idx_cust_status ON orders(customer_id, status);
Susun kolom composite index berdasarkan: (1) kolom yang paling sering di-WHERE, (2) kolom dengan kardinalitas tinggi (banyak nilai unik), (3) kolom yang di-ORDER BY. Jangan buat terlalu banyak composite index β setiap index menambah overhead saat INSERT/UPDATE.
5. Membaca & Menganalisis Query Plans
Query plan (execution plan) adalah roadmap yang dibuat optimizer sebelum mengeksekusi query. Memahami query plan adalah keterampilan krusial untuk debugging performa.
-- =============================================
-- QUERY PLAN: Contoh Analisis
-- =============================================
-- Query yang kompleks:
EXPLAIN ANALYZE
SELECT
c.name AS customer_name,
COUNT(o.id) AS total_orders,
SUM(o.amount) AS total_spent
FROM customers c
JOIN orders o ON c.id = o.customer_id
WHERE o.order_date >= '2026-01-01'
AND o.status IN ('shipped', 'delivered')
GROUP BY c.id, c.name
HAVING COUNT(o.id) > 5
ORDER BY total_spent DESC
LIMIT 10;
-- Analisis Output EXPLAIN:
-- 1. Join Order: customers β orders (atau sebaliknya)
-- 2. Join Method: Hash Join vs Nested Loop vs Merge Join
-- 3. Access Path: Index Scan vs Seq Scan
-- 4. Filter: WHERE diterapkan sebelum/sesudah join
-- 5. Sort: In-memory sort vs filesort
-- 6. Aggregation: HashAggregate vs GroupAggregate
-- =============================================
-- POSTGRESQL: EXPLAIN ANALYZE VERBOSE
-- =============================================
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE customer_id = 1234
ORDER BY order_date DESC
LIMIT 10;
-- Output:
-- Limit (cost=0.43..8.45 rows=10 width=56) (actual time=0.025..0.032 rows=10)
-- -> Index Scan using idx_orders_cust_date on orders
-- (cost=0.43..805.43 rows=1000 width=56) (actual time=0.024..0.030 rows=10)
-- Index Cond: (customer_id = 1234)
-- Buffers: shared hit=4
-- Planning Time: 0.150 ms
-- Execution Time: 0.045 ms
-- =============================================
-- JOIN METHODS: Kapan Digunakan
-- =============================================
-- Nested Loop Join:
-- Cocok: Tabel kecil, ada index pada join column
-- Contoh: customers (100 rows) JOIN orders dengan index
-- Hash Join:
-- Cocok: Tabel besar, TANPA index, equality join
-- Contoh: 2 tabel besar JOIN dengan =
-- Merge Join:
-- Cocok: Kedua tabel sudah ter-sort pada join column
-- Contoh: Tabel yang di-ORDER BY kolom join
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β JENIS JOIN METHODS β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β β β 1. NESTED LOOP JOIN β β βββββββββββ β β β Table A β (outer) β β ββββββ¬βββββ β β β untuk setiap baris A: β β βΌ cari di Table B (pakai index) β β βββββββββββ β β β Table B β (inner, indexed) β β βββββββββββ β β Time: O(n * log m) β sangat cepat kalau ada index β β β β 2. HASH JOIN β β βββββββββββ ββββββββββββ β β β Table A ββββββββΆ β Hash β βββββββββββ β β β (build) β β Table ββββββ Table B β β β βββββββββββ ββββββ¬ββββββ β (probe) β β β β βββββββββββ β β βΌ β β Hasil Join β β Time: O(n + m) β efisien untuk tabel besar tanpa index β β β β 3. MERGE JOIN β β Table A (sorted) βββΆ ββββββββ βββ Table B (sorted) β β 1,3,5,7,9... βMerge β 2,3,5,8,12... β β ββββ¬ββββ β β βΌ β β 3, 5 (matches) β β Time: O(n log n + m log m) β butuh sorted data β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
6. Teknik Optimasi Query Umum
Berikut teknik-teknik optimasi yang paling sering digunakan untuk mempercepat query SQL.
-- =============================================
-- 1. HINDARI FUNCTION pada indexed column
-- =============================================
-- β BURUK: Index tidak bisa dipakai karena YEAR()
SELECT * FROM orders WHERE YEAR(order_date) = 2026;
-- β
BAIK: Gunakan range pada kolom asli
SELECT * FROM orders
WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01';
-- β BURUK: LOWER() pada indexed column
SELECT * FROM customers WHERE LOWER(email) = 'test@example.com';
-- β
BAIK: Simpan data sudah lowercase, atau buat expression index (PG)
CREATE INDEX idx_customers_email_lower
ON customers(LOWER(email)); -- PostgreSQL expression index
-- =============================================
-- 2. HINDARI SELECT * β ambil kolom yang diperlukan
-- =============================================
-- β BURUK: Mengambil semua kolom termasuk TEXT/BLOB besar
SELECT * FROM orders WHERE customer_id = 1234;
-- β
BAIK: Ambil hanya kolom yang dibutuhkan
SELECT id, amount, status, order_date
FROM orders
WHERE customer_id = 1234;
-- Bisa memanfaatkan covering index!
-- =============================================
-- 3. OPTIMASI LIKE
-- =============================================
-- β BURUK: Leading wildcard β index tidak dipakai
SELECT * FROM products WHERE nama_produk LIKE '%laptop%';
-- β
BAIK: Trailing wildcard β index bisa dipakai
SELECT * FROM products WHERE nama_produk LIKE 'laptop%';
-- β
ALTERNATIF: Full-text search untuk pencarian bebas
SELECT * FROM products
WHERE MATCH(nama_produk) AGAINST('laptop gaming' IN BOOLEAN MODE);
-- =============================================
-- 4. OPTIMASI IN dengan banyak nilai
-- =============================================
-- β BURUK: Terlalu banyak nilai dalam IN
SELECT * FROM orders
WHERE customer_id IN (1,2,3,...,10000);
-- β
BAIK: Gunakan JOIN dengan temporary table
CREATE TEMPORARY TABLE tmp_ids (id INT PRIMARY KEY);
-- INSERT ke tmp_ids ...
SELECT o.* FROM orders o
JOIN tmp_ids t ON o.customer_id = t.id;
-- =============================================
-- 5. OPTIMASI COUNT
-- =============================================
-- β LAMBAT: COUNT(*) pada tabel besar tanpa filter
SELECT COUNT(*) FROM orders;
-- Scans entire table
-- β
LEBIH CEPAT: Hitung estimasi dari metadata (MySQL)
SELECT TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'orders';
-- β
LEBIH CEPAT: Hitung estimasi (PostgreSQL)
SELECT reltuples::bigint AS estimate
FROM pg_class WHERE relname = 'orders';
-- =============================================
-- 6. EXISTS vs IN untuk subquery
-- =============================================
-- β IN: Memproses semua hasil subquery
SELECT * FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE amount > 1000000);
-- β
EXISTS: Berhenti begitu menemukan match pertama
SELECT * FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id AND o.amount > 1000000
);
7. Subquery vs JOIN β Mana Lebih Cepat?
Pertanyaan klasik: apakah lebih cepat menggunakan subquery atau JOIN? Jawabannya tergantung pada konteks, ukuran data, dan versi database.
-- =============================================
-- CONTOH 1: Cari pelanggan yang pernah beli
-- =============================================
-- Menggunakan Subquery (correlated):
SELECT nama, email FROM pelanggan p
WHERE EXISTS (
SELECT 1 FROM pesanan ps
WHERE ps.id_pelanggan = p.id_pelanggan
AND ps.total > 500000
);
-- β
Bagus untuk: mengecek keberadaan (EXISTS)
-- Correlated subquery dijalankan per baris outer
-- Menggunakan JOIN:
SELECT DISTINCT p.nama, p.email
FROM pelanggan p
JOIN pesanan ps ON ps.id_pelanggan = p.id_pelanggan
WHERE ps.total > 500000;
-- β
Bagus untuk: mengambil data dari tabel lain
-- DISTINCT diperlukan karena JOIN bisa menghasilkan duplikat
-- =============================================
-- CONTOH 2: Agregasi per grup
-- =============================================
-- Subquery dalam FROM (Derived Table):
SELECT p.nama, sub.total_belanja
FROM pelanggan p
JOIN (
SELECT id_pelanggan, SUM(total) AS total_belanja
FROM pesanan
GROUP BY id_pelanggan
) sub ON p.id_pelanggan = sub.id_pelanggan
WHERE sub.total_belanja > 1000000;
-- JOIN langsung dengan HAVING:
SELECT p.nama, SUM(ps.total) AS total_belanja
FROM pelanggan p
JOIN pesanan ps ON p.id_pelanggan = ps.id_pelanggan
GROUP BY p.id_pelanggan, p.nama
HAVING SUM(ps.total) > 1000000;
-- =============================================
-- CONTOH 3: Window Function (Modern SQL)
-- =============================================
-- Lebih efisien dari subquery untuk ranking:
SELECT nama, total_belanja, ranking
FROM (
SELECT
p.nama,
SUM(ps.total) AS total_belanja,
RANK() OVER (ORDER BY SUM(ps.total) DESC) AS ranking
FROM pelanggan p
JOIN pesanan ps ON p.id_pelanggan = ps.id_pelanggan
GROUP BY p.id_pelanggan, p.nama
) ranked
WHERE ranking <= 10;
| Pendekatan | Kelebihan | Kekurangan |
|---|---|---|
| Correlated Subquery | Mudah dibaca untuk EXISTS | Bisa lambat jika data besar (N+1) |
| JOIN | Umumnya lebih cepat untuk data besar | Perlu DISTINCT jika one-to-many |
| Derived Table | Bisa pre-aggregate sebelum join | Tidak bisa di-index |
| Window Function | Paling fleksibel, performa baik | Perlu MySQL 8+ / PG 9+ |
8. Slow Query Log & Monitoring
Slow Query Log adalah fitur database yang mencatat query yang berjalan lebih lama dari threshold tertentu. Ini adalah alat utama untuk menemukan query yang perlu dioptimalkan.
-- =============================================
-- MYSQL: Mengaktifkan Slow Query Log
-- =============================================
-- Cek status slow query log
SHOW VARIABLES LIKE 'slow_query_log';
-- Aktifkan (sementara, hilang saat restart)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- Query > 1 detik
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- Aktifkan permanen: tambahkan di my.cnf
-- [mysqld]
-- slow_query_log = 1
-- slow_query_log_file = /var/log/mysql/slow.log
-- long_query_time = 1
-- log_queries_not_using_indexes = 1
-- =============================================
-- MYSQL: Performance Schema
-- =============================================
-- Query paling lambat (MySQL 8+)
SELECT
DIGEST_TEXT AS query_pattern,
COUNT_STAR AS exec_count,
ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_ms,
ROUND(MAX_TIMER_WAIT / 1000000000, 2) AS max_ms,
SUM_ROWS_EXAMINED AS rows_scanned,
SUM_ROWS_SENT AS rows_returned
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
-- =============================================
-- POSTGRESQL: pg_stat_statements
-- =============================================
-- Pastikan extension aktif:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Query paling lambat:
SELECT
query,
calls,
ROUND(mean_exec_time::numeric, 2) AS avg_ms,
ROUND(max_exec_time::numeric, 2) AS max_ms,
rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- =============================================
-- MONITORING: Cek index usage
-- =============================================
-- MySQL: Index yang tidak pernah dipakai
SELECT
object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND index_name != 'PRIMARY'
AND count_star = 0
ORDER BY object_schema, object_name;
-- PostgreSQL: Index yang jarang dipakai
SELECT
schemaname, tablename, indexname,
idx_scan AS times_used,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan < 10
ORDER BY pg_relation_size(indexrelid) DESC;
9. Best Practices & Anti-Patterns
β Best Practices
| # | Best Practice | Alasan |
|---|---|---|
| 1 | Selalu gunakan EXPLAIN sebelum deploy query baru | Mendeteksi masalah sebelum production |
| 2 | Buat index sesuai pola query, bukan sebaliknya | Index yang tidak dipakai hanya buang resource |
| 3 | Pagination menggunakan keyset, bukan OFFSET | OFFSET scan semua baris sebelumnya |
| 4 | Pisahkan query besar menjadi batch kecil | Hindari lock panjang dan timeout |
| 5 | Gunakan connection pooling | Mengurangi overhead koneksi |
| 6 | Monitor slow query log secara rutin | Deteksi dini query bermasalah |
| 7 | Update statistik tabel secara berkala | Optimizer butuh data akurat untuk keputusan |
| 8 | Test dengan data volume production | Query cepat di dev belum tentu cepat di prod |
β Anti-Patterns yang Harus Dihindari
-- ============================================= -- ANTI-PATTERN 1: OFFSET besar -- ============================================= -- β BURUK: Halaman 1000 dengan 10 per halaman SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 10000; -- Database membaca 10,010 baris, buang 10,000 -- β BAIK: Keyset pagination SELECT * FROM orders WHERE id > 10000 -- ID terakhir dari halaman sebelumnya ORDER BY id LIMIT 10; -- Hanya membaca 10 baris! -- ============================================= -- ANTI-PATTERN 2: N+1 Query Problem -- ============================================= -- β BURUK (dalam aplikasi): -- 1 query: SELECT * FROM customers -- N query: SELECT * FROM orders WHERE customer_id = ? -- (1000 customers = 1001 query!) -- β BAIK: Batch dengan JOIN atau IN -- 1 query: SELECT c.*, o.* FROM customers c -- JOIN orders o ON c.id = o.customer_id -- ============================================= -- ANTI-PATTERN 3: Index berlebihan -- ============================================= -- β BURUK: Terlalu banyak index -- INSERT/UPDATE jadi lambat karena setiap index di-update CREATE INDEX idx_a ON orders(customer_id); CREATE INDEX idx_b ON orders(status); CREATE INDEX idx_c ON orders(order_date); CREATE INDEX idx_d ON orders(amount); CREATE INDEX idx_e ON orders(product_id); -- 5 index = 5x overhead saat INSERT! -- β BAIK: Composite index yang terencana CREATE INDEX idx_main ON orders(customer_id, status, order_date); -- 1 index yang mendukung berbagai pola query -- ============================================= -- ANTI-PATTERN 4: Menggunakan ORM tanpa cek query -- ============================================= -- ORM seperti Hibernate / ActiveRecord bisa generate query buruk -- Selalu cek query yang di-generate di development! -- Aktifkan query logging pada framework Anda -- ============================================= -- ANTI-PATTERN 5: Tidak pakai parameterized query -- ============================================= -- β BURUK: String concatenation (SQL Injection risk + no plan cache) -- "SELECT * FROM orders WHERE id = " + userInput -- β BAIK: Parameterized / prepared statement -- "SELECT * FROM orders WHERE id = ?"
Checklist Optimasi Sebelum Production
- β Sudah jalankan EXPLAIN untuk semua query baru?
- β Tidak ada full table scan pada tabel > 100K baris?
- β Composite index sudah sesuai pola query?
- β Tidak ada SELECT * dalam query production?
- β Pagination menggunakan keyset / cursor?
- β Parameterized query sudah digunakan?
- β Slow query log sudah diaktifkan?
- β Sudah test dengan data volume production?
- β Connection pooling sudah dikonfigurasi?
- β Index yang tidak dipakai sudah dihapus?
10. Quiz Pemahaman
Uji pemahaman Anda tentang SQL Query Optimization!
1. Apa arti type=ALL pada hasil EXPLAIN?
2. Urutan kolom composite index (A, B, C) bisa dipakai untuk query WHERE A=? AND C=? karena leftmost prefix?
3. Mengapa keyset pagination lebih cepat dari OFFSET?
4. Apa yang dimaksud dengan covering index?
5. Mana yang lebih cepat untuk mengecek keberadaan data?
Rangkuman
- EXPLAIN adalah alat utama β selalu cek query plan sebelum deploy
- Indexes mempercepat READ tapi menambah overhead pada WRITE
- Composite index β ikuti leftmost prefix rule
- Covering index β hilangkan kebutuhan akses ke data rows
- Hindari full table scan, SELECT *, function pada indexed column
- Keyset pagination lebih baik dari OFFSET untuk tabel besar
- Monitor slow query log dan hapus index yang tidak dipakai