Tantangan Skalabilitas Metadata Video Interaktif

Aplikasi video interaktif berbasis branching narrative atau arsitektur state machine (seperti Aval) membutuhkan latensi pengambilan data yang sangat rendah. Setiap frame atau interval waktu tertentu membawa metadata berupa cue points, koordinat interaksi, pilihan cabang (decision branches), serta mutasi state.

Masalah muncul ketika metadata tersebut disimpan dalam satu dokumen monolithic besar atau di-query menggunakan pendekatan naive berbasis OFFSET. Saat ribuan pengguna melakukan seeking atau memutar video berdensitas cue tinggi secara bersamaan, database mengalami I/O spike dan query latency memburuk secara eksponensial.

Pemisahan Skema: Video Asset vs. Timeline Cues

Langkah awal adalah memisahkan metadata statis aset video dari metadata dinamis timeline. Menyimpan seluruh cues di dalam kolom JSON pada tabel video menyebabkan amplifikasi pembacaan buffer pool dan overhead serialisasi data yang tidak dibutuhkan.

CREATE TABLE interactive_videos (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    duration_frames INT NOT NULL,
    fps SMALLINT NOT NULL DEFAULT 30,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE video_cue_points (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    video_id BIGINT NOT NULL REFERENCES interactive_videos(id) ON DELETE CASCADE,
    start_frame INT NOT NULL,
    end_frame INT NOT NULL,
    event_type VARCHAR(50) NOT NULL,
    payload JSONB NOT NULL,
    CONSTRAINT check_frame_order CHECK (end_frame >= start_frame)
);

Normalisasi ini membatasi ukuran baris pada tabel utama tetap ramping, sekaligus mengisolasi modifikasi event timeline tanpa perlu melakukan write-lock pada metadata inti video.

Indexing Komposit untuk Range Query Timeline

Player video umumnya melakukan pre-fetching metadata untuk window buffer tertentu (misal: 300 frame ke depan dari frame saat ini). Pola query yang dijalankan berbentuk range query berbasis frame:

SELECT id, start_frame, end_frame, event_type, payload
FROM video_cue_points
WHERE video_id = 1042
  AND start_frame >= 1800
  AND start_frame < 2100
ORDER BY start_frame ASC;

Single-column index pada start_frame tidak optimal karena database harus menggabungkan index scan terpisah atau melakukan filter berulang pada baris milik video_id lain. Solusi yang tepat adalah membuat B-Tree composite index dengan urutan selektivitas yang presisi:

CREATE INDEX idx_cue_points_video_frame 
ON video_cue_points (video_id, start_frame ASC, id ASC);

Kolom video_id berada di posisi pertama untuk eliminasi partisi data melalui kesetaraan (equality), diikuti oleh start_frame untuk mengarahkan pencarian rentang (range scan) secara langsung pada daun B-Tree tanpa scanning seluruh indeks. Kolom id ditambahkan sebagai tie-breaker penjamin stabilitas urutan.

Keyset Pagination Tanpa N+1 Query

Penggunaan LIMIT 50 OFFSET 10000 mengharuskan database memindai dan membuang 10.000 baris sebelum mengembalikan hasil. Pada timeline video interaktif dengan ribuan cue points per detik (seperti e-commerce tags atau live interaction overlay), mekanisme pagination harus beralih ke keyset pagination (seek method).

-- Mengambil batch metadata selanjutnya secara efisien
SELECT id, start_frame, end_frame, event_type, payload
FROM video_cue_points
WHERE video_id = 1042
  AND (start_frame, id) > (2100, 45892)
ORDER BY start_frame ASC, id ASC
LIMIT 100;

Klausa (start_frame, id) > (:last_frame, :last_id) memungkinkan engine database langsung melompat ke node indeks B-Tree yang tepat dalam kompleksitas waktu O(log N) tanpa membaca baris sebelumnya. Strategi ini juga mengeliminasi masalah record berulang atau terlewat ketika data cue baru di-insert secara real-time.

Optimasi Storage: Range Types vs. Integer Frames

Jika validasi tumpang tindih (overlap collision) pada event timeline diperlukan pada level engine database, engine seperti PostgreSQL menyediakan tipe data int4range dengan GiST index:

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE video_exclusive_states (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    video_id BIGINT NOT NULL,
    frame_range INT4RANGE NOT NULL,
    node_state VARCHAR(64) NOT NULL,
    EXCLUDE USING gist (video_id WITH =, frame_range WITH &&)
);

Meskipun GiST mencegah inkonsistensi state machine secara instan, indeks ini memiliki write overhead dan jejak memori lebih besar dibanding B-Tree standard. Gunakan tipe data integer diskret (start_frame, end_frame) untuk pipeline read-heavy berkepadatan tinggi, dan batasi GiST hanya untuk entitas core node yang membutuhkan constraint exclusion mutlak.