Pada tabel yang terus membesar, OFFSET pagination biasanya menjadi sumber masalah performa yang terasa nyata: halaman awal masih cepat, tetapi page tinggi makin lambat, urutan hasil kadang tidak stabil, dan beban query meningkat. Untuk kasus feed, timeline, daftar aktivitas, atau daftar transaksi yang terus bertambah, cursor pagination di Laravel umumnya lebih cocok karena database tidak perlu menghitung lalu melewati banyak baris setiap kali pindah halaman.
Intinya, cursor pagination mengganti pola "lompat ke halaman ke-N" menjadi "ambil data setelah item terakhir yang sudah saya lihat". Pendekatan ini lebih efisien untuk dataset besar, terutama jika pengurutan dan indeksnya benar. Namun, ada syarat penting: kolom pengurutan harus stabil, idealnya unik atau dibuat unik secara efektif dengan kombinasi seperti created_at dan id.
Kapan OFFSET Pagination Mulai Bermasalah
OFFSET cocok untuk banyak kasus sederhana, terutama tabel kecil atau halaman admin internal dengan jumlah data terbatas. Masalah muncul ketika tabel tumbuh besar dan query berjalan terus-menerus pada urutan yang sama, misalnya feed artikel, log audit, notifikasi, atau riwayat transaksi.
Gejala nyata di produksi
- Page tinggi makin lambat. Query
LIMIT 20 OFFSET 200000memaksa database melewati banyak baris sebelum mengembalikan 20 data. - Beban query membesar. Walaupun hasil yang diambil sedikit, pekerjaan internal database tetap besar.
- Sort tidak stabil. Jika urutan hanya berdasarkan kolom yang tidak unik, data bisa terduplikasi atau terlewat antar halaman.
- Hasil berubah saat ada insert baru. Pada feed aktif, data baru yang masuk di depan dapat menggeser posisi baris sehingga halaman berikutnya tidak konsisten.
Contoh pola OFFSET di Laravel
// Pola umum paginate berbasis OFFSET
$posts = Post::query()
->orderByDesc('created_at')
->paginate(20);Secara konseptual, query SQL-nya akan mirip seperti ini:
SELECT *
FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 200000;Untuk halaman awal, ini sering masih terasa aman. Tetapi untuk halaman tinggi, database tetap harus memproses banyak baris sebelum sampai ke offset yang diminta. Pada feed besar, pola ini cenderung tidak efisien.
Cara Kerja Cursor Pagination
Cursor pagination tidak memakai OFFSET. Sebagai gantinya, aplikasi mengirim penunjuk posisi berdasarkan nilai kolom urutan dari item terakhir pada halaman sebelumnya. Query berikutnya menjadi "ambil 20 data setelah titik ini".
Contoh konsep query
Misalkan feed diurutkan dari terbaru ke terlama berdasarkan created_at dan id:
SELECT *
FROM posts
WHERE (created_at < '2025-06-01 10:00:00')
OR (created_at = '2025-06-01 10:00:00' AND id < 98765)
ORDER BY created_at DESC, id DESC
LIMIT 20;Secara praktis, ini jauh lebih efisien karena database bisa langsung mulai dari posisi yang relevan, terutama bila ada indeks yang sesuai. Ia tidak perlu menghitung lalu melewati ratusan ribu baris terlebih dahulu.
Contoh di Laravel
$posts = Post::query()
->orderByDesc('created_at')
->orderByDesc('id')
->cursorPaginate(20);Laravel akan menghasilkan URL pagination berbasis cursor, bukan nomor halaman biasa. Ini cocok untuk API feed, infinite scroll, timeline, dan daftar data besar yang diakses berurutan.
Catatan: Cursor pagination tidak ditujukan untuk kebutuhan utama "langsung lompat ke halaman 500". Jika antarmuka Anda sangat bergantung pada nomor halaman absolut, OFFSET mungkin masih diperlukan. Untuk feed besar, biasanya kebutuhan itu tidak dominan.
Syarat Pengurutan yang Aman: Jangan Hanya Mengandalkan Kolom yang Tidak Unik
Kesalahan paling umum saat migrasi ke cursor pagination adalah memakai urutan yang tidak stabil. Misalnya hanya ORDER BY created_at DESC, padahal banyak baris bisa memiliki created_at yang sama. Akibatnya, data bisa duplikat atau terlewat saat pengguna memuat halaman berikutnya.
Masalah jika urutan tidak unik
Anggap ada beberapa post dengan timestamp identik sampai detik yang sama. Jika cursor hanya menyimpan created_at, maka posisi antar baris dengan nilai sama menjadi ambigu. Database tahu kelompok timestamp tersebut, tetapi tidak punya aturan pasti untuk memilih urutan internal yang konsisten.
Solusi yang aman
Gunakan urutan gabungan yang menghasilkan posisi unik dan stabil, misalnya:
ORDER BY created_at DESC, id DESCORDER BY published_at DESC, id DESCORDER BY score DESC, id DESCjika memang feed diurutkan berdasarkan skor
Dengan pendekatan ini, jika created_at sama, id menjadi tie-breaker. Ini penting bukan hanya untuk cursor pagination, tetapi juga untuk menjaga konsistensi urutan hasil secara umum.
Contoh Eloquent yang benar
$posts = Post::query()
->where('status', 'published')
->orderByDesc('created_at')
->orderByDesc('id')
->cursorPaginate(20);Secara konseptual, SQL-nya akan menyerupai:
SELECT id, title, created_at
FROM posts
WHERE status = 'published'
ORDER BY created_at DESC, id DESC
LIMIT 20;Untuk halaman berikutnya, kondisi cursor akan menambahkan pembatas berdasarkan kombinasi created_at dan id.
Index yang Sesuai: created_at Saja atau created_at,id?
Cursor pagination baru memberi hasil maksimal jika urutan dan indeks saling mendukung. Tanpa indeks yang tepat, query tetap bisa mahal meskipun sudah tidak memakai OFFSET.
Minimal: indeks pada kolom urutan utama
Jika Anda mengurutkan berdasarkan created_at, indeks pada created_at adalah dasar yang penting. Ini membantu database menavigasi urutan waktu dengan lebih efisien.
Lebih aman untuk urutan gabungan: indeks komposit
Jika query menggunakan:
ORDER BY created_at DESC, id DESCmaka indeks komposit pada (created_at, id) sering lebih tepat daripada hanya indeks tunggal di created_at. Alasannya, database dapat memanfaatkan struktur indeks yang sesuai dengan pola pengurutan dan pencarian cursor.
Contoh migrasi indeks
Schema::table('posts', function ($table) {
$table->index(['created_at', 'id']);
});Jika feed juga selalu difilter, pertimbangkan urutan indeks berdasarkan pola query nyata. Misalnya bila hampir semua query adalah:
WHERE status = 'published'
ORDER BY created_at DESC, id DESCmaka dalam banyak kasus indeks komposit yang memasukkan status bisa relevan. Namun, desain indeks harus mengikuti query produksi yang paling sering dipakai, bukan sekadar menambahkan banyak indeks tanpa evaluasi. Setiap indeks punya biaya tulis dan biaya penyimpanan.
Prinsip praktis: indeks sebaiknya selaras dengan kombinasi
WHEREdanORDER BYyang benar-benar digunakan oleh endpoint feed.
Mengganti OFFSET Pagination ke Cursor Pagination di Laravel
Sebelum
public function index()
{
$posts = Post::query()
->where('status', 'published')
->orderByDesc('created_at')
->paginate(20);
return view('posts.index', compact('posts'));
}Sesudah
public function index()
{
$posts = Post::query()
->where('status', 'published')
->orderByDesc('created_at')
->orderByDesc('id')
->cursorPaginate(20);
return view('posts.index', compact('posts'));
}Perubahan kuncinya:
paginate()diganti menjadicursorPaginate().- Urutan dibuat stabil dengan menambahkan
idsebagai tie-breaker. - Indeks disesuaikan dengan pola urutan dan filter.
Untuk API JSON
Cursor pagination sangat natural untuk API karena klien cukup menyimpan cursor dari respons sebelumnya lalu memakainya untuk request berikutnya. Ini umum dipakai pada mobile app atau infinite scroll di web.
public function index()
{
return Post::query()
->where('status', 'published')
->orderByDesc('created_at')
->orderByDesc('id')
->cursorPaginate(20);
}Di sisi klien, pola yang dipakai bukan lagi ?page=2, tetapi URL atau parameter cursor yang diberikan oleh respons sebelumnya.
Perbandingan Performa Secara Konseptual
OFFSET pagination
- Mudah dipahami.
- Cocok untuk daftar kecil atau kebutuhan lompat ke halaman tertentu.
- Semakin mahal pada page tinggi karena database harus melewati banyak baris.
- Rentan hasil tidak konsisten pada feed yang aktif berubah.
Cursor pagination
- Lebih efisien untuk dataset besar dan akses berurutan.
- Lebih stabil pada feed yang terus menerima data baru.
- Sangat cocok untuk infinite scroll dan API.
- Kurang cocok jika UI sangat bergantung pada nomor halaman absolut.
Jadi, jika kasus Anda adalah feed besar yang terus bertambah, cursor pagination biasanya lebih masuk akal. Jika kasus Anda adalah tabel admin kecil dengan kebutuhan page number navigation yang jelas, OFFSET masih bisa diterima.
Jebakan Umum dan Cara Debug
1. Data duplikat atau terlewat
Penyebab paling sering adalah urutan tidak unik atau berubah-ubah. Pastikan ada tie-breaker yang konsisten, misalnya created_at lalu id.
2. Query masih lambat setelah pakai cursor
Biasanya masalah ada pada indeks. Cursor pagination bukan pengganti indeks. Periksa apakah kolom pada WHERE dan ORDER BY didukung indeks yang relevan.
3. Mengurutkan dengan kolom yang nilainya sering berubah
Jika feed diurutkan berdasarkan kolom yang sering diperbarui, posisi item bisa berpindah antar request. Ini dapat menyebabkan pengalaman paginasi terasa aneh. Untuk feed besar, urutan berbasis waktu publikasi atau ID sering lebih stabil dibanding skor yang berubah terus-menerus, kecuali Anda memang siap menangani dinamika itu.
4. Menggabungkan cursor dengan orderBy yang tidak konsisten
Semua request untuk endpoint yang sama harus memakai urutan yang identik. Jika ada cabang kode yang kadang menambah orderBy lain atau mengubah arah sort, cursor sebelumnya bisa menjadi tidak valid secara logis.
5. Salah mengukur performa
Jangan hanya melihat waktu respons aplikasi. Periksa juga query yang dijalankan, rencana eksekusi, dan apakah database memakai indeks yang diharapkan. Pada praktiknya, perbaikan cursor pagination paling terasa pada page tinggi dan trafik query berulang ke feed besar.
Checklist Migrasi Aman di Produksi
- Identifikasi endpoint yang bermasalah
Fokus pada feed, timeline, log, notifikasi, atau daftar besar yang page tingginya sering diakses. - Pastikan urutan stabil
Jangan hanya memakaicreated_atjika nilainya tidak unik. Tambahkanidsebagai tie-breaker. - Tambahkan indeks yang sesuai
Minimal indeks pada kolom urutan utama. Untuk urutan gabungan, pertimbangkan indeks komposit seperti(created_at, id). - Uji hasil halaman berurutan
Verifikasi tidak ada item duplikat atau hilang saat berpindah cursor. - Uji pada data yang aktif berubah
Simulasikan insert baru saat pengguna sedang melakukan paginasi untuk memastikan perilaku tetap masuk akal. - Periksa kontrak API dan frontend
Frontend yang sebelumnya mengandalkanpage=2harus disesuaikan untuk mengikuti cursor dari respons. - Rollout bertahap
Bila memungkinkan, terapkan pada endpoint tertentu atau di balik feature flag agar mudah dibandingkan dan di-rollback. - Monitor query dan error
Pantau latensi endpoint, pola query database, dan laporan duplikasi/kehilangan item dari pengguna atau log aplikasi.
Kapan Tetap Memakai OFFSET
Cursor pagination bukan jawaban untuk semua kasus. OFFSET masih relevan jika:
- dataset kecil atau sedang dan performa masih baik,
- pengguna benar-benar perlu lompat ke halaman tertentu,
- urutan data tidak cocok untuk cursor yang stabil,
- halaman lebih bersifat administratif daripada feed real-time.
Namun untuk feed besar yang terus tumbuh, terutama yang diakses secara berurutan dari data terbaru ke lama, cursor pagination di Laravel adalah pilihan yang lebih efisien dan lebih stabil.
Penutup
Jika Anda melihat gejala seperti halaman tinggi makin lambat, query list makin berat, atau hasil pagination tidak konsisten pada data yang terus berubah, itu tanda kuat bahwa OFFSET mulai tidak cocok. Migrasi ke cursor pagination di Laravel biasanya cukup sederhana di level kode, tetapi keberhasilannya sangat bergantung pada dua hal: urutan yang stabil dan indeks yang sesuai.
Mulailah dari endpoint feed yang paling berat, ubah urutan menjadi deterministik seperti created_at DESC, id DESC, tambahkan indeks yang relevan, lalu uji perilaku di bawah data yang aktif berubah. Dengan langkah ini, Anda tidak hanya mengurangi beban query, tetapi juga membuat feed besar tetap cepat dan konsisten saat tabel terus membesar.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!