Untuk optimasi query Postgres di Rust, mulai dengan mencatat durasi setiap query dan membaca plan dari EXPLAIN ANALYZE agar tahu titik hambatnya. Jika backend sudah menunjukkan query yang konsisten lambat, lanjutkan dengan analisis plan dan index yang mendukung filter serta urutan data, sebelum menambahkan pagination dan monitoring produksi.
Panduan ini menjelaskan langkah konkret: identifikasi query bermasalah, interpretasi query plan, pemilihan index (termasuk index komposit untuk WHERE+ORDER BY), strategi pagination ukuran besar, metrik monitoring SQL, hingga kapan pertimbangkan schema restructuring atau caching.
Identifikasi Query Lambat pada Backend Rust
Mulai dari logging durasi query. Jika menggunakan sqlx::PgPool atau tokio-postgres, bungkus eksekusi dalam middleware sederhana yang mencatat waktu mulai dan selesai. Simpan hasil ke log atau sistem observability (misal Grafana Loki) agar bisa melacak query paling lambat per endpoint.
async fn execute_with_logging(pool: &PgPool, query: &str, params: &[&(dyn Encode<'_> + Type)]) -> Result<(), sqlx::Error> {
let start = Instant::now();
sqlx::query_with(query, params).execute(pool).await?;
let duration = Instant::now() - start;
tracing::debug!(duration_ms = ?duration.as_millis(), "query selesai", query = %query);
Ok(())
}
Jika satu query terus berada jauh di atas rata-rata, jalankan query tersebut langsung via psql dengan EXPLAIN (ANALYZE, BUFFERS) untuk melihat berapa lama setiap node membutuhkan waktu, jumlah baris yang diproses, dan apakah terjadi sequential scan.
Membaca dan Memanfaatkan Query Plan
Pahami struktur plan: Seq Scan berarti database membaca seluruh tabel, Index Scan mencerminkan penggunaan index, dan Sort bisa menjadi bottleneck jika data diurutkan tanpa index. Prioritaskan node yang memakan waktu terbesar di bagian bawah plan.
Contoh: plan menunjukkan "Sort" setelah "Seq Scan" karena tidak ada index untuk urutan yang diminta. Solusinya bisa menambahkan index komposit (lihat bagian selanjutnya). Jika plan memiliki "Bitmap Heap Scan" yang lambat, periksa apakah cost tinggi berasal dari banyaknya row yang ditemukan namun tidak langsung dipakai—mungkin perlu mempersempit filter.
Perhatikan juga perbedaan antara actual rows dan estimated rows. Selisih besar mengindikasikan statistik yang kadaluarsa; jalankan ANALYZE atau atur autovacuum agar lebih agresif untuk tabel tersebut.
Pilih Index yang Tepat untuk WHERE dan ORDER BY
Jika query memfilter berdasarkan beberapa kolom dan mengurutkan hasil, index komposit harus menyusun kolom dalam urutan filter dan sort. Misalnya:
SELECT *
FROM orders
WHERE customer_id = $1 AND status = 'confirmed'
ORDER BY created_at DESC
LIMIT 20;
Index idealnya adalah (customer_id, status, created_at DESC). Postgres bisa memakai index untuk filter dan urutan ketika kolom sort berada setelah kolom filter di index. Perhatikan bahwa urutan kolom dan arah sort penting, terutama jika menggunakan DESC.
Jika data memiliki banyak kombinasi status dan customer, pertimbangkan index partial: CREATE INDEX ON orders (customer_id, created_at DESC) WHERE status = 'confirmed'; Partial index mengurangi ukuran index dan meningkatkan selektivitas. Namun, jika filter berbeda-beda, tambahkan beberapa index masing-masing kondisi utama—tetapi pastikan tidak berlebihan karena index menambah biaya penulisan.
Pagination Efisien untuk Pertumbuhan Data
Limit-offset sederhana jadi mahal saat offset besar karena Postgres tetap menghitung dan membuang offset baris. Untuk tabel yang terus tumbuh, gunakan keyset pagination:
SELECT *
FROM orders
WHERE customer_id = $1
AND created_at < $2
ORDER BY created_at DESC
LIMIT 20;
Dengan menyimpan cursor (misalnya timestamp terakhir atau kombinasi (created_at, id)), backend hanya memindai segmen data berikutnya tanpa menghitung seluruh offset. Di Rust, simpan nilai cursor dan pasangkan ke query seperti contoh di atas. Keyset pagination bekerja paling baik bila kolom yang diurutkan sudah ada index terurut dan dipakai juga dalam filter.
Trade-off: keyset pagination tidak cocok untuk akses ke halaman tertentu (misalnya page 10). Jika pengguna perlu memundurkan page secara arbitrary, tetap pertimbangkan limit-offset untuk halaman awal, lalu tawarkan “muat lebih” untuk sisanya.
Monitoring dan Indikator Bottleneck SQL
Di produksi, pantau metrik-metrik berikut:
- Durasi rata-rata query (misal histogram latency dari middleware Rust atau observabilitas DB). Filtering dengan
duration > 200msmembantu fokus ke outlier. - Persentase query dengan full table scan – dapat dipantau lewat
pg_stat_statementsdenganshared_blks_readvsshared_blks_hit. - Active connections dan waiting locks – menandakan kontensi pada index atau query yang menunggu.
- Log slow query – aktifkan
log_min_duration_statementagar Postgres mencatat query yang mengambil terlalu lama.
Gunakan dashboard seperti Grafana untuk memvisualisasikan metrik ini dan atur alert saat di luar batas aman.
Kapan Pertimbangkan Schema Restructuring atau Caching
Jika indeks terbaik pun masih belum mampu menurunkan latensi karena tabel sangat denormalisasi atau query perlu menggabungkan banyak tabel besar, evaluasi dua pendekatan:
- Restructuring schema: Denormalisasi sebagian data yang sering dibaca bersama ke tabel materialized view atau summary table agar query tidak perlu join berat. Pastikan ada mekanisme update (trigger atau batch job) untuk konsistensi.
- Arsitektur caching: Cache hasil query read-heavy yang jarang berubah di Redis atau in-memory store. Gunakan invalidasi berbasis event (misal: cache key dihapus ketika data underlying berubah) untuk menghindari data kadaluarsa.
Pertimbangkan juga untuk memindahkan sebagian logika ke background job agar pengguna tidak menunggu query kompleks selesai dalam satu permintaan.
Kesimpulannya, kombinasikan identifikasi query lambat, interpretasi plan, index yang sesuai, pagination yang efisien, dan monitoring metrik agar backend Rust dengan Postgres tetap responsif seiring pertumbuhan data.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!