Migrasi vendor AI tanpa melambatkan SQL di jalur kritis berarti satu hal: perubahan integrasi model tidak boleh merusak performa query yang sebelumnya stabil. Dalam praktiknya, masalah jarang datang dari pemanggilan model itu sendiri, melainkan dari perubahan skema dan pola query di backend: menambah kolom provider, menyimpan prompt/response, mencatat fallback, atau membuat tabel log yang tumbuh sangat cepat.

Tren perpindahan ke open models masuk akal dari sisi biaya, kontrol, dan portabilitas. Namun ketika tim terburu-buru menambahkan riwayat prompt, audit trail, dan fallback multi-provider ke database yang sama dengan jalur transaksi utama, query SQL inti sering ikut melambat. Artikel ini fokus pada masalah itu: bottleneck SQL yang muncul, gejala di production, cara membaca EXPLAIN, strategi indeks untuk filter dan sort gabungan, trade-off offset vs keyset pagination, kapan memisahkan data hot dan cold, serta langkah mitigasi tanpa downtime.

Mengapa migrasi provider AI sering memperlambat database

Saat integrasi AI masih sederhana, backend biasanya hanya menyimpan hasil akhir. Begitu sistem berkembang menjadi multi-provider atau pindah ke open models, kebutuhan datanya berubah:

  • Perlu menyimpan provider, model, latency, token usage, dan status fallback.
  • Perlu menyimpan riwayat prompt untuk debugging, evaluasi, atau audit.
  • Perlu menyimpan setiap percobaan request jika ada retry atau fallback ke provider lain.
  • Perlu menampilkan log atau histori di dashboard internal dengan filter yang makin kompleks.

Masalah muncul ketika semua kebutuhan itu dimasukkan ke tabel yang juga dipakai jalur kritis, misalnya tabel messages, orders, tickets, atau chat_sessions. Gejala yang sering terjadi:

  • Query listing utama yang dulu cepat menjadi lambat setelah join ke tabel log AI.
  • Filter berdasarkan workspace_id, status, dan created_at tiba-tiba memakai scan besar.
  • Insert log AI menaikkan contention pada indeks yang sama dengan query baca utama.
  • Dashboard histori memakai ORDER BY created_at DESC LIMIT 50 OFFSET ... pada tabel yang terus membesar, lalu makin lambat setiap hari.
  • Kolom teks besar seperti prompt atau response ikut terbaca pada query yang sebenarnya hanya butuh metadata.

Kesalahan desain yang umum: menambah kolom AI ke tabel inti karena terlihat praktis, lalu memakai tabel yang sama untuk transaksi utama, analitik ringan, dan audit trail. Hasilnya bukan hanya tabel makin lebar, tetapi pola aksesnya juga saling mengganggu.

Contoh skema yang realistis: pisahkan data transaksi, request AI, dan attempt provider

Untuk sistem yang mendukung fallback provider, simpan metadata utama terpisah dari payload besar. Struktur berikut cukup umum dan aman untuk dikembangkan.

Tabel inti aplikasi

CREATE TABLE documents (
  id BIGINT PRIMARY KEY,
  workspace_id BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  title VARCHAR(255) NOT NULL,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

Tabel ini sebaiknya tetap fokus pada kebutuhan aplikasi utama.

Tabel request AI per entitas

CREATE TABLE ai_requests (
  id BIGINT PRIMARY KEY,
  workspace_id BIGINT NOT NULL,
  document_id BIGINT NOT NULL,
  task_type VARCHAR(64) NOT NULL,
  final_provider VARCHAR(64),
  final_model VARCHAR(128),
  status VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL,
  FOREIGN KEY (document_id) REFERENCES documents(id)
);

Tabel ini menyimpan satu request logis, misalnya “ringkas dokumen” atau “klasifikasi konten”.

Tabel attempt untuk retry dan fallback provider

CREATE TABLE ai_request_attempts (
  id BIGINT PRIMARY KEY,
  ai_request_id BIGINT NOT NULL,
  workspace_id BIGINT NOT NULL,
  provider VARCHAR(64) NOT NULL,
  model VARCHAR(128) NOT NULL,
  attempt_no INT NOT NULL,
  status VARCHAR(32) NOT NULL,
  latency_ms INT,
  prompt_tokens INT,
  completion_tokens INT,
  error_code VARCHAR(64),
  created_at TIMESTAMP NOT NULL,
  FOREIGN KEY (ai_request_id) REFERENCES ai_requests(id)
);

Di sinilah data fallback sebaiknya ditempatkan, bukan menambah kolom provider_1, provider_2, provider_3 ke tabel inti.

Tabel payload besar untuk prompt/response

CREATE TABLE ai_request_payloads (
  ai_request_id BIGINT PRIMARY KEY,
  prompt_text TEXT,
  response_text TEXT,
  response_json TEXT,
  FOREIGN KEY (ai_request_id) REFERENCES ai_requests(id)
);

Pemisahan ini penting karena query operasional biasanya tidak perlu membaca TEXT atau payload besar. Menaruhnya di tabel terpisah membantu mengurangi I/O dan mencegah query daftar biasa ikut berat.

Bottleneck SQL yang umum setelah menambah log AI

1. Join ke tabel log yang tumbuh cepat

Contoh pola yang terlihat wajar, tetapi sering bermasalah:

SELECT d.id, d.title, d.status, ar.final_provider, ar.created_at
FROM documents d
LEFT JOIN ai_requests ar ON ar.document_id = d.id
WHERE d.workspace_id = ?
  AND d.status = 'ready'
ORDER BY d.created_at DESC
LIMIT 50;

Query ini bisa memburuk jika satu dokumen memiliki banyak request AI atau jika optimizer harus memindai banyak baris pada ai_requests. Jika kebutuhan UI hanya menampilkan dokumen, jangan langsung join ke tabel AI kecuali benar-benar perlu. Sering kali lebih efisien memakai dua query: satu untuk daftar dokumen, satu lagi untuk metadata AI berdasarkan ID yang sudah terpilih.

2. Filter gabungan tidak didukung indeks yang tepat

Query seperti berikut sangat umum:

SELECT id, document_id, final_provider, status, created_at
FROM ai_requests
WHERE workspace_id = ?
  AND status = 'completed'
  AND task_type = 'summary'
ORDER BY created_at DESC
LIMIT 50;

Tanpa indeks yang sesuai urutan filter dan sort-nya, database bisa melakukan scan besar lalu sort tambahan. Untuk query seperti ini, biasanya dibutuhkan indeks komposit yang mengikuti pola akses utama.

3. OFFSET pada tabel log yang terus membesar

Untuk halaman awal, query berikut mungkin masih terasa baik:

SELECT id, provider, model, status, created_at
FROM ai_request_attempts
WHERE workspace_id = ?
ORDER BY created_at DESC
LIMIT 50 OFFSET 5000;

Masalahnya, makin besar offset, makin banyak baris yang harus dilewati. Pada tabel log AI yang terus bertambah, performa akan memburuk seiring waktu.

4. SELECT * pada tabel lebar

Jika tabel menyimpan metadata plus kolom besar, SELECT * memperbesar beban I/O dan memori. Query daftar seharusnya memilih kolom minimum yang benar-benar dipakai.

5. Indeks terlalu banyak setelah migrasi

Reaksi umum setelah query melambat adalah menambah indeks untuk setiap kolom baru. Ini membantu sebagian query baca, tetapi memperlambat insert dan update pada tabel log yang write-heavy. Pada sistem AI, volume insert bisa sangat tinggi, jadi indeks harus dipilih berdasarkan query nyata, bukan asumsi.

Gejala di production yang perlu dicurigai

Regresi performa akibat migrasi provider AI sering terlihat dari pola berikut:

  • P95/P99 latency endpoint daftar atau detail naik, padahal waktu inferensi model tidak berubah signifikan.
  • Connection pool database mulai penuh saat ada lonjakan request AI atau dashboard internal dibuka.
  • CPU database naik karena sorting dan scanning, bukan karena lock berat semata.
  • Slow query log mulai didominasi query histori AI, listing admin, atau join antara tabel inti dan tabel log.
  • Autovacuum/maintenance atau pekerjaan housekeeping makin berat pada tabel log yang tumbuh cepat.
  • Cache hit rate turun karena query memakai parameter yang beragam dan hasilnya tidak stabil untuk dicache.

Jika setelah migrasi model latency API memburuk tetapi waktu provider AI tidak terlihat dominan, periksa SQL terlebih dahulu. Banyak tim terlalu fokus pada SDK atau jaringan ke model, padahal bottleneck utama ada di query backend.

Cara membaca EXPLAIN tanpa tersesat

EXPLAIN dipakai untuk melihat bagaimana database berencana mengeksekusi query. Nama output spesifik berbeda antar engine, tetapi prinsip membacanya mirip.

Hal yang perlu diperhatikan

  • Apakah indeks dipakai? Jika query dengan filter sempit tetap melakukan full scan, indeks kemungkinan tidak cocok atau statistik tidak membantu optimizer.
  • Berapa estimasi baris yang dibaca? Jika jauh lebih besar dari hasil akhir, query membuang banyak kerja.
  • Apakah ada sort terpisah? Jika ORDER BY created_at DESC tidak bisa mengikuti urutan indeks, database harus menyortir hasil setelah filter.
  • Apakah join memperbanyak baris? Join satu-ke-banyak ke tabel attempt bisa membuat hasil meledak sebelum akhirnya dipotong LIMIT.
  • Apakah ada filter yang diterapkan terlambat? Jika kondisi penting baru diterapkan setelah join, biaya eksekusi meningkat.

Contoh query dan analisis

EXPLAIN
SELECT id, document_id, final_provider, status, created_at
FROM ai_requests
WHERE workspace_id = 42
  AND status = 'completed'
  AND task_type = 'summary'
ORDER BY created_at DESC
LIMIT 50;

Jika hasil EXPLAIN menunjukkan scan besar lalu sort, maka indeks yang ada belum mengikuti pola query. Untuk kasus ini, indeks komposit seperti berikut biasanya relevan:

CREATE INDEX idx_ai_requests_workspace_status_task_created
ON ai_requests (workspace_id, status, task_type, created_at DESC);

Mengapa urutannya demikian? Karena query memfilter kuat pada workspace_id, status, dan task_type, lalu mengurutkan dengan created_at. Dengan indeks yang sejalan, database bisa mencari subset sempit terlebih dahulu lalu mengambil hasil dalam urutan yang sudah sesuai, mengurangi kebutuhan sort tambahan.

Jangan menghafal satu aturan indeks untuk semua query. Urutan kolom indeks harus mengikuti pola filter dan sort yang benar-benar dominan di production.

Strategi indeks untuk filter dan sort gabungan

Indeks komposit untuk query daftar utama

Untuk tabel request AI, query yang paling sering biasanya memfilter tenant/workspace, status, tipe task, dan mengurutkan berdasarkan waktu. Indeks komposit lebih efektif daripada banyak indeks tunggal yang berdiri sendiri.

CREATE INDEX idx_ai_requests_workspace_status_created
ON ai_requests (workspace_id, status, created_at DESC);

Pilih kombinasi yang sesuai dengan query paling penting. Jika ada banyak variasi query, prioritaskan yang berada di jalur kritis dan paling sering dipanggil.

Indeks untuk tabel attempt yang write-heavy

Pada ai_request_attempts, kebutuhan umum adalah melihat histori attempt per request atau daftar histori per workspace.

CREATE INDEX idx_attempts_request_created
ON ai_request_attempts (ai_request_id, created_at DESC);
CREATE INDEX idx_attempts_workspace_created
ON ai_request_attempts (workspace_id, created_at DESC);

Hindari menambahkan indeks untuk setiap kolom seperti provider, model, error_code jika belum ada query nyata yang menggunakannya secara intensif. Setiap indeks tambahan meningkatkan biaya tulis.

Indeks yang tampak benar tapi salah sasaran

  • Indeks hanya pada created_at padahal query selalu memfilter workspace_id.
  • Indeks pada status saja untuk kolom dengan kardinalitas rendah.
  • Indeks terpisah pada workspace_id dan created_at saat query utama butuh kombinasi keduanya untuk filter dan sort.
  • Indeks komposit dengan urutan kolom tidak cocok dengan pola filter utama.

Jangan indeks payload besar tanpa alasan kuat

Kolom prompt/response berukuran besar sebaiknya tidak ikut berada pada jalur query operasional. Jika butuh pencarian teks, pertimbangkan mekanisme khusus yang terpisah dari query daftar utama, bukan memaksa semua kebutuhan ke satu tabel besar.

Offset vs keyset pagination untuk log AI yang terus tumbuh

Kapan OFFSET masih cukup

Offset pagination sederhana dan mudah dipakai pada UI dengan nomor halaman. Untuk dataset kecil atau halaman awal, pendekatan ini sering cukup:

SELECT id, provider, status, created_at
FROM ai_request_attempts
WHERE workspace_id = ?
ORDER BY created_at DESC, id DESC
LIMIT 50 OFFSET 100;

Kekurangannya jelas: semakin besar offset, semakin banyak baris yang harus dilompati.

Mengapa keyset lebih cocok untuk tabel log AI

Pada tabel append-heavy seperti log request AI, keyset pagination biasanya lebih stabil. Alih-alih meminta “halaman ke-101”, klien meminta “50 baris berikutnya setelah cursor terakhir”.

SELECT id, provider, status, created_at
FROM ai_request_attempts
WHERE workspace_id = ?
  AND (created_at, id) < (?, ?)
ORDER BY created_at DESC, id DESC
LIMIT 50;

Pendekatan ini bekerja baik jika ada indeks yang mendukung urutan tersebut, misalnya:

CREATE INDEX idx_attempts_workspace_created_id
ON ai_request_attempts (workspace_id, created_at DESC, id DESC);

Trade-off yang perlu dipahami

  • Offset lebih mudah untuk UI berbasis nomor halaman, tetapi memburuk pada offset besar.
  • Keyset lebih stabil dan efisien untuk scrolling histori yang terus bertambah, tetapi tidak natural untuk lompat langsung ke halaman tertentu.
  • Keyset membutuhkan urutan yang deterministik. Karena itu tambahkan tie-breaker seperti id selain created_at.

Untuk log AI, keyset hampir selalu pilihan yang lebih aman jika volume data bertambah terus dan query histori sering dipakai.

Kapan memisahkan tabel hot dan cold data

Tidak semua data AI perlu berada di tabel yang sama selamanya. Saat tabel log membesar, pertimbangkan pemisahan antara hot data dan cold data.

Hot data

Data yang sering diakses aplikasi operasional, misalnya 7-30 hari terakhir atau request yang masih aktif. Data ini perlu indeks yang baik dan query cepat.

Cold data

Data lama untuk audit, investigasi, atau analisis sesekali. Data ini bisa dipindahkan ke tabel arsip, partisi lama, atau storage terpisah, tergantung kemampuan sistem Anda.

Tanda bahwa pemisahan sudah layak dipertimbangkan

  • Tabel log AI menjadi salah satu tabel terbesar di database utama.
  • Mayoritas query operasional hanya butuh data terbaru.
  • Pekerjaan maintenance pada tabel log mulai mengganggu workload utama.
  • Indeks yang dibutuhkan untuk data lama berbeda dengan data baru.

Pola implementasi sederhana

Salah satu pendekatan aman adalah mempertahankan tabel aktif untuk data terbaru dan memindahkan data lama secara bertahap ke tabel arsip dengan skema mirip. Query operasional membaca tabel aktif terlebih dahulu. Dashboard audit atau investigasi bisa membaca tabel arsip secara eksplisit.

Jika beban utama adalah transaksi aplikasi, jangan jadikan tabel log AI sebagai pusat semua kebutuhan. Memisahkan hot/cold data sering memberi hasil lebih nyata daripada menambah indeks terus-menerus.

Mitigasi tanpa downtime saat performa mulai turun

1. Tambah struktur baru, jangan langsung ubah jalur lama

Daripada menambahkan kolom besar atau join baru ke query inti, buat tabel baru untuk metadata AI dan payload. Biarkan jalur lama tetap berjalan, lalu migrasikan pembacaan secara bertahap.

2. Backfill bertahap

Jika perlu memindahkan data lama, lakukan dalam batch kecil. Hindari satu transaksi besar yang memakan lock lama dan membanjiri I/O.

3. Dual write sementara

Selama masa transisi, aplikasi bisa menulis ke struktur lama dan baru. Setelah pembacaan sudah dipindahkan dan diverifikasi, barulah write ke struktur lama dihentikan.

4. Tambah indeks secara hati-hati

Pembuatan indeks pada tabel besar harus direncanakan. Jadwalkan pada waktu beban rendah jika perlu, monitor dampaknya, dan pastikan query yang hendak diperbaiki memang sesuai dengan indeks tersebut.

5. Pindahkan payload besar keluar dari query operasional

Jika prompt dan response saat ini berada di tabel yang sama dengan metadata request, pindahkan ke tabel terpisah. Ini sering memberi perbaikan langsung untuk query daftar.

6. Ubah pagination histori ke keyset

Ini salah satu perbaikan dengan rasio dampak terhadap usaha yang sangat baik untuk tabel log AI.

7. Pecah query besar menjadi dua langkah

Untuk halaman daftar, ambil dulu ID dan metadata minimal dari tabel inti, lalu ambil data AI hanya untuk item yang tampil. Pendekatan ini sering mengurangi join mahal pada jalur kritis.

Checklist audit sebelum migrasi provider AI

  1. Identifikasi jalur kritis: endpoint mana yang paling sensitif terhadap latency dan tidak boleh ikut melambat.
  2. Petakan query saat ini: query paling lambat, paling sering, dan paling penting sebelum migrasi.
  3. Daftar data baru: provider, model, usage, prompt, response, retry, fallback, error code, audit metadata.
  4. Tentukan lokasi penyimpanan: mana yang masuk tabel operasional, mana yang masuk tabel log, mana yang dipisah sebagai payload.
  5. Review join baru: apakah ada join ke tabel log pada query yang sebelumnya sederhana.
  6. Desain indeks berdasarkan query nyata, bukan berdasarkan semua kolom yang tersedia.
  7. Uji pagination pada dataset yang realistis, terutama histori AI.
  8. Siapkan observabilitas: slow query log, latency endpoint, error rate, dan ukuran tabel.
  9. Rencanakan rollback: jika query baru bermasalah, bagaimana kembali ke jalur lama tanpa menghentikan layanan.
  10. Siapkan retensi data: berapa lama payload penuh disimpan di database utama.

Kesalahan umum indexing setelah menambah fitur AI

  • Menambah indeks tunggal pada semua kolom baru lalu berharap optimizer akan selalu menggabungkannya secara efisien.
  • Membuat indeks komposit tanpa melihat urutan filter dan ORDER BY.
  • Mengindeks kolom berkardinalitas rendah tanpa konteks query lain.
  • Mengabaikan biaya tulis pada tabel log yang menerima insert terus-menerus.
  • Tidak membedakan query operasional dan query audit.
  • Membiarkan dashboard internal menjalankan query berat pada database yang sama dengan traffic pengguna utama tanpa pembatasan.

Contoh perbaikan query di jalur kritis

Misalkan ada halaman daftar dokumen yang awalnya memakai join ke request AI terbaru dan mulai lambat.

Pola yang berisiko

SELECT d.id, d.title, d.status, ar.final_provider
FROM documents d
LEFT JOIN ai_requests ar ON ar.document_id = d.id
WHERE d.workspace_id = ?
  AND d.status = 'ready'
ORDER BY d.created_at DESC
LIMIT 50;

Pendekatan yang lebih aman

Langkah 1: ambil dokumen terlebih dahulu.

SELECT id, title, status, created_at
FROM documents
WHERE workspace_id = ?
  AND status = 'ready'
ORDER BY created_at DESC
LIMIT 50;

Langkah 2: ambil metadata AI hanya untuk dokumen yang tampil.

SELECT document_id, final_provider, final_model, status
FROM ai_requests
WHERE document_id IN (?, ?, ?, ...);

Pendekatan ini bekerja karena query pertama tetap fokus pada indeks tabel inti, sementara query kedua dibatasi pada himpunan kecil ID yang sudah dipilih. Dalam banyak kasus, total biaya lebih rendah daripada satu join besar yang memaksa optimizer menyeimbangkan dua workload berbeda.

Debugging tip saat regresi performa sudah terjadi

  • Bandingkan query sebelum dan sesudah migrasi provider AI, bukan hanya kode aplikasi.
  • Periksa apakah query baru mulai membaca kolom besar yang sebelumnya tidak disentuh.
  • Lihat distribusi ukuran tabel dan pertumbuhan harian tabel log AI.
  • Ambil beberapa query teratas dari slow query log, lalu cocokan dengan endpoint yang terdampak.
  • Gunakan EXPLAIN pada parameter yang realistis, misalnya workspace besar, bukan data lokal kecil.
  • Uji apakah mengganti OFFSET dengan keyset langsung menurunkan waktu respons histori.
  • Pastikan fallback provider tidak menghasilkan ledakan jumlah row attempt yang tidak diantisipasi pada UI atau laporan.

Penutup

Migrasi ke open models atau arsitektur multi-provider bukan sekadar urusan SDK dan pemilihan model. Perubahan paling berbahaya sering terjadi di database: tabel inti menjadi terlalu lebar, join ke log AI masuk ke jalur kritis, indeks tidak mengikuti pola query baru, dan histori yang terus tumbuh tetap dipaginasi dengan offset.

Jika ingin melakukan migrasi vendor AI tanpa melambatkan SQL di jalur kritis, prinsipnya sederhana: pisahkan data operasional dari payload besar, desain indeks berdasarkan query nyata, gunakan keyset untuk tabel log yang tumbuh terus, dan pertimbangkan pemisahan hot/cold data sebelum tabel log menjadi beban permanen. Fokus pada SQL lebih awal akan menghindarkan tim dari regresi performa yang sering keliru disalahkan pada model AI itu sendiri.