Sejauh ini kita memperlakukan view sebagai jendela baca-saja: SELECT ke view, angka laporan keluar, tabel dasar tidak tersentuh. Pertanyaan yang hampir selalu muncul di titik ini — “kalau view terasa seperti tabel, bisakah saya INSERT, UPDATE, atau DELETE lewat view?” — punya jawaban yang mengejutkan: kadang ya, dan tanpa satu baris konfigurasi pun. Untuk kelas view tertentu, PostgreSQL otomatis menerjemahkan perintah tulis pada view menjadi perintah tulis pada tabel dasarnya. Kapabilitas ini membuka pola desain yang kuat — view sebagai gerbang terbatas ke tabel — sekaligus satu jebakan klasik: tanpa pengaman, view bisa kehilangan baris yang barusan Anda insert. Chapter ini membedah syarat otomatisnya, jebakannya, dan CHECK OPTION yang menutupnya.
Domain Latihan yang Sama #
Domain Toko Maju Jaya yang sama dengan chapter-chapter sebelumnya: produk, pelanggan, dan penjualan. File setup berikut membuat ulang ketiga tabel dari nol, sehingga contoh-contoh chapter ini bisa dijalankan sendiri:
-- Domain latihan section View dan Materialized View:
-- katalog produk, pelanggan, dan transaksi Toko Maju Jaya.
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
kategori text NOT NULL,
harga numeric(10,2) NOT NULL,
stok integer NOT NULL
);
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
kota text NOT NULL
);
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
produk_id integer NOT NULL REFERENCES produk(id),
pelanggan_id integer NOT NULL REFERENCES pelanggan(id),
jumlah integer NOT NULL
);
INSERT INTO produk (id, nama, kategori, harga, stok) VALUES
(1, 'Kopi Gayo', 'Kopi', 85000.00, 40),
(2, 'Kopi Toraja', 'Kopi', 90000.00, 25),
(3, 'Teh Melati', 'Teh', 35000.00, 100),
(4, 'Teh Hijau', 'Teh', 40000.00, 60),
(5, 'Gula Merah', 'Gula', 15000.00, 200),
(6, 'Gula Putih', 'Gula', 16000.00, 150),
(7, 'Kopi Lampung', 'Kopi', 78000.00, 0);
INSERT INTO pelanggan (id, nama, kota) VALUES
(1, 'Budi Santoso', 'Jakarta'),
(2, 'Siti Aminah', 'Bandung'),
(3, 'Andi Wijaya', 'Surabaya'),
(4, 'Dewi Lestari', 'Yogyakarta'),
(5, 'Rina Marlina', 'Medan');
INSERT INTO penjualan (id, tanggal, produk_id, pelanggan_id, jumlah) VALUES
( 1, DATE '2026-01-05', 1, 1, 10),
( 2, DATE '2026-01-08', 3, 2, 25),
( 3, DATE '2026-01-12', 5, 3, 40),
( 4, DATE '2026-01-15', 2, 1, 8),
( 5, DATE '2026-01-20', 4, 4, 15),
( 6, DATE '2026-01-22', 1, 2, 12),
( 7, DATE '2026-02-02', 3, 5, 20),
( 8, DATE '2026-02-05', 6, 3, 30),
( 9, DATE '2026-02-10', 2, 4, 6),
(10, DATE '2026-02-14', 4, 1, 18);
SELECT id, nama, kategori, harga, stok FROM produk ORDER BY id;
SELECT id, nama, kota FROM pelanggan ORDER BY id;
SELECT id, tanggal, produk_id, pelanggan_id, jumlah FROM penjualan ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 7
INSERT 0 5
INSERT 0 10
id | nama | kategori | harga | stok
----+--------------+----------+----------+------
1 | Kopi Gayo | Kopi | 85000.00 | 40
2 | Kopi Toraja | Kopi | 90000.00 | 25
3 | Teh Melati | Teh | 35000.00 | 100
4 | Teh Hijau | Teh | 40000.00 | 60
5 | Gula Merah | Gula | 15000.00 | 200
6 | Gula Putih | Gula | 16000.00 | 150
7 | Kopi Lampung | Kopi | 78000.00 | 0
(7 rows)
id | nama | kota
----+--------------+------------
1 | Budi Santoso | Jakarta
2 | Siti Aminah | Bandung
3 | Andi Wijaya | Surabaya
4 | Dewi Lestari | Yogyakarta
5 | Rina Marlina | Medan
(5 rows)
id | tanggal | produk_id | pelanggan_id | jumlah
----+------------+-----------+--------------+--------
1 | 2026-01-05 | 1 | 1 | 10
2 | 2026-01-08 | 3 | 2 | 25
3 | 2026-01-12 | 5 | 3 | 40
4 | 2026-01-15 | 2 | 1 | 8
5 | 2026-01-20 | 4 | 4 | 15
6 | 2026-01-22 | 1 | 2 | 12
7 | 2026-02-02 | 3 | 5 | 20
8 | 2026-02-05 | 6 | 3 | 30
9 | 2026-02-10 | 2 | 4 | 6
10 | 2026-02-14 | 4 | 1 | 18
(10 rows)INSERT, UPDATE, DELETE Lewat View #
Kita mulai dari view yang paling sederhana: produk_teh membaca satu tabel (produk), menyaring satu kategori, tanpa agregat maupun pembatasan hasil. Tiga perintah tulis kemudian diarahkan ke view itu — dan ketiganya berhasil:
-- View sederhana = satu tabel, tanpa agregat/DISTINCT/GROUP BY/LIMIT:
-- PostgreSQL menerjemahkan INSERT/UPDATE/DELETE padanya menjadi
-- operasi pada tabel dasar produk.
CREATE VIEW produk_teh AS
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE kategori = 'Teh';
SELECT id, nama, harga, stok
FROM produk_teh
ORDER BY id;
-- INSERT lewat view: baris mendarat di tabel produk.
INSERT INTO produk_teh (id, nama, kategori, harga, stok)
VALUES (8, 'Teh Oolong', 'Teh', 55000.00, 50);
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE id = 8;
-- UPDATE lewat view: hanya baris yang terlihat view yang tersentuh.
UPDATE produk_teh
SET harga = 52000.00, stok = stok + 10
WHERE nama = 'Teh Oolong';
SELECT id, nama, harga, stok
FROM produk
WHERE id = 8;
-- DELETE lewat view: sama, terjemahan DELETE pada produk
-- dengan predicate view ikut serta.
DELETE FROM produk_teh
WHERE nama = 'Teh Oolong';
SELECT count(*) AS jumlah_produk
FROM produk;Output yang Dihasilkan:
CREATE VIEW
id | nama | harga | stok
----+------------+----------+------
3 | Teh Melati | 35000.00 | 100
4 | Teh Hijau | 40000.00 | 60
(2 rows)
INSERT 0 1
id | nama | kategori | harga | stok
----+------------+----------+----------+------
8 | Teh Oolong | Teh | 55000.00 | 50
(1 row)
UPDATE 1
id | nama | harga | stok
----+------------+----------+------
8 | Teh Oolong | 52000.00 | 60
(1 row)
DELETE 1
jumlah_produk
---------------
7
(1 row)Mari kita bedah apa yang sebenarnya terjadi:
INSERT INTO produk_teh VALUES (8, 'Teh Oolong', 'Teh', ...)— outputINSERT 0 1muncul, dan query keproduk WHERE id = 8memperlihatkan barisTeh Oolongdi tabel dasar. PostgreSQL menulis ulang statement ini menjadiINSERT INTO produk ...di belakang layar — mekanisme query rewrite yang sama dengan yang menjalankanSELECTpada view.UPDATE produk_teh SET harga = 52000.00 ...— statement diterjemahkan menjadiUPDATE produk SET harga = 52000.00 ... WHERE kategori = 'Teh' AND nama = 'Teh Oolong': predicate view di-AND-kan dengan predicate statement, sehingga baris di luar jangkauan view mustahil tersentuh. Angka stok naik dari 50 ke 60 (stok + 10) membuktikan ekspresi kolom padaSETdievaluasi normal.DELETE FROM produk_teh WHERE nama = 'Teh Oolong'— terjemahan yang sama berlaku;count(*)akhir kembali ke 7, bentuk data bersih seperti semula.- Kolom pada INSERT harus lengkap untuk constraint tabel — view mengekspos kelima kolom
produk, jadi nilaikategori = 'Teh'ikut disertakan; kolom tabel dasar yang tidak diekspos view akan menerima nilai default-nya.
Perhatikan pola besar di balik tiga poin itu: view yang bisa ditulis adalah peta satu-ke-satu ke sebagian baris tabel dasar. Selama pemetaan itu jelas — satu tabel sumber, tanpa pengubahan bentuk — PostgreSQL tahu persis ke mana setiap baris tulis harus mendarat.
Syarat View yang Otomatis Bisa Ditulis #
Kelas view yang “otomatis bisa ditulis” (automatically updatable) punya syarat yang bisa didaftar pendek. Aturan-aturan ini bukan arbitrer; masing-masing menjaga pemetaan satu-ke-satu antara baris view dan baris tabel:
| Syarat | Contoh pelanggaran | Akibat |
|---|---|---|
FROM berisi tepat satu tabel atau view updatable lain | penjualan JOIN produk | View tidak bisa ditulis |
Tanpa DISTINCT, GROUP BY, HAVING, LIMIT, OFFSET, UNION/INTERSECT/EXCEPT, WITH | GROUP BY pr.kategori pada ringkasan_penjualan | View tidak bisa ditulis |
Daftar SELECT tanpa agregat, window function, atau set-returning function | count(*), sum(...) | View tidak bisa ditulis |
| Kolom yang ingin ditulis berupa referensi langsung kolom sumber | harga * 2 AS harga_mahal | View tetap terbaca; kolom ekspresi tidak bisa ditulis |
Intuisinya per baris: agregat dan GROUP BY mengubah banyak baris menjadi satu — UPDATE pada baris hasil agregat tidak punya padanan tunggal di tabel dasar; DISTINCT dan LIMIT memotong baris tanpa identitas yang stabil — baris ke-berapa yang dimaksud DELETE?; join menggabungkan dua sumber — kolom dari tabel sebelah mana yang harus ditulis? View yang melanggar salah satu syarat tetap sepenuhnya berfungsi untuk membaca; yang hilang hanyalah kemampuan menulis. Kita akan melihat penolakan konkretnya pada ringkasan_penjualan di paruh akhir chapter, dan status updatability setiap view bisa dibaca dari catalog information_schema.views — kolom is_updatable dan is_insertable_into.
Jebakan Tanpa CHECK OPTION: Baris yang Hilang #
Ada satu celah pada demo sebelumnya yang sengaja belum dibahas. produk_teh menyaring kategori = 'Teh', tetapi INSERT lewat view itu sama sekali tidak diwajibkan mengirim kategori Teh. Padahal nilai kolom kategori tetap harus dikirim — kolomnya NOT NULL di tabel dasar. Hasilnya: baris yang lolos ke tabel dasar namun tersaring keluar dari view:
-- TANPA CHECK OPTION, view tidak menjaga batasnya sendiri:
-- baris yang TIDAK memenuhi WHERE view tetap lolos ke tabel dasar.
INSERT INTO produk_teh (id, nama, kategori, harga, stok)
VALUES (8, 'Kopi Robusta', 'Kopi', 60000.00, 20);
-- View tidak menampilkan baris itu: kategori-nya 'Kopi',
-- tersaring oleh WHERE kategori = 'Teh'.
SELECT id, nama, kategori
FROM produk_teh
WHERE id = 8;
-- ... padahal barisnya benar-benar ada di tabel dasar.
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE id = 8;
-- Baris yang tak terlihat view juga tak bisa dihapus lewat view;
-- pembersihannya harus langsung dari tabel dasar.
DELETE FROM produk
WHERE id = 8;
SELECT count(*) AS jumlah_produk
FROM produk;Output yang Dihasilkan:
INSERT 0 1
id | nama | kategori
----+------+----------
(0 rows)
id | nama | kategori | harga | stok
----+--------------+----------+----------+------
8 | Kopi Robusta | Kopi | 60000.00 | 20
(1 row)
DELETE 1
jumlah_produk
---------------
7
(1 row)INSERT mengembalikan INSERT 0 1 — sukses di mata PostgreSQL — tetapi query id = 8 pada view menghasilkan (0 rows), sementara query yang sama pada produk menemukan Kopi Robusta lengkap dengan harganya. Baris itu berstatus hantu: ada di tabel, tak terlihat dari view, dan UPDATE/DELETE lewat view tidak akan pernah menyentuhnya karena terjemahan otomatisnya menyertakan predicate view. Pembersihannya harus turun ke tabel dasar, seperti DELETE FROM produk WHERE id = 8 di akhir file.
WITH CHECK OPTION: Gerbang di Depan View #
WITH CHECK OPTION ditempelkan di akhir definisi view dan mengubah aturan mainnya: setiap baris baru hasil INSERT, dan setiap baris hasil UPDATE, wajib tetap terlihat oleh view itu. Baris di luar jangkauan ditolak sebelum menyentuh tabel:
-- CHECK OPTION memasang gerbang: setiap baris baru atau baris hasil
-- UPDATE harus tetap terlihat oleh view -- kalau tidak, ditolak.
DROP VIEW produk_teh;
CREATE VIEW produk_teh AS
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE kategori = 'Teh'
WITH CHECK OPTION;
SELECT id, nama, harga, stok
FROM produk_teh
ORDER BY id;Output yang Dihasilkan:
DROP VIEW
CREATE VIEW
id | nama | harga | stok
----+------------+----------+------
3 | Teh Melati | 35000.00 | 100
4 | Teh Hijau | 40000.00 | 60
(2 rows)Definisi view-nya sama persis dengan sebelumnya — satu tabel, predicate kategori — hanya ditambah satu baris WITH CHECK OPTION. Sekarang INSERT yang sama yang tadinya menghasilkan baris hantu berubah nasib:
-- INSERT baris berkategori 'Kopi' lewat view Teh: DITOLAK.
-- Baris semacam itu akan tersaring WHERE kategori = 'Teh',
-- sehingga tidak akan pernah terlihat oleh view ini.
INSERT INTO produk_teh (id, nama, kategori, harga, stok)
VALUES (8, 'Kopi Robusta', 'Kopi', 60000.00, 20);Output yang Dihasilkan:
psql:check-option-error.sql:5: ERROR: new row violates check option for view "produk_teh"
DETAIL: Failing row contains (8, Kopi Robusta, Kopi, 60000.00, 20).Error new row violates check option for view "produk_teh" datang lengkap dengan DETAIL yang menampilkan baris gagal — Failing row contains (8, Kopi Robusta, Kopi, 60000.00, 20) — sehingga penyebabnya langsung terbaca. Statement UPDATE yang mencoba menggeser baris keluar dari view (misalnya SET kategori = 'Gula' pada Teh Melati) ditolak dengan error yang sama. Perintah DELETE tidak terpengaruh check option; view memang hanya bisa menghapus baris yang terlihatnya sendiri.
LOCAL vs CASCADED pada View Berlapis #
Untuk view tunggal di atas tabel, WITH CHECK OPTION selesai di situ. Pembeda LOCAL dan CASCADED baru berperan pada view berlapis — view yang membaca view lain — karena pertanyaannya menjadi: predicate lapis mana saja yang ikut diperiksa? Kita bangun lapisan bawah stok_menipis (stok di bawah 50, tanpa check option) dan lapisan atas teh_menipis_local dengan WITH LOCAL CHECK OPTION:
-- Pembeda LOCAL dan CASCADED hanya relevan untuk view berlapis.
-- Lapis bawah: produk dengan stok di bawah 50, TANPA check option.
CREATE VIEW stok_menipis AS
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE stok < 50;
-- Lapis atas dengan LOCAL CHECK OPTION:
-- hanya predicate lapis atas (kategori = 'Teh') yang diperiksa.
CREATE VIEW teh_menipis_local AS
SELECT id, nama, kategori, harga, stok
FROM stok_menipis
WHERE kategori = 'Teh'
WITH LOCAL CHECK OPTION;
-- stok 80 melanggar predicate lapis bawah (stok < 50),
-- tetapi LOCAL hanya memeriksa lapis atas -- INSERT LOLOS.
INSERT INTO teh_menipis_local (id, nama, kategori, harga, stok)
VALUES (9, 'Teh Chamomile', 'Teh', 45000.00, 80);
-- Akibatnya baris itu masuk ke tabel dasar ...
SELECT id, nama, kategori, stok
FROM produk
WHERE id = 9;
-- ... tetapi tidak terlihat dari view (dan juga dari lapis bawah).
SELECT id, nama, stok
FROM teh_menipis_local
WHERE id = 9;
-- Bersihkan, lalu buat versi CASCADED untuk perbandingan.
DELETE FROM produk
WHERE id = 9;
CREATE VIEW teh_menipis_casc AS
SELECT id, nama, kategori, harga, stok
FROM stok_menipis
WHERE kategori = 'Teh'
WITH CASCADED CHECK OPTION;
-- Satu view agregat untuk pengujian tabel aturan di bawah.
CREATE VIEW ringkasan_penjualan AS
SELECT pr.kategori,
count(*) AS transaksi,
sum(pe.jumlah) AS total_unit,
sum(pe.jumlah * pr.harga) AS total_nilai
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
GROUP BY pr.kategori;
-- information_schema.views membaca status updatability
-- dan level check option dari semua view milik schema public.
SELECT table_name, check_option, is_updatable, is_insertable_into
FROM information_schema.views
WHERE table_schema = 'public'
ORDER BY table_name;Output yang Dihasilkan:
CREATE VIEW
CREATE VIEW
INSERT 0 1
id | nama | kategori | stok
----+---------------+----------+------
9 | Teh Chamomile | Teh | 80
(1 row)
id | nama | stok
----+------+------
(0 rows)
DELETE 1
CREATE VIEW
CREATE VIEW
table_name | check_option | is_updatable | is_insertable_into
---------------------+--------------+--------------+--------------------
produk_teh | CASCADED | YES | YES
ringkasan_penjualan | NONE | NO | NO
stok_menipis | NONE | YES | YES
teh_menipis_casc | CASCADED | YES | YES
teh_menipis_local | LOCAL | YES | YES
(5 rows)Hasilnya membuktikan LOCAL mengecek predicate lapis atas saja: INSERT Teh Chamomile dengan stok 80 lolos — kategori = 'Teh' terpenuhi — padahal stok 80 melanggar predicate stok < 50 milik stok_menipis. Barisnya mendarat di produk, namun tidak terlihat dari teh_menipis_local maupun dari stok_menipis: baris hantu versi berlapis. Versi CASCADED memeriksa seluruh rantai, dan INSERT yang sama langsung gagal:
-- INSERT yang sama lewat versi CASCADED: DITOLAK.
-- Pemeriksaan menjalar ke seluruh rantai view di bawahnya:
-- predicate stok < 50 milik stok_menipis ikut dievaluasi,
-- dan stok 80 jelas gagal melewatinya.
INSERT INTO teh_menipis_casc (id, nama, kategori, harga, stok)
VALUES (9, 'Teh Chamomile', 'Teh', 45000.00, 80);Output yang Dihasilkan:
psql:check-option-cascaded-error.sql:6: ERROR: new row violates check option for view "stok_menipis"
DETAIL: Failing row contains (9, Teh Chamomile, Teh, 45000.00, 80).Error new row violates check option for view "stok_menipis" menyebut nama lapisan bawah — bukan teh_menipis_casc yang kita tulis di statement — karena pelanggaran ditemukan pada predicate stok_menipis dalam penelusuran berjenjang. Tabel catalog di file sebelumnya merangkum status kelimanya: produk_teh tercatat CASCADED padahal kita menulis WITH CHECK OPTION polos (CASCADED memang default-nya), teh_menipis_local tercatat LOCAL, teh_menipis_casc tercatat CASCADED, dan baris ringkasan_penjualan memperlihatkan is_updatable = NO — subjek bagian penutup.
View Agregat Menolak Ditulis #
Tertutupnya bab ini adalah penolakan yang menegaskan tabel syarat di awal: view agregat memang tidak punya padanan tulis. ringkasan_penjualan menggabungkan join, GROUP BY, dan tiga agregat — tiga pelanggaran sekaligus — sehingga satu-satunya perintah yang sah padanya adalah membaca:
-- View agregat TIDAK otomatis bisa ditulis:
-- satu baris INSERT tidak punya makna yang jelas untuk hasil GROUP BY.
INSERT INTO ringkasan_penjualan (kategori, transaksi)
VALUES ('Susu', 0);Output yang Dihasilkan:
psql:insert-agregat-error.sql:4: ERROR: cannot insert into view "ringkasan_penjualan"
DETAIL: Views containing GROUP BY are not automatically updatable.
HINT: To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.Penolakan lengkapnya tiga lapis: cannot insert into view "ringkasan_penjualan", DETAIL: Views containing GROUP BY are not automatically updatable, dan HINT yang menyebut dua jalan keluar eksplisit — trigger INSTEAD OF INSERT atau aturan ON INSERT DO INSTEAD. Hint itu bukan basa-basi: keduanya adalah mekanisme resmi untuk memberi view kompleks perilaku tulis buatan, misalnya INSERT kategori baru yang diterjemahkan menjadi serangkaian operasi tabel. Logika macam itu adalah materi course Triggers, bukan tambalan yang perlu ditakuti di sini.
View kini utuh gambarnya: bisa dibaca selalu, bisa ditulis kalau strukturnya sederhana, dan bisa dijaga batasnya dengan CHECK OPTION. Namun semua itu tetap membayar biaya komputasi setiap kali dibaca — view agregat yang dibaca sering akan menghitung ulang join dan GROUP BY-nya lagi dan lagi. Bagi laporan berat yang jarang berubah datanya, PostgreSQL menyediakan varian yang membekukan hasilnya ke tabel fisik dan menyegarkannya sesuai perintah: Materialized View
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
baris-hilang.sql
Unduh berkas-- TANPA CHECK OPTION, view tidak menjaga batasnya sendiri:
-- baris yang TIDAK memenuhi WHERE view tetap lolos ke tabel dasar.
INSERT INTO produk_teh (id, nama, kategori, harga, stok)
VALUES (8, 'Kopi Robusta', 'Kopi', 60000.00, 20);
-- View tidak menampilkan baris itu: kategori-nya 'Kopi',
-- tersaring oleh WHERE kategori = 'Teh'.
SELECT id, nama, kategori
FROM produk_teh
WHERE id = 8;
-- ... padahal barisnya benar-benar ada di tabel dasar.
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE id = 8;
-- Baris yang tak terlihat view juga tak bisa dihapus lewat view;
-- pembersihannya harus langsung dari tabel dasar.
DELETE FROM produk
WHERE id = 8;
SELECT count(*) AS jumlah_produk
FROM produk;
buat-tabel.sql
Unduh berkas-- Domain latihan section View dan Materialized View:
-- katalog produk, pelanggan, dan transaksi Toko Maju Jaya.
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
kategori text NOT NULL,
harga numeric(10,2) NOT NULL,
stok integer NOT NULL
);
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
kota text NOT NULL
);
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
produk_id integer NOT NULL REFERENCES produk(id),
pelanggan_id integer NOT NULL REFERENCES pelanggan(id),
jumlah integer NOT NULL
);
INSERT INTO produk (id, nama, kategori, harga, stok) VALUES
(1, 'Kopi Gayo', 'Kopi', 85000.00, 40),
(2, 'Kopi Toraja', 'Kopi', 90000.00, 25),
(3, 'Teh Melati', 'Teh', 35000.00, 100),
(4, 'Teh Hijau', 'Teh', 40000.00, 60),
(5, 'Gula Merah', 'Gula', 15000.00, 200),
(6, 'Gula Putih', 'Gula', 16000.00, 150),
(7, 'Kopi Lampung', 'Kopi', 78000.00, 0);
INSERT INTO pelanggan (id, nama, kota) VALUES
(1, 'Budi Santoso', 'Jakarta'),
(2, 'Siti Aminah', 'Bandung'),
(3, 'Andi Wijaya', 'Surabaya'),
(4, 'Dewi Lestari', 'Yogyakarta'),
(5, 'Rina Marlina', 'Medan');
INSERT INTO penjualan (id, tanggal, produk_id, pelanggan_id, jumlah) VALUES
( 1, DATE '2026-01-05', 1, 1, 10),
( 2, DATE '2026-01-08', 3, 2, 25),
( 3, DATE '2026-01-12', 5, 3, 40),
( 4, DATE '2026-01-15', 2, 1, 8),
( 5, DATE '2026-01-20', 4, 4, 15),
( 6, DATE '2026-01-22', 1, 2, 12),
( 7, DATE '2026-02-02', 3, 5, 20),
( 8, DATE '2026-02-05', 6, 3, 30),
( 9, DATE '2026-02-10', 2, 4, 6),
(10, DATE '2026-02-14', 4, 1, 18);
SELECT id, nama, kategori, harga, stok FROM produk ORDER BY id;
SELECT id, nama, kota FROM pelanggan ORDER BY id;
SELECT id, tanggal, produk_id, pelanggan_id, jumlah FROM penjualan ORDER BY id;
buat-view-check-option.sql
Unduh berkas-- CHECK OPTION memasang gerbang: setiap baris baru atau baris hasil
-- UPDATE harus tetap terlihat oleh view -- kalau tidak, ditolak.
DROP VIEW produk_teh;
CREATE VIEW produk_teh AS
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE kategori = 'Teh'
WITH CHECK OPTION;
SELECT id, nama, harga, stok
FROM produk_teh
ORDER BY id;
check-option-berlapis.sql
Unduh berkas-- Pembeda LOCAL dan CASCADED hanya relevan untuk view berlapis.
-- Lapis bawah: produk dengan stok di bawah 50, TANPA check option.
CREATE VIEW stok_menipis AS
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE stok < 50;
-- Lapis atas dengan LOCAL CHECK OPTION:
-- hanya predicate lapis atas (kategori = 'Teh') yang diperiksa.
CREATE VIEW teh_menipis_local AS
SELECT id, nama, kategori, harga, stok
FROM stok_menipis
WHERE kategori = 'Teh'
WITH LOCAL CHECK OPTION;
-- stok 80 melanggar predicate lapis bawah (stok < 50),
-- tetapi LOCAL hanya memeriksa lapis atas -- INSERT LOLOS.
INSERT INTO teh_menipis_local (id, nama, kategori, harga, stok)
VALUES (9, 'Teh Chamomile', 'Teh', 45000.00, 80);
-- Akibatnya baris itu masuk ke tabel dasar ...
SELECT id, nama, kategori, stok
FROM produk
WHERE id = 9;
-- ... tetapi tidak terlihat dari view (dan juga dari lapis bawah).
SELECT id, nama, stok
FROM teh_menipis_local
WHERE id = 9;
-- Bersihkan, lalu buat versi CASCADED untuk perbandingan.
DELETE FROM produk
WHERE id = 9;
CREATE VIEW teh_menipis_casc AS
SELECT id, nama, kategori, harga, stok
FROM stok_menipis
WHERE kategori = 'Teh'
WITH CASCADED CHECK OPTION;
-- Satu view agregat untuk pengujian tabel aturan di bawah.
CREATE VIEW ringkasan_penjualan AS
SELECT pr.kategori,
count(*) AS transaksi,
sum(pe.jumlah) AS total_unit,
sum(pe.jumlah * pr.harga) AS total_nilai
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
GROUP BY pr.kategori;
-- information_schema.views membaca status updatability
-- dan level check option dari semua view milik schema public.
SELECT table_name, check_option, is_updatable, is_insertable_into
FROM information_schema.views
WHERE table_schema = 'public'
ORDER BY table_name;
check-option-cascaded-error.sql
Unduh berkas-- INSERT yang sama lewat versi CASCADED: DITOLAK.
-- Pemeriksaan menjalar ke seluruh rantai view di bawahnya:
-- predicate stok < 50 milik stok_menipis ikut dievaluasi,
-- dan stok 80 jelas gagal melewatinya.
INSERT INTO teh_menipis_casc (id, nama, kategori, harga, stok)
VALUES (9, 'Teh Chamomile', 'Teh', 45000.00, 80);
check-option-error.sql
Unduh berkas-- INSERT baris berkategori 'Kopi' lewat view Teh: DITOLAK.
-- Baris semacam itu akan tersaring WHERE kategori = 'Teh',
-- sehingga tidak akan pernah terlihat oleh view ini.
INSERT INTO produk_teh (id, nama, kategori, harga, stok)
VALUES (8, 'Kopi Robusta', 'Kopi', 60000.00, 20);
insert-agregat-error.sql
Unduh berkas-- View agregat TIDAK otomatis bisa ditulis:
-- satu baris INSERT tidak punya makna yang jelas untuk hasil GROUP BY.
INSERT INTO ringkasan_penjualan (kategori, transaksi)
VALUES ('Susu', 0);
tulis-lewat-view.sql
Unduh berkas-- View sederhana = satu tabel, tanpa agregat/DISTINCT/GROUP BY/LIMIT:
-- PostgreSQL menerjemahkan INSERT/UPDATE/DELETE padanya menjadi
-- operasi pada tabel dasar produk.
CREATE VIEW produk_teh AS
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE kategori = 'Teh';
SELECT id, nama, harga, stok
FROM produk_teh
ORDER BY id;
-- INSERT lewat view: baris mendarat di tabel produk.
INSERT INTO produk_teh (id, nama, kategori, harga, stok)
VALUES (8, 'Teh Oolong', 'Teh', 55000.00, 50);
SELECT id, nama, kategori, harga, stok
FROM produk
WHERE id = 8;
-- UPDATE lewat view: hanya baris yang terlihat view yang tersentuh.
UPDATE produk_teh
SET harga = 52000.00, stok = stok + 10
WHERE nama = 'Teh Oolong';
SELECT id, nama, harga, stok
FROM produk
WHERE id = 8;
-- DELETE lewat view: sama, terjemahan DELETE pada produk
-- dengan predicate view ikut serta.
DELETE FROM produk_teh
WHERE nama = 'Teh Oolong';
SELECT count(*) AS jumlah_produk
FROM produk;