Audit SQL untuk log agen AI biasanya menjadi kebutuhan mendesak saat tabel event, tool call, dan hasil evaluasi mulai tumbuh jauh lebih cepat daripada query yang membacanya. Gejalanya jelas: feed insiden melambat, pencarian per run makin berat, filter status dan rentang waktu tidak stabil, dan dashboard memicu query mahal berulang-ulang.

Masalah ini jarang selesai hanya dengan menambah CPU atau RAM. Pada platform agent atau multi-agent, bottleneck paling umum justru ada di pola query, desain index, strategi pagination, dan kebijakan retensi data. Artikel ini fokus pada audit SQL yang praktis untuk PostgreSQL dan MySQL, dengan contoh kasus tabel log eksekusi agen yang volumenya tinggi.

Struktur data yang biasanya menjadi sumber bottleneck

Pada platform agen, tabel log sering tumbuh dari kebutuhan observabilitas operasional. Skema umumnya terlihat masuk akal di awal, tetapi menjadi mahal saat volume data meningkat.

Tabel yang umum ditemukan

  • agent_runs: satu baris per eksekusi run agen.
  • agent_events: event detail selama lifecycle run, misalnya started, tool_called, tool_failed, completed.
  • tool_calls: metadata pemanggilan tool, argumen ringkas, durasi, status.
  • evaluation_results: hasil evaluasi kualitas, skor, label, atau feedback otomatis.

Contoh skema sederhana:

CREATE TABLE agent_runs (
  id BIGINT PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  agent_id BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  started_at TIMESTAMP NOT NULL,
  finished_at TIMESTAMP NULL,
  created_at TIMESTAMP NOT NULL
);

CREATE TABLE agent_events (
  id BIGINT PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  run_id BIGINT NOT NULL,
  event_type VARCHAR(32) NOT NULL,
  severity VARCHAR(16) NULL,
  status VARCHAR(32) NULL,
  created_at TIMESTAMP NOT NULL,
  payload JSON NULL
);

CREATE TABLE tool_calls (
  id BIGINT PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  run_id BIGINT NOT NULL,
  tool_name VARCHAR(128) NOT NULL,
  status VARCHAR(32) NOT NULL,
  duration_ms INT NULL,
  created_at TIMESTAMP NOT NULL
);

CREATE TABLE evaluation_results (
  id BIGINT PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  run_id BIGINT NOT NULL,
  evaluator VARCHAR(64) NOT NULL,
  score NUMERIC(5,2) NULL,
  verdict VARCHAR(32) NULL,
  created_at TIMESTAMP NOT NULL
);

Masalah dimulai ketika semua kebutuhan dibebankan ke tabel yang sama atau query membaca terlalu banyak data mentah untuk kebutuhan UI yang sebenarnya sederhana.

Gejala query lambat yang paling sering muncul

1. Feed insiden lambat saat diurutkan berdasarkan waktu

Contoh kebutuhan: tampilkan event error atau warning terbaru untuk tenant tertentu dalam 24 jam terakhir.

SELECT id, run_id, event_type, severity, created_at
FROM agent_events
WHERE tenant_id = ?
  AND severity IN ('error', 'warning')
  AND created_at >= ?
ORDER BY created_at DESC
LIMIT 50;

Jika index tidak cocok dengan pola filter dan urutan, database bisa membaca banyak row lalu melakukan sort terpisah. Ini sering terlihat sebagai filesort di MySQL atau sort mahal setelah scan di PostgreSQL.

2. Pencarian per run terlihat sederhana tetapi tetap mahal

SELECT id, event_type, status, created_at
FROM agent_events
WHERE tenant_id = ? AND run_id = ?
ORDER BY created_at ASC;

Query ini tampak ringan, tetapi tanpa index yang diawali oleh tenant_id dan run_id, database bisa jatuh ke scan besar, terutama pada tabel event yang jumlah row-nya jauh lebih tinggi daripada tabel run.

3. Filter status dan waktu pada dashboard tidak stabil

SELECT status, COUNT(*)
FROM agent_runs
WHERE tenant_id = ?
  AND created_at BETWEEN ? AND ?
GROUP BY status;

Jika dashboard memanggil banyak agregasi serupa, latensi sering naik tajam pada jam sibuk. Penyebabnya bukan hanya jumlah row, tetapi juga fakta bahwa query agregasi harus menyentuh rentang data luas berulang kali.

4. Pagination halaman besar makin lama makin berat

SELECT id, run_id, event_type, created_at
FROM agent_events
WHERE tenant_id = ?
ORDER BY created_at DESC
LIMIT 50 OFFSET 10000;

OFFSET besar memaksa database melewati banyak row sebelum mengembalikan hasil. Pada feed log yang terus bertambah, pola ini buruk untuk performa dan konsistensi.

Cara melakukan audit SQL yang benar

Audit SQL bukan dimulai dari menebak index, tetapi dari mengumpulkan query nyata yang paling mahal dan paling sering dipakai.

Langkah audit yang disarankan

  1. Inventaris query utama: feed insiden, detail run, dashboard status, daftar tool call gagal, pencarian berdasarkan waktu.
  2. Kelompokkan per pola akses: lookup per run, feed terbaru, agregasi dashboard, retention cleanup.
  3. Lihat execution plan dengan EXPLAIN atau EXPLAIN ANALYZE bila aman digunakan.
  4. Periksa cardinality filter: kolom tenant, run_id, created_at, status, severity.
  5. Pastikan index sesuai urutan WHERE dan ORDER BY, bukan hanya “ada index”.
  6. Bandingkan jumlah row yang dibaca vs row yang dikembalikan. Jika jauh berbeda, index atau bentuk query biasanya salah.

Contoh hal yang perlu dilihat di EXPLAIN

  • Apakah terjadi full table scan atau range scan yang terlalu lebar.
  • Apakah ORDER BY memanfaatkan index atau tetap butuh sort tambahan.
  • Apakah join membaca tabel besar di urutan yang buruk.
  • Apakah estimasi row sangat meleset dari kondisi nyata, yang bisa menandakan statistik usang.

Contoh penggunaan:

EXPLAIN
SELECT id, run_id, event_type, severity, created_at
FROM agent_events
WHERE tenant_id = 42
  AND severity IN ('error', 'warning')
  AND created_at >= '2026-07-01 00:00:00'
ORDER BY created_at DESC
LIMIT 50;

Catatan: pada PostgreSQL, EXPLAIN ANALYZE mengeksekusi query sungguhan. Gunakan hati-hati pada query berat di sistem produksi.

Desain index komposit yang relevan untuk log agen AI

Kesalahan paling umum adalah membuat index satu kolom untuk setiap field penting, lalu berharap optimizer akan selalu menggabungkannya secara efisien. Untuk workload log, pendekatan ini sering kalah dibanding index komposit yang mengikuti pola query.

Prinsip dasar

  • Awali index dengan kolom filter yang paling selektif dan paling konsisten dipakai.
  • Jika query selalu dibatasi per tenant, tenant_id hampir selalu perlu di depan.
  • Masukkan kolom waktu jika query sering melakukan range atau urutan berdasarkan waktu.
  • Sesuaikan urutan index dengan pola WHERE lalu ORDER BY.

Contoh index untuk feed insiden

Untuk query:

SELECT id, run_id, event_type, severity, created_at
FROM agent_events
WHERE tenant_id = ?
  AND severity IN ('error', 'warning')
  AND created_at >= ?
ORDER BY created_at DESC
LIMIT 50;

Index yang lebih masuk akal:

CREATE INDEX idx_agent_events_tenant_severity_created_at
  ON agent_events (tenant_id, severity, created_at DESC);

Index ini membantu karena filter tenant dan severity diterapkan lebih dulu, lalu hasil sudah dekat dengan urutan waktu yang dibutuhkan.

Contoh index untuk detail per run

CREATE INDEX idx_agent_events_tenant_run_created_at
  ON agent_events (tenant_id, run_id, created_at ASC);

Untuk detail event dalam satu run, index ini biasanya lebih efektif daripada index terpisah pada run_id dan created_at.

Contoh index untuk tool call gagal terbaru

CREATE INDEX idx_tool_calls_tenant_status_created_at
  ON tool_calls (tenant_id, status, created_at DESC);

Ini cocok bila UI sering menampilkan tool call gagal atau timeout per tenant berdasarkan waktu terbaru.

Tentang low-cardinality column

Kolom seperti status, severity, atau verdict sering memiliki cardinality rendah: nilainya sedikit dan berulang. Index pada kolom seperti ini sendirian sering tidak efektif, karena tidak cukup menyaring data.

Contoh anti-pattern:

CREATE INDEX idx_agent_events_severity ON agent_events (severity);

Jika sebagian besar row hanya berisi beberapa nilai severity, index ini cenderung kurang berguna. Lebih baik gabungkan dengan kolom pembatas kuat seperti tenant_id dan created_at.

Kapan terlalu banyak index menjadi masalah

Setiap index tambahan memperlambat operasi tulis: insert, update, delete. Pada sistem log agen AI, laju insert biasanya tinggi. Jadi jangan membuat index untuk semua kemungkinan query. Buat index berdasarkan query yang benar-benar kritis dan sering dipakai.

Cursor pagination lebih tepat daripada OFFSET untuk feed log

Pada tabel log yang terus bertambah, cursor pagination hampir selalu lebih baik daripada offset pagination. Alasannya ada dua: performa dan konsistensi hasil.

Masalah OFFSET

SELECT id, run_id, event_type, created_at
FROM agent_events
WHERE tenant_id = ?
ORDER BY created_at DESC, id DESC
LIMIT 50 OFFSET 10000;

Database tetap harus melewati ribuan row sebelum mengambil 50 hasil akhir. Selain mahal, hasil juga bisa bergeser jika data baru masuk di antara request halaman.

Pola cursor yang lebih sehat

SELECT id, run_id, event_type, created_at
FROM agent_events
WHERE tenant_id = ?
  AND (created_at, id) < (?, ?)
ORDER BY created_at DESC, id DESC
LIMIT 50;

Index yang mendukung:

CREATE INDEX idx_agent_events_tenant_created_id
  ON agent_events (tenant_id, created_at DESC, id DESC);

Mengapa ini bekerja? Karena database dapat langsung melanjutkan pembacaan dari posisi terakhir pada index, bukan menghitung ulang offset dari awal.

Gunakan pasangan (created_at, id) jika timestamp tidak unik. Mengandalkan created_at saja bisa menghasilkan duplikasi atau baris terlewat antar halaman.

Partisi, retensi data, dan kapan archive table lebih tepat

Pada log agen AI, pertumbuhan data bukan pengecualian tetapi sifat dasar sistem. Karena itu, audit SQL harus selalu dihubungkan dengan retensi data. Tanpa retensi yang jelas, index yang awalnya sehat tetap akan membengkak.

Kapan partisi layak dipertimbangkan

Partisi cocok bila:

  • query hampir selalu dibatasi berdasarkan waktu, misalnya harian atau bulanan;
  • hapus data lama dilakukan rutin;
  • ukuran tabel dan index utama sudah menekan performa maintenance dan vacuum atau optimize.

Pendekatan umum adalah partisi berdasarkan created_at. Keuntungannya:

  • query rentang waktu bisa menyentuh partisi yang relevan saja;
  • retensi data lama lebih mudah karena bisa drop partisi lama, bukan delete jutaan row;
  • maintenance index per partisi cenderung lebih ringan.

Namun partisi bukan obat universal. Jika query utama Anda adalah lookup per run_id tanpa filter waktu, partisi berbasis waktu tidak otomatis membuat query itu cepat.

Kapan archive table lebih tepat

Archive table sering lebih tepat jika data lama masih perlu disimpan, tetapi jarang diakses oleh aplikasi utama. Contohnya:

  • feed operasional hanya butuh 30-90 hari data panas;
  • audit historis 1-2 tahun masih wajib untuk compliance;
  • query produk harian tidak perlu membaca data lama.

Pola umumnya:

  1. Simpan data aktif di tabel utama.
  2. Pindahkan data lama ke tabel arsip dengan skema serupa atau lebih ringkas.
  3. Batasi UI operasional agar membaca tabel aktif saja.

Keuntungan archive table:

  • tabel utama tetap kecil dan cepat;
  • index produksi lebih fokus ke workload harian;
  • analisis historis bisa dipisahkan ke jalur query berbeda.

Kekurangannya:

  • query lintas data aktif dan arsip jadi lebih rumit;
  • aplikasi perlu tahu kapan harus membaca arsip;
  • proses pemindahan data harus aman dan idempoten.

Delete massal adalah anti-pattern umum

Menghapus jutaan row lama dengan satu query besar sering menyebabkan lock panjang, tekanan I/O, dan pembengkakan maintenance. Jika belum memakai partisi, lebih aman menghapus bertahap dalam batch kecil.

DELETE FROM agent_events
WHERE created_at < ?
LIMIT 10000;

Implementasi tepatnya berbeda antar database, tetapi prinsipnya sama: lakukan bertahap, ukur dampaknya, dan hindari transaksi raksasa.

Anti-pattern SQL yang sering muncul pada log agen

1. SELECT *

Pada tabel event yang punya kolom JSON besar, SELECT * membuat I/O lebih mahal daripada yang dibutuhkan UI.

Lebih baik:

SELECT id, run_id, event_type, severity, created_at
FROM agent_events
WHERE tenant_id = ?
ORDER BY created_at DESC
LIMIT 50;

2. Index ada, tapi urutannya salah

Misalnya query memakai WHERE tenant_id = ? AND run_id = ? ORDER BY created_at, tetapi index yang dibuat adalah (created_at, tenant_id, run_id). Secara teori bisa dipakai pada sebagian kondisi, tetapi tidak optimal untuk pola ini.

3. Filter pada fungsi terhadap kolom waktu

WHERE DATE(created_at) = '2026-07-06'

Pola ini sering menghambat pemanfaatan index karena kolom dibungkus fungsi. Lebih baik gunakan rentang waktu eksplisit:

WHERE created_at >= '2026-07-06 00:00:00'
  AND created_at < '2026-07-07 00:00:00'

4. Join dashboard langsung ke tabel log mentah

Dashboard ringkasan yang menghitung semua metrik langsung dari tabel event mentah biasanya cepat rusak saat skala naik. Untuk metrik yang sering diakses, pertimbangkan tabel agregasi periodik atau materialisasi sesuai kebutuhan sistem Anda.

5. Menganggap index status pasti membantu

Kolom status dengan nilai sedikit sering tidak selektif. Jika query Anda selalu menggabungkan status dengan tenant dan waktu, desain index harus mencerminkan kombinasi itu.

Contoh audit query dan perbaikannya

Kasus: feed insiden tenant melambat

Query awal:

SELECT id, run_id, event_type, severity, created_at
FROM agent_events
WHERE tenant_id = 42
  AND severity IN ('error', 'warning')
ORDER BY created_at DESC
LIMIT 100;

Gejala dari EXPLAIN yang biasanya muncul:

  • scan membaca terlalu banyak row tenant;
  • sort dilakukan setelah filtering;
  • index yang ada hanya pada tenant_id atau hanya pada created_at.

Perbaikan yang umum efektif:

  1. Buat index komposit (tenant_id, severity, created_at DESC).
  2. Hindari mengambil kolom payload besar jika tidak dipakai feed.
  3. Jika feed benar-benar hanya butuh insiden, pertimbangkan tabel atau stream terpisah untuk event prioritas tinggi.

Kasus: detail run lambat karena event terlalu banyak

Query awal:

SELECT *
FROM agent_events
WHERE run_id = ?
ORDER BY created_at ASC;

Masalahnya:

  • tidak dibatasi tenant, padahal sistem multi-tenant;
  • mengambil semua kolom termasuk JSON besar;
  • index hanya ada pada created_at.

Perbaikan:

SELECT id, event_type, status, created_at, severity
FROM agent_events
WHERE tenant_id = ? AND run_id = ?
ORDER BY created_at ASC;
CREATE INDEX idx_agent_events_tenant_run_created_at
  ON agent_events (tenant_id, run_id, created_at ASC);

Checklist tuning untuk PostgreSQL dan MySQL

Checklist berikut bisa dipakai saat melakukan audit SQL untuk log agen AI.

Checklist query dan index

  • Daftar 5-10 query paling lambat dan paling sering dipanggil.
  • Pastikan setiap query penting punya index yang sesuai pola WHERE dan ORDER BY.
  • Hindari index tunggal pada kolom low-cardinality kecuali ada alasan kuat.
  • Evaluasi apakah query bisa memakai cursor pagination.
  • Kurangi SELECT *, terutama pada tabel dengan kolom JSON atau teks besar.
  • Periksa apakah ada fungsi pada kolom terindeks di klausa filter.

Checklist operasional

  • Pastikan statistik optimizer diperbarui secara rutin.
  • Audit index yang jarang dipakai tetapi membebani write path.
  • Jadwalkan retensi data, jangan menunggu tabel membengkak.
  • Jika delete data lama berat, evaluasi partisi atau archive table.
  • Pisahkan kebutuhan operasional real-time dari analitik historis jika beban query berbeda jauh.

Checklist khusus PostgreSQL

  • Gunakan EXPLAIN (ANALYZE, BUFFERS) secara hati-hati untuk melihat biaya nyata.
  • Perhatikan efek dead tuples dan kebutuhan vacuum pada tabel dengan delete tinggi.
  • Evaluasi apakah index tambahan benar-benar sepadan dengan laju insert.

Checklist khusus MySQL

  • Periksa apakah query memicu full scan, temporary table, atau filesort.
  • Pastikan urutan kolom pada index komposit sesuai pola query dominan.
  • Uji kembali query agregasi dashboard yang berjalan terlalu sering pada data mentah.

Penutup

Audit SQL untuk log agen AI bukan sekadar menambah index sebanyak mungkin. Kuncinya adalah memahami pola akses nyata: feed insiden, pencarian per run, filter status dan waktu, serta dashboard. Dari sana, baru tentukan index komposit, pagination, retensi, dan strategi penyimpanan data panas versus data dingin.

Jika Anda mengelola platform agent atau multi-agent dengan volume log tinggi, tiga perbaikan yang biasanya memberi dampak paling cepat adalah: mengganti offset dengan cursor pagination, merancang index komposit berdasarkan query nyata, dan menerapkan retensi atau arsip sebelum tabel utama membesar tanpa kontrol. Setelah itu, gunakan EXPLAIN secara disiplin dan ukur perubahan per query, bukan berdasarkan asumsi.