Fitur filtering, sorting, dan pagination pada tabel data Inertia.js sering kali menjadi sumber degradasi performa utama saat volume data menyentuh jutaan baris. Pola reaktif Inertia yang meminta pembaruan props secara parsial via XHR kerap memicu query database lambat di backend. Masalah ini umumnya bukan disebabkan oleh ketiadaan indeks, melainkan pola pembacaan random I/O (heap fetch) akibat query backend meminta kolom yang berada di luar jangkauan daun (leaf pages) B-Tree indeks.
Akar Masalah: Heap Fetch dan Bitmap Heap Scan
Saat Anda membuat indeks komposit standar pada kolom pencarian dan pengurutan (misal: (status, created_at)), database engine dapat mempersempit baris dengan cepat. Namun, jika Anda mengeksekusi SELECT * atau memproyeksikan kolom tambahan seperti total_amount dan customer_id untuk kebutuhan render UI komponen Inertia, mesin database harus melompat ke tabel utama (heap) untuk mengambil nilai kolom tersebut.
Pada tabel besar dengan korelasi data fisik yang rendah, perilaku ini memaksa disk melakukan random I/O masif. PostgreSQL biasanya beralih dari Index Scan menjadi Bitmap Heap Scan, yang membebani disk cache (shared buffers).
Deteksi Bottleneck dengan EXPLAIN (ANALYZE, BUFFERS)
Jalankan query pagination bawaan sebelum optimasi untuk melihat beban buffer dan tipe scan yang digunakan:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, total_amount, customer_id, created_at
FROM orders
WHERE status = 'processing'
ORDER BY created_at DESC
LIMIT 25 OFFSET 0;Output tipikal pada tabel tanpa covering index:
Bitmap Heap Scan on orders (cost=425.10..12890.30 rows=25 width=48) (actual time=14.120..48.810 rows=25 loops=1)
Recheck Cond: (status = 'processing'::order_status)
Rows Removed by Index Recheck: 0
Buffers: shared hit=820 read=4100
-> Bitmap Index Scan on idx_orders_status_created (cost=0.00..425.09 rows=4500 width=0) (actual time=4.210..4.210 rows=4500 loops=1)
Index Cond: (status = 'processing'::order_status)
Buffers: shared hit=42
Planning Time: 0.180 ms
Execution Time: 49.120 msPerhatikan metrik Buffers: shared read=4100. Mesin database harus membaca lebih dari 4.000 block dari disk/cache hanya untuk mengambil 25 baris data karena data kolom non-indeks tersebar di seluruh disk heap.
Solusi: Implementasi Covering Index
Covering index adalah indeks B-Tree yang mencakup semua kolom yang diminta oleh query tertentu. PostgreSQL 11+ menyediakan klausa INCLUDE, yang menaruh kolom payload non-kunci di level daun tanpa memengaruhi struktur percabangan atau batasan ukuran B-Tree untuk pencarian kunci.
Migration Schema PostgreSQL
Gunakan migration Laravel berikut untuk membuat covering index:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration {
public function up(): void
{
// Kolom pencarian & sorting di kunci B-Tree; payload kolom di INCLUDE
DB::statement('
CREATE INDEX CONCURRENTLY idx_orders_covering_inertia
ON orders (status, created_at DESC)
INCLUDE (id, total_amount, customer_id);
');
}
public function down(): void
{
DB::statement('DROP INDEX CONCURRENTLY IF EXISTS idx_orders_covering_inertia;');
}
};Catatan: Gunakan
CREATE INDEX CONCURRENTLYdi lingkungan produksi agar proses indexing tidak mengunci operasi tulis tabel secara eksklusif.
Sinkronisasi Query Backend dengan Serialization Props
Covering index menjadi tidak berguna jika backend melakukan SELECT *. Eloquent model di Laravel secara default memproyeksikan seluruh kolom jika tidak dibatasi secara eksplisit.
Implementasi di Controller
Pastikan proyeksi kolom pada Eloquent selaras dengan kolom pada indeks komposit dan klausa INCLUDE:
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class OrderController extends Controller
{
public function index(Request $request): Response
{
$status = $request->input('status', 'processing');
$orders = Order::query()
// Wajib: Hanya ambil kolom yang dicakup oleh indeks
->select(['id', 'status', 'total_amount', 'customer_id', 'created_at'])
->where('status', $status)
->orderByDesc('created_at')
->paginate(25)
->withQueryString()
->through(fn ($order) => [
'id' => $order->id,
'status' => $order->status,
'total_amount' => $order->total_amount,
'customer_id' => $order->customer_id,
'created_at' => $order->created_at->toISOString(),
]);
return Inertia::render('Orders/Index', [
'orders' => $orders,
'filters' => ['status' => $status],
]);
}
}Verifikasi: Menghasilkan Index Only Scan
Jalankan kembali EXPLAIN (ANALYZE, BUFFERS) setelah covering index dibuat dan proyeksi kolom diperketat:
Index Only Scan using idx_orders_covering_inertia on orders (cost=0.43..12.45 rows=25 width=48) (actual time=0.035..0.055 rows=25 loops=1)
Index Cond: (status = 'processing'::order_status)
Heap Fetches: 0
Buffers: shared hit=5
Planning Time: 0.125 ms
Execution Time: 0.082 msHasil eksekusi membuktikan:
- Tipe scan berubah total menjadi Index Only Scan.
- Heap Fetches: 0, database tidak lagi menyentuh disk heap sama sekali.
- Konsumsi buffer turun drastis dari 4.920 buffers menjadi 5 buffers.
Perbandingan Metrik Kinerja
Data berikut diambil dari tabel pengujian berisi 3.500.000 baris data pada PostgreSQL 16 (shared_buffers = 512MB) dan runtime Laravel/Inertia.js:
- Shared Buffers Read/Hit: 4.920 pages (sebelum) vs 5 pages (sesudah) — Penurunan 99.8%.
- PostgreSQL Execution Time: 49.12 ms (sebelum) vs 0.08 ms (sesudah) — Peningkatan kecepatan ~600x lipat.
- Inertia HTTP Response Latency: 94 ms (sebelum) vs 18 ms (sesudah) pada jaringan lokal.
Trade-off dan Batasan
- Ukuran Storage Indeks: Penambahan klausa
INCLUDEmeningkatkan ukuran file indeks di disk. Hindari memasukkan kolom bertipe teks panjang (misal: JSONB, TEXT) ke dalamINCLUDE. - Overhead Operasi Tulis: Setiap operasi
INSERT,UPDATE, atauDELETEpada tabel akan sedikit lebih lambat karena database harus memperbarui struktur indeks covering. - Dependensi Visibility Map: Index Only Scan memerlukan halaman tabel dalam kondisi bersih dari transaksi lama. Pastikan daemon
autovacuumberjalan optimal agar bit Visibility Map (VM) selalu terbarui. Jika VM belum diproses oleh VACUUM, PostgreSQL tetap akan mengecek heap (terlihat pada metrikHeap Fetches > 0).
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!