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:
- Mengambil data untuk halaman tertentu
- 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 paginationCOUNT(*)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
WHEREdengan 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.