Model bahasa skala kecil atau budget LLM (seperti Llama 3 8B, Mistral 7B, atau model distilasi) sering dimanfaatkan untuk tugas automasi backend karena biaya inferensinya yang rendah. Masalah utamanya ada pada kualitas optimasi: model-model ini kerap menghasilkan query SQL yang valid secara sintaksis, namun fatal di level performa produksi. Pola umum yang sering muncul meliputi penggunaan OFFSET bernilai besar, fungsi predikat non-sargable (seperti LOWER(email) tanpa functional index), dan filter multi-kolom yang memicu Seq Scan pada tabel ratusan juta baris.
Solusi deterministik untuk masalah ini adalah memasang verification harness berbasis Rust di antara output LLM dan codebase backend. Harness ini bertindak sebagai filter ganda: static analysis berbasis AST menggunakan crate sqlparser, diikuti verifikasi dinamis via PostgreSQL EXPLAIN (FORMAT JSON) untuk menginspeksi query plan sebelum query disetujui.
Anatomi Anti-Pattern SQL dari Budget Model
Saat diminta membuat endpoint pagination atau filter data, budget LLM cenderung memilih jalur kode paling sederhana:
- Deep OFFSET Pagination: Menggunakan
LIMIT 50 OFFSET 100000. PostgreSQL tetap harus memindai dan membaca 100.050 baris dari disk sebelum membuang 100.000 baris pertama. Pola yang benar adalah keyset pagination (misal:WHERE id > $last_seen_id ORDER BY id ASC LIMIT 50). - Unindexed Leading Predicate: Menghasilkan
WHERE status = 'active' AND tenant_id = 42. Jika index hanya dibuat pada kolomstatusyang kardinalitasnya rendah, PostgreSQL akan memilihSeq ScandaripadaBitmap Index Scanyang tidak efisien. - Fungsi pada Kolom Index:
WHERE DATE(created_at) = '2026-03-30'yang membatalkan penggunaan b-tree index standar padacreated_at.
Arsitektur Rust Verification Harness
Harness ini beroperasi sebagai proxy analyzer dengan 4 tahapan eksekusi:
- Lexical & AST Analysis: Memvalidasi struktur query menggunakan
sqlparser-rs. Jika ditemukan penggunaanOFFSETatau kueri tanpa klausa pembatas, query langsung ditolak tanpa menyentuh koneksi database. - Shadow DB Explain: Query dikirim ke PostgreSQL staging/shadow instance menggunakan perintah
EXPLAIN (COSTS, FORMAT JSON). Shadow instance harus memiliki schema yang identik dan statistik tabel (viaANALYZE) yang mencerminkan volume produksi. - Plan Tree Inspection: JSON execution plan diparsing secara rekursif untuk mendeteksi simpul
Node Type == "Seq Scan"pada tabel dengan estimasi baris di atas batas ambang (threshold). - Feedback Loop Generation: Bila terjadi pelanggaran, harness menyusun failure context terstruktur untuk dikirimkan kembali ke LLM agar query direvisi.
Implementasi: Static Linting dengan sqlparser-rs
Langkah pertama adalah memblokir anti-pattern di tingkat AST. Tambahkan dependensi berikut pada Cargo.toml:
[dependencies]
sqlparser = "0.44.0"
tokio-postgres = "0.7.10"
tokio = { version = "1.38", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Berikut adalah modul analyzer untuk mendeteksi klausa OFFSET dan wildcard SELECT *:
use sqlparser::ast::{Expr, Query, SelectItem, SetExpr, Statement};
use sqlparser::dialect::PostgreSqlDialect;
use sqlparser::parser::Parser;
#[derive(Debug, PartialEq)]
pub enum StaticViolation {
OffsetDetected,
SelectWildcardForbidden,
MissingWhereClause,
}
pub fn lint_ast(sql: &str) -> Result<(), Vec<StaticViolation>> {
let dialect = PostgreSqlDialect {};
let statements = Parser::parse_sql(&dialect, sql)
.map_err(|_| vec![StaticViolation::MissingWhereClause])?;
let mut violations = Vec::new();
for stmt in statements {
if let Statement::Query(query) = stmt {
check_query_rules(&query, &mut violations);
}
}
if violations.is_empty() {
Ok(())
} else {
Err(violations)
}
}
fn check_query_rules(query: &Query, violations: &mut Vec<StaticViolation>) {
// 1. Larang deep pagination via OFFSET
if query.offset.is_some() {
violations.push(StaticViolation::OffsetDetected);
}
if let SetExpr::Select(select) = &*query.body {
// 2. Larang SELECT *
for item in &select.projection {
if let SelectItem::Wildcard(_) = item {
violations.push(StaticViolation::SelectWildcardForbidden);
}
}
// 3. Wajibkan WHERE clause pada tabel operasional
if select.selection.is_none() {
violations.push(StaticViolation::MissingWhereClause);
}
}
}
Implementasi: Dynamic Plan Inspection & Seq Scan Detection
Jika query lolos pemeriksaan statis, harness mengevaluasi query plan menggunakan tokio-postgres. Kita tidak mengeksekusi query dengan ANALYZE agar database tidak menjalankan mutasi data atau melakukan I/O berat; cukup ambil estimasi cost dari engine planner.
use serde::Deserialize;
use tokio_postgres::Client;
#[derive(Debug, Deserialize)]
pub struct ExplainPlan {
#[serde(rename = "Plan")]
pub plan: PlanNode,
}
#[derive(Debug, Deserialize)]
pub struct PlanNode {
#[serde(rename = "Node Type")]
pub node_type: String,
#[serde(rename = "Relation Name")]
pub relation_name: Option<String>,
#[serde(rename = "Plan Rows")]
pub plan_rows: f64,
#[serde(rename = "Total Cost")]
pub total_cost: f64,
#[serde(rename = "Plans")]
pub plans: Option<Vec<PlanNode>>,
}
#[derive(Debug)]
pub struct PlanViolation {
pub reason: String,
pub table: String,
pub rows: f64,
}
pub async fn evaluate_query_plan(
client: &Client,
sql: &str,
max_scan_rows_threshold: f64,
) -> Result<(), Vec<PlanViolation>> {
let explain_query = format!("EXPLAIN (FORMAT JSON) {}", sql);
let rows = client
.query(&explain_query, &[])
.await
.map_err(|e| vec![PlanViolation {
reason: format!("Syntax / Planner error: {}", e),
table: "unknown".into(),
rows: 0.0,
}])?;
let json_val: serde_json::Value = rows[0].get(0);
let mut plans: Vec<ExplainPlan> = serde_json::from_value(json_val)
.map_err(|e| vec![PlanViolation {
reason: format!("Failed to deserialize plan: {}", e),
table: "unknown".into(),
rows: 0.0,
}])?;
let root_node = plans.remove(0).plan;
let mut violations = Vec::new();
walk_plan_nodes(&root_node, max_scan_rows_threshold, &mut violations);
if violations.is_empty() {
Ok(())
} else {
Err(violations)
}
}
fn walk_plan_nodes(node: &PlanNode, threshold: f64, violations: &mut Vec<PlanViolation>) {
// Deteksi Sequential Scan pada relasi dengan estimasi row besar
if node.node_type == "Seq Scan" && node.plan_rows > threshold {
violations.push(PlanViolation {
reason: format!("Unindexed Seq Scan detected. Cost: {:.2}", node.total_cost),
table: node.relation_name.clone().unwrap_or_else(|| "unnamed".into()),
rows: node.plan_rows,
});
}
if let Some(children) = &node.plans {
for child in children {
walk_plan_nodes(child, threshold, violations);
}
}
}
Otomasi Feedback Loop ke LLM
Kunci keberhasilan harness ini adalah mengubah error teknis menjadi prompt koreksi yang spesifik. Saat harness menemukan pelanggaran, payload feedback disusun kembali dan dikirim ke API model untuk generasi ulang:
Contoh Output Diagnosis Harness:
REJECTED: Table 'orders' had Seq Scan with estimated 450,000 rows. Reason: Predicate 'tenant_id' + 'created_at' does not match any composite index. Keyset pagination is required.
Bila LLM mendapati pesan tersebut, sertakan instruksi terarah pada system message:
{"role": "user", "content": "Kueri Anda ditolak oleh DB Guardrail:\n- Table: orders\n- Issue: Seq Scan detected (rows: 450,000)\n- Required Fix: Ubah OFFSET ke Keyset Pagination atau gunakan kombinasi kolom yang tercakup dalam index 'idx_orders_tenant_created' (tenant_id, created_at). Perbaiki query sekarang."}Pendekatan ini mengeliminasi halusinasi model. Budget model tidak perlu dilatih ulang (fine-tuning); harness menyediakan batasan deterministik yang memaksa model menghasilkan query optimal dalam 1-2 iterasi.
Pertimbangan Operasional dan Edge Cases
- Parameter Bindings: PostgreSQL query planner menghasilkan plan yang berbeda untuk nilai literal versus parameter (
$1,$2) karena ketiadaan kalkulasi selektivitas histogram saat runtime belum diketahui. Untuk validasi plan via harness, gantikan parameter sementara dengan representative values sebelum menjalankanEXPLAIN. - Cold Cache Shadow DB: Pastikan shadow DB memiliki nilai
reltuplesdanrelpagesyang sinkron dengan produksi (dapat diambil berkala via dump catalogpg_classdanpg_stats). Tanpa statistik yang akurat, engine planner dapat keliru mengasumsikan table kosong dan mengembalikanSeq Scanmeskipun index sudah tersedia.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!