Kita tahu indexing dapat mempercepat pengambilan data pada database. Tapi bagaimana sebenarnya index bekerja?
Inilah yang perlu kita ketahui tentang indexing pada Postgres:
- Bagaimana Postgres menyimpan data, membangun index dan menggunakannya?
- Bagaimana mengetahui kapan index diperlukan?
- Bagaimana cara kita membangun index yang efektif dan mengubah query yang lambat menjadi secepat kilat?
Skema DB
Misal kita punya sebuah database dari suatu sistem peminjaman buku sederhana dengan 3 tabel: users
, books
, dan borrowings
.
CREATE TABLE users (
id uuid NOT NULL,
roles jsonb NOT NULL DEFAULT '[]'::jsonb,
email varchar(255) NOT NULL,
CONSTRAINT users_email_unique UNIQUE (email),
CONSTRAINT users_pkey PRIMARY KEY (id)
);
CREATE TABLE books (
id uuid NOT NULL,
title varchar(255) NOT NULL,
description text NOT NULL,
CONSTRAINT pk_books_id PRIMARY KEY (id)
);
CREATE TABLE borrowings (
id uuid NOT NULL,
borrower_id uuid NOT NULL,
book_id uuid NOT NULL,
borrow_date timestamptz NOT NULL,
must_return_date timestamptz NOT NULL,
return_date timestamptz NULL,
CONSTRAINT pk_borrowings_id PRIMARY KEY (id)
);
ALTER TABLE borrowings ADD CONSTRAINT fk_borrowings_book_id_books_id FOREIGN KEY (book_id) REFERENCES books(id);
ALTER TABLE borrowings ADD CONSTRAINT fk_borrowings_borrower_id_users_id FOREIGN KEY (borrower_id) REFERENCES users(id);
Setelah kita seeds 100 users
, 1.000 borrowings
, dan 10.000 books
, kita mendapatkan sizes sebagai berikut:
table_name | total | table | index
------------+-------+-------+------
books | 6.4M | 6.1M | 400K
borrowings | 200K | 144K | 56K
users | 112K | 80K | 32K
Cara Postgres Menyimpan dan Membaca Data
Tabel books
memiliki total size 6.4MB. Ukuran 6.4MB ini tersusun dari banyak page data berukuran masing-masing 8KB. Jadi records pada tabel books
tersimpan sebagai ±800 pages.
🔑 Ketika kita menjalankan suatu query, Postgres akan mengoptimasi query tersebut dengan cost seminimal mungkin. Ada banyak faktor yang mempengaruhi cost. Salah satu faktor yang paling berpengaruh adalah jumlah page yang terlibat pada suatu query. Semakin banyak page yang di scan, semakin besar cost-nya. Di sisi kita, tentu semakin besar cost, semakin lama pula execution time-nya.
Misal kita mau mencari buku dengan judul Buku Ejnirza
.
Kita jalankan query berikut:
select * from books b where title = 'Buku Ejnirza'
Kita pakai explain analyze
untuk mendapatkan query plan-nya.
explain analyze
select * from books b where title = 'Buku Ejnirza'
Result-nya sebagai berikut:
Seq Scan on books b (cost=0.00..895.00 rows=1 width=583) (actual time=0.890..2.828 rows=1 loops=1)
Filter: ((title)::text = 'Buku Ejnirza'::text)
Rows Removed by Filter: 9999
Planning time: 0.117 ms
Execution time: 2.852 ms
Mari kita perhatikan result di atas.
Seq Scan
menunjukkan bahwa query ini melakukan scanning pada 800 pages / 10.000 rows dan 'membuang' 9.999 sisanya.
Cost
-nya 895.00.
Dan memakan waktu sekitar 2 detik lebih. Cukup lama untuk mengambil 1 data saja.
Di sinilah index diperlukan
Kita coba tambahkan index untuk kolom title
.
CREATE INDEX ix_books_title ON books USING btree (title);
Lalu kita jalankan ulang query yang sama.
explain analyze
select * from books b where title = 'Buku Ejnirza'
Kita cek query plan-nya:
Index Scan using ix_books_title on books b (cost=0.29..8.30 rows=1 width=583) (actual time=0.016..0.017 rows=1 loops=1)
Index Cond: ((title)::text = 'Buku Ejnirza'::text)
Planning time: 0.127 ms
Execution time: 0.026 ms
Dengan Index Scan
:
Cost
-nya turun drastis dari 895.00 menjadi 8.30
Execution time
-nya juga lebih cepat — dari 2.852 ms menjadi 0.026 ms.
Apakah ini keajaiban? Mungkin semacam itu.😆
Tapi ada harga yang harus dibayar.
Mari kita perhatikan sizes dari tabel kita sekarang:
table_name | total | table | index
------------+-------+-------+------
books | 6.8M | 6.1M | 728K
borrowings | 200K | 144K | 56K
users | 112K | 80K | 32K
Index dari tabel books
meningkat size-nya.
Karena yang terjadi di sini adalah query membaca data dalam index yang "hanya" 41 page. Tentu jauh lebih cepat jika dibandingkan query sebelumnya yang membaca data 800 page.
🔑 Jadi kita "membayar" dengan storage untuk mendapatkan query yang lebih cepat.
Index yang lebih efektif
Kita coba query lain. Misalnya kita mau mengetahui Buku Orkenji
sedang dipinjam oleh berapa user.
Kita jalankan query berikut:
explain analyze
select
count(*)
from
borrowings b
inner join books b2 on
b2.id = b.book_id
where
b2.title = 'Buku Orkenji'
Result-nya sebagai berikut:
Aggregate (cost=35.94..35.95 rows=1 width=8) (actual time=0.145..0.146 rows=1 loops=1)
-> Hash Join (cost=8.31..35.94 rows=1 width=0) (actual time=0.111..0.144 rows=2 loops=1)
Hash Cond: (b.book_id = b2.id)
-> Seq Scan on borrowings b (cost=0.00..25.00 rows=1000 width=16) (actual time=0.003..0.055 rows=1000 loops=1)
-> Hash (cost=8.30..8.30 rows=1 width=16) (actual time=0.009..0.009 rows=1 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
-> Index Scan using ix_books_title on books b2 (cost=0.29..8.30 rows=1 width=16) (actual time=0.007..0.007 rows=1 loops=1)
Index Cond: ((title)::text = 'Buku Orkenji'::text)
Planning time: 0.145 ms
Execution time: 0.165 ms
Kita bisa melihat bahwa index ix_books_title
yang kita buat sebelumnya berkontribusi pada optimasi query ini.
Kita menemukan juga Seq Scan
pada tabel borrowings
.
🔑 Sepertinya ini clue bahwa index bisa membantu.
Kita tambahkan index untuk book_id
pada borrowings
:
CREATE INDEX ix_borrowings_book_id ON borrowings USING btree (book_id);
Lalu kita ulangi query sebelumnya:
explain analyze
select
count(*)
from
borrowings b
inner join books b2 on
b2.id = b.book_id
where
b2.title = 'Buku Orkenji'
Dan inilah hasilnya:
Aggregate (cost=16.61..16.62 rows=1 width=8) (actual time=0.021..0.022 rows=1 loops=1)
-> Nested Loop (cost=0.56..16.61 rows=1 width=0) (actual time=0.018..0.020 rows=2 loops=1)
-> Index Scan using ix_books_title on books b2 (cost=0.29..8.30 rows=1 width=16) (actual time=0.008..0.008 rows=1 loops=1)
Index Cond: ((title)::text = 'Buku Orkenji'::text)
-> Index Only Scan using ix_borrowings_book_id on borrowings b (cost=0.28..8.29 rows=1 width=16) (actual time=0.009..0.010 rows=2 loops=1)
Index Cond: (book_id = b2.id)
Heap Fetches: 2
Planning time: 0.242 ms
Execution time: 0.039 ms
Terjadi penurunan Execution time yang signifikan ya, dari 0.165 ms menjadi 0.039 ms.
Jumlah cost-nya juga berkurang jauh.
Case selanjutnya. Kita jalankan query berikut:
explain analyze
select
count(*)
from
borrowings b
where
return_date is null
and borrower_id = '03229391-cce9-44d6-8a36-705c90d55e10'
Query plan-nya:
Aggregate (cost=27.52..27.54 rows=1 width=8) (actual time=0.291..0.292 rows=1 loops=1)
-> Seq Scan on borrowings b (cost=0.00..27.50 rows=10 width=0) (actual time=0.035..0.286 rows=10 loops=1)
Filter: ((return_date IS NULL) AND (borrower_id = '03229391-cce9-44d6-8a36-705c90d55e10'::uuid))
Rows Removed by Filter: 990
Planning time: 0.093 ms
Execution time: 0.327 ms
Tentu kita tahu apa yang perlu kita lakukan.
Misalnya ternyata kita perlu menambahkan filter pada query tersebut menjadi:
explain analyze
select
count(*)
from
borrowings b
where
return_date is null
and borrower_id = '03229391-cce9-44d6-8a36-705c90d55e10' and book_id = '05938c72-1774-4a03-be68-218a69d24c22'
Apakah kita masih perlu menambahkan index untuk menghindari Seq Scan? Kita cek saja query plan-nya.
Aggregate (cost=8.30..8.31 rows=1 width=8) (actual time=0.038..0.039 rows=1 loops=1)
-> Index Scan using ix_borrowings_book_id on borrowings b (cost=0.28..8.29 rows=1 width=0) (actual time=0.030..0.033 rows=1 loops=1)
Index Cond: (book_id = '05938c72-1774-4a03-be68-218a69d24c22'::uuid)
Filter: ((return_date IS NULL) AND (borrower_id = '03229391-cce9-44d6-8a36-705c90d55e10'::uuid))
Planning time: 0.182 ms
Execution time: 0.097 ms
Ternyata tidak perlu.
Karena Seq Scan tidak muncul dan andai kita tetap menambahkan index, hanya akan menambah storage yang terpakai.
Jadi...
Setelah kita mengerti (mengingat kembali) bagaimana index bekerja, barangkali kita akan menangani optimasi query dengan lebih baik ke depannya.
Mengerti bagaimana data disimpan dan dibaca, kapan index diperlukan, dan bagaimana index yang efektif.
Gunakan ini dan kita akan dapat mempercepat pengambilan data pada database dengan optimal.
Top comments (0)