Proyek laporan yang sehat punya siklus pembersihan: view percobaan masa krisis, view antara yang definisinya sudah ditimpa dua kali, view laporan yang pemakainya sudah pindah divisi. Membiarkan view mati menumpuk di schema membuat katalog sulit dibaca dan menaikkan risiko bentrok nama di masa depan. DROP VIEW adalah perintahnya — sederhana untuk view yang berdiri sendiri, tetapi menyimpan dua jebakan klasik: script yang berhenti karena view ternyata sudah tidak ada, dan penolakan keras ketika view lain masih membacanya. Chapter ini membedah keduanya, lalu membahas CASCADE yang menyelesaikan kebuntuan dependensi dengan harga yang perlu dipahami sebelum ditebus.
Domain Latihan yang Sama #
Masih dengan domain Toko Maju Jaya dari chapter-chapter sebelumnya — produk, pelanggan, dan penjualan dengan data yang identik. File setup berikut membuat ulang ketiga tabel itu dari nol agar 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)Menjatuhkan Satu atau Beberapa View #
Kita mulai dengan tiga view filter kategori di atas tabel produk. Menjatuhkannya tidak menyentuh tabel dasar sama sekali — yang hilang hanya definisi query tersimpan beserta namanya:
-- Menjatuhkan view: objeknya hilang, tabel dasarnya TIDAK tersentuh.
CREATE VIEW daftar_teh AS
SELECT id, nama, harga, stok
FROM produk
WHERE kategori = 'Teh';
CREATE VIEW daftar_kopi AS
SELECT id, nama, harga, stok
FROM produk
WHERE kategori = 'Kopi';
CREATE VIEW daftar_gula AS
SELECT id, nama, harga, stok
FROM produk
WHERE kategori = 'Gula';
-- Satu view dijatuhkan ...
DROP VIEW daftar_teh;
-- ... dua view tersisa terdaftar di catalog pg_views.
SELECT viewname
FROM pg_views
WHERE schemaname = 'public'
ORDER BY viewname;
-- Beberapa view sekaligus dalam satu statement, dipisah koma.
DROP VIEW daftar_kopi, daftar_gula;
SELECT viewname
FROM pg_views
WHERE schemaname = 'public'
ORDER BY viewname;Output yang Dihasilkan:
CREATE VIEW
CREATE VIEW
CREATE VIEW
DROP VIEW
viewname
-------------
daftar_gula
daftar_kopi
(2 rows)
DROP VIEW
viewname
----------
(0 rows)Dua kebiasaan praktis terlihat di file tersebut. Pertama, DROP VIEW daftar_teh menjatuhkan satu view; query berikutnya ke pg_views memanggil dua view tersisa — daftar_kopi dan daftar_gula. Kedua, statement DROP VIEW daftar_kopi, daftar_gula menjatuhkan dua view sekaligus dalam satu perintah; daftar pg_views berikutnya kosong. Sepanjang proses itu tabel produk tidak kehilangan satu baris pun — view memang tidak punya data sendiri, sehingga tidak ada yang perlu dibersihkan darinya. Yang juga tidak ikut hilang: izin akses dan komentar tidak ada pada view baru ini, karena keduanya melekat pada objek view yang kita jatuhkan.
DROP VIEW pada View yang Tidak Ada #
Script migrasi yang dijalankan berulang kali adalah sumber error paling umum di topik ini: hari pertama view dijatuhkan bersih, hari kedua script yang sama berjalan lagi dan DROP VIEW menabrak nama yang sudah tidak ada. PostgreSQL memperlakukan keadaan itu sebagai error keras, bukan peringatan:
-- DROP VIEW pada nama yang sudah tidak ada = error keras.
-- Perhatikan: daftar_teh sudah dijatuhkan pada contoh sebelumnya,
-- jadi statement ini memang gagal dengan sengaja.
DROP VIEW daftar_teh;Output yang Dihasilkan:
psql:drop-view-tak-ada.sql:4: ERROR: view "daftar_teh" does not existError view "daftar_teh" does not exist menghentikan file SQL di tempat — dengan ON_ERROR_STOP aktif, psql keluar dengan exit code 3 dan statement-statement setelahnya tidak pernah dijalankan. Untuk script yang harus tahan dijalankan berulang, PostgreSQL menyediakan IF EXISTS:
-- IF EXISTS mengubah error menjadi "dilewati diam-diam":
-- script tetap lanjut ke statement berikutnya.
-- Untuk view yang benar-benar tidak ada, PostgreSQL hanya mengeluarkan
-- NOTICE (tercetak di stderr, bukan stdout) dan tetap melanjutkan.
DROP VIEW IF EXISTS daftar_teh;
-- Untuk view yang ada, behavior-nya biasa: dijatuhkan.
CREATE VIEW daftar_gula AS
SELECT id, nama, harga, stok
FROM produk
WHERE kategori = 'Gula';
DROP VIEW IF EXISTS daftar_gula;
-- Statement ini terbukti tetap tereksekusi: file tidak berhenti
-- di statement yang melewati view yang tidak ada.
SELECT count(*) AS jumlah_produk
FROM produk;Output yang Dihasilkan:
DROP VIEW
CREATE VIEW
DROP VIEW
jumlah_produk
---------------
7
(1 row)Perhatikan kejanggalan kecil yang justru mendidik: output di atas diawali tag DROP VIEW padahal daftar_teh tidak ada. Psql mencetak tag perintah itu untuk setiap DROP VIEW IF EXISTS yang selesai — baik yang benar-benar menjatuhkan view maupun yang hanya melewatkannya. Pembeda kedua kasus itu adalah NOTICE view "daftar_teh" does not exist, skipping yang PostgreSQL kirim ke stderr, sehingga tidak tampil di blok output stdout ini. Bukti terakhir bahwa eksekusi berlanjut ada di baris jumlah_produk: query penghitung di akhir file tetap tereksekusi dan mengembalikan angka 7, artinya pengecualian tidak menghentikan script.
Ketergantungan: Ketika View Lain Masih Membaca #
Jebakan kedua lebih serius. Kita bangun kembali pasangan view bertingkat dari chapter sebelumnya — ringkasan_penjualan sebagai lapisan agregat dan laporan_besar yang membacanya — lalu periksa dulu jejak ketergantungannya di catalog pg_depend sebelum mencoba menjatuhkan lapisan bawah:
-- Rantai dependensi: laporan_besar membaca ringkasan_penjualan.
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;
CREATE VIEW laporan_besar AS
SELECT kategori, total_nilai
FROM ringkasan_penjualan
WHERE total_nilai >= 1500000;
SELECT kategori, total_nilai
FROM laporan_besar
ORDER BY kategori;
-- pg_depend mencatat ketergantungan sampai ke tingkat kolom:
-- dua baris ini adalah alasan DROP VIEW ringkasan_penjualan ditolak.
SELECT src.relname AS view_pemakai,
dst.relname AS objek_rujukan,
a.attname AS kolom_rujukan
FROM pg_depend d
JOIN pg_rewrite r ON r.oid = d.objid
JOIN pg_class src ON src.oid = r.ev_class
JOIN pg_class dst ON dst.oid = d.refobjid
JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
WHERE d.classid = 'pg_rewrite'::regclass
AND d.refclassid = 'pg_class'::regclass
AND src.relname = 'laporan_besar'
ORDER BY dst.relname, a.attname;Output yang Dihasilkan:
CREATE VIEW
CREATE VIEW
kategori | total_nilai
----------+-------------
Kopi | 3130000.00
Teh | 2895000.00
(2 rows)
view_pemakai | objek_rujukan | kolom_rujukan
---------------+---------------------+---------------
laporan_besar | ringkasan_penjualan | kategori
laporan_besar | ringkasan_penjualan | total_nilai
(2 rows)Query catalog tersebut menyusuri jalur pg_depend → pg_rewrite (tempat definisi view tersimpan sebagai aturan penulisan ulang) → pg_class, dan hasilnya lugas: laporan_besar mencatat ketergantungan pada dua kolom ringkasan_penjualan, yaitu kategori dan total_nilai. Baris-baris catalog inilah yang dihapus PostgreSQL saat view dijatuhkan — dan keberadaan merekalah yang membuat penolakan berikutnya masuk akal:
-- Menjatuhkan view yang masih dibaca view lain: DITOLAK.
-- PostgreSQL menolak diam-diam sebelum ada kerusakan:
-- laporan_besar akan rusak kalau ringkasan_penjualan dijatuhkan.
DROP VIEW ringkasan_penjualan;Output yang Dihasilkan:
psql:drop-dependensi.sql:4: ERROR: cannot drop view ringkasan_penjualan because other objects depend on it
DETAIL: view laporan_besar depends on view ringkasan_penjualan
HINT: Use DROP ... CASCADE to drop the dependent objects too.Error lengkapnya tiga lapis: cannot drop view ringkasan_penjualan because other objects depend on it, diikuti DETAIL: view laporan_besar depends on view ringkasan_penjualan, dan ditutup HINT: Use DROP ... CASCADE to drop the dependent objects too. Ini penolakan protektif, bukan keterbatasan: kalau PostgreSQL membiarkan ringkasan_penjualan dijatuhkan, setiap query ke laporan_besar akan rusak dengan error relation "ringkasan_penjualan" does not exist — kerusakan yang menjalar ke semua laporan dan aplikasi pemakai. Perlindungan yang sama berlaku untuk tabel dasar: DROP TABLE produk juga ditolak selama ada view yang membacanya.
DROP VIEW … CASCADE #
Ketika keputusan “view bawah memang harus hilang, view pemakainya ikut saja” sudah disadari sepenuhnya, CASCADE menyelesaikan kebuntuan dalam satu statement:
-- CASCADE menjatuhkan view beserta semua objek yang bergantung padanya.
-- NOTICE "drop cascades to ..." tercetak di stderr;
-- di stdout hanya terlihat tag DROP VIEW.
DROP VIEW ringkasan_penjualan CASCADE;
-- Bukti dampaknya: SELURUH view di schema public kini lenyap,
-- termasuk laporan_besar yang tidak kita sebut di statement atas.
SELECT viewname
FROM pg_views
WHERE schemaname = 'public'
ORDER BY viewname;Output yang Dihasilkan:
DROP VIEW
viewname
----------
(0 rows)Statement itu hanya menyebut satu nama, tetapi dampaknya berlipat: PostgreSQL menghapus ringkasan_penjualan, mengikuti jejak pg_depend, lalu ikut menjatuhkan laporan_besar. NOTICE drop cascades to view laporan_besar yang memberi tahu hal itu tercetak di stderr — blok output stdout hanya menampilkan tag DROP VIEW, dan query pg_views di bawahnya memperlihatkan schema public yang benar-benar kosong. Bandingkan kasusnya dengan error sebelumnya: tanpa CASCADE, satu baris ketergantungan menghentikan seluruh perintah; dengan CASCADE, satu baris yang sama menjatuhkan dua view sekaligus.
View kini lengkap siklus hidupnya: dibuat, diubah, dan dijatuhkan dengan sadar akan dependensinya. Satu sifat view masih belum kita sentuh — dan sifat itu mengejutkan sebagian besar pemula: view tertentu bisa menerima INSERT, UPDATE, dan DELETE layaknya tabel, dan tulisan itu benar-benar mendarat di tabel dasar. Bab berikutnya membahas syarat otomatis kemampuan ditulis itu, jebakan baris yang “hilang setelah di-insert”, serta CHECK OPTION yang menjaganya: Updatable View
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
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;
drop-cascade.sql
Unduh berkas-- CASCADE menjatuhkan view beserta semua objek yang bergantung padanya.
-- NOTICE "drop cascades to ..." tercetak di stderr;
-- di stdout hanya terlihat tag DROP VIEW.
DROP VIEW ringkasan_penjualan CASCADE;
-- Bukti dampaknya: SELURUH view di schema public kini lenyap,
-- termasuk laporan_besar yang tidak kita sebut di statement atas.
SELECT viewname
FROM pg_views
WHERE schemaname = 'public'
ORDER BY viewname;
drop-dependensi.sql
Unduh berkas-- Menjatuhkan view yang masih dibaca view lain: DITOLAK.
-- PostgreSQL menolak diam-diam sebelum ada kerusakan:
-- laporan_besar akan rusak kalau ringkasan_penjualan dijatuhkan.
DROP VIEW ringkasan_penjualan;
drop-if-exists.sql
Unduh berkas-- IF EXISTS mengubah error menjadi "dilewati diam-diam":
-- script tetap lanjut ke statement berikutnya.
-- Untuk view yang benar-benar tidak ada, PostgreSQL hanya mengeluarkan
-- NOTICE (tercetak di stderr, bukan stdout) dan tetap melanjutkan.
DROP VIEW IF EXISTS daftar_teh;
-- Untuk view yang ada, behavior-nya biasa: dijatuhkan.
CREATE VIEW daftar_gula AS
SELECT id, nama, harga, stok
FROM produk
WHERE kategori = 'Gula';
DROP VIEW IF EXISTS daftar_gula;
-- Statement ini terbukti tetap tereksekusi: file tidak berhenti
-- di statement yang melewati view yang tidak ada.
SELECT count(*) AS jumlah_produk
FROM produk;
drop-view-dasar.sql
Unduh berkas-- Menjatuhkan view: objeknya hilang, tabel dasarnya TIDAK tersentuh.
CREATE VIEW daftar_teh AS
SELECT id, nama, harga, stok
FROM produk
WHERE kategori = 'Teh';
CREATE VIEW daftar_kopi AS
SELECT id, nama, harga, stok
FROM produk
WHERE kategori = 'Kopi';
CREATE VIEW daftar_gula AS
SELECT id, nama, harga, stok
FROM produk
WHERE kategori = 'Gula';
-- Satu view dijatuhkan ...
DROP VIEW daftar_teh;
-- ... dua view tersisa terdaftar di catalog pg_views.
SELECT viewname
FROM pg_views
WHERE schemaname = 'public'
ORDER BY viewname;
-- Beberapa view sekaligus dalam satu statement, dipisah koma.
DROP VIEW daftar_kopi, daftar_gula;
SELECT viewname
FROM pg_views
WHERE schemaname = 'public'
ORDER BY viewname;
drop-view-tak-ada.sql
Unduh berkas-- DROP VIEW pada nama yang sudah tidak ada = error keras.
-- Perhatikan: daftar_teh sudah dijatuhkan pada contoh sebelumnya,
-- jadi statement ini memang gagal dengan sengaja.
DROP VIEW daftar_teh;
view-berantai.sql
Unduh berkas-- Rantai dependensi: laporan_besar membaca ringkasan_penjualan.
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;
CREATE VIEW laporan_besar AS
SELECT kategori, total_nilai
FROM ringkasan_penjualan
WHERE total_nilai >= 1500000;
SELECT kategori, total_nilai
FROM laporan_besar
ORDER BY kategori;
-- pg_depend mencatat ketergantungan sampai ke tingkat kolom:
-- dua baris ini adalah alasan DROP VIEW ringkasan_penjualan ditolak.
SELECT src.relname AS view_pemakai,
dst.relname AS objek_rujukan,
a.attname AS kolom_rujukan
FROM pg_depend d
JOIN pg_rewrite r ON r.oid = d.objid
JOIN pg_class src ON src.oid = r.ev_class
JOIN pg_class dst ON dst.oid = d.refobjid
JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
WHERE d.classid = 'pg_rewrite'::regclass
AND d.refclassid = 'pg_class'::regclass
AND src.relname = 'laporan_besar'
ORDER BY dst.relname, a.attname;