Database Views: Abstraksi, Keamanan, dan Jebakannya
21 min read

Database Views: Abstraksi, Keamanan, dan Jebakannya

Hampir setiap engineer pernah menemukan query SQL sepanjang lima puluh baris yang di-copy paste ke banyak tempat berbeda — satu di report bulanan, satu lagi di endpoint API, satu lagi di dashboard admin. Ketika definisi bisnisnya berubah, semua tempat itu harus diubah satu per satu, dan hampir selalu ada yang kelewat. View hadir untuk memecahkan masalah ini, tapi ironisnya banyak juga tim yang memakainya secara sembarangan sampai akhirnya view jadi sumber kebingungan baru — nested view berlapis-lapis yang tidak jelas lagi query aslinya seperti apa. Artikel ini membahas apa itu view, bagaimana database memprosesnya di balik layar, dan yang paling penting: kapan keputusan memakai view itu tepat, dan kapan justru sebaiknya dihindari.

Apa itu View

View adalah virtual table — hasil dari sebuah query SELECT yang diberi nama dan disimpan sebagai objek di schema database. Kata kuncinya di sini adalah “virtual”: view tidak menyimpan data secara fisik. Setiap kali kamu query ke view, database menjalankan ulang query yang mendasarinya secara real-time, lalu mengembalikan hasilnya seolah-olah itu tabel biasa.

Bayangkan view sebagai alias pintar untuk query. Kamu definisikan sekali, lalu pakai berulang kali dengan sintaks yang sama seperti query ke tabel biasa.

CREATE VIEW v_pelanggan_aktif AS
SELECT
    p.id,
    p.nama,
    p.email,
    COUNT(o.id) AS total_order,
    MAX(o.created_at) AS order_terakhir
FROM pelanggan p
JOIN orders o ON o.pelanggan_id = p.id
WHERE o.created_at >= NOW() - INTERVAL '30 days'
GROUP BY p.id, p.nama, p.email;

Setelah view ini dibuat, kamu bisa query seperti tabel biasa:

SELECT * FROM v_pelanggan_aktif WHERE total_order > 5;

Tidak ada tabel fisik bernama v_pelanggan_aktif yang tersimpan di disk. Yang tersimpan hanyalah definisi query-nya. Setiap eksekusi SELECT * FROM v_pelanggan_aktif, database menerjemahkannya kembali menjadi query JOIN dan GROUP BY di atas, lalu mengeksekusinya terhadap tabel pelanggan dan orders yang sebenarnya.

flowchart TD
    A[Aplikasi: SELECT * FROM v_pelanggan_aktif] --> B[Database Engine]
    B --> C{Cek definisi view}
    C --> D[Expand ke query asli: JOIN + GROUP BY]
    D --> E[Eksekusi terhadap tabel pelanggan & orders]
    E --> F[Hasil dikembalikan ke aplikasi]

Karena sifatnya virtual, view selalu menampilkan data ter-update — tidak ada risiko data basi seperti pada cache, kecuali kamu sengaja memakai materialized view yang akan dibahas nanti.

Penting dipahami bahwa view bukan konsep baru atau eksklusif untuk database tertentu. Hampir semua database relasional — PostgreSQL, MySQL, SQL Server, Oracle — mendukung view sejak versi paling awal, karena konsep ini bagian dari standar SQL (ANSI SQL) itu sendiri. Ini artinya skill membuat dan memahami view bersifat portable; sekali kamu paham konsepnya di satu database, kamu bisa menerapkannya nyaris tanpa perubahan mental model di database lain, meski sintaks detailnya mungkin sedikit berbeda antar vendor.

Satu kesalahpahaman umum adalah menganggap view sebagai “tabel yang lebih ringan” atau semacam shortcut performa. Padahal secara konseptual, view lebih tepat dipikirkan sebagai nama untuk sebuah query, bukan sebagai tabel dalam pengertian penyimpanan data. Ketika kamu men-drop tabel dasar yang direferensikan oleh view, view tersebut akan langsung rusak dan gagal dieksekusi — bukti nyata bahwa view tidak punya eksistensi data yang independen dari tabel sumbernya.

Cara Kerja View di Balik Layar

Memahami mekanisme di balik layar penting supaya kamu tidak salah ekspektasi soal performa. Ketika kamu membuat view, database tidak menjalankan query-nya saat itu juga dan menyimpan hasilnya. Yang terjadi adalah proses bernama view expansion atau query rewriting — parser database menyimpan teks query sebagai metadata, dan setiap kali view dipanggil, query planner menyisipkan definisi tersebut ke dalam query yang sedang berjalan sebelum dioptimasi dan dieksekusi.

sequenceDiagram
    participant App as Aplikasi
    participant Parser as Query Parser
    participant Optimizer as Query Optimizer
    participant Engine as Storage Engine
    App->>Parser: SELECT * FROM v_pelanggan_aktif WHERE total_order > 5
    Parser->>Parser: Deteksi v_pelanggan_aktif adalah view
    Parser->>Parser: Substitusi dengan definisi query asli
    Parser->>Optimizer: Query gabungan (view + kondisi WHERE luar)
    Optimizer->>Optimizer: Rencanakan execution plan optimal
    Optimizer->>Engine: Eksekusi terhadap tabel dasar
    Engine-->>App: Hasil query

Implikasi praktisnya: kondisi WHERE total_order > 5 di query luar bisa saja digabung oleh optimizer ke dalam logika view sebelum dieksekusi, tergantung seberapa pintar query planner-nya. Di database modern seperti PostgreSQL atau MySQL versi baru, optimizer cukup baik dalam melakukan predicate pushdown — mendorong kondisi filter ke bagian paling dalam dari query gabungan supaya tidak perlu memproses baris yang tidak relevan. Tapi ini bukan jaminan mutlak, terutama untuk view yang kompleks dengan banyak JOIN dan agregasi bertingkat.

Ini juga menjelaskan mengapa view yang dibungkus di dalam view lain (nested view) bisa jadi masalah performa — setiap layer nesting menambah kompleksitas bagi optimizer untuk menyusun rencana eksekusi yang efisien.

Hal lain yang perlu dipahami adalah bahwa view tidak membekukan struktur data pada saat pembuatan. Jika kamu menambah kolom baru ke tabel dasar setelah view dibuat, kolom baru itu tidak otomatis muncul di view kecuali kamu mendefinisikan view dengan SELECT * — dan bahkan dengan SELECT *, sebagian database (seperti PostgreSQL) tetap mengunci daftar kolom pada saat definisi view dibuat, sehingga kolom baru di tabel dasar tetap tidak terlihat di view sampai kamu menjalankan CREATE OR REPLACE VIEW ulang. Perilaku ini sering mengejutkan developer yang berasumsi view selalu sinkron sempurna dengan struktur tabel sumber.

Selain itu, karena view dieksekusi sebagai bagian dari query yang memanggilnya, semua constraint keamanan seperti row-level security atau kondisi WHERE tambahan dari pemanggil akan digabung ke dalam rencana eksekusi akhir. Ini berarti index yang dipakai untuk mengoptimalkan query terhadap tabel dasar tetap berlaku efektif meski diakses lewat view — selama query planner mampu melakukan predicate pushdown dengan baik.

View vs Menjalankan Query Langsung

Pertanyaan yang sering muncul: kalau view cuma “menyimpan” query dan tetap dieksekusi ulang setiap saat, apa untungnya dibanding menjalankan query itu langsung setiap kali dibutuhkan?

AspekViewQuery Langsung
MaintainabilitySatu definisi, diubah di satu tempatLogic terduplikasi di banyak file/report
SecurityBisa grant akses granular per kolom/barisButuh kontrol akses di level tabel penuh
Performa (standar)Sama dengan query aslinya, tidak lebih cepatBaseline performa
ReusabilityTinggal SELECT * FROM nama_viewCopy-paste query panjang berulang kali
Kompleksitas setupButuh definisi awal + governance namaTidak butuh setup tambahan
ReadabilityQuery konsumen jadi pendek dan jelas maksudnyaQuery panjang membingungkan pembaca baru

Poin performa ini penting untuk diluruskan sejak awal: view standar tidak membuat query jadi lebih cepat. Karena di balik layar dia tetap menjalankan query yang sama persis. Kalau kamu berharap view bisa jadi solusi query lambat, itu ekspektasi yang salah — yang kamu butuhkan mungkin adalah indexing yang lebih baik, atau materialized view kalau datanya memang bisa sedikit stale.

Keuntungan view sesungguhnya ada di sisi manusia, bukan mesin: maintainability, keamanan, dan konsistensi logika bisnis.

Analoginya mirip dengan alasan kamu menulis fungsi di kode aplikasi alih-alih menyalin logika yang sama berulang kali di banyak file. Fungsi tidak membuat CPU bekerja lebih cepat — instruksi yang dijalankan pada akhirnya tetap sama. Tapi fungsi membuat kode lebih mudah dipahami, diuji, dan diubah di satu tempat. View berperan persis seperti itu, hanya saja levelnya di lapisan database, bukan di lapisan aplikasi. Ketika kamu berpikir “haruskah saya buat view untuk ini?”, pertanyaan yang lebih tepat sebenarnya adalah pertanyaan yang sama seperti saat kamu memutuskan membuat fungsi baru: apakah logic ini akan dipakai lebih dari satu kali, dan apakah menuliskannya di satu tempat akan mengurangi risiko inkonsistensi di masa depan.

Purpose Utama View

Abstraksi Kompleksitas Query

Query dengan lima JOIN, beberapa CASE WHEN, dan subquery bersarang sulit dipahami oleh developer baru. Dengan membungkusnya jadi view bernama jelas seperti v_pelanggan_aktif, konsumen query cukup paham maksudnya dari nama, tanpa perlu membaca detail implementasi.

Access Control Tanpa Duplikasi Data

Kamu bisa membuat view yang hanya menampilkan sebagian kolom, lalu grant permission SELECT ke view tersebut tanpa memberi akses ke tabel aslinya.

-- View yang menyembunyikan kolom sensitif
CREATE VIEW v_karyawan_public AS
SELECT id, nama, departemen, jabatan
FROM karyawan;

-- Grant hanya ke view, bukan ke tabel karyawan asli
GRANT SELECT ON v_karyawan_public TO role_hr_junior;

Ini juga berlaku untuk row-level security di aplikasi multi-tenant. Misalnya kamu ingin setiap tenant hanya bisa melihat datanya sendiri:

CREATE VIEW v_orders_tenant_a AS
SELECT * FROM orders WHERE tenant_id = 'tenant_a';

Pola ini sering dikombinasikan dengan mekanisme session atau connection role di aplikasi, sehingga masing-masing tenant otomatis diarahkan ke view yang sesuai tanpa perlu menambahkan filter WHERE tenant_id = ? secara manual di setiap query aplikasi — meski di database modern, pendekatan row-level security (RLS) native (seperti di PostgreSQL) sering jadi alternatif yang lebih robust untuk kasus ini.

Single Source of Truth untuk Logika Bisnis

Definisi “pelanggan aktif”, “transaksi valid”, atau “produk best-seller” seringkali melibatkan beberapa kondisi yang mudah berbeda interpretasi antar tim. Kalau logic itu didefinisikan di view, semua orang — baik tim data, backend, maupun BI — memakai definisi yang identik.

-- ANTI-PATTERN: definisi "pelanggan aktif" diulang di banyak tempat
-- File report-bulanan.sql
SELECT * FROM pelanggan p
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.pelanggan_id = p.id AND o.created_at >= NOW() - INTERVAL '30 days'
);

-- File endpoint-api.sql (kondisi sedikit berbeda, celah bug!)
SELECT * FROM pelanggan p
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.pelanggan_id = p.id AND o.created_at >= NOW() - INTERVAL '31 days'
);

-- BENAR: satu definisi di view, dipakai di semua tempat
CREATE VIEW v_pelanggan_aktif_flag AS
SELECT p.*
FROM pelanggan p
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.pelanggan_id = p.id AND o.created_at >= NOW() - INTERVAL '30 days'
);

Interface Stability Saat Refactor Schema

Kalau kamu perlu memecah tabel pelanggan menjadi pelanggan dan pelanggan_kontak karena alasan normalisasi, aplikasi yang sudah terlanjur query langsung ke tabel lama akan rusak. Kalau aplikasi itu query lewat view, kamu cukup update definisi view supaya tetap menghasilkan struktur output yang sama, sementara konsumen di luar tidak perlu tahu apa-apa soal perubahan di baliknya.

Simplifikasi Reporting dan BI

Tools BI seperti dashboard internal biasanya query ke view yang sudah pre-joined dan pre-aggregated, sehingga analyst tidak perlu memahami skema database yang rumit untuk membuat laporan. Ini juga mengurangi risiko analyst membuat kesalahan join yang menghasilkan data ganda (fan-out) — masalah klasik ketika seseorang yang tidak familiar dengan skema database menggabungkan tabel dengan relasi one-to-many tanpa agregasi yang tepat, sehingga angka di laporan jadi berlipat ganda tanpa disadari.

Dengan view yang sudah didesain dan diverifikasi oleh tim yang memahami skema database secara mendalam, analyst BI cukup fokus pada logika bisnis laporan, bukan pada detail teknikal join yang rawan kesalahan. Ini membagi tanggung jawab secara jelas: tim data/engineering menjaga kebenaran struktur query di level view, sementara tim BI menjaga kebenaran interpretasi bisnis di level laporan.

Beberapa database modern seperti PostgreSQL juga mendukung updatable view — kamu bisa melakukan INSERT, UPDATE, atau DELETE langsung ke view asalkan view tersebut memenuhi syarat tertentu (biasanya: berbasis satu tabel, tanpa agregasi, tanpa DISTINCT). View dengan JOIN atau GROUP BY umumnya read-only kecuali kamu definisikan trigger INSTEAD OF secara eksplisit.

Membuat dan Menggunakan View

Sintaks dasar view relatif seragam di berbagai database relasional, meski ada sedikit variasi dialect.

-- Membuat view baru
CREATE VIEW v_produk_terlaris AS
SELECT
    pr.id,
    pr.nama,
    SUM(oi.qty) AS total_terjual
FROM produk pr
JOIN order_items oi ON oi.produk_id = pr.id
GROUP BY pr.id, pr.nama
ORDER BY total_terjual DESC;

-- Mengubah definisi view yang sudah ada tanpa drop dulu
CREATE OR REPLACE VIEW v_produk_terlaris AS
SELECT
    pr.id,
    pr.nama,
    pr.kategori,
    SUM(oi.qty) AS total_terjual
FROM produk pr
JOIN order_items oi ON oi.produk_id = pr.id
GROUP BY pr.id, pr.nama, pr.kategori
ORDER BY total_terjual DESC;

-- Menghapus view
DROP VIEW v_produk_terlaris;

CREATE OR REPLACE VIEW sangat berguna dalam workflow deployment karena kamu tidak perlu khawatir soal dependency yang rusak akibat DROP lalu CREATE ulang — permission dan dependency ke view tersebut tetap terjaga selama struktur kolom output-nya kompatibel.

Updatable View vs Read-Only View

Tidak semua view bisa di-INSERT atau di-UPDATE langsung. Aturannya bervariasi antar database, tapi umumnya:

-- Updatable: berbasis satu tabel, tanpa agregasi
CREATE VIEW v_karyawan_aktif AS
SELECT id, nama, departemen, status
FROM karyawan
WHERE status = 'aktif';

-- Ini valid di kebanyakan database
UPDATE v_karyawan_aktif SET departemen = 'Engineering' WHERE id = 42;

-- ANTI-PATTERN: mengharapkan view dengan JOIN + GROUP BY bisa di-UPDATE langsung
-- View ini read-only di kebanyakan database tanpa trigger tambahan
CREATE VIEW v_pelanggan_dengan_total_order AS
SELECT p.id, p.nama, COUNT(o.id) AS total_order
FROM pelanggan p
JOIN orders o ON o.pelanggan_id = p.id
GROUP BY p.id, p.nama;

-- BENAR: kalau memang butuh update lewat view kompleks, definisikan trigger INSTEAD OF
CREATE TRIGGER trg_update_pelanggan
INSTEAD OF UPDATE ON v_pelanggan_dengan_total_order
FOR EACH ROW
EXECUTE FUNCTION fn_handle_pelanggan_update();

View vs CTE — Kapan Pakai yang Mana

Banyak developer bingung antara view dan CTE (WITH ... AS) karena keduanya sama-sama membungkus query supaya lebih terbaca. Perbedaan mendasarnya ada pada lifecycle dan scope.

Kebingungan ini wajar karena secara sintaks, keduanya sama-sama menghasilkan hasil query yang bisa direferensikan dengan nama seolah-olah itu tabel. Tapi konteks pemakaiannya sangat berbeda. CTE didefinisikan di awal satu statement SELECT dan hanya hidup selama statement itu berjalan — begitu query selesai dieksekusi, definisi CTE hilang begitu saja dan tidak meninggalkan jejak apapun di schema database. View sebaliknya adalah objek permanen di schema, terus ada sampai kamu secara eksplisit menjalankan DROP VIEW, dan bisa dipanggil dari query manapun, kapanpun, oleh siapapun yang punya permission.

AspekViewCTE
LifecyclePersisten, tersimpan di schema databaseHanya hidup selama satu statement query
Reusability antar queryBisa dipakai di banyak query berbedaHanya bisa dipakai di query tempat ia didefinisikan
PermissionBisa di-grant terpisah dari tabel dasarTidak punya permission sendiri
Kebutuhan setupPerlu CREATE VIEW sekali di awalCukup ditulis inline di awal query
Cocok untukLogic yang dipakai berulang lintas query/aplikasiQuery kompleks satu kali, langkah antara dalam satu statement
-- CTE cocok untuk query sekali pakai dengan langkah antara
WITH pelanggan_dengan_total AS (
    SELECT pelanggan_id, SUM(total) AS total_belanja
    FROM orders
    GROUP BY pelanggan_id
)
SELECT p.nama, t.total_belanja
FROM pelanggan p
JOIN pelanggan_dengan_total t ON t.pelanggan_id = p.id
WHERE t.total_belanja > 1000000;

Aturan praktisnya: kalau logic itu hanya relevan untuk satu query spesifik dan tidak akan dipakai lagi di tempat lain, pakai CTE. Kalau logic itu adalah definisi bisnis yang akan dipakai berulang kali di berbagai query dan aplikasi berbeda, itu kandidat kuat untuk jadi view.

Materialized View: Ketika View Biasa Tidak Cukup

View standar selalu dieksekusi ulang setiap dipanggil, yang berarti untuk query berat — agregasi jutaan baris misalnya — view standar tidak membantu performa sama sekali. Di sinilah materialized view berperan: hasil query benar-benar disimpan secara fisik di disk seperti tabel biasa, sehingga query terhadapnya jadi jauh lebih cepat.

-- PostgreSQL
CREATE MATERIALIZED VIEW mv_penjualan_bulanan AS
SELECT
    DATE_TRUNC('month', created_at) AS bulan,
    kategori,
    SUM(total) AS total_penjualan
FROM orders o
JOIN produk pr ON pr.id = o.produk_id
GROUP BY DATE_TRUNC('month', created_at), kategori;

-- Data tidak otomatis update -- perlu refresh manual atau terjadwal
REFRESH MATERIALIZED VIEW mv_penjualan_bulanan;

-- Refresh tanpa mengunci tabel untuk pembacaan (butuh unique index)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_penjualan_bulanan;

Konsekuensinya, data di materialized view bisa stale — tidak mencerminkan perubahan terbaru sampai proses refresh dijalankan. Trade-off ini harus disadari betul: kamu menukar realtime-ness dengan kecepatan baca.

flowchart LR
    A[Tabel Dasar Berubah] -.tidak otomatis.-> B[Materialized View]
    C[Scheduler / Cron] -->|REFRESH terjadwal| B
    D[Query Aplikasi] -->|baca cepat| B
MySQL tidak mendukung materialized view secara native seperti PostgreSQL atau Oracle. Solusi umumnya adalah membuat tabel biasa yang di-populate lewat scheduled job atau trigger, lalu memperlakukannya seperti materialized view secara manual. Pastikan kamu tahu batasan platform database yang dipakai sebelum mendesain arsitektur berbasis materialized view.

Kapan pakai materialized view dibanding view biasa:

GUNAKAN materialized view jika:
  ✓ query dasar berat (agregasi besar, banyak JOIN kompleks)
  ✓ data boleh sedikit stale (menit, jam, bahkan harian)
  ✓ dipakai berulang kali dengan frekuensi tinggi (dashboard, report)

GUNAKAN view biasa jika:
  ✗ data harus selalu realtime
  ✗ query dasar sudah cukup cepat tanpa materialisasi
  ✗ perubahan data terjadi sangat sering sehingga refresh jadi mahal

Performa: View vs Materialized View

Setelah memahami cara kerja keduanya, penting untuk membedah lebih detail karakteristik performa masing-masing, karena di sinilah kesalahpahaman paling sering terjadi di lapangan.

View Standar Tidak Menambah maupun Mengurangi Performa

Poin ini sudah disinggung sebelumnya, tapi layak ditegaskan lebih jauh: karena view hanyalah lapisan abstraksi di atas query aslinya, waktu eksekusi query lewat view pada dasarnya identik dengan waktu eksekusi query yang sama jika ditulis langsung — dengan sedikit overhead tambahan dari proses parsing dan view expansion, yang biasanya dapat diabaikan (mikrodetik) dibanding waktu eksekusi query itu sendiri.

Yang justru berpengaruh besar terhadap performa adalah index pada tabel dasar. View tidak punya index sendiri — dia hanya mewarisi index yang sudah ada di tabel yang direferensikannya. Kalau query dasar suatu view lambat karena kurang index, membungkusnya jadi view tidak akan membuatnya lebih cepat sedikit pun. Sebaliknya, kalau kamu menambah index yang tepat di tabel dasar, semua view yang bergantung pada tabel tersebut otomatis ikut lebih cepat, tanpa perlu mengubah definisi view itu sendiri.

-- View ini lambat bukan karena "view", tapi karena kolom created_at tidak terindex
CREATE VIEW v_order_bulan_ini AS
SELECT * FROM orders WHERE created_at >= DATE_TRUNC('month', NOW());

-- Solusinya bukan mengubah struktur view, tapi menambah index di tabel dasar
CREATE INDEX idx_orders_created_at ON orders (created_at);

Risiko Query Planner pada View Kompleks

Untuk view sederhana berbasis satu tabel dengan filter WHERE, optimizer modern hampir selalu mampu melakukan predicate pushdown dengan sempurna, sehingga performanya identik dengan query manual. Tapi untuk view yang melibatkan banyak JOIN, subquery bersarang, atau agregasi window function, optimizer terkadang kesulitan menyusun rencana eksekusi optimal — terutama ketika view semacam itu dipanggil dengan kondisi WHERE tambahan dari luar yang seharusnya bisa didorong jauh ke dalam, tapi tidak selalu berhasil didorong sepenuhnya tergantung dialect database dan versinya.

Nested view memperparah risiko ini. Setiap layer nesting menambah kerja bagi optimizer untuk “membongkar” seluruh rantai definisi sebelum bisa menyusun rencana eksekusi tunggal yang efisien. Pada beberapa kasus ekstrem, nested view tiga sampai empat layer bisa membuat query yang seharusnya berjalan dalam hitungan milidetik menjadi berjalan dalam hitungan detik, karena optimizer gagal mengenali bahwa banyak kondisi filter di layer luar sebenarnya bisa disederhanakan jauh sebelum join besar terjadi.

Materialized View: Trade-off Kecepatan Baca vs Kesegaran Data

Materialized view menyelesaikan masalah performa dengan cara yang sama sekali berbeda: alih-alih mengandalkan optimizer untuk mempercepat eksekusi ulang, ia menghilangkan kebutuhan eksekusi ulang itu sendiri dengan menyimpan hasil secara fisik. Konsekuensinya, query terhadap materialized view kecepatannya setara dengan query terhadap tabel biasa — karena secara fisik memang begitu adanya.

Namun kecepatan ini datang dengan dua biaya tersembunyi yang sering luput dari perhatian:

Biaya storage. Materialized view menduplikasi data secara fisik, sehingga menambah kebutuhan disk. Untuk agregasi besar dari tabel berjuta-juta baris, materialized view itu sendiri bisa jadi cukup besar tergantung kardinalitas hasil agregasinya.

Biaya refresh. Proses REFRESH MATERIALIZED VIEW pada dasarnya menjalankan ulang query dasarnya secara penuh (kecuali kamu memakai incremental refresh yang didukung sebagian database). Untuk query yang berat, proses refresh itu sendiri bisa memakan waktu signifikan dan membebani database, terutama kalau dijadwalkan terlalu sering atau dijalankan bersamaan dengan traffic tinggi.

AspekView StandarMaterialized View
Kecepatan bacaSama dengan query asliSetara tabel biasa (cepat)
Kesegaran dataSelalu realtimeStale sampai di-refresh
Kebutuhan storageTidak ada (hanya metadata)Data fisik tersimpan, butuh disk
Biaya di setiap queryEksekusi ulang setiap panggilTidak ada eksekusi ulang saat baca
Biaya tersembunyiQuery lambat kalau kurang index di tabel dasarProses refresh bisa berat dan membebani database
Cocok untukData yang harus realtime, query relatif ringanQuery berat yang toleran terhadap delay data

Indexing pada Materialized View

Satu hal yang sering terlewat: karena materialized view menyimpan data secara fisik, kamu bisa — dan sebaiknya — membuat index di atasnya, persis seperti tabel biasa.

-- Tambahkan index pada materialized view untuk mempercepat query lanjutan
CREATE INDEX idx_mv_penjualan_bulanan_kategori
ON mv_penjualan_bulanan (kategori);

-- Unique index juga diperlukan agar REFRESH CONCURRENTLY bisa dipakai
CREATE UNIQUE INDEX idx_mv_penjualan_bulanan_unique
ON mv_penjualan_bulanan (bulan, kategori);

Tanpa index tambahan ini, query terhadap materialized view yang besar tetap bisa lambat meski datanya sudah “dibekukan” — karena database tetap harus melakukan sequential scan kalau tidak ada index yang mendukung kondisi filter yang dipakai.

Kapan Sebaiknya Pakai View

PAKAI view jika:
  ✓ logic query dipakai berulang di banyak tempat (report, API, dashboard)
  ✓ butuh membatasi akses kolom/baris tanpa duplikasi data
  ✓ definisi bisnis (misal "pelanggan aktif") perlu konsisten lintas tim
  ✓ ingin menjaga interface stabil saat schema tabel dasar berubah
  ✓ query cukup kompleks sehingga readability jadi masalah nyata

Kapan Sebaiknya Tidak Pakai View

HINDARI view jika:
  ✗ query hanya dipakai sekali dan tidak akan direuse
  ✗ kamu berharap view otomatis mempercepat query berat (pakai materialized view)
  ✗ akan menyebabkan nested view berlapis-lapis yang sulit di-debug
  ✗ tim belum punya konvensi penamaan/dokumentasi, sehingga view malah jadi "black box"
Nested view — view yang memanggil view lain, yang memanggil view lain lagi — adalah jebakan performa paling umum. Setiap layer nesting menambah beban bagi query optimizer untuk menyusun execution plan yang efisien, dan di beberapa database optimizer bisa gagal melakukan predicate pushdown dengan baik melewati banyak layer. Selain itu, debugging jadi mimpi buruk karena kamu harus menelusuri berlapis-lapis definisi hanya untuk tahu tabel dasar mana yang sebenarnya diakses.

Anti-Pattern Umum

-- ✗ Nested view berlapis-lapis, sulit ditelusuri dan berat untuk optimizer
CREATE VIEW v_layer1 AS SELECT * FROM orders WHERE status = 'paid';
CREATE VIEW v_layer2 AS SELECT * FROM v_layer1 WHERE created_at >= NOW() - INTERVAL '90 days';
CREATE VIEW v_layer3 AS SELECT pelanggan_id, SUM(total) FROM v_layer2 GROUP BY pelanggan_id;

-- ✓ Satu definisi langsung ke tabel dasar, lebih mudah dioptimasi dan dibaca
CREATE VIEW v_total_belanja_90_hari AS
SELECT pelanggan_id, SUM(total) AS total_belanja
FROM orders
WHERE status = 'paid'
  AND created_at >= NOW() - INTERVAL '90 days'
GROUP BY pelanggan_id;

-- ✗ View yang menyembunyikan query mahal tanpa disadari pemanggilnya
CREATE VIEW v_semua_order_dengan_detail AS
SELECT o.*, p.nama, pr.nama AS nama_produk, k.nama AS kategori
FROM orders o
JOIN pelanggan p ON p.id = o.pelanggan_id
JOIN order_items oi ON oi.order_id = o.id
JOIN produk pr ON pr.id = oi.produk_id
JOIN kategori k ON k.id = pr.kategori_id;
-- Dipanggil tanpa WHERE oleh developer baru yang tidak tahu ini full table scan berlapis JOIN

-- ✓ Beri nama yang menyiratkan skala data, atau tambahkan dokumentasi/komentar
-- View ini WAJIB dipanggil dengan filter WHERE order_id atau pelanggan_id
COMMENT ON VIEW v_semua_order_dengan_detail IS
    'Berat -- selalu filter berdasarkan order_id atau pelanggan_id, jangan query tanpa WHERE';

Best Practice View dan Materialized View

Setelah memahami konsep, purpose, dan performa view, berikut kumpulan praktik yang membantu kamu memakai view secara sehat dalam jangka panjang — bukan sekadar berfungsi hari ini, tapi tetap mudah dipelihara enam bulan atau setahun ke depan.

1. Gunakan Konvensi Penamaan yang Konsisten

Prefix seperti v_ untuk view dan mv_ untuk materialized view membuat siapa pun langsung tahu jenis objek yang mereka pakai tanpa perlu cek metadata database. Konsistensi ini juga memudahkan pencarian di tools database client dan dokumentasi otomatis.

-- ✗ Nama ambigu, tidak jelas ini view atau tabel biasa
CREATE VIEW pelanggan_aktif AS ...;

-- ✓ Prefix jelas menandakan jenis objek
CREATE VIEW v_pelanggan_aktif AS ...;
CREATE MATERIALIZED VIEW mv_penjualan_bulanan AS ...;

2. Dokumentasikan Definisi dan Batasan Pemakaian

View yang mahal secara komputasi atau punya batasan tertentu (misalnya wajib difilter dengan kolom tertentu) harus didokumentasikan langsung di level database, bukan hanya di wiki internal yang gampang basi.

COMMENT ON VIEW v_pelanggan_aktif IS
    'Pelanggan dengan minimal satu order dalam 30 hari terakhir. Dipakai oleh report bulanan dan endpoint /api/pelanggan/aktif.';

3. Hindari Nested View Lebih dari Dua Layer

Batasi kedalaman nesting maksimal dua layer. Kalau kamu mendapati diri membuat view ketiga yang memanggil view kedua yang memanggil view pertama, ini sinyal kuat untuk refactor menjadi satu definisi langsung ke tabel dasar, seperti yang sudah dibahas di bagian anti-pattern.

4. Selalu Uji Execution Plan Sebelum Deploy

Sebelum mem-push view baru ke production, jalankan EXPLAIN ANALYZE terhadap query yang memanggilnya untuk memastikan optimizer benar-benar melakukan predicate pushdown seperti yang diharapkan, terutama untuk view dengan banyak JOIN.

EXPLAIN ANALYZE
SELECT * FROM v_pelanggan_aktif WHERE total_order > 5;

Kalau hasil EXPLAIN menunjukkan sequential scan besar pada tabel dasar padahal kamu berharap index scan, itu tanda view perlu direvisi atau tabel dasar butuh index tambahan.

5. Tetapkan Strategi Refresh yang Jelas untuk Materialized View

Jangan biarkan jadwal refresh materialized view ditentukan secara ad-hoc. Tetapkan interval yang sesuai dengan toleransi staleness bisnis — dashboard executive mungkin cukup di-refresh tiap jam, sementara laporan harian cukup di-refresh sekali semalam.

-- Contoh scheduled refresh dengan pg_cron di PostgreSQL
SELECT cron.schedule(
    'refresh-penjualan-bulanan',
    '0 * * * *',  -- setiap jam
    'REFRESH MATERIALIZED VIEW CONCURRENTLY mv_penjualan_bulanan'
);

6. Selalu Buat Unique Index Sebelum Memakai Refresh Concurrently

REFRESH MATERIALIZED VIEW CONCURRENTLY memungkinkan refresh tanpa mengunci tabel untuk pembacaan, tapi mensyaratkan adanya unique index. Tanpa ini, refresh akan mengunci materialized view sepenuhnya selama proses berjalan, yang bisa berdampak pada aplikasi yang sedang membacanya.

7. Audit Penggunaan View Secara Berkala

Seiring waktu, beberapa view menjadi tidak terpakai lagi karena fitur yang menggunakannya sudah dihapus, tapi view-nya sendiri tertinggal di schema. View yang menumpuk tanpa audit membuat schema database sulit dipahami oleh developer baru dan meningkatkan risiko seseorang secara tidak sengaja bergantung pada view yang seharusnya sudah deprecated.

-- PostgreSQL: cek dependency sebuah view sebelum menghapusnya
SELECT dependent_ns.nspname AS dependent_schema,
       dependent_view.relname AS dependent_view
FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
JOIN pg_class AS dependent_view ON pg_rewrite.ev_class = dependent_view.oid
JOIN pg_namespace AS dependent_ns ON dependent_view.relnamespace = dependent_ns.oid
JOIN pg_class AS source_table ON pg_depend.refobjid = source_table.oid
WHERE source_table.relname = 'v_pelanggan_aktif';

8. Batasi Kolom yang Diekspos, Jangan Selalu SELECT *

Untuk view yang berfungsi sebagai lapisan access control, hindari SELECT * karena kolom sensitif yang ditambahkan ke tabel dasar di masa depan bisa tanpa sengaja ikut terekspos kalau view didefinisikan ulang secara serampangan. Sebutkan kolom secara eksplisit.

-- ✗ Rawan kebocoran kolom sensitif di masa depan
CREATE VIEW v_karyawan_public AS SELECT * FROM karyawan;

-- ✓ Kolom eksplisit, aman dari penambahan kolom sensitif baru
CREATE VIEW v_karyawan_public AS
SELECT id, nama, departemen, jabatan FROM karyawan;

Checklist singkat yang bisa dipakai sebagai referensi cepat:

SEBELUM MEMBUAT VIEW BARU:
  □ Nama memakai prefix konsisten (v_ / mv_)
  □ Definisi dan batasan pemakaian sudah didokumentasikan
  □ Kolom disebutkan eksplisit, tidak asal SELECT *
  □ EXPLAIN ANALYZE sudah dicek untuk view dengan banyak JOIN
  □ Tidak menambah nested view lebih dari dua layer

KHUSUS MATERIALIZED VIEW:
  □ Strategi refresh (interval, trigger, atau manual) sudah ditentukan
  □ Unique index dibuat agar REFRESH CONCURRENTLY bisa dipakai
  □ Index tambahan dibuat sesuai pola query yang sering dipakai
  □ Tim yang bergantung pada data ini paham toleransi staleness-nya

PERAWATAN BERKALA:
  □ Audit view yang sudah tidak dipakai lagi
  □ Review ulang execution plan setelah volume data bertambah signifikan

Ringkasan

  • View adalah virtual table hasil dari query SELECT yang disimpan sebagai definisi, bukan data fisik — setiap pemanggilan dieksekusi ulang terhadap tabel dasar.
  • Performa view standar sama dengan query aslinya — view bukan solusi untuk mempercepat query lambat.
  • Keuntungan utama view ada di sisi maintainability, security, dan konsistensi logika bisnis — bukan performa.
  • Gunakan view untuk abstraksi kompleksitas, access control granular, single source of truth definisi bisnis, dan menjaga interface stabil saat schema berubah.
  • CTE cocok untuk logic sekali pakai dalam satu query; view cocok untuk logic yang dipakai berulang lintas query dan aplikasi.
  • Materialized view menyimpan hasil secara fisik sehingga query jadi cepat, tapi data bisa stale sampai di-refresh — cocok untuk query berat dengan toleransi delay data.
  • Hindari nested view berlapis-lapis karena menyulitkan query optimizer dan membuat debugging sulit.
  • Jangan pakai view untuk query yang hanya dipakai sekali, dan selalu dokumentasikan view yang membungkus query berat agar tidak dipanggil sembarangan tanpa filter.
  • Terapkan konvensi penamaan konsisten (v_ / mv_), sebutkan kolom secara eksplisit alih-alih SELECT *, dan audit view yang sudah tidak terpakai secara berkala.
  • Untuk materialized view, pastikan strategi refresh dan unique index sudah ditentukan sejak awal agar REFRESH CONCURRENTLY bisa dipakai tanpa mengunci pembacaan.

Portofolio