Pagination

Pagination #

Pagination hampir selalu dianggap sebagai fitur wajib di halaman admin. Data ratusan ribu atau jutaan baris “terlihat aman” karena dibagi per halaman: 10, 25, atau 50 row per page.

Namun di balik UI yang rapi, pagination—terutama di admin—sering menjadi silent performance killer, terutama karena query COUNT(*) yang dieksekusi setiap request.

Artikel ini akan membedah:

  • Pola pagination admin yang umum
  • Kenapa COUNT(*) sangat bermasalah
  • Kenapa kadang muncul 2 query count sekaligus
  • Dampaknya pada database besar
  • Best practice pagination tanpa (atau minim) COUNT

Pola Pagination Admin yang Umum #

Secara sederhana, pagination admin biasanya melakukan dua hal:

  1. Mengambil data untuk halaman tertentu
  2. Menghitung total data untuk ditampilkan di UI

Contoh pola query klasik:

  • Query data:

    SELECT * FROM orders
    WHERE status = 'PAID'
    ORDER BY created_at DESC
    LIMIT 20 OFFSET 40;
    
  • Query count:

    SELECT COUNT(*) FROM orders WHERE status = 'PAID';
    

Hasilnya dipakai untuk:

  • Menentukan total halaman
  • Menampilkan teks seperti: Showing 41–60 of 12,483 records

Masalahnya: query count inilah sumber petaka.


Kenapa Kadang Ada 2 Query COUNT? #

Di banyak sistem admin modern, tanpa disadari, COUNT dieksekusi dua kali.

Beberapa penyebab umum:

1. Backend & ORM #

ORM sering melakukan:

  • COUNT(*) untuk pagination
  • COUNT(*) lagi untuk validasi page (misalnya page > total page?)

2. Frontend Admin Framework #

Framework admin (React Admin, Ant Design Table, dll) sering:

  • Meminta total count untuk pagination
  • Meminta ulang count saat filter / sort berubah

3. Query Terpisah untuk Filter #

Contoh:

  • Count total data
  • Count data setelah filter

Padahal keduanya melakukan full scan tabel yang sama.


Kenapa Query COUNT Sangat Bermasalah? #

1. COUNT Tidak Selalu Bisa Pakai Index #

Terutama jika:

  • Ada WHERE dengan kolom tidak di-index
  • Ada LIKE '%keyword%'
  • Ada fungsi di WHERE (DATE(created_at))

Akibatnya:

  • Database melakukan full table scan
  • Membaca jutaan row hanya untuk angka

2. COUNT Itu Blocking & Mahal #

COUNT bukan operasi ringan:

  • Membaca data page demi page
  • Mengunci resource I/O
  • Menghabiskan CPU

Pada tabel besar, COUNT bisa:

  • Lebih lambat dari query SELECT ... LIMIT
  • Mengganggu query lain (terutama di primary DB)

3. COUNT Tidak Cache-Friendly #

  • Data admin sering berubah
  • Cache count cepat invalid
  • Akhirnya COUNT tetap dieksekusi ulang

Ilusi Aman: “Kan Cuma Admin” #

Ini jebakan klasik.

Admin sering:

  • Query tanpa limit filter ketat
  • Akses data lintas waktu (historical)
  • Dipakai untuk audit, export, dan investigasi

Jumlah admin sedikit tidak relevan jika:

  • Query berat
  • Data besar
  • Jalan di jam sibuk

Satu admin page bisa mengganggu seluruh sistem.


Best Practice Pagination Tanpa COUNT #

Hilangkan Total Count dari UI #

Pertanyaan penting:

Apakah admin benar-benar butuh tahu “total ada 1.234.567 data”?

Jawaban jujur: hampir selalu tidak.

Ganti:

  • Page 3 of 12.483

Menjadi:

  • Showing 41–60
  • ✅ Tombol Next / Previous

Tanpa total page.

Gunakan “Has Next Page” (LIMIT + 1) #

Alih-alih COUNT, ambil 1 data ekstra.

Contoh:

SELECT * FROM orders
WHERE status = 'PAID'
ORDER BY created_at DESC
LIMIT 21 OFFSET 40;

Jika:

  • Result > 20 → masih ada next page
  • Result ≤ 20 → halaman terakhir

Keuntungan:

  • Tanpa COUNT
  • Query tetap ringan
  • UX tetap bagus

Gunakan Cursor-Based Pagination #

Untuk data besar, OFFSET adalah musuh.

Gunakan cursor:

SELECT * FROM orders
WHERE created_at < :last_created_at
ORDER BY created_at DESC
LIMIT 20;

Keuntungan:

  • Tidak perlu COUNT
  • Tidak ada OFFSET scan
  • Sangat scalable

Cocok untuk:

  • Log
  • Audit trail
  • Order history

Jika COUNT Wajib, Buat Approximation #

Jika bisnis memaksa menampilkan total:

Beberapa opsi:

a. Approximate Count #

  • PostgreSQL: reltuples
  • MySQL: metadata statistik

Tidak akurat, tapi cukup untuk UI admin.

b. Precomputed Counter #

  • Tabel summary
  • Diupdate via async job / trigger

Contoh:

orders_summary
- status
- total_count

Admin membaca dari summary, bukan tabel utama.

Pastikan Kolom Filter Terindex #

Jika COUNT tetap dipakai:

  • Index kolom WHERE
  • Index sesuai urutan filter paling sering

Contoh:

CREATE INDEX idx_orders_status_created_at
ON orders(status, created_at);

Tanpa ini, COUNT hampir pasti full scan.


Prinsip Penting: Admin ≠ Reporting #

Admin panel bukan reporting system.

Jika butuh:

  • Total data akurat
  • Statistik besar
  • Query lintas tahun

Pisahkan:

  • Admin transactional
  • Reporting / analytics system

Jangan bebani primary DB.


Checklist Praktis #

Sebelum pakai pagination di admin, tanyakan:

  • Apakah user benar-benar butuh total count?
  • Apakah COUNT dieksekusi lebih dari sekali?
  • Apakah kolom filter terindex?
  • Apakah OFFSET sudah terlalu besar?
  • Apakah ini seharusnya cursor-based?

Jika ragu, hapus COUNT dulu.


Penutup #

Pagination terlihat sederhana, tapi di skala besar ia sering menjadi sumber bottleneck tersembunyi, terutama di admin.

Rule of thumb:

Jika sebuah halaman admin terasa “aman”, biasanya database-lah yang sedang menanggung bebannya.

Desain pagination dengan empati pada database—bukan hanya pada UI.

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact