Rendering data tabular bervolume jutaan baris pada antarmuka desktop atau data grid virtualized (seperti pola mold-desktop data windowing) menuntut database engine mengembalikan slice data secara instan. Menggunakan klausa standar OFFSET pada dataset besar menyebabkan latensi tinggi akibat pemindaian baris secara linier, merusak frame rate interaksi UI. Solusi deterministik untuk mempertahankan kompleksitas waktu O(1) saat traversal jendela data adalah menggabungkan keyset pagination dengan keyset indexing yang presisi.
Kegagalan Skalabilitas LIMIT-OFFSET pada Dataset Besar
Pola pagination tradisional mengandalkan pergeseran kursor berbasis integer baris:
SELECT id, created_at, amount
FROM ledger_entries
ORDER BY created_at DESC
LIMIT 50 OFFSET 1000000;Database engine tidak melompat langsung ke baris ke-1.000.000. Engine harus memindai (scan), mengurutkan, dan membuang 1.000.000 tuple sebelum mengambil 50 baris yang diminta. Pola ini menghasilkan kompleksitas O(N) terhadap nilai OFFSET. Masalah bertambah parah pada UI data grid: data drift terjadi ketika tuple baru masuk atau dihapus secara concurrent di tengah sesi scrolling, menyebabkan rekaman terlewat atau duplikat pada grid.
Mekanisme Keyset Pagination Komposit
Keyset pagination (seek method) menggunakan nilai tuple terakhir yang telah diambil sebagai bookmark untuk mengambil jendela berikutnya. Karena kolom non-unik seperti created_at dapat memiliki nilai identik di beberapa baris, query membutuhkan tie-breaker deterministik—biasanya primary key (id).
Sintaks row constructor pada PostgreSQL menyederhanakan perbandingan tuple komposit:
-- Mengambil window data berikutnya
SELECT id, created_at, amount
FROM ledger_entries
WHERE (created_at, id) < ('2026-03-30 08:00:00+00', 4820192)
ORDER BY created_at DESC, id DESC
LIMIT 50;Query di atas mengevaluasi urutan leksikografis: baris yang dicocokkan adalah yang memiliki created_at lebih kecil, atau jika created_at sama, baris dengan id yang lebih kecil.
Arsitektur Keyset Indexing: Menjamin Eksekusi O(1)
Keyset pagination tidak menjamin performa tanpa indeks B-Tree yang tepat. B-Tree harus disusun searah dengan klausa sorting dan predikat cursor.
-- Composite index B-Tree untuk backward scanning yang efisien
CREATE INDEX idx_ledger_created_at_id_desc
ON ledger_entries (created_at DESC, id DESC);Index ini memungkinkan Postgres melakukan Index Scan langsung ke daun (leaf node) target menggunakan Binary Search, lalu membaca 50 daun berurutan tanpa memindai tuple yang tidak relevan. Waktu eksekusi stabil berapapun kedalaman navigasi data grid.
Analisis Query Plan: EXPLAIN ANALYZE
Berikut perbandingan rencana eksekusi pada tabel dengan 5.000.000 baris.
1. Pendekatan OFFSET (Halaman Dalam)
EXPLAIN ANALYZE
SELECT id, created_at, amount
FROM ledger_entries
ORDER BY created_at DESC, id DESC
LIMIT 50 OFFSET 2000000;
-- Execution plan:
-- Limit (cost=183421.12..183425.71 rows=50 width=24) (actual time=412.302..412.315 rows=50 loops=1)
-- -> Index Scan using idx_ledger_created_at_id_desc on ledger_entries (cost=0.43..458552.80 rows=5000000 width=24) (actual time=0.065..321.411 rows=2000050 loops=1)
-- Planning Time: 0.112 ms
-- Execution Time: 412.348 msEngine membuang 2.000.000 tuple dari memori/disk, menghabiskan waktu lebih dari 400 ms.
2. Pendekatan Keyset Pagination
EXPLAIN ANALYZE
SELECT id, created_at, amount
FROM ledger_entries
WHERE (created_at, id) < ('2025-11-14 12:43:01.125+00', 3120194)
ORDER BY created_at DESC, id DESC
LIMIT 50;
-- Execution plan:
-- Limit (cost=0.43..5.02 rows=50 width=24) (actual time=0.041..0.063 rows=50 loops=1)
-- -> Index Scan using idx_ledger_created_at_id_desc on ledger_entries (cost=0.43..275131.68 rows=3000000 width=24) (actual time=0.039..0.058 rows=50 loops=1)
-- Index Cond: (ROW(created_at, id) < ROW('2025-11-14 12:43:01.125+00'::timestamp with time zone, 3120194))
-- Planning Time: 0.089 ms
-- Execution Time: 0.082 msWaktu eksekusi turun menjadi sub-milidetik (0.082 ms) karena engine langsung mengarahkan pointer traversal ke posisi indeks yang ditentukan.
Menangani Edge Cases pada Implementasi Produksi
1. Filter Dinamis
Jika grid desktop mendukung filtering dinamis (misal: tenant_id atau status), kolom filter dengan kardinalitas selektif harus berada di posisi paling depan (leading columns) pada composite index:
CREATE INDEX idx_ledger_tenant_status_created_id
ON ledger_entries (tenant_id, status, created_at DESC, id DESC);Query pencarian windowing disesuaikan:
SELECT id, created_at, amount
FROM ledger_entries
WHERE tenant_id = 42
AND status = 'POSTED'
AND (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 50;2. Concurrent Insertion dan Read Anomalies
Pada data stream berkecepatan tinggi, penyisipan data baru saat scrolling dapat memicu anomali visual:
- Monotonic Id vs Wall Clock: Nilai
created_atrentan terhadap skew clock antar-server atau transaksi konkuren dengan durasi commit berbeda. Gunakan UUIDv7 atau sequence ID monolitik (seperti Bigserial atau Snowflake ID) sebagai tie-breaker utama agar perbandingan urutan bersifat absolut. - Klausa Row Constructor Optimization: Pada PostgreSQL versi lama, perbandingan row constructor
(a, b) < (x, y)terkadang tidak memanfaatkan index seek optimal. Jika execution plan beralih keFilter, uraikan predikat secara eksplisit:
WHERE (created_at < :last_created_at)
OR (created_at = :last_created_at AND id < :last_id)Ringkasan Trade-off
Keyset pagination menghilangkan kemampuan jump ke sembarang halaman (misal: tombol loncat langsung ke halaman 400). Namun, untuk pola data grid modern, infinite scrolling, dan continuous windowing, pendekatan ini adalah standar baku untuk menjaga kestabilan latensi database pada kompleksitas O(1).
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!