Pagination Patterns — Database

Mengapa Pagination? Mengembalikan semua data sekaligus boros memori dan lambat. Pagination membagi data menjadi halaman-halaman kecil yang lebih efisien. 1…

Pagination adalah membagi hasil query jadi halaman-halaman kecil, misalnya 5 pesanan per halaman, alih-alih mengirim 100.000 pesanan sekaligus ke browser. Caranya ada dua, dan bedanya mirip dua cara melanjutkan baca buku. Cara pertama mengingat nomor halaman: "aku sampai halaman 37". Cara kedua memakai pembatas buku: "aku berhenti di sini". Kalau seseorang menyelipkan beberapa halaman baru di depan buku, halaman 37 sekarang isinya beda, tapi pembatas bukumu tetap di tempat yang benar.

🧪 Tebakan cepat: riwayat pesanan Kedai Senja menampilkan pesanan terbaru dulu, 5 per halaman. Kamu buka halaman 1 dan melihat pesanan #12 sampai #8. Saat itu pesanan baru #13 masuk. Lalu kamu buka halaman 2. Pesanan pertama di halaman 2 itu #7, #8, atau #3? Tebak dulu, jawabannya ada di bagian "Data bergeser: muncul dua kali atau terlewat".

Tabel yang kita pakai

Tabel orders Kedai Senja berisi 12 pesanan, id 1 sampai 12. Halaman riwayat menampilkan yang terbaru dulu, jadi urutannya ORDER BY id DESC: #12, #11, #10, dan seterusnya sampai #1.

Offset pagination: LIMIT dan OFFSET

-- halaman 1
SELECT * FROM orders ORDER BY id DESC LIMIT 5 OFFSET 0;   -- #12, #11, #10, #9, #8
-- halaman 2
SELECT * FROM orders ORDER BY id DESC LIMIT 5 OFFSET 5;   -- #7, #6, #5, #4, #3
-- halaman 3
SELECT * FROM orders ORDER BY id DESC LIMIT 5 OFFSET 10;  -- #2, #1

Nilai OFFSET dihitung dari nomor halaman: OFFSET = (halaman - 1) × perHalaman. Halaman 1 melewati 0 baris, halaman 2 dengan 10 per halaman melewati 10 baris, halaman 5 dengan 20 per halaman melewati 80 baris.

Cara ini namanya offset-based pagination, dan inilah yang memakai nomor halaman. Di Laravel, Order::orderByDesc('id')->paginate(5) membaca ?page=2 dari URL dan mengirim LIMIT 5 OFFSET 5, ditambah satu query COUNT(*) untuk menghitung jumlah halaman. simplePaginate(5) melewatkan query COUNT(*) itu, jadi cuma ada tombol sebelumnya dan berikutnya.

Halaman jauh makin lambat

OFFSET gak bisa langsung melompat. Untuk OFFSET 99990, database membaca 99.990 baris, lalu membuang semuanya, baru mengambil 10 baris berikutnya. Index gak menolong, karena database tetap harus menghitung baris satu per satu.

EXPLAIN QUERY PLAN
SELECT * FROM orders ORDER BY id DESC LIMIT 10 OFFSET 99990;
SCAN orders

Di Lab, dengan 100.000 pesanan di SQLite sungguhan, halaman 1 butuh sekitar 0,1 ms dan halaman 10.000 sekitar 1,5 ms: belasan kali lebih lambat, padahal isinya sama-sama 10 baris. Angka pastinya tergantung perangkatmu. Di database production yang barisnya lebar dan query-nya pakai JOIN, selisih ini bisa jadi detik.

Data bergeser: muncul dua kali atau terlewat

Kamu sudah melihat halaman 1: #12 sampai #8. Lalu pesanan #13 masuk, dan kamu minta halaman 2 dengan OFFSET 5. Sekarang 5 baris terbaru adalah #13, #12, #11, #10, #9, jadi yang dilewati ya lima itu:

halaman 2: #8, #7, #6, #5, #4

#8 muncul lagi di halaman 2, padahal sudah tampil di halaman 1. Itu jawaban tebakannya. Kebalikannya juga bisa terjadi: kalau pesanan #12 dihapus setelah kamu melihat halaman 1, OFFSET 5 melompati #7, dan #7 gak pernah tampil sama sekali.

Cursor pagination: ingat posisi terakhir

Alih-alih menghitung baris yang dilewati, simpan id baris terakhir yang sudah tampil, lalu minta baris sesudahnya:

-- halaman 1
SELECT * FROM orders ORDER BY id DESC LIMIT 5;                -- #12 ... #8
-- halaman berikutnya: setelah #8, baris terakhir yang tampil
SELECT * FROM orders WHERE id < 8 ORDER BY id DESC LIMIT 5;   -- #7 ... #3

Di Laravel: Order::orderByDesc('id')->cursorPaginate(5). URL-nya membawa ?cursor=..., teks yang menyimpan id terakhir tadi, dan query-nya WHERE id < ? ORDER BY id DESC LIMIT 6. Baris keenam cuma untuk mengecek apakah masih ada halaman berikutnya.

Harganya: cursor pagination gak bisa melompat ke "halaman 37", cuma ada sebelumnya dan berikutnya. Kolom urutannya juga harus unik, misalnya id.

Pilih yang mana

Pakai offset (paginate) Pakai cursor (cursorPaginate)
Butuh nomor halaman, misalnya tabel di halaman admin Infinite scroll dan feed
Datanya kecil atau jarang berubah Tabel besar, ratusan ribu baris atau lebih
Pengguna perlu lompat ke halaman tertentu Data terus bertambah saat orang sedang membaca

⚠️ Jebakan yang sering ketemu

TL;DR: Offset pagination memakai LIMIT dan OFFSET, dengan OFFSET = (halaman - 1) × perHalaman. Dia memakai nomor halaman (paginate() di Laravel), tapi makin lambat di halaman jauh karena database membaca lalu membuang semua baris sebelum offset, dan baris bisa muncul dua kali atau terlewat kalau data bertambah atau terhapus saat orang pindah halaman. Cursor pagination (atau keyset) menyimpan id terakhir dan meminta WHERE id < 8 ORDER BY id DESC LIMIT 5 (cursorPaginate() di Laravel): cepat di halaman berapa pun dan stabil, tapi gak bisa lompat ke nomor halaman.

Yang akan kamu pelajari