Back to blog

Pagination Yang Bunuh Database

Pagination kelihatannya simple, tapi salah implementasi bisa bikin query PostgreSQL makin lama makin berat

backenddatabasepostgresqldjangoperformance

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
  • Instagram
  • TikTok
  • Facebook

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 πŸ”₯