Mengelola dan menampilkan data dalam jumlah besar di aplikasi web sering jadi tantangan. Bayangkan kamu punya jutaan, bahkan puluhan juta baris data, dan harus menampilkannya dengan cepat dan efisien. Paginasi biasa dengan LIMIT
dan OFFSET
mungkin sudah cukup untuk data kecil, tapi saat data mencapai skala “raksasa”, kamu akan mulai merasakan kelambatannya.
Di sinilah Keyset Pagination atau Cursor Pagination datang sebagai solusi ampuh. Metode paginasi ini dirancang khusus untuk performa optimal pada database dengan volume data yang sangat besar, menghindarkan kamu dari masalah performa yang kerap muncul pada paginasi tradisional. Untuk kamu yang baru mulai mengutak-atik paginasi di PHP, kamu bisa mulai dari pembahasan dasar pagination di PHP ini.
Artikel ini akan mengupas tuntas kenapa Keyset Pagination lebih unggul, bagaimana cara kerjanya, dan bagaimana kita bisa mengimplementasikannya dalam aplikasi PHP kamu. Bersiaplah mengucapkan selamat tinggal pada halaman web yang lambat!
Kenapa Paginasi Biasa Jadi Lemot untuk Data Besar?
Sebelum menyelam ke Keyset Pagination, penting untuk memahami batasan paginasi tradisional yang menggunakan LIMIT
dan OFFSET
dalam kueri SQL.
Ketika kamu melakukan kueri seperti SELECT * FROM tabel_besar LIMIT 100 OFFSET 1000000
, database masih harus memindai (atau setidaknya menghitung) 1.000.000 baris pertama sebelum akhirnya mengambil 100 baris yang kamu inginkan. Semakin besar nilai OFFSET
, semakin banyak pekerjaan yang harus dilakukan database, dan semakin lambat kueri tersebut.
Ini seperti kamu mencoba menemukan halaman 1000 dari sebuah buku tebal tanpa daftar isi; kamu harus membolak-balik satu per satu hingga sampai di halaman yang dituju. Tentu saja, itu tidak efisien.
LIMIT
dan OFFSET
memang sederhana untuk diimplementasikan, namun kelemahan ini membuatnya kurang cocok untuk aplikasi yang menangani data berjuta-juta baris dan sering diakses.
Memahami Keyset Pagination: Solusi Anti Lemot
Keyset Pagination bekerja dengan cara yang fundamental berbeda. Alih-alih melompati sejumlah baris (OFFSET
), ia menggunakan nilai dari kolom berindeks sebagai “titik mulai” untuk halaman berikutnya. Kolom ini biasanya adalah kolom unik dan terurut, seperti primary key (id
) atau kolom timestamp
yang berurutan.
Idenya sederhana:
- Ambil data untuk halaman pertama.
- Catat nilai terakhir dari kolom unik (misalnya,
id_terakhir
) dari data yang baru saja diambil. - Untuk halaman berikutnya, minta data yang nilainya lebih besar (atau lebih kecil, tergantung urutan) dari
id_terakhir
tersebut.
Dengan cara ini, database dapat langsung melompat ke lokasi yang relevan menggunakan indeks, tanpa perlu memindai baris-baris sebelumnya. Ini jauh lebih cepat karena database tidak perlu menghitung atau mengabaikan baris yang tidak relevan.
Keunggulan Keyset Pagination
- Performa Unggul: Peningkatan kecepatan yang drastis, terutama pada halaman-halaman yang dalam, karena tidak ada lagi operasi
OFFSET
yang memakan waktu. - Efisiensi Sumber Daya: Mengurangi beban pada database karena kueri lebih ringan.
- Skalabilitas: Sangat cocok untuk database berukuran sangat besar dan aplikasi dengan lalu lintas tinggi.
- Menghindari Masalah Data Ganda/Hilang: Pada paginasi
LIMIT
/OFFSET
, jika ada data baru masuk atau dihapus di antara dua kueri paginasi, data yang ditampilkan bisa ganda atau ada yang terlewat. Keyset Pagination lebih tahan terhadap masalah ini karena patokannya adalah nilai spesifik.
Keterbatasan Keyset Pagination
Meskipun kuat, Keyset Pagination punya beberapa batasan:
- Memerlukan Kolom Unik dan Terurut: Kamu harus punya kolom seperti
id
(auto-increment) atautimestamp
yang unik dan secara alami terurut. - Tidak Bisa Langsung Lompat ke Halaman Spesifik: Karena navigasi berbasis “berikutnya” atau “sebelumnya”, pengguna tidak bisa langsung melompat dari halaman 1 ke halaman 500 seperti pada paginasi tradisional. Navigasinya lebih seperti “gulir tak terbatas” atau tombol “muat lebih banyak”.
- Arah Pengurutan: Jika kamu ingin mengurutkan data berdasarkan kolom lain, kamu harus menyertakan kolom urutan tersebut bersama dengan kolom kunci (misalnya
id
) dalam klausaWHERE
danORDER BY
.
Implementasi Keyset Pagination di PHP
Mari kita lihat bagaimana mengimplementasikan Keyset Pagination dengan PHP dan MySQL (menggunakan PDO). Kita akan berasumsi tabel kita punya kolom id
(PRIMARY KEY, AUTO_INCREMENT) dan created_at
(DATETIME).
Struktur Tabel Contoh
CREATE TABLE IF NOT EXISTS articles (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Logika Keyset Pagination di PHP
Kita akan menggunakan id
sebagai kolom kunci untuk paginasi.
<?php
// Koneksi ke database (gunakan koneksi PDO dari contoh artikel sebelumnya)
$host = 'localhost';
$db = 'nama_database_kamu';
$user = 'user_database_kamu';
$pass = 'password_database_kamu';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
$items_per_page = 20; // Jumlah item per halaman
// Ambil ID terakhir dari URL, jika ada. Untuk halaman pertama, biarkan kosong/null.
$last_id = isset($_GET['last_id']) ? (int)$_GET['last_id'] : null;
// Tentukan arah paginasi (maju atau mundur)
$direction = isset($_GET['direction']) ? $_GET['direction'] : 'next'; // 'next' atau 'prev'
$where_clause = '';
$order_clause = '';
$params = [];
if ($direction === 'next') {
if ($last_id) {
$where_clause = " WHERE id > :last_id";
$params[':last_id'] = $last_id;
}
$order_clause = " ORDER BY id ASC";
} elseif ($direction === 'prev') {
if ($last_id) {
$where_clause = " WHERE id < :last_id";
$params[':last_id'] = $last_id;
}
$order_clause = " ORDER BY id DESC"; // Balik urutan untuk 'sebelumnya'
}
// Kueri untuk mendapatkan data
$sql = "SELECT id, title, created_at FROM articles" . $where_clause . $order_clause . " LIMIT :limit";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':limit', $items_per_page, PDO::PARAM_INT);
foreach ($params as $key => &$val) {
$stmt->bindParam($key, $val, PDO::PARAM_INT);
}
$stmt->execute();
$data = $stmt->fetchAll();
// Jika arahnya 'prev', balikkan array data agar urutannya benar di tampilan
if ($direction === 'prev') {
$data = array_reverse($data);
}
// Dapatkan ID pertama dan terakhir dari data yang diambil untuk navigasi
$first_item_id = !empty($data) ? $data[0]['id'] : null;
$last_item_id = !empty($data) ? $data[count($data) - 1]['id'] : null;
// Tampilkan data
echo "<h1>Daftar Artikel</h1>";
echo "<table border='1'>";
echo "<thead><tr><th>ID</th><th>Judul</th><th>Tanggal Dibuat</th></tr></thead>";
echo "<tbody>";
foreach ($data as $row) {
echo "<tr>";
echo "<td>" . htmlspecialchars($row['id']) . "</td>";
echo "<td>" . htmlspecialchars($row['title']) . "</td>";
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>";
echo "</tr>";
}
echo "</tbody>";
echo "</table>";
// Tampilkan navigasi paginasi
echo "<div>";
// Tombol 'Sebelumnya'
if ($first_item_id) {
// Kita perlu mendapatkan ID terakhir dari halaman sebelumnya untuk 'prev'
// Ini agak rumit karena tidak ada 'total_pages', perlu kueri tambahan
// Untuk sederhana, kita asumsikan selalu bisa mundur jika ada data di halaman ini
echo "<a href='?last_id=" . $first_item_id . "&direction=prev'>Sebelumnya</a> ";
}
// Tombol 'Selanjutnya'
if (count($data) == $items_per_page) { // Artinya, mungkin masih ada data di halaman berikutnya
echo "<a href='?last_id=" . $last_item_id . "&direction=next'>Selanjutnya</a>";
}
echo "</div>";
?>
Penjelasan Kode:
$last_id
: Ini adalah kunci utama. Kita menangkapid
terakhir dari halaman sebelumnya (untuk navigasi “selanjutnya”) atauid
pertama dari halaman saat ini (untuk navigasi “sebelumnya”).$direction
: Menentukan apakah kita ingin maju (next
) atau mundur (prev
) dalam paginasi.WHERE id > :last_id
: Untuk halaman berikutnya, kita minta data yangid
nya lebih besar dariid
terakhir yang kita lihat.WHERE id < :last_id
danORDER BY id DESC
: Untuk navigasi “sebelumnya”, kita minta data yangid
nya lebih kecil, tapi kita harus mengurutkannya secara terbalik (DESC
) agar bisa mendapatkan data “sebelumnya”. Setelah diambil, array$data
kemudian dibalik lagi (array_reverse
) agar urutan tampilannya benar.- Navigasi: Tombol “Selanjutnya” muncul jika jumlah data yang diambil sama dengan
$items_per_page
, menunjukkan kemungkinan masih ada data di halaman berikutnya. Untuk tombol “Sebelumnya”, logikanya bisa lebih kompleks karena kita tidak tahu “batas atas” dari data sebelumnya tanpa kueri tambahan, tapi untuk contoh ini kita sederhanakan dengan hanya memunculkannya jika ada data yang ditampilkan.
Paginasi Berdasarkan Multiple Kolom (Compound Keys)
Kadang, kamu perlu mengurutkan data berdasarkan lebih dari satu kolom, misalnya created_at
dan id
. Ini disebut compound keys atau kunci gabungan.
Contoh kueri untuk compound keys:
-- Untuk halaman berikutnya, diurutkan berdasarkan created_at ASC, kemudian id ASC
SELECT id, title, created_at
FROM articles
WHERE (created_at > :last_created_at) OR (created_at = :last_created_at AND id > :last_id)
ORDER BY created_at ASC, id ASC
LIMIT :limit;
-- Untuk halaman sebelumnya, diurutkan berdasarkan created_at DESC, kemudian id DESC
SELECT id, title, created_at
FROM articles
WHERE (created_at < :first_created_at) OR (created_at = :first_created_at AND id < :first_id)
ORDER BY created_at DESC, id DESC
LIMIT :limit;
Di sini, kita perlu menyimpan created_at
dan id
dari item terakhir/pertama untuk menentukan “titik mulai” halaman berikutnya. Logikanya sedikit lebih kompleks, namun prinsip dasarnya sama: menggunakan nilai kolom berindeks untuk “melompat” ke lokasi yang benar.
Pertimbangan Lanjutan untuk Keyset Pagination
- Navigasi “Halaman Spesifik”: Keyset Pagination tidak mendukung navigasi langsung ke halaman nomor tertentu (misalnya, lompat ke halaman 100). Jika fitur ini sangat dibutuhkan, kamu mungkin perlu menggabungkan Keyset Pagination dengan beberapa bentuk caching atau menyimpan “bookmark” ID pada interval tertentu.
- Indeks yang Tepat: Pastikan kolom yang kamu gunakan sebagai kunci paginasi (misalnya
id
,created_at
) memiliki indeks yang sesuai di database kamu. Tanpa indeks, performa Keyset Pagination tidak akan optimal. - User Experience (UX): Keyset Pagination sangat cocok untuk implementasi “gulir tak terbatas” (infinite scrolling) atau tombol “muat lebih banyak” (load more), di mana pengguna terus melihat data baru saat mereka menggulir ke bawah, tanpa perlu me-refresh halaman atau melihat nomor halaman.
Kesimpulan: Keyset Pagination, Pilihan Terbaik untuk Skala Besar
Jika kamu berhadapan dengan jutaan baris data dan paginasi tradisional mulai terasa lambat, Keyset Pagination adalah solusi yang patut kamu pertimbangkan serius. Dengan memanfaatkan indeks database dan menghindari operasi OFFSET
yang mahal, ia menawarkan performa yang jauh lebih baik, memastikan aplikasi kamu tetap responsif dan memberikan pengalaman terbaik bagi pengguna.
Meskipun implementasinya sedikit lebih kompleks dibanding LIMIT
/OFFSET
biasa, manfaat performa yang ditawarkan Keyset Pagination akan sangat berharga untuk aplikasi skala besar. Mulailah adopsi teknik ini dan rasakan sendiri perbedaannya!