Pagination Yang Bunuh Database
Pagination kelihatannya simple, tapi salah implementasi bisa bikin query PostgreSQL makin lama makin berat
Pagination Yang Bunuh Database
Awalnya gw kira pagination itu:
tinggal kasih LIMIT dan OFFSET π
Simple.
Kayak:
SELECT * FROM donation ORDER BY created_at DESC LIMIT 20 OFFSET 1000;
Kelihatannya aman.
Kelihatannya normal.
Kelihatannya scalable.
Tapi ternyataβ¦
pagination juga bisa jadi pembunuh database π
Semua Terasa Aman di Awal
Pas data masih:
100 row
query pagination cepat banget.
Pas:
1000 row
masih aman.
Developer jadi pede:
βsip amanβ
π
Lalu Data Production Datang
Tiba-tiba tabel jadi:
5 juta row
Dan endpoint list mulai:
- random lambat
- CPU database naik
- query makin berat
- latency naik terus
Padahal query-nya:
LIMIT OFFSET doang π
Masalah OFFSET
Contoh:
LIMIT 20 OFFSET 100000
Kelihatannya:
ambil 20 row
Padahal PostgreSQL harus:
- scan row sebelumnya
- skip 100 ribu row dulu
- baru ambil 20
π
Jadi makin besar OFFSET:
makin mahal query-nya.
Analoginya
Bayangin lu baca buku.
User bilang:
"gw mau halaman 500"
Database gak bisa teleport.
Dia tetap harus:
- buka halaman awal
- lewatin satu-satu
- baru sampai halaman 500
π
Kenapa Ini Bahaya
Karena biasanya pagination dipakai di:
- transaction history
- donation list
- activity feed
- logs
- timeline
- dashboard
Yang datanya:
terus nambah.
Artinya query lama-lama makin berat sendiri.
Query Yang Dulu Cepat Jadi Monster
Awalnya:
20ms
Lama-lama:
300ms
Terus:
2 detik π
Padahal code gak berubah sama sekali.
Yang berubah cuma:
jumlah data
π
Classic Django Pagination
Biasanya orang bikin:
queryset = Donation.objects.all()
paginator = Paginator(queryset, 20)
Kelihatannya aman.
Tapi di belakang layar:
- ada COUNT(*)
- ada OFFSET
- ada sorting
Dan kalau row jutaan:
database mulai menangis π
COUNT(*) Itu Kadang Mahal
Banyak orang lupa:
queryset.count()
itu bukan magic.
Di PostgreSQL:
SELECT COUNT(*)
Dan di tabel besar?
bisa mahal banget π
Apalagi kalau:
- filter kompleks
- join banyak
- query belum indexed
Solusi Yang Lebih Waras: Cursor Pagination
Daripada:
OFFSET 100000
lebih bagus:
WHERE id < last_id LIMIT 20
π₯
Ini biasa disebut:
- cursor pagination
- keyset pagination
Contoh Cursor Pagination
Misalnya:
Donation.objects.filter( id__lt=last_id ).order_by("-id")[:20]
Artinya:
ambil data setelah item terakhir sebelumnya.
Bukan skip dari awal lagi.
Kenapa Cursor Pagination Lebih Cepat
Karena database bisa:
- langsung lompat via index
- gak perlu scan ribuan row
- gak perlu skip massive data
Dan performanya jauh lebih stabil.
OFFSET Pagination Masih Boleh Gak?
Boleh.
Kalau:
- data kecil
- admin panel
- internal tools
- row belum banyak
OFFSET pagination itu:
simple dan nyaman.
Masalahnya muncul pas:
scale π
Feed Besar Biasanya Pakai Cursor
Makanya:
- Twitter/X
- TikTok
jarang pakai OFFSET besar.
Karena feed massive:
OFFSET jadi mahal banget.
Yang Sering Dilupakan
Pagination bukan cuma soal:
LIMIT 20
Tapi juga:
- sorting
- indexing
- counting
- filtering
- scanning
- memory usage
Dan semua itu bisa jadi bottleneck.
Index Itu Penting
Kalau cursor pagination:
WHERE created_at < ...
Pastikan:
created_at
punya index.
Kalau gak:
tetap scan π
Pagination + ORDER BY = Bahaya
Contoh:
ORDER BY created_at DESC
Kalau gak ada index:
PostgreSQL harus sorting massive row π
Dan itu bisa brutal banget.
Pelajaran Yang Gw Dapet
Dulu gw pikir:
pagination pasti aman
Ternyata:
pagination juga bisa jadi sumber latency.
Sekarang tiap bikin endpoint list, gw selalu mikir:
- data bakal seberapa besar?
- OFFSET bakal sampai mana?
- COUNT(*) mahal gak?
- sorting sudah indexed?
- perlu cursor gak?
Karena pagination yang terlihat innocent bisa berubah jadi:
monster production π
Penutup
Pagination itu deceptively simple.
Awalnya:
LIMIT 20 OFFSET 0
kelihatan harmless.
Tapi pas data makin besar:
- query makin berat
- CPU naik
- latency naik
- database ngos-ngosan
Dan kadang problem production terbesar ternyata cuma:
pagination yang salah π₯