Paginasi Berbasis Kursor vs Paginasi Offset: Mana yang Sebaiknya Digunakan API Anda

Perbandingan paginasi berbasis kursor dan paginasi offset: pergeseran halaman, biaya offset dalam, Keyset SQL, contoh Stripe dan Slack, serta cara menguji keduanya di Apidog.

INEZA Felin-Michel

INEZA Felin-Michel

31 August 2026

Paginasi Berbasis Kursor vs Paginasi Offset: Mana yang Sebaiknya Digunakan API Anda

Apidog untuk Perusahaan

Penerapan On-Premises

SSO & RBAC

Sesuai SOC 2

Jelajahi Apidog Enterprise

Setiap endpoint daftar pada akhirnya menghadapi pertanyaan yang sama: bagaimana Anda membagi 2 juta pesanan menjadi halaman-halaman yang dapat dijelajahi oleh klien? Pilih pagination offset dan Anda akan mendapatkan SQL yang sederhana serta nomor halaman yang dipahami pengguna. Pilih pagination berbasis kursor dan Anda akan mendapatkan hasil yang stabil serta latensi yang konsisten di kedalaman mana pun, tetapi Anda kehilangan fitur "loncat ke halaman 47."

Sebagian besar tim memilih offset karena itu adalah pengaturan default di setiap tutorial. Kemudian tabel pesanan mencapai beberapa juta baris, halaman 4.000 mulai mengalami batas waktu, dan pengguna melaporkan melihat catatan yang sama dua kali saat menggulir. Panduan ini membahas cara kerja kedua gaya, di mana offset gagal, mengapa Stripe dan Slack menggunakan kursor, dan cara menguji kedua gaya dengan permintaan berantai di Apidog. Pada akhirnya Anda akan tahu persis mana yang cocok untuk endpoint Anda.

Jika Anda ingin gambaran yang lebih luas terlebih dahulu, panduan pagination API kami mencakup setiap strategi secara berdampingan. Artikel ini membahas secara mendalam dua hal yang paling penting.

Bagaimana pagination offset bekerja

Pagination offset memetakan langsung ke SQL. Klien mengirimkan nomor halaman dan ukuran halaman; server menerjemahkannya menjadi LIMIT dan OFFSET.

SELECT id, customer_id, total_cents, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 25 OFFSET 50;

Kueri itu mengembalikan halaman 3 dari daftar pesanan Anda dengan 25 baris per halaman. Permintaan terlihat seperti ini:

GET /v1/orders?page=3&per_page=25

Dan respons tipikal:

{
  "data": [
    {
      "id": "ord_8821",
      "customer_id": "cus_1932",
      "total_cents": 4599,
      "created_at": "2026-08-30T14:22:07Z"
    }
  ],
  "page": 3,
  "per_page": 25,
  "total": 1848203,
  "total_pages": 73929
}

Daya tariknya jelas. Klien dapat melompat ke halaman mana pun. Server dapat mengembalikan jumlah total. Setiap pengembang dapat membangunnya dalam waktu singkat. Untuk tabel admin kecil, ini adalah pilihan yang tepat, dan panduan langkah demi langkah kami untuk pagination di REST API menjelaskan pembangunan offset secara lengkap.

Namun offset membawa dua masalah struktural, dan tidak ada satupun yang muncul dalam pengembangan. Keduanya muncul dalam produksi.

Masalah 1: pergeseran halaman (page drift)

Offset menghitung baris dari bagian atas hasil yang diurutkan. Ia tidak tahu baris mana yang sudah dilihat klien. Jadi, ketika baris dimasukkan atau dihapus di antara permintaan, halaman-halaman bergeser di bawah klien.

Misalnya, seorang pengguna memuat halaman 1 pesanan yang diurutkan dari terbaru, baris 1 hingga 25. Saat mereka membaca, 3 pesanan baru tiba. Mereka meminta halaman 2, yaitu OFFSET 25. Baris 23, 24, dan 25 dari respons pertama kini telah didorong ke posisi 26 hingga 28. Pengguna melihatnya lagi. Duplikat.

Penghapusan membalik keadaannya. Hapus 3 baris dari halaman 1 saat pengguna membacanya, dan OFFSET 25 sekarang melewatkan 3 baris yang tidak pernah dilihat pengguna. Kehilangan data diam-diam, dan tidak ada yang mendapatkan kesalahan.

Untuk laporan bulanan yang tidak ada yang menggulir secara real time, pergeseran tidak berbahaya. Untuk umpan aktivitas, endpoint sinkronisasi, atau apa pun yang dijelajahi skrip halaman demi halaman saat penulisan berlanjut, pergeseran berarti catatan yang duplikat atau hilang. Konsumen akan menyadarinya.

Masalah 2: offset mendalam memindai semua yang mereka lewati

OFFSET 500000 tidak berteleportasi ke baris 500.001. Basis data menelusuri indeks melalui setengah juta entri, membuangnya, dan kemudian mengembalikan 25 baris Anda. Biaya tumbuh secara linear dengan kedalaman: O(n) di mana n adalah offset.

Angka konkret menjadikan ini nyata. Pada tabel pesanan Postgres dengan 2 juta baris dan indeks pada created_at:

Tulisan tentang tanpa offset oleh Markus Winand di Use The Index, Luke menunjukkan biaya ini dengan rencana kueri dan patut dibaca sepenuhnya. Pola dalam produksi adalah log kueri lambat yang didominasi oleh permintaan offset tinggi, seringkali dari satu perayap yang dengan setia menjelajahi setiap halaman API publik Anda. Satu klien saja, dan p99 Anda berlipat ganda.

Bagaimana pagination berbasis kursor bekerja

Pagination berbasis kursor, juga disebut pagination keyset, menghilangkan penghitung baris. Alih-alih "lewati 50 baris," klien mengatakan "berikan saya baris setelah catatan spesifik ini." Kursor mengidentifikasi baris terakhir yang dilihat klien, sehingga server dapat langsung mencari ke batch berikutnya.

SQL menggunakan perbandingan baris pada kunci urut alih-alih OFFSET:

SELECT id, customer_id, total_cents, created_at
FROM orders
WHERE (created_at, id) < ('2026-08-30T14:22:07Z', 'ord_8821')
ORDER BY created_at DESC, id DESC
LIMIT 25;

Perhatikan perbandingan dua kolom. created_at saja tidak unik; dua pesanan dapat mendarat di milidetik yang sama, dan kunci urut yang tidak unik berarti baris dilewati atau diulang pada batas halaman. Menambahkan id sebagai pemecah seri (tiebreaker) membuat urutan menjadi total dan pagination menjadi tepat. Dengan indeks komposit pada (created_at, id), basis data langsung mencari ke batas dan membaca 25 entri. Halaman 1 dan halaman 60.000 memiliki biaya yang sama.

Namun, API tidak boleh mengekspos nilai mentah tersebut. Implementasi nyata menyandikan kunci urut ke dalam token opak, biasanya base64:

GET /v1/orders?limit=25&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNDoyMjowN1oiLCJpZCI6Im9yZF84ODIxIn0

Opasitas adalah keputusan desain, bukan pengaburan demi dirinya sendiri. Klien yang tidak dapat mengurai kursor tidak dapat membuat URL secara manual, yang memberi Anda kebebasan untuk mengubah kunci urut, menambahkan petunjuk shard, atau mengganti mesin penyimpanan tanpa merusak siapa pun. Kontraknya menjadi "kembalikan apa yang kami berikan kepada Anda," tidak lebih.

Kerugiannya: tidak ada halaman 47. Sebuah kursor hanya tahu "setelah baris ini," sehingga klien bergerak maju (dan mundur, jika Anda mengeluarkan kursor sebelumnya) satu halaman pada satu waktu. Jumlah total juga tidak datang secara gratis; penghitungan adalah kueri terpisah. Untuk desain di mana kumpulan data itu sendiri sangat besar, panduan kami tentang mendesain pagination API untuk jutaan catatan membahas sisi skalabilitas secara lebih mendalam.

Perbandingan singkat

Dimensi Pagination offset Pagination berbasis kursor
Loncat ke halaman acak Ya, nomor halaman berapa pun Tidak, hanya penelusuran berurutan
Jumlah total / jumlah halaman Murah untuk disertakan Kueri hitungan terpisah
Kinerja halaman dalam (deep-page) O(n), menurun dengan kedalaman O(1) per halaman di kedalaman berapa pun
Stabilitas di bawah penulisan Bergeser: duplikat dan celah Stabil, tertambat pada baris
Biaya pembangunan Sangat mudah (Trivial) Sedang: penyandian (encoding), pemecah seri (tiebreakers), desain indeks
Persyaratan pengurutan ORDER BY apa pun berfungsi Membutuhkan kunci urut yang unik dan terindeks
Penyimpanan cache URL halaman Mudah, URL dapat diprediksi Lebih sulit, kursor bervariasi per penelusuran
Kompleksitas klien Rendah Rendah, jika envelopenya bersih

Satu hal halus dalam tabel itu layak ditekankan: pagination kursor menuntut pengurutan yang deterministik. Jika endpoint Anda memungkinkan klien mengurutkan berdasarkan kolom yang dapat diubah dan tidak unik seperti status, logika keyset akan menjadi sulit dengan cepat. Offset mentolerir pengurutan yang kurang rapi; kursor menghukumnya.

Mana yang harus Anda pilih?

Sesuaikan gayanya dengan cara data dikonsumsi.

Tabel admin dan dasbor: offset. Alat internal dengan beberapa ribu baris, manusia mengklik nomor halaman, dan hitungan "1.848 hasil" yang terlihat. Pergeseran tidak masalah, kedalaman tetap dangkal, dan fitur loncat ke halaman adalah fitur nyata. Offset unggul dalam biaya pembangunan.

Umpan gulir tak terbatas (infinite scroll): kursor. Tidak ada yang loncat ke halaman 47 dari sebuah umpan. Pengguna hanya memuat "lebih banyak," penulisan terjadi terus-menerus, dan duplikat terlihat serta memalukan. Ini adalah kasus kursor yang paling sesuai.

API publik: kursor. Anda tidak mengontrol konsumen Anda. Seseorang akan menulis loop yang menelusuri setiap halaman, dan dengan offset, halaman dalam menjadi masalah Anda pada jam 3 pagi. Kursor membuat setiap halaman murah dan memungkinkan Anda mengembangkan internal di balik token opak. Panduan pagination REST API kami membahas konvensi URL dan header secara detail.

Ekspor dan pekerjaan sinkronisasi: kursor. Pekerjaan batch yang menarik semua 2 juta pesanan membutuhkan dua jaminan: tidak ada baris yang terlewat meskipun ada penulisan bersamaan, dan biaya per halaman yang tetap. Offset tidak menyediakan keduanya. Kursor juga memberi Anda titik melanjutkan gratis ketika pekerjaan terhenti pada baris 1,4 juta.

Aturan praktis yang jujur: offset untuk antarmuka kecil, yang dijelajahi manusia, dan banyak hitungan; kursor untuk apa pun yang besar, langsung, atau publik.

Bagaimana API nyata menanganinya

Stripe sepenuhnya berbasis kursor. Setiap endpoint daftar menerima starting_after (ID objek) dan limit, dan responsnya mencakup has_more. Untuk mengambil halaman biaya berikutnya, Anda meneruskan ID biaya terakhir yang Anda terima. Dokumentasi pagination Stripe menunjukkan polanya; perhatikan tidak ada jumlah total di mana pun, sebuah penghilangan yang disengaja mengingat volume penulisan mereka.

REST API GitHub masih mengekspos page dan per_page pada sebagian besar endpoint, dengan header Link yang menunjuk ke halaman berikutnya dan terakhir. Namun baca dokumentasi pagination GitHub dengan cermat: mereka menginstruksikan klien untuk mengikuti header Link secara harfiah alih-alih membuat URL halaman, dan endpoint yang lebih baru telah beralih ke kursor, persis karena penelusuran offset yang dalam pada repositori besar sangat merugikan.

Slack memigrasikan Web API-nya ke pagination kursor dan sekarang menandainya sebagai pendekatan yang digunakan semua metode baru. Metode seperti conversations.history mengembalikan response_metadata.next_cursor, dan string kursor kosong berarti Anda telah mencapai akhir, seperti yang dijelaskan dalam dokumentasi pagination Slack.

Tiga API dengan lalu lintas tinggi, dan arah perjalanannya searah: menuju kursor.

Mendesain amplop respons

Sebuah API kursor berhasil atau gagal tergantung pada amplopnya. Buatlah sederhana dan dapat diprediksi:

{
  "data": [
    {
      "id": "ord_8846",
      "customer_id": "cus_2201",
      "total_cents": 12900,
      "created_at": "2026-08-30T16:01:44Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNjowMTo0NFoiLCJpZCI6Im9yZF84ODQ2In0"
}

Empat aturan membuatnya kokoh:

Menguji kedua gaya di Apidog

Bug pagination tersembunyi di batas-batas: halaman terakhir, halaman kosong, kursor yang baris penambatnya terhapus. Klik manual tidak akan menangkapnya, tetapi skenario pengujian berantai akan, dan di sinilah Apidog mendapatkan tempatnya dalam alur kerja.

Untuk endpoint kursor, bangun skenario pengujian dengan dua langkah:

  1. Panggil endpoint dan ekstrak kursor. Tambahkan post-processor ke permintaan pertama dengan JSONPath $.next_cursor, dan simpan dalam variabel seperti nextCursor. Apidog memungkinkan Anda menyalin JSONPath langsung dari panel respons; panduan lengkapnya ada di cara mengatur assertion dan mengekstrak variabel dengan JSONPath.
  2. Ulangi permintaan halaman berikutnya. Bungkus permintaan kedua dalam langkah ForEach atau loop, teruskan {{nextCursor}} sebagai parameter kursor, ekstrak ulang $.next_cursor setiap iterasi, dan keluar saat has_more adalah false. Pastikan di setiap langkah bahwa tidak ada id yang berulang dari halaman sebelumnya dan ukuran halaman tidak pernah melebihi limit.

Untuk endpoint offset, struktur yang sama berlaku dengan variabel penghitung: tambahkan page, pastikan panjang data sama dengan per_page hingga halaman terakhir, dan pastikan total tetap konsisten selama penelusuran.

Kemudian tambahkan kasus-kasus tepi sebagai langkah-langkahnya sendiri, masing-masing dengan assertion eksplisit:

Setelah skenario lulus secara lokal, jalankan di CI pada setiap penggabungan. Unduh Apidog secara gratis dan Anda dapat memiliki skenario penelusuran kursor lengkap, termasuk loop dan assertion, berjalan dalam waktu kurang dari setengah jam.

FAQ

Apakah pagination kursor selalu lebih baik?

Tidak. Offset lebih cocok ketika pengguna membutuhkan nomor halaman, total, dan akses acak pada kumpulan data yang sederhana, yang menggambarkan sebagian besar alat admin internal. Kursor lebih baik ketika kumpulan datanya besar, penulisan sering, atau API bersifat publik. Mode kegagalan adalah menggunakan offset sebagai default untuk endpoint daftar publik dan menemukan biaya O(n) setelah diluncurkan.

Bagaimana saya mendapatkan jumlah total dengan pagination kursor?

Jalankan SELECT COUNT(*) terpisah dengan filter yang sama, baik sebagai endpoint yang berbeda atau parameter kueri opt-in seperti include_count=true. Lakukan caching secara agresif; jumlah perkiraan yang diperbarui setiap menit memenuhi hampir setiap UI. Stripe sepenuhnya melewatkan total, yang menunjukkan seberapa sering klien benar-benar membutuhkannya.

Bisakah saya menawarkan kedua gaya pagination pada satu endpoint?

Anda bisa, dan GitHub secara efektif melakukannya selama masa transisinya, tetapi hindarilah pada API baru. Dua gaya berarti dua set kasus tepi, dua matriks pengujian, dan kebingungan klien tentang mana yang harus digunakan. Pilih satu per endpoint. Jika Anda merancang kontrak dari awal, pola dalam panduan pagination REST API kami akan menjaga konsistensi penamaan parameter di seluruh antarmuka Anda.

Apa yang terjadi jika baris penambat kursor dihapus?

Dengan pagination keyset, tidak ada yang rusak. Perbandingan WHERE (created_at, id) < (?, ?) tidak mengharuskan baris penambat ada; ia mencari ke posisi batas dan melanjutkan. Ini adalah keuntungan nyata dibandingkan desain "kursor sebagai pencarian baris", dan ini persis kasus tepi yang patut ditegaskan dalam skenario pengujian Apidog Anda sebelum konsumen menemukannya untuk Anda.

Mengembangkan API dengan Apidog

Apidog adalah alat pengembangan API yang membantu Anda mengembangkan API dengan lebih mudah dan efisien.