Pipeline evaluasi LLM biasanya dimulai dengan beberapa dataset, eksperimen, dan metrik sederhana. Seiring jumlah model, prompt, versi dataset, dan hasil penilaian bertambah, database dapat menjadi bottleneck utama: query daftar eksperimen melambat, proses agregasi memakan CPU, tabel hasil membesar cepat, dan biaya operasional meningkat.
Masalah ini bukan sekadar isu performa. Seperti konteks yang dibahas dalam artikel Eval Startups, biaya infrastruktur dan beban operasional dapat memengaruhi keberlangsungan produk evaluasi. Karena itu, optimasi database perlu diperlakukan sebagai bagian dari desain produk, bukan pekerjaan tuning yang dilakukan setelah sistem bermasalah.
Artikel ini menggunakan contoh SQL bergaya PostgreSQL. Prinsipnya tetap relevan untuk database relasional lain, tetapi sintaks indeks parsial, operator JSON, dan rencana eksekusi dapat berbeda.
1. Modelkan Data Evaluasi Berdasarkan Pola Akses
Skema yang baik harus menjawab dua kebutuhan yang sering bertentangan: menulis hasil evaluasi dalam jumlah besar secara efisien dan membaca hasil tersebut untuk dashboard, analisis, atau perbandingan eksperimen. Hindari menyimpan seluruh data dalam satu tabel besar jika sebagian kolom memiliki siklus hidup dan pola akses berbeda.
Contoh skema inti
CREATE TABLE datasets (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
version TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (name, version)
);
CREATE TABLE experiments (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
project_id BIGINT NOT NULL,
name TEXT NOT NULL,
model_name TEXT NOT NULL,
prompt_version TEXT NOT NULL,
dataset_id BIGINT NOT NULL REFERENCES datasets(id),
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
completed_at TIMESTAMPTZ
);
CREATE TABLE evaluation_cases (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
dataset_id BIGINT NOT NULL REFERENCES datasets(id),
external_key TEXT NOT NULL,
input JSONB NOT NULL,
expected_output JSONB,
UNIQUE (dataset_id, external_key)
);
CREATE TABLE evaluation_results (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
experiment_id BIGINT NOT NULL REFERENCES experiments(id),
case_id BIGINT NOT NULL REFERENCES evaluation_cases(id),
status TEXT NOT NULL,
output TEXT,
score NUMERIC,
latency_ms INTEGER,
token_input INTEGER,
token_output INTEGER,
error_code TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (experiment_id, case_id)
);
CREATE TABLE evaluator_scores (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
result_id BIGINT NOT NULL REFERENCES evaluation_results(id),
evaluator_name TEXT NOT NULL,
evaluator_version TEXT NOT NULL,
score NUMERIC,
explanation TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (result_id, evaluator_name, evaluator_version)
);
Pemisahan tersebut membantu karena evaluation_results dapat tumbuh jauh lebih cepat daripada experiments. Data mentah seperti output model dan penjelasan evaluator juga sering lebih besar daripada metadata. Jika dashboard hanya memerlukan skor dan status, jangan selalu mengambil kolom teks besar tersebut.
Gunakan identifier dan constraint untuk menjaga konsistensi
Constraint seperti UNIQUE (experiment_id, case_id) mencegah hasil yang sama tertulis dua kali ketika worker melakukan retry. Ini penting pada pipeline berbasis queue karena pengiriman pekerjaan dan transaksi database biasanya bersifat at-least-once, bukan tepat satu kali.
Jika hasil boleh memiliki beberapa percobaan eksekusi, gunakan kolom seperti attempt atau tabel eksekusi terpisah secara eksplisit. Jangan mengandalkan duplikasi baris yang tidak memiliki aturan bisnis jelas, karena agregasi skor dapat menjadi tidak konsisten.
2. Temukan Query Lambat Sebelum Menambah Indeks
Menambah indeks secara membabi buta dapat mempercepat satu query, tetapi memperlambat proses insert dan update, memperbesar penyimpanan, serta menambah pekerjaan vacuum atau maintenance. Mulailah dengan mengumpulkan query yang benar-benar dijalankan oleh worker dan endpoint penting.
Ukur setidaknya:
- waktu eksekusi dan p95 atau p99 latency;
- jumlah baris yang dibaca dan dikembalikan;
- frekuensi query;
- beban CPU, I/O, koneksi, dan lock;
- ukuran tabel serta pertumbuhannya dari waktu ke waktu.
Untuk PostgreSQL, EXPLAIN (ANALYZE, BUFFERS) dapat menunjukkan apakah database melakukan sequential scan, index scan, sort besar, atau menunggu I/O. Jalankan pada query representatif, bukan hanya dataset kecil di laptop.
EXPLAIN (ANALYZE, BUFFERS)
SELECT r.id, r.case_id, r.score, r.latency_ms
FROM evaluation_results AS r
WHERE r.experiment_id = 842
AND r.status = 'completed'
ORDER BY r.id
LIMIT 100;
Perhatikan perbedaan antara estimated rows dan actual rows. Estimasi yang jauh meleset dapat membuat optimizer memilih rencana yang buruk. Setelah perubahan besar pada data, pastikan statistik database diperbarui sesuai mekanisme maintenance database yang digunakan.
Catatan keamanan:
EXPLAIN ANALYZEbenar-benar menjalankan query. Untuk operasi tulis, gunakan transaksi yang dapat di-rollback atau opsi explain yang tidak mengeksekusi perubahan jika didukung oleh database Anda.
3. Rancang Composite Index dari Filter dan Urutan Query
Indeks harus mengikuti pola query, bukan hanya nama kolom yang dianggap penting. Untuk query yang selalu memfilter berdasarkan eksperimen, lalu mengurutkan berdasarkan ID, indeks berikut biasanya lebih relevan daripada indeks terpisah pada masing-masing kolom:
CREATE INDEX CONCURRENTLY idx_results_experiment_id_id
ON evaluation_results (experiment_id, id);
Kolom experiment_id berada di depan karena digunakan untuk membatasi himpunan data. Kolom id mendukung urutan dan pagination di dalam eksperimen. Indeks terpisah pada experiment_id dan id tidak selalu memberikan rencana yang sama efisien, khususnya ketika database perlu menggabungkan hasil dua indeks dan melakukan sort tambahan.
Untuk query agregasi yang sering digunakan, buat indeks berdasarkan predicate yang nyata:
CREATE INDEX CONCURRENTLY idx_results_experiment_completed
ON evaluation_results (experiment_id, id)
WHERE status = 'completed';
Indeks parsial tersebut berguna jika sebagian besar query hanya membaca hasil berstatus completed dan baris dengan status itu merupakan subset yang cukup kecil. Namun, indeks parsial bukan pilihan otomatis. Query harus memiliki kondisi yang dapat dikenali sebagai sesuai dengan predicate indeks, dan setiap perubahan status dapat menambah pekerjaan pemeliharaan indeks.
Hindari indeks berlebihan
- Setiap indeks menambah biaya insert dan update.
- Indeks pada kolom dengan kardinalitas rendah, seperti status, belum tentu berguna jika hampir semua baris memiliki nilai yang sama.
- Indeks pada kolom JSON atau teks besar harus dibuat berdasarkan query yang jelas, bukan karena kolom tersebut terlihat fleksibel.
- Indeks duplikat atau indeks dengan prefix yang sudah tercakup indeks lain perlu ditinjau dan dihapus secara terencana.
4. Gunakan Keyset Pagination untuk Hasil yang Besar
OFFSET sederhana, tetapi menjadi semakin mahal ketika halaman berada jauh di dalam tabel. Database tetap perlu melewati baris-baris sebelumnya sebelum mengembalikan halaman yang diminta. Untuk worker yang memproses seluruh hasil evaluasi, gunakan keyset pagination atau seek pagination.
-- Halaman pertama
SELECT id, case_id, score, output
FROM evaluation_results
WHERE experiment_id = 842
AND id > 0
ORDER BY id
LIMIT 500;
-- Halaman berikutnya, menggunakan ID terakhir dari halaman sebelumnya
SELECT id, case_id, score, output
FROM evaluation_results
WHERE experiment_id = 842
AND id > 128499
ORDER BY id
LIMIT 500;
Dengan indeks (experiment_id, id), database dapat melanjutkan dari posisi indeks terakhir. Simpan last_seen_id sebagai cursor, bukan nomor halaman. Teknik ini sangat cocok untuk pemrosesan berurutan dan sinkronisasi tambahan.
Jika urutan tidak unik, tambahkan tie-breaker. Misalnya, untuk urutan berdasarkan waktu dan ID:
CREATE INDEX CONCURRENTLY idx_results_experiment_created_id
ON evaluation_results (experiment_id, created_at, id);
SELECT id, case_id, score
FROM evaluation_results
WHERE experiment_id = 842
AND (created_at, id) > ('2025-01-15T10:00:00Z', 128499)
ORDER BY created_at, id
LIMIT 500;
Pastikan format cursor menyimpan seluruh kolom pengurutan. Jangan hanya menyimpan timestamp jika beberapa baris dapat memiliki timestamp yang sama.
5. Batch Insert dan Update Tanpa Membebani Database
Menulis satu baris per query menambah round trip, overhead parsing, dan tekanan pada connection pool. Sebaliknya, batch yang terlalu besar dapat menghasilkan transaksi panjang, lock lebih lama, penggunaan memori tinggi, dan retry yang mahal. Ukuran batch perlu diuji berdasarkan ukuran row, latency jaringan, dan kapasitas database.
Gunakan bulk insert atau multi-row insert untuk hasil yang sudah dikumpulkan worker. Constraint unik dapat dipakai agar retry aman:
INSERT INTO evaluation_results (
experiment_id,
case_id,
status,
output,
score,
latency_ms,
token_input,
token_output
)
VALUES
(842, 1001, 'completed', '...', 0.91, 820, 120, 35),
(842, 1002, 'completed', '...', 0.74, 910, 118, 42),
(842, 1003, 'failed', NULL, NULL, NULL, 130, 0)
ON CONFLICT (experiment_id, case_id)
DO UPDATE SET
status = EXCLUDED.status,
output = EXCLUDED.output,
score = EXCLUDED.score,
latency_ms = EXCLUDED.latency_ms,
token_input = EXCLUDED.token_input,
token_output = EXCLUDED.token_output;
Pilih perilaku konflik sesuai semantik pekerjaan. DO UPDATE cocok jika retry harus memperbarui hasil, sedangkan DO NOTHING lebih aman jika hasil pertama bersifat final. Hindari update yang tidak perlu karena setiap update dapat membuat versi row baru dan menambah beban maintenance.
Batasi konkurensi, bukan hanya ukuran batch
Jika puluhan worker melakukan batch insert secara bersamaan, bottleneck dapat berpindah ke WAL, disk, lock, atau connection pool. Gunakan batas konkurensi yang eksplisit dan pantau:
- jumlah koneksi aktif dan koneksi yang menunggu;
- durasi transaksi;
- throughput insert;
- deadlock dan lock wait;
- lag replika jika pembacaan menggunakan read replica.
Untuk pipeline yang dapat menerima keterlambatan singkat, buffer hasil di queue atau staging table dapat memisahkan kecepatan model dari kecepatan database. Namun, staging menambah kompleksitas dan membutuhkan mekanisme retry serta deduplikasi yang jelas.
6. Pisahkan Jalur Tulis, Baca, dan Agregasi
Dashboard yang menjalankan agregasi berat langsung pada tabel hasil mentah dapat mengganggu worker yang sedang menulis. Beberapa strategi yang dapat dipilih:
- Pre-aggregation: simpan ringkasan per eksperimen, evaluator, model, atau dataset setelah batch selesai.
- Materialized view: cocok untuk laporan yang diperbarui berkala dan tidak harus real-time.
- Read replica: memindahkan sebagian beban baca, tetapi hasil dapat mengalami replication lag.
- Data warehouse atau object storage: gunakan untuk analisis historis dan eksplorasi besar yang tidak cocok dijalankan pada database transaksi.
Jangan memindahkan semua data ke sistem analitik terlalu dini. Untuk aplikasi yang masih kecil, query terindeks pada database utama mungkin lebih sederhana dan murah. Migrasi layak dipertimbangkan ketika pola analitik mulai mengganggu SLA penulisan atau ukuran data membuat backup dan maintenance menjadi sulit.
7. Terapkan Retensi Data Secara Eksplisit
Hasil evaluasi dapat berisi output panjang, token usage, trace, dan penjelasan evaluator. Menyimpan semuanya tanpa batas akan meningkatkan biaya storage, backup, indeks, dan query. Tentukan kebijakan berdasarkan nilai data, bukan hanya usia.
- Simpan metadata eksperimen dan skor ringkas lebih lama.
- Arsipkan output mentah lama ke object storage dengan format dan checksum yang terdokumentasi.
- Hapus data sementara, percobaan gagal, atau trace debug setelah masa retensi yang disepakati.
- Gunakan partisi berbasis waktu bila tabel sangat besar dan penghapusan berdasarkan rentang waktu menjadi operasi rutin.
Partitioning dapat membuat penghapusan satu rentang waktu lebih murah melalui penghapusan atau pelepasan partisi, tetapi menambah kompleksitas routing query, indeks, migration, dan monitoring. Jangan menerapkannya hanya karena tabel terlihat besar; pastikan pola retensi dan query memang berbasis kolom partisi.
Retensi juga harus mempertimbangkan privasi. Input dan output evaluasi dapat mengandung data sensitif. Tetapkan siapa yang boleh mengaksesnya, enkripsi sesuai kebutuhan, dan hindari menyalin payload mentah ke log aplikasi atau sistem observability.
8. Cara Mengukur Sebelum dan Sesudah Optimasi
Optimasi yang baik harus dapat diverifikasi. Buat baseline untuk query penting sebelum mengubah skema:
- Catat query, parameter representatif, jumlah row, dan kondisi data.
- Ambil execution plan dengan statistik buffer jika tersedia.
- Ukur latency pada kondisi normal dan saat worker sedang menulis.
- Catat penggunaan CPU, I/O, koneksi, lock, dan ukuran indeks.
- Terapkan satu perubahan utama dalam satu waktu.
- Ulangi pengukuran pada volume data yang sebanding.
Jangan menyimpulkan keberhasilan hanya dari waktu satu query. Indeks baru mungkin menurunkan latency dashboard, tetapi meningkatkan waktu insert atau ukuran backup. Evaluasi setidaknya dua sisi: read path dan write path.
Gunakan traffic atau replay yang aman bila memungkinkan. Uji migration pada salinan database atau lingkungan staging dengan distribusi data yang mirip produksi. Untuk indeks besar, gunakan mekanisme pembuatan indeks online yang sesuai dengan database Anda dan pahami konsekuensi lock selama prosesnya.
9. Checklist Implementasi yang Aman
- Dokumentasikan query utama untuk dashboard, worker, retry, dan ekspor.
- Ambil baseline latency, row scanned, I/O, lock, dan throughput insert.
- Verifikasi execution plan dengan parameter dan volume data yang realistis.
- Buat composite index berdasarkan kombinasi filter serta urutan yang benar-benar digunakan.
- Gunakan partial index hanya jika predicate query stabil dan subset datanya memang menguntungkan.
- Ganti pagination berbasis offset dengan keyset pagination untuk pemrosesan data besar.
- Gunakan batch write dan idempotency key atau constraint unik untuk menghadapi retry worker.
- Batasi konkurensi worker agar connection pool dan storage tidak jenuh.
- Pisahkan agregasi berat dari jalur tulis melalui ringkasan, replica, atau sistem analitik bila diperlukan.
- Tetapkan retensi, arsip, backup, dan penghapusan data sensitif sejak awal.
- Monitor perubahan read latency, write latency, ukuran indeks, bloat, lock, dan replication lag.
- Siapkan rollback untuk migration dan uji perubahan pada data yang menyerupai produksi.
Penutup
Optimasi database untuk pipeline evaluasi LLM dimulai dari pemahaman pola akses, bukan dari menambah sebanyak mungkin indeks. Skema yang memisahkan eksperimen, kasus, hasil, dan skor evaluator membuat constraint serta lifecycle data lebih jelas. Setelah itu, gunakan EXPLAIN untuk menemukan bottleneck, composite index untuk filter dan urutan yang tepat, keyset pagination untuk pemrosesan besar, serta batching dengan konkurensi yang terkendali.
Keputusan teknis harus mempertimbangkan trade-off read dan write. Data yang tidak lagi diperlukan untuk operasi harian sebaiknya diarsipkan atau dihapus sesuai kebijakan retensi. Dengan pengukuran sebelum dan sesudah, optimasi tidak menjadi tebakan, melainkan proses terukur yang membantu menjaga biaya dan operasional pipeline tetap terkendali.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!