Aplikasi React Native dengan arsitektur offline-first sering mengalami masalah regresi performa query sesaat setelah proses sinkronisasi data selesai. Query yang awalnya merespons dalam hitungan milidetik mendadak melambat hingga ratusan milidetik setelah puluhan ribu baris baru dimasukkan ke database. Masalah ini berakar pada SQLite Cost-Based Optimizer (CBO) yang bekerja tanpa informasi statistik distribusi data terbaru.
Penyebab Regresi Performa Query Pasca Sinkronisasi
SQLite mengandalkan Cost-Based Optimizer (CBO) untuk menentukan cara paling efisien dalam mengeksekusi query: apakah melakukan full table scan, memanfaatkan indeks tunggal, atau memadukan beberapa indeks. CBO mengkalkulasi estimasi biaya I/O dan CPU menggunakan tabel internal sqlite_stat1.
Ketika aplikasi diinstal pertama kali atau baru saja menyelesaikan sinkronisasi data skala besar (misalnya mengunduh puluhan ribu rekaman transaksi dari server), tabel sqlite_stat1 sering kali kosong atau masih mencatat statistik lama. Tanpa data statistik ini, SQLite mengasumsikan nilai heuristik default, seperti menganggap setiap indeks mengurangi ruang pencarian hingga 1/10 dari total tabel.
Asumsi default tersebut sering meleset pada tabel dengan distribusi data timpang (skewed distribution). Akibatnya, query planner memilih indeks yang salah atau melakukan pemindaian menyeluruh (full scan) meski indeks komposit yang tepat telah tersedia.
Diagnosis Rencana Eksekusi Menggunakan EXPLAIN QUERY PLAN
Gunakan perintah EXPLAIN QUERY PLAN untuk memverifikasi apakah SQLite memilih indeks optimal pada tabel yang mengalami degradasi performa.
-- Jalankan pemeriksaan rencana eksekusi
EXPLAIN QUERY PLAN
SELECT id, amount, created_at
FROM transactions
WHERE user_id = 'usr_1024' AND status = 'COMPLETED'
ORDER BY created_at DESC
LIMIT 20;
Jika statistik data tidak akurat, output query planner umumnya memperlihatkan indeks yang sub-optimal atau bahkan scan penuh:
-- Output sub-optimal (mengabaikan indeks komposit idx_user_status_created)
QUERY PLAN
`--SCAN transactions
--FILTER user_id = 'usr_1024' AND status = 'COMPLETED'
Setelah statistik diperbarui, query planner beralih menggunakan indeks komposit yang tepat tanpa memindai baris yang tidak relevan:
-- Output optimal pasca pembaruan statistik
QUERY PLAN
`--SEARCH transactions USING INDEX idx_user_status_created (user_id=? AND status=?)
Solusi: PRAGMA optimize dan PRAGMA analysis_limit
Pendekatan klasik untuk memperbarui statistik adalah mengeksekusi perintah ANALYZE;. Namun, menjalankan ANALYZE penuh pada perangkat mobile berisiko memblokir I/O penyimpanan dan menghabiskan daya baterai, karena perintah ini memindai ulang setiap baris pada seluruh indeks database.
SQLite menyediakan solusi yang jauh lebih efisien melalui kombinasi dua konfigurasi:
PRAGMA analysis_limit = N;: Membatasi sampel pemindaian hanya sebanyakNsel per indeks selama kalkulasi statistik. Angka400hingga1000sudah cukup memberikan representasi kurva distribusi data yang akurat bagi CBO tanpa beban baca disk berlebih.PRAGMA optimize;: Menjalankan analisa cerdas. SQLite melacak tabel dan indeks mana saja yang digunakan oleh query selama koneksi aktif, lalu hanya menganalisis objek yang statistiknya belum ada atau dinilai usang (outdated).
Implementasi pada React Native (op-sqlite)
Library seperti @op-engineering/op-sqlite mengeksekusi panggilan SQLite langsung melalui C++ JSI tanpa serialisasi React Native Bridge. Agar proses optimasi tidak memblokir render UI, jalankan PRAGMA optimize saat aplikasi berpindah status ke background atau tepat setelah proses sinkronisasi background selesai.
import { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import { open, DB } from '@op-engineering/op-sqlite';
export const db: DB = open({ name: 'offline_app.sqlite' });
export function configureDatabasePragmas(database: DB): void {
// Batasi sampel sampling statistik agar I/O tetap minimal
database.execute('PRAGMA analysis_limit = 400;');
database.execute('PRAGMA foreign_keys = ON;');
}
export function runDatabaseOptimization(database: DB): void {
try {
const startTime = Date.now();
// PRAGMA optimize memeriksa dan memperbarui indeks yang relevan saja
database.execute('PRAGMA optimize;');
const duration = Date.now() - startTime;
console.info(`[SQLite] PRAGMA optimize completed in ${duration}ms`);
} catch (error) {
console.error('[SQLite] Failed to run PRAGMA optimize:', error);
}
}
export function useDatabaseOptimizer(database: DB): void {
const appState = useRef<AppStateStatus>(AppState.currentState);
useEffect(() => {
configureDatabasePragmas(database);
const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
// Jalankan optimasi saat pengguna menutup atau meninggalkan aplikasi
if (
appState.current === 'active' &&
nextAppState.match(/inactive|background/)
) {
runDatabaseOptimization(database);
}
appState.current = nextAppState;
});
return () => {
subscription.remove();
};
}, [database]);
}
Catatan arsitektur: Jika sinkronisasi data dilakukan dalam worker thread terpisah atau background sync task, letakkandatabase.execute('PRAGMA optimize;')pada blok finalisasi sinkronisasi setelah transaksi data ditutup (pasca-COMMIT).
Hasil Benchmark dan Latensi Query
Pengujian dilakukan pada perangkat Android kelas menengah (Snapdragon 778G) menggunakan tabel transactions berisi 150.000 baris data tersinkronisasi:
- Query:
SELECT * FROM transactions WHERE user_id = ? AND status = ? ORDER BY created_at DESC LIMIT 20 - Tanpa Optimasi / Statistik Kosong: ~280ms (Rencana eksekusi:
SCAN TABLEparsial akibat distribusi status yang tidak merata). - ANALYZE Penuh: Waktu eksekusi analisa mencapai 3.800ms (Risiko ANR / frame drop tinggi bila terpanggil di main thread).
- PRAGMA analysis_limit = 400 + PRAGMA optimize: Waktu eksekusi optimasi hanya butuh 12ms, dan latensi query transaksi turun drastis menjadi 2.8ms (Rencana eksekusi beralih ke
SEARCH TABLE USING INDEX).
Ringkasan Praktik Terbaik
- Konfigurasikan
PRAGMA analysis_limit = 400;segera setelah membuka koneksi database. - Hindari menjalankan
ANALYZEmenyeluruh tanpa limitasi pada runtime React Native. - Jalankan
PRAGMA optimize;secara berkala pada lifecycleAppStateke background atau di akhir transaksi sinkronisasi data masif. - Gunakan
EXPLAIN QUERY PLANsaat memvalidasi query lambat untuk memastikan CBO memanfaatkan indeks secara tepat.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!