Performance Debugging — Ketika Pembacaan Database Membunuh Latensi Anda
Engineering Team
Gejala: API Lambat, CPU Rendah
Anda memiliki endpoint API yang seharusnya merespons dalam 50ms tetapi memakan 2 detik. CPU server rendah. Memory normal. Masalahnya hampir pasti di database.
Artikel ini membahas diagnosis dan perbaikan masalah performa database paling umum di aplikasi Rust/Axum dengan PostgreSQL.
Masalah 1: N+1 Query
Pattern N+1 terjadi ketika Anda memuat daftar, lalu memuat relasi untuk setiap item secara individual:
// BURUK: N+1 query
async fn list_orders(pool: &PgPool) -> Vec<OrderWithItems> {
let orders = sqlx::query_as::<_, Order>("SELECT * FROM orders LIMIT 100")
.fetch_all(pool).await.unwrap();
let mut result = Vec::new();
for order in orders {
// +1 query per order!
let items = sqlx::query_as::<_, OrderItem>(
"SELECT * FROM order_items WHERE order_id = $1"
)
.bind(order.id)
.fetch_all(pool).await.unwrap();
result.push(OrderWithItems { order, items });
}
result // 101 query total!
}
// BAIK: 2 query total
async fn list_orders(pool: &PgPool) -> Vec<OrderWithItems> {
let orders = sqlx::query_as::<_, Order>("SELECT * FROM orders LIMIT 100")
.fetch_all(pool).await.unwrap();
let order_ids: Vec<Uuid> = orders.iter().map(|o| o.id).collect();
let items = sqlx::query_as::<_, OrderItem>(
"SELECT * FROM order_items WHERE order_id = ANY($1)"
)
.bind(&order_ids)
.fetch_all(pool).await.unwrap();
// Group items by order_id
let items_map: HashMap<Uuid, Vec<OrderItem>> = items
.into_iter()
.into_group_map_by(|i| i.order_id);
orders.into_iter().map(|order| {
let items = items_map.get(&order.id)
.cloned().unwrap_or_default();
OrderWithItems { order, items }
}).collect()
}
Masalah 2: Missing Index
-- Query lambat:
SELECT * FROM articles WHERE locale = 'id' AND published = true ORDER BY published_at DESC;
-- EXPLAIN ANALYZE menunjukkan Seq Scan:
Seq Scan on articles (cost=0.00..1234.56 rows=100 width=567)
Filter: ((locale = 'id') AND published)
Rows Removed by Filter: 9900
Perbaikan:
CREATE INDEX idx_articles_locale_published
ON articles (locale, published, published_at DESC)
WHERE published = true;
Setelah index:
Index Scan using idx_articles_locale_published on articles
(cost=0.28..12.34 rows=100 width=567)
Masalah 3: Connection Pool Saturation
Ketika semua koneksi di pool sedang digunakan, query baru harus menunggu:
// Konfigurasi pool
let pool = PgPoolOptions::new()
.max_connections(10) // Terlalu rendah untuk load tinggi!
.acquire_timeout(Duration::from_secs(3))
.connect(&database_url)
.await?;
Diagnosa dengan tracing:
// Tambahkan metric connection pool
async fn health_check(State(pool): State<PgPool>) -> Json<HealthStatus> {
let pool_status = pool.size(); // Koneksi aktif
let idle = pool.num_idle(); // Koneksi menganggur
Json(HealthStatus {
pool_size: pool_status,
pool_idle: idle,
pool_max: 10,
})
}
Perbaikan:
- Tingkatkan
max_connections(tergantung CPU PostgreSQL) - Gunakan PgBouncer untuk connection pooling di level terpisah
- Optimalkan query yang berjalan lama
Masalah 4: Full Table Scan pada JOIN
-- Lambat: join tanpa index pada FK
SELECT a.*, c.name as category_name
FROM articles a
JOIN categories c ON c.id = a.category_id
WHERE a.locale = 'id'
ORDER BY a.published_at DESC
LIMIT 20;
Pastikan FK memiliki index:
CREATE INDEX idx_articles_category_id ON articles(category_id);
Monitoring dengan tracing
use tracing::instrument;
#[instrument(skip(pool))]
async fn get_articles(
pool: &PgPool,
locale: &str,
page: i64,
) -> Result<Vec<Article>, DbError> {
let start = std::time::Instant::now();
let articles = sqlx::query_as::<_, Article>(
"SELECT * FROM articles WHERE locale = $1 AND published = true \
ORDER BY published_at DESC LIMIT 20 OFFSET $2"
)
.bind(locale)
.bind((page - 1) * 20)
.fetch_all(pool)
.await?;
let elapsed = start.elapsed();
if elapsed > Duration::from_millis(100) {
tracing::warn!(
query = "get_articles",
locale = locale,
duration_ms = elapsed.as_millis(),
"Query lambat terdeteksi"
);
}
Ok(articles)
}
Checklist Performance Database
- Periksa EXPLAIN ANALYZE untuk semua query yang lambat
- Buat index untuk kolom di WHERE, JOIN, dan ORDER BY
- Hindari N+1 — Gunakan batch query atau JOIN
- Monitor connection pool — Track utilisasi dan wait time
- Cache hasil — Untuk data yang jarang berubah
- Pagination — Selalu gunakan LIMIT/OFFSET atau keyset pagination
- Gunakan RETURNING — Hindari query SELECT setelah INSERT/UPDATE
Kesimpulan
Masalah performa database adalah penyebab paling umum latensi API tinggi. N+1 query, missing index, dan connection pool saturation masing-masing bisa membuat endpoint berjalan 10-100x lebih lambat. Diagnosis yang sistematik dengan EXPLAIN ANALYZE, monitoring pool, dan slow query logging membuat perbaikan menjadi mudah.