Database

SQL Indexes: Panduan Advanced

B-tree, Hash Index, Composite Index, Covering Index, Partial Index — pahami cara kerja index di dalam dan optimasi query production

1. Pengenalan: Mengapa Index Penting?

Bayangkan sebuah buku telepon dengan ribuan nama. Tanpa index (daftar abjad di awal buku), Anda harus membaca halaman satu per satu untuk menemukan nama tertentu. Dengan index, Anda langsung tahu halaman berapa nama itu berada.

Database index bekerja dengan cara yang sama — struktur data tambahan yang mempercepat pencarian tanpa harus memindai seluruh tabel (full table scan). Tanpa index, database harus membaca setiap baris satu per satu. Dengan index, database bisa langsung "melompat" ke baris yang dicari.

Dampak Index pada Performa

Skenario Tanpa Index Dengan Index
Tabel 1 juta baris, cari 1 baris~1.000ms (full scan)~1ms (index seek)
JOIN 2 tabel besar~5.000ms~50ms
ORDER BY tanpa index~2.000ms (filesort)~10ms (index already sorted)
COUNT dengan WHERE~800ms~5ms
Diagram: Full Table Scan vs Index Seek
┌─────────────────────────────────────────────────────────────────┐
│  FULL TABLE SCAN (tanpa index)    INDEX SEEK (dengan index)    │
│                                                                 │
│  Tabel: 1.000.000 baris           Index (B-tree):              │
│  ┌─────┬───────┬──────┐           ┌──────────────────┐         │
│  │ 1   │ Ali   │ JKT  │           │     [M]          │         │
│  │ 2   │ Budi  │ BDG  │           │    /    \         │         │
│  │ 3   │ Citra │ JKT  │          [D]        [R]      │         │
│  │ ... │ ...   │ ...  │          / \       /  \       │         │
│  │999999│ Zara │ JKT  │       [B] [F]   [N]  [W]     │         │
│  └─────┴───────┴──────┘         │   │     │    │      │         │
│                                  ▼   ▼     ▼    ▼      │         │
│  Cari "Rudi" → scan semua      Langsung ke "R" → "Rudi"│       │
│  1 juta baris! (lambat)        ~20 langkah! (cepat)    │         │
└─────────────────────────────────────────────────────────────────┘

Setup Data Latihan

SQL — Setup Tabel untuk Latihan Index
-- =============================================
-- SETUP DATABASE UNTUK LATIHAN INDEX
-- =============================================

CREATE TABLE orders (
    id              SERIAL PRIMARY KEY,
    customer_id     INT NOT NULL,
    customer_email  VARCHAR(200) NOT NULL,
    product_name    VARCHAR(300) NOT NULL,
    category        VARCHAR(50) NOT NULL,
    quantity        INT NOT NULL,
    unit_price      DECIMAL(12,2) NOT NULL,
    total_amount    DECIMAL(15,2) NOT NULL,
    order_status    VARCHAR(20) NOT NULL DEFAULT 'pending',
    order_date      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    ship_date       TIMESTAMP,
    ship_city       VARCHAR(100),
    ship_country    VARCHAR(50),
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insert sample data (gunakan generate_series di PostgreSQL)
INSERT INTO orders (customer_id, customer_email, product_name, category,
                    quantity, unit_price, total_amount, order_status,
                    order_date, ship_city, ship_country)
SELECT
    (random() * 10000)::int,
    'user' || (random() * 10000)::int || '@email.com',
    'Product-' || (random() * 500)::int,
    CASE (random() * 4)::int
        WHEN 0 THEN 'Elektronik'
        WHEN 1 THEN 'Fashion'
        WHEN 2 THEN 'Makanan'
        WHEN 3 THEN 'Kesehatan'
        ELSE 'Rumah'
    END,
    (random() * 10 + 1)::int,
    (random() * 500000 + 10000)::decimal(12,2),
    (random() * 5000000 + 50000)::decimal(15,2),
    CASE (random() * 3)::int
        WHEN 0 THEN 'pending'
        WHEN 1 THEN 'shipped'
        WHEN 2 THEN 'delivered'
        ELSE 'cancelled'
    END,
    CURRENT_TIMESTAMP - (random() * 365)::int * INTERVAL '1 day',
    CASE (random() * 4)::int
        WHEN 0 THEN 'Jakarta'
        WHEN 1 THEN 'Bandung'
        WHEN 2 THEN 'Surabaya'
        WHEN 3 THEN 'Yogyakarta'
        ELSE 'Medan'
    END,
    'Indonesia'
FROM generate_series(1, 500000);

2. B-tree Index — Struktur Paling Umum

B-tree (Balanced Tree) adalah tipe index default di hampir semua database relasional — PostgreSQL, MySQL (InnoDB), SQL Server, dan Oracle. B-tree menjaga data tetap terurut dan seimbang, sehingga pencarian selalu cepat — O(log n).

Cara Kerja B-tree

Diagram: Struktur B-tree
┌─────────────────────────────────────────────────────────────────┐
│                    B-TREE INDEX (3 level)                       │
│                                                                 │
│                        ┌──────────┐                             │
│                        │ Root     │                             │
│                        │ [10|50]  │                             │
│                        └────┬─────┘                             │
│                   ┌─────────┼─────────┐                         │
│              ┌────┴────┐  ┌─┴───┐  ┌──┴────┐                   │
│              │ [3|7]   │  │[20|35]│ │[75|90]│                   │
│              └────┬────┘  └──┬───┘  └──┬────┘                   │
│           ┌───┬───┼───┐  ┌──┼──┐   ┌──┼──┐                     │
│           ▼   ▼   ▼   ▼  ▼  ▼  ▼   ▼  ▼  ▼                     │
│          [1] [3] [7] [10][20][35][50][75][90] ← Leaf nodes     │
│          ────────────────────────────────────── (data pointers) │
│                                                                 │
│  Cari angka 35: Root(10|50) → 10<35<50 → Mid child → 35 ✓     │
│  Hanya 3 langkah, bukan scan semua!                            │
└─────────────────────────────────────────────────────────────────┘
SQL — B-tree Index
-- =============================================
-- DEFAULT INDEX = B-tree
-- =============================================

-- Index sederhana pada satu kolom
CREATE INDEX idx_orders_customer_id
ON orders (customer_id);

-- Index dengan tipe eksplisit (PostgreSQL)
CREATE INDEX idx_orders_customer_email
ON orders USING btree (customer_email);

-- Index DESCENDING (untuk ORDER BY DESC yang sering)
CREATE INDEX idx_orders_date_desc
ON orders (order_date DESC);

-- Index dengan NULL handling (PostgreSQL)
CREATE INDEX idx_orders_ship_date_nulls_first
ON orders (ship_date NULLS FIRST);


-- =============================================
-- B-tree mendukung semua operator ini:
-- =============================================
-- =  (equal)
-- <  (less than)
-- <= (less than or equal)
-- >  (greater than)
-- >= (greater than or equal)
-- BETWEEN
-- IN
-- LIKE 'abc%' (prefix matching saja!)
-- IS NULL / IS NOT NULL
-- ORDER BY (ascending/descending)


-- =============================================
-- Contoh query yang MEMANFAATKAN B-tree index:
-- =============================================
SELECT * FROM orders WHERE customer_id = 1234;
SELECT * FROM orders WHERE customer_id BETWEEN 100 AND 200;
SELECT * FROM orders WHERE order_date >= '2026-01-01';
SELECT * FROM orders ORDER BY order_date DESC LIMIT 20;
SELECT * FROM orders WHERE customer_email LIKE 'user123%';  -- prefix!


-- =============================================
-- Contoh query yang TIDAK bisa pakai B-tree index:
-- =============================================
SELECT * FROM orders WHERE customer_email LIKE '%gmail.com'; -- suffix!
SELECT * FROM orders WHERE UPPER(product_name) = 'LAPTOP';  -- function!
SELECT * FROM orders WHERE total_amount + 1000 > 50000;     -- expression!
💡 B-tree dan AUTO_INCREMENT

Primary key berbasis auto-increment (SERIAL, AUTO_INCREMENT) secara otomatis membuat B-tree index. Ini menghindari masalah page split karena data selalu ditambah di ujung kanan tree — sangat efisien untuk INSERT.

3. Hash Index — Lookup Cepat untuk Equality

Hash Index menggunakan fungsi hash untuk memetakan nilai ke bucket. Hanya mendukung operator = (equality), tetapi sangat cepat — O(1) lookup. Tidak mendukung range query atau sorting.

SQL — Hash Index
-- =============================================
-- HASH INDEX (PostgreSQL)
-- =============================================
-- PostgreSQL mendukung hash index sejak versi 10
-- (sebelumnya tidak reliable karena tidak WAL-logged)

CREATE INDEX idx_orders_email_hash
ON orders USING hash (customer_email);

-- Query yang cocok untuk hash index:
SELECT * FROM orders WHERE customer_email = 'user1234@email.com';

-- Query yang TIDAK cocok:
-- SELECT * FROM orders WHERE customer_email > 'user5000@email.com';  -- range!
-- SELECT * FROM orders ORDER BY customer_email;                       -- sort!
-- SELECT * FROM orders WHERE customer_email LIKE 'user123%';          -- prefix!


-- =============================================
-- HASH INDEX di MySQL (MEMORY engine)
-- =============================================
-- MySQL InnoDB TIDAK mendukung hash index eksplisit
-- Tapi InnoDB punya "adaptive hash index" (otomatis)
-- MEMORY engine mendukung HASH index:

CREATE TABLE sessions_memory (
    session_id  VARCHAR(128) NOT NULL,
    user_id     INT NOT NULL,
    data        TEXT,
    expires_at  TIMESTAMP NOT NULL,
    PRIMARY KEY (session_id) USING HASH
) ENGINE=MEMORY;


-- =============================================
-- PERBANDINGAN: B-tree vs Hash
-- =============================================
-- ┌──────────────┬─────────────┬─────────────┐
-- │ Operasi      │ B-tree      │ Hash        │
-- ├──────────────┼─────────────┼─────────────┤
-- │ = (equal)    │ ✅ Bagus    │ ✅ Terbaik  │
-- │ < > <= >=    │ ✅ Bagus    │ ❌ Tidak    │
-- │ BETWEEN      │ ✅ Bagus    │ ❌ Tidak    │
-- │ ORDER BY     │ ✅ Bagus    │ ❌ Tidak    │
-- │ LIKE 'abc%'  │ ✅ Bagus    │ ❌ Tidak    │
-- │ IS NULL      │ ✅ Bagus    │ ✅ Bagus    │
-- │ IN (...))    │ ✅ Bagus    │ ✅ Bagus    │
-- │ Ukuran index │ Lebih besar │ Lebih kecil │
-- │ Lookup time  │ O(log n)    │ O(1)        │
-- └──────────────┴─────────────┴─────────────┘
⚠️ Kapan Pakai Hash Index?

Gunakan hash index HANYA jika query Anda selalu menggunakan equality (=) dan TIDAK PERNAH range, sort, atau prefix LIKE. Untuk sebagian besar kasus, B-tree tetap lebih fleksibel dan performanya cukup baik.

4. Composite Index — Multi-Kolom

Composite Index (multi-column index) adalah index yang mencakup lebih dari satu kolom. Ini sangat powerful tetapi ada aturan penting tentang urutan kolom.

SQL — Composite Index
-- =============================================
-- MEMBUAT COMPOSITE INDEX
-- =============================================

-- Index pada category + order_date
CREATE INDEX idx_orders_cat_date
ON orders (category, order_date);

-- Index pada status + category + date
CREATE INDEX idx_orders_status_cat_date
ON orders (order_status, category, order_date);


-- =============================================
-- ATURAN "LEFTMOST PREFIX"
-- =============================================
-- Index: (category, order_date)

-- ✅ Query ini MEMANFAATKAN index (pakai kolom pertama):
SELECT * FROM orders WHERE category = 'Elektronik';
SELECT * FROM orders WHERE category = 'Elektronik' AND order_date >= '2026-01-01';
SELECT * FROM orders WHERE category = 'Elektronik' ORDER BY order_date;

-- ❌ Query ini TIDAK memanfaatkan index (skip kolom pertama):
SELECT * FROM orders WHERE order_date >= '2026-01-01';  -- tidak ada category!
SELECT * FROM orders ORDER BY order_date;               -- tidak ada category!


-- =============================================
-- URUTAN KOLOM SANGAT PENTING!
-- =============================================
-- Index (A, B) ≠ Index (B, A)

-- Index idx_cat_date = (category, order_date)
-- Mendukung: WHERE category = ?, ORDER BY order_date
-- Tidak mendukung: WHERE order_date = ? (tanpa category)

-- Index idx_date_cat = (order_date, category)
-- Mendukung: WHERE order_date = ?, ORDER BY category
-- Tidak mendukung: WHERE category = ? (tanpa order_date)


-- =============================================
-- PRINSIP: Urutkan kolom berdasarkan selektivitas
-- =============================================
-- Kolom dengan lebih banyak nilai unik (kardinalitas tinggi) → duluan
-- Contoh:
--   customer_id (jutaan unik) → SANGAT selektif → duluan
--   status (hanya 4 nilai)    → kurang selektif   → belakangan

-- ✅ BAGUS: selektif duluan
CREATE INDEX idx_orders_cust_status
ON orders (customer_id, order_status);

-- ❌ KURANG BAIUS: kurang selektif duluan
CREATE INDEX idx_orders_status_cust
ON orders (order_status, customer_id);


-- =============================================
-- GABUNGAN: Composite + Expression
-- =============================================
CREATE INDEX idx_orders_cust_lower_status
ON orders (customer_id, LOWER(order_status));

Visualisasi Composite Index

Diagram: Composite Index (category, order_date)
┌─────────────────────────────────────────────────────────────────┐
│  COMPOSITE INDEX: (category, order_date)                       │
│                                                                 │
│  Category level:         Date level (sorted per category):     │
│  ┌─────────────┐        ┌──────────────────────────┐           │
│  │ Elektronik  │───────▶│ 2026-01-05 → row_ptr_3   │           │
│  │ Fashion     │        │ 2026-01-12 → row_ptr_7   │           │
│  │ Kesehatan   │        │ 2026-02-20 → row_ptr_1   │           │
│  │ Makanan     │        └──────────────────────────┘           │
│  │ Rumah       │                                                │
│  └─────────────┘                                                │
│                                                                 │
│  Query: WHERE category='Elektronik' AND order_date >= '2026-02'│
│  → Langsung ke 'Elektronik' → scan date dari 2026-02           │
│  → Tidak perlu scan Fashion, Kesehatan, dll!                   │
└─────────────────────────────────────────────────────────────────┘

5. Covering Index — Tanpa Table Lookup

Covering Index adalah index yang mencakup SEMUA kolom yang dibutuhkan oleh query. Dengan covering index, database bisa menjawab query hanya dari index saja — tanpa perlu mengakses tabel utama (heap). Ini disebut Index-Only Scan.

SQL — Covering Index
-- =============================================
-- TANPA covering index: butuh table access
-- =============================================

-- Query:
SELECT customer_id, total_amount, order_status
FROM orders
WHERE category = 'Elektronik' AND order_date >= '2026-01-01';

-- Index yang ada: idx_orders_cat_date (category, order_date)
-- Database harus:
--   1. Cari di index → dapat row pointers
--   2. Akses tabel utama (heap) untuk ambil customer_id, total_amount, order_status
-- Langkah 2 ini = "table lookup" atau "heap fetch" → LAMBAT


-- =============================================
-- DENGAN covering index: index-only scan!
-- =============================================

-- Tambahkan kolom yang dibutuhkan ke index
CREATE INDEX idx_orders_covering1
ON orders (category, order_date, customer_id, total_amount, order_status);

-- Sekarang query di atas hanya perlu baca index!
-- Tidak perlu akses tabel utama sama sekali → JAUH lebih cepat


-- =============================================
-- PostgreSQL: INCLUDE clause (lebih efisien)
-- =============================================
-- Kolom di INCLUDE tidak diurutkan, tapi tetap tersimpan di leaf nodes
-- Lebih kecil dan lebih cepat dari menambah ke key columns

CREATE INDEX idx_orders_covering2
ON orders (category, order_date)
INCLUDE (customer_id, total_amount, order_status);

-- Kolom key (diurutkan): category, order_date
-- Kolom include (hanya disimpan): customer_id, total_amount, order_status


-- =============================================
-- MySQL: Tidak ada INCLUDE, tapi composite index bisa jadi covering
-- =============================================
-- MySQL 8.0: cukup tambahkan semua kolom ke composite index
CREATE INDEX idx_orders_covering_mysql
ON orders (category, order_date, customer_id, total_amount, order_status);

Kapan Covering Index Tepat?

Situasi Rekomendasi
Query yang sangat sering dijalankan✅ Buat covering index
Query hanya butuh 3-5 kolom✅ Tambahkan ke index
Query SELECT banyak kolom (10+)❌ Tidak praktis — index terlalu besar
Tabel sering di-INSERT/UPDATE⚠️ Hati-hati — index besar lambat untuk write
Tabel sangat besar (100jt+ baris)✅ Sangat bermanfaat — hemat banyak I/O

6. Partial Index — Index Bersyarat

Partial Index (filtered index) hanya mengindeks baris yang memenuhi kondisi tertentu. Ini menghasilkan index yang lebih kecil dan lebih cepat, terutama ketika Anda hanya tertarik pada subset data.

SQL — Partial Index (PostgreSQL)
-- =============================================
-- PARTIAL INDEX: Hanya untuk data yang relevan
-- =============================================

-- Misalnya 90% pesanan sudah delivered, hanya 10% yang pending
-- Query kita hampir selalu mencari yang pending
-- Index semua baris = boros, lebih baik partial index

CREATE INDEX idx_orders_pending
ON orders (customer_id, order_date)
WHERE order_status = 'pending';

-- Index hanya berisi baris WHERE order_status = 'pending'
-- → Lebih kecil → lebih cepat → lebih hemat storage


-- =============================================
-- Partial Index untuk data tidak NULL
-- =============================================

-- Hanya index data yang sudah di-shipping
CREATE INDEX idx_orders_shipped_date
ON orders (ship_date)
WHERE ship_date IS NOT NULL;


-- =============================================
-- Partial Unique Index
-- =============================================

-- Pastikan email unik HANYA untuk user aktif
CREATE UNIQUE INDEX idx_unique_active_email
ON orders (customer_email)
WHERE order_status != 'cancelled';


-- =============================================
-- Query yang cocok dengan partial index:
-- =============================================
SELECT * FROM orders
WHERE order_status = 'pending' AND customer_id = 1234;

SELECT * FROM orders
WHERE order_status = 'pending' AND order_date >= '2026-06-01';


-- =============================================
-- SQL Server: Filtered Index (sama konsep)
-- =============================================
CREATE NONCLUSTERED INDEX idx_orders_pending_sqlserver
ON orders (customer_id, order_date)
WHERE order_status = 'pending';
💡 MySQL Tidak Mendukung Partial Index

MySQL (hingga versi 8.x) tidak mendukung partial index. Alternatif: gunakan composite index yang mencakup kolom filter, atau gunakan PostgreSQL/SQL Server jika partial index sangat dibutuhkan.

7. Expression Index — Index Fungsi

Expression Index (functional index) mengindeks hasil dari sebuah ekspresi atau fungsi, bukan nilai kolom mentah. Ini berguna ketika query menggunakan fungsi pada kolom.

SQL — Expression Index
-- =============================================
-- MASALAH: Fungsi pada WHERE mematikan index
-- =============================================

-- Index pada customer_email ada, tapi...
SELECT * FROM orders WHERE LOWER(customer_email) = 'user1234@email.com';
-- ❌ Index TIDAK terpakai karena LOWER() pada kolom!

-- Solusi: Expression Index
CREATE INDEX idx_orders_lower_email
ON orders (LOWER(customer_email));

-- Sekarang query di atas ✅ menggunakan index!


-- =============================================
-- CONTOH LAINNYA
-- =============================================

-- Index pada tahun order
CREATE INDEX idx_orders_year
ON orders (EXTRACT(YEAR FROM order_date));

SELECT * FROM orders WHERE EXTRACT(YEAR FROM order_date) = 2026;


-- Index pada panjang nama produk
CREATE INDEX idx_orders_name_length
ON orders (LENGTH(product_name));

SELECT * FROM orders WHERE LENGTH(product_name) > 20;


-- Index pada date truncation
CREATE INDEX idx_orders_month
ON orders (DATE_TRUNC('month', order_date));

SELECT * FROM orders WHERE DATE_TRUNC('month', order_date) = '2026-06-01';


-- Index pada kombinasi kolom dengan fungsi
CREATE INDEX idx_orders_category_lower
ON orders (category, LOWER(product_name));

SELECT * FROM orders
WHERE category = 'Elektronik' AND LOWER(product_name) LIKE '%laptop%';


-- =============================================
-- MySQL 8.0: Expression Index (generated columns)
-- =============================================
-- MySQL tidak mendukung expression index langsung,
-- tapi bisa pakai generated column + index:

ALTER TABLE orders
ADD COLUMN email_lower VARCHAR(200) GENERATED ALWAYS AS (LOWER(customer_email)) STORED;

CREATE INDEX idx_email_lower_mysql ON orders (email_lower);

SELECT * FROM orders WHERE email_lower = 'user1234@email.com';

8. EXPLAIN ANALYZE — Membaca Query Plan

EXPLAIN menunjukkan bagaimana database mengeksekusi query Anda — termasuk apakah index digunakan atau tidak. Ini adalah tool terpenting untuk optimasi query.

SQL — EXPLAIN ANALYZE
-- =============================================
-- EXPLAIN: Lihat rencana eksekusi
-- =============================================

-- PostgreSQL
EXPLAIN SELECT * FROM orders WHERE customer_id = 1234;
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 1234;
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT * FROM orders WHERE customer_id = 1234;

-- MySQL
EXPLAIN SELECT * FROM orders WHERE customer_id = 1234;
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 1234;


-- =============================================
-- MEMBACA HASIL EXPLAIN (PostgreSQL)
-- =============================================
-- Cari operasi berikut di output EXPLAIN:

-- 1. Index Scan → ✅ BAGUS (menggunakan index + akses tabel)
-- 2. Index Only Scan → ✅ TERBAIK (hanya baca index, tidak akses tabel)
-- 3. Bitmap Index Scan → ✅ CUKUP BAGUS (kombinasi multiple index)
-- 4. Seq Scan → ⚠️ HATI-HATI (full table scan!)

-- Contoh output EXPLAIN ANALYZE:
/*
Seq Scan on orders  (cost=0.00..9570.00 rows=1 width=168) (actual time=0.015..45.230 rows=1 loops=1)
  Filter: (customer_id = 1234)
  Rows Removed by Filter: 499999
Planning Time: 0.050 ms
Execution Time: 45.250 ms
*/
-- ↑ Seq Scan! Tidak ada index pada customer_id yang cocok


-- Setelah membuat index:
/*
Index Scan using idx_orders_customer_id on orders  (cost=0.42..8.44 rows=1 width=168)
  (actual time=0.020..0.022 rows=1 loops=1)
  Index Cond: (customer_id = 1234)
Planning Time: 0.080 ms
Execution Time: 0.035 ms
*/
-- ↑ Index Scan! 1000x lebih cepat!


-- =============================================
-- MySQL: Membaca EXPLAIN output
-- =============================================
-- Kolom penting di EXPLAIN MySQL:
--   type:  const > eq_ref > ref > range > index > ALL
--   key:   nama index yang digunakan (NULL = tidak pakai index)
--   rows:  estimasi baris yang di-scan
--   Extra: Using index = covering index ✅
--          Using filesort = perlu sort manual ⚠️
--          Using temporary = buat tabel sementara ⚠️

EXPLAIN SELECT * FROM orders
WHERE category = 'Elektronik' AND order_date >= '2026-01-01';
-- Cek kolom "type": ref = bagus, ALL = buruk!
-- Cek kolom "key": idx_orders_cat_date = index digunakan ✅


-- =============================================
-- STATISTICS: Update statistik untuk query plan akurat
-- =============================================
-- PostgreSQL:
ANALYZE orders;

-- MySQL:
ANALYZE TABLE orders;

9. Anti-Pattern: Kesalahan Umum Index

Kesalahan 1: Terlalu Banyak Index

SQL — Anti-Pattern: Index Berlebihan
-- ❌ SALAH: Membuat index untuk setiap kolom
CREATE INDEX idx_1 ON orders (customer_id);
CREATE INDEX idx_2 ON orders (order_date);
CREATE INDEX idx_3 ON orders (category);
CREATE INDEX idx_4 ON orders (order_status);
CREATE INDEX idx_5 ON orders (ship_city);
CREATE INDEX idx_6 ON orders (total_amount);
-- Setiap INSERT/UPDATE/DELETE = update 6 index! Sangat lambat!

-- ✅ BENAR: Composite index yang dirancang untuk query spesifik
CREATE INDEX idx_main ON orders (customer_id, order_date);
CREATE INDEX idx_cat_status ON orders (category, order_status, order_date);

-- Cara cek index yang ada:
-- PostgreSQL:
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders';

-- MySQL:
SHOW INDEX FROM orders;


-- =============================================
-- CARI INDEX YANG TIDAK PERNAH DIPAKAI
-- =============================================

-- PostgreSQL: Index usage statistics
SELECT
    schemaname, relname AS table_name,
    indexrelname AS index_name,
    idx_scan AS times_used,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan ASC;
-- Index dengan idx_scan = 0 → TIDAK PERNAH dipakai → hapus!

Kesalahan 2: Urutan Kolom Salah

SQL — Anti-Pattern: Urutan Kolom
-- ❌ Index kurang selektif duluan
CREATE INDEX idx_bad ON orders (order_status, category, customer_id);
-- order_status hanya punya 4 nilai → tidak selektif

-- ✅ Index selektif duluan
CREATE INDEX idx_good ON orders (customer_id, category, order_status);
-- customer_id punya ribuan nilai → sangat selektif

-- Kecuali query Anda SELALU filter order_status duluan,
-- maka urutan di index harus mengikuti urutan WHERE clause


-- =============================================
-- Cek selektivitas kolom
-- =============================================
SELECT
    'customer_id' AS kolom,
    COUNT(DISTINCT customer_id) AS nilai_unik,
    COUNT(*) AS total_baris,
    ROUND(COUNT(DISTINCT customer_id) * 100.0 / COUNT(*), 2) AS selektivitas_pct
FROM orders
UNION ALL
SELECT
    'category',
    COUNT(DISTINCT category),
    COUNT(*),
    ROUND(COUNT(DISTINCT category) * 100.0 / COUNT(*), 2)
FROM orders
UNION ALL
SELECT
    'order_status',
    COUNT(DISTINCT order_status),
    COUNT(*),
    ROUND(COUNT(DISTINCT order_status) * 100.0 / COUNT(*), 2)
FROM orders;
-- customer_id: ~2% selektif (sangat bagus → duluan)
-- category: ~0.001% selektif (kurang bagus → belakangan)
-- order_status: ~0.0008% selektif (sangat kurang → paling belakang)

Kesalahan 3: Function pada Kolom Terindeks

SQL — Anti-Pattern: Function di WHERE
-- ❌ Fungsi pada kolom mematikan index
SELECT * FROM orders WHERE YEAR(order_date) = 2026;
SELECT * FROM orders WHERE DATE(order_date) = '2026-06-26';
SELECT * FROM orders WHERE UPPER(category) = 'ELEKTRONIK';
SELECT * FROM orders WHERE total_amount * 1.1 > 1000000;

-- ✅ Rewrite tanpa fungsi pada kolom
SELECT * FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01';
SELECT * FROM orders WHERE order_date >= '2026-06-26' AND order_date < '2026-06-27';
SELECT * FROM orders WHERE category = 'Elektronik';  -- asumsi data konsisten
SELECT * FROM orders WHERE total_amount > 1000000 / 1.1;  -- pindahkan ke konstanta

-- Atau gunakan expression index jika memang butuh fungsi:
CREATE INDEX idx_orders_year ON orders (EXTRACT(YEAR FROM order_date));

10. Studi Kasus Optimasi

SQL — Studi Kasus: E-Commerce Performance
-- =============================================
-- SKENARIO: Dashboard admin e-commerce
-- Query lambat, perlu dioptimasi
-- =============================================

-- Query 1: Laporan penjualan per kategori bulan ini
-- SEBELUM (lambat - 3 detik):
SELECT category, COUNT(*) AS orders, SUM(total_amount) AS revenue
FROM orders
WHERE order_date >= '2026-06-01' AND order_date < '2026-07-01'
  AND order_status != 'cancelled'
GROUP BY category;

-- Analisis EXPLAIN: Seq Scan + filesort
-- Solusi: Composite index yang mencakup semua kebutuhan
CREATE INDEX idx_orders_report_cat
ON orders (order_date, order_status, category, total_amount);

-- SESUDAH (cepat - 50ms): Index Only Scan!


-- Query 2: Detail pesanan customer tertentu
-- SEBELUM (lambat - 1.5 detik):
SELECT * FROM orders
WHERE customer_id = 5678
ORDER BY order_date DESC
LIMIT 20;

-- Solusi: Index dengan sort order
CREATE INDEX idx_orders_cust_date_desc
ON orders (customer_id, order_date DESC);

-- SESUDAH (cepat - 2ms): Index Scan dengan sort sudah tersedia


-- Query 3: Cari pesanan berdasarkan email
-- SEBELUM:
SELECT * FROM orders WHERE customer_email = 'user5678@email.com';

-- Solusi: Hash index (kalau hanya equality) atau B-tree
CREATE INDEX idx_orders_email ON orders (customer_email);

-- SESUDAH (cepat - 1ms): Index Scan


-- =============================================
-- MONITORING: Index bloat (PostgreSQL)
-- =============================================
SELECT
    schemaname, tablename, indexname,
    pg_size_pretty(pg_relation_size(indexid)) AS index_size,
    idx_scan AS scans,
    idx_tup_read AS tuples_read,
    idx_tup_fetch AS tuples_fetched
FROM pg_stat_user_indexes
JOIN pg_index USING (indexrelid)
WHERE schemaname = 'public'
ORDER BY pg_relation_size(indexid) DESC;


-- =============================================
-- MAINTENANCE: Reindex (PostgreSQL)
-- =============================================
-- Index bisa "bloat" seiring waktu (terutama B-tree)
-- Lakukan REINDEX secara berkala:
REINDEX INDEX idx_orders_cat_date;
-- Atau untuk semua index di tabel:
REINDEX TABLE orders;

-- PostgreSQL modern: gunakan CONCURRENTLY agar tidak lock tabel
REINDEX INDEX CONCURRENTLY idx_orders_cat_date;

11. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang SQL Indexes:

Pertanyaan 1: Tipe index default di PostgreSQL dan MySQL InnoDB adalah?

a) Hash Index
b) B-tree Index
c) GIN Index
d) Bitmap Index

Pertanyaan 2: Composite index (A, B, C) bisa digunakan oleh query dengan kondisi?

a) WHERE B = 1 AND C = 2
b) WHERE A = 1 AND C = 2
c) WHERE C = 2
d) WHERE A = 1 AND B = 2

Pertanyaan 3: Apa keuntungan utama Covering Index?

a) Mengurangi ukuran database
b) Memungkinkan Index-Only Scan (tanpa akses tabel utama)
c) Mendukung semua jenis query
d) Menghapus kebutuhan primary key

Pertanyaan 4: Hash Index TIDAK mendukung operasi?

a) Equality (=)
b) Range query (>, <, BETWEEN)
c) IS NULL
d) IN (...)

Pertanyaan 5: Mengapa query WHERE YEAR(order_date) = 2026 tidak menggunakan index?

a) Karena YEAR() bukan fungsi SQL
b) Karena fungsi pada kolom mencegah penggunaan index B-tree
c) Karena 2026 bukan tahun valid
d) Karena index hanya untuk kolom VARCHAR
🔍 Zoom
100%
🎨 Tema