Langsung ke konten utama
RekayasaMar 28, 2026

Performance Debugging — Ketika Pembacaan Database Membunuh Latensi Anda

OS
Open Soft Team

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

  1. Periksa EXPLAIN ANALYZE untuk semua query yang lambat
  2. Buat index untuk kolom di WHERE, JOIN, dan ORDER BY
  3. Hindari N+1 — Gunakan batch query atau JOIN
  4. Monitor connection pool — Track utilisasi dan wait time
  5. Cache hasil — Untuk data yang jarang berubah
  6. Pagination — Selalu gunakan LIMIT/OFFSET atau keyset pagination
  7. 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.