Aplikasi React Native offline-first umumnya mengandalkan SQLite untuk persistensi data lokal. Seiring bertambahnya volume data transaksi hingga puluhan ribu baris, performa query kerap terdegradasi. Salah satu penyebab utama UI jank (frame drops) adalah eksekusi query antrean lokal pada thread database yang memicu I/O disk tinggi akibat full table scan atau pemindaian B-Tree indeks penuh yang terfragmentasi.
Skenario paling umum adalah tabel sinkronisasi di mana 95% data berstatus selesai (is_synced = 1) dan hanya 5% data yang belum terkirim (is_synced = 0). Mengindeks seluruh kolom status tersebut membuang ruang disk dan memperlambat operasi INSERT. Solusi optimal untuk pola akses ini adalah memanfaatkan Partial Index pada SQLite.
Mekanisme Partial Index pada SQLite
Secara default, indeks standar (full index) membuat node B-Tree untuk setiap baris yang ada di dalam tabel. Sebaliknya, partial index menyertakan klausa WHERE pada deklarasi DDL. SQLite hanya akan mendaftarkan baris yang memenuhi predikat tersebut ke dalam struktur B-Tree indeks.
Karakteristik teknis partial index meliputi:
- Ukuran B-Tree minimal: Node indeks yang lebih sedikit mengurangi konsumsi RAM (SQLite page cache) dan mengurangi page read dari media penyimpanan.
- Write Amplification rendah: Operasi
INSERTatauUPDATEpada baris yang tidak memenuhi predikat klausaWHEREtidak akan memicu penulisan ulang atau penyeimbangan cabang B-Tree indeks. - Pencarian deterministik: Query planner SQLite langsung menggunakan indeks ini jika klausa
WHEREpada query mencakup subset atau ekspresi yang identik dengan predikat indeks.
Analisis Eksekusi via EXPLAIN QUERY PLAN
Untuk memvalidasi efektivitas indeks pada runtime React Native, gunakan modul berbasis JSI langsung seperti @op-engineering/op-sqlite atau react-native-quick-sqlite. Perintah EXPLAIN QUERY PLAN menyediakan informasi strategi pembacaan data oleh query engine SQLite.
import { open } from '@op-engineering/op-sqlite';
const db = open({ name: 'sync_engine.db' });
// Setup skema tabel transaksi
db.execute(`
CREATE TABLE IF NOT EXISTS mutations (
id TEXT PRIMARY KEY,
payload TEXT NOT NULL,
created_at INTEGER NOT NULL,
is_synced INTEGER NOT NULL DEFAULT 0
);
`);
// 1. Uji query tanpa indeks optimal
const planBefore = db.execute(`
EXPLAIN QUERY PLAN
SELECT id, payload FROM mutations
WHERE is_synced = 0
ORDER BY created_at ASC;
`);
console.log('Rencana Eksekusi Sebelum:', planBefore.rows?._array);
// Output detail: "SCAN mutations" (Full Table Scan)
Jika output menampilkan SCAN mutations, SQLite membaca seluruh blok memori tabel dari baris pertama hingga terakhir. Pada dataset 50.000 baris, operasi ini memicu disk I/O berulang dan menahan thread JS atau worker bridge.
Implementasi DDL Partial Index
Untuk membatasi indeks hanya pada data mutasi yang belum tersinkronisasi, buat indeks bersyarat pada kolom pengurutan (created_at) dengan predikat is_synced = 0:
// 2. Terapkan DDL Partial Index
db.execute(`
CREATE INDEX IF NOT EXISTS idx_mutations_unsynced_order
ON mutations (created_at)
WHERE is_synced = 0;
`);
// 3. Uji kembali rencana eksekusi
const planAfter = db.execute(`
EXPLAIN QUERY PLAN
SELECT id, payload FROM mutations
WHERE is_synced = 0
ORDER BY created_at ASC;
`);
console.log('Rencana Eksekusi Sesudah:', planAfter.rows?._array);
// Output detail: "SEARCH mutations USING INDEX idx_mutations_unsynced_order (created_at=?)"
Label SEARCH mengonfirmasi bahwa SQLite langsung menavigasi node B-Tree indeks parsial secara biner tanpa memindai sisa data yang berstatus is_synced = 1.
Aturan Pencocokan Query Optimizer
SQLite Query Planner memiliki aturan ketat agar partial index dapat digunakan. Query harus memuat kondisi logis yang merupakan subset dari kondisi indeks.
- Kueri Valid (Indeks Digunakan):
SELECT * FROM mutations WHERE is_synced = 0;
Kondisi identik dengan predikat DDL. - Kueri Tidak Valid (Indeks Diabaikan):
SELECT * FROM mutations WHERE is_synced = 1;atauSELECT * FROM mutations;
SQLite tidak dapat menggunakanidx_mutations_unsynced_orderkarena predikat indeks mengecualikan baris dengan nilai tersebut. - Tipe Data Parameter Binding:
Pastikan tipe data binding konsisten. Jika indeks didefinisikan dengan integer (WHERE is_synced = 0), jangan gunakan binding string literal sepertiWHERE is_synced = '0'karena affinity matching SQLite dapat membatalkan penggunaan indeks.
Analisis Efisiensi dan Batasan
Penerapan partial index memberikan rasio efisiensi yang signifikan pada skenario data bertumbuh:
- Storage Footprint: Pada tabel dengan 100.000 baris di mana hanya 200 item belum tersinkronisasi, full index membutuhkan ribuan page leaf B-Tree. Partial index hanya membutuhkan 1 hingga 2 page (4KB–8KB), mencegah membengkaknya ukuran berkas
.dbpada penyimpanan internal perangkat seluler. - Latensi Eksekusi: Penghindaran full table scan memangkas waktu eksekusi query dari ratusan milidetik menjadi rentang sub-milidetik, meniadakan delay render list komponen UI.
- Overhead Mutasi Status: Saat item selesai disinkronkan via
UPDATE mutations SET is_synced = 1 WHERE id = ?, baris tersebut secara otomatis dihapus dari struktur B-Tree indeks parsial. Pastikan kolomidtetap memiliki index primary key agar mutasi baris tetap instan.
Gunakan partial index secara spesifik untuk kolom dengan distribusi data timpang (skewed distribution) atau status transient seperti flag aktif, soft-delete (deleted_at IS NULL), dan antrean sync. Hindari penggunaannya pada kolom yang sebaran nilainya merata (high cardinality).
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!