Chapter sebelumnya menutup dengan satu keterbukaan yang jujur: materialized view menampilkan salinan lama selamanya — sampai seseorang menyegarkannya. Perintah REFRESH MATERIALIZED VIEW adalah tombol pembaharuan itu: PostgreSQL menjalankan ulang query definisi yang tersimpan di catalog, lalu mengganti isi fisik materialized view dengan hasil terbaru. Kedengarannya satu perintah sederhana, tetapi di baliknya ada tiga keputusan operasional yang menentukan apakah sistem produksi Anda tahan banting: refresh penuh yang mengunci pembaca, opsi CONCURRENTLY yang menyegarkan tanpa mengusir pembaca asalkan ada index unik, dan opsi WITH NO DATA yang sengaja mengosongkan isi. Chapter ini menguji ketiganya di atas kasus ringkasan penjualan yang sama dengan chapter sebelumnya.
Menyiapkan Bahan Latihan #
Kita mulai dari keadaan yang bersih: tabel penjualan berisi sembilan transaksi Januari 2026 dan materialized view ringkasan_penjual yang langsung terisi saat diciptakan. Perhatikan bahwa CREATE MATERIALIZED VIEW tanpa opsi tambahan memang mengisi data seketika — keadaan “belum terisi” nanti di paruh akhir chapter adalah hasil pilihan eksplisit, bukan kebetulan:
-- Bahan latihan chapter Refresh Materialized View:
-- tabel penjualan + materialized view yang sudah terisi.
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
penjual text NOT NULL,
produk text NOT NULL,
jumlah integer NOT NULL,
harga numeric(10,2) NOT NULL
);
INSERT INTO penjualan (id, tanggal, penjual, produk, jumlah, harga) VALUES
( 1, DATE '2026-01-05', 'Budi', 'Kopi Gayo', 10, 85000.00),
( 2, DATE '2026-01-08', 'Siti', 'Teh Melati', 25, 35000.00),
( 3, DATE '2026-01-12', 'Andi', 'Gula Merah', 40, 15000.00),
( 4, DATE '2026-01-15', 'Budi', 'Kopi Toraja', 8, 90000.00),
( 5, DATE '2026-01-20', 'Dewi', 'Kopi Gayo', 12, 85000.00),
( 6, DATE '2026-01-22', 'Siti', 'Teh Hijau', 15, 40000.00),
( 7, DATE '2026-01-25', 'Andi', 'Gula Merah', 30, 15000.00),
( 8, DATE '2026-01-28', 'Dewi', 'Kopi Gayo', 6, 85000.00),
( 9, DATE '2026-01-30', 'Budi', 'Teh Melati', 20, 35000.00);
CREATE MATERIALIZED VIEW ringkasan_penjual AS
SELECT penjual,
count(*) AS transaksi,
sum(jumlah) AS total_item,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY penjual;
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 9
SELECT 4
penjual | transaksi | total_item | total_nilai
---------+-----------+------------+-------------
Andi | 2 | 70 | 1050000.00
Budi | 3 | 38 | 2270000.00
Dewi | 2 | 18 | 1530000.00
Siti | 2 | 40 | 1475000.00
(4 rows)Siklus Stale, Refresh, Segar #
Refresh penuh paling mudah dipahami sebagai satu siklus empat tahap: data dasar berubah, materialized view masih menampilkan salinan lama, REFRESH menjalankan ulang query definisi, dan pembacaan berikutnya melihat angka baru. File berikut menjalankan keempat tahap itu secara berurutan:
-- Siklus stale -> refresh -> segar dengan REFRESH penuh.
-- Tahap 1: transaksi baru masuk ke tabel dasar.
INSERT INTO penjualan (id, tanggal, penjual, produk, jumlah, harga) VALUES
(10, DATE '2026-01-31', 'Dewi', 'Kopi Toraja', 5, 90000.00);
-- Tahap 2: materialized view masih menampilkan salinan lama
-- (Dewi 2 transaksi, 18 item) -- data stale.
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
-- Tahap 3: REFRESH menjalankan ulang query definisi dan mengganti
-- seluruh isi materialized view dengan hasil terbaru.
REFRESH MATERIALIZED VIEW ringkasan_penjual;
-- Tahap 4: pembacaan berikutnya melihat angka baru
-- (Dewi kini 3 transaksi, 23 item).
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;Output yang Dihasilkan:
INSERT 0 1
penjual | transaksi | total_item | total_nilai
---------+-----------+------------+-------------
Andi | 2 | 70 | 1050000.00
Budi | 3 | 38 | 2270000.00
Dewi | 2 | 18 | 1530000.00
Siti | 2 | 40 | 1475000.00
(4 rows)
REFRESH MATERIALIZED VIEW
penjual | transaksi | total_item | total_nilai
---------+-----------+------------+-------------
Andi | 2 | 70 | 1050000.00
Budi | 3 | 38 | 2270000.00
Dewi | 3 | 23 | 1980000.00
Siti | 2 | 40 | 1475000.00
(4 rows)Mari kita bedah inti siklusnya:
INSERTke tabel dasar — transaksi Dewi kelima masuk; perubahan ini sama sekali tidak menyentuh materialized view.SELECTpertama — pembacaan menampilkan salinan lama: Dewi masih 2 transaksi dan 18 item. Inilah keadaan stale yang dibahas chapter sebelumnya.REFRESH MATERIALIZED VIEW ringkasan_penjual— PostgreSQL membuka definisi tersimpan, menjalankan ulang agregasinya terhadap 10 baris terbaru, dan mengganti seluruh isi fisik materialized view. Tag outputREFRESH MATERIALIZED VIEWmenandai penyelesaian proses penggantian ini.SELECTkedua — angka Dewi naik menjadi 3 transaksi dan 23 item; salinan kini segar.
Refresh penuh ini mengganti isi secara total: hasil query baru ditulis menggantikan seluruh baris lama, bukan ditambahkan. Konsekuensinya di level penguncian penting dan tidak terlihat pada data kecil kita.
Refresh Concurrent Tanpa Mengunci Pembaca #
Varian CONCURRENTLY menyegarkan isi tanpa memblokir pembacaan: alih-alih mengganti seluruh isi sekaligus, server mencocokkan baris lama dengan hasil query baru lalu hanya menghapus dan menyisipkan baris yang berbeda. Mekanisme pencocokan itu butuh penanda baris, dan penandannya adalah index unik tanpa klausa WHERE di atas materialized view. Tanpa index demikian, PostgreSQL menolak perintahnya — kita buktikan dulu penolakan itu:
-- REFRESH ... CONCURRENTLY ditolak karena materialized view
-- belum memiliki index unik tanpa klausa WHERE.
REFRESH MATERIALIZED VIEW CONCURRENTLY ringkasan_penjual;Output yang Dihasilkan:
psql:refresh-concurrent-error.sql:3: ERROR: cannot refresh materialized view "public.ringkasan_penjual" concurrently
HINT: Create a unique index with no WHERE clause on one or more columns of the materialized view.Pesan errornya jujur menyebut penyebab dan obatnya: cannot refresh materialized view "public.ringkasan_penjual" concurrently, disertai HINT yang meminta index unik tanpa klausa WHERE. Kita penuhi syaratnya, lalu jalankan siklus stale–refresh–segar yang sama dengan varian concurrent:
-- Setelah index unik tersedia, refresh concurrent diterima.
-- Index unik menjadi penanda baris agar server bisa mencocokkan
-- isi lama dengan hasil query baru dan hanya mengubah bedanya.
CREATE UNIQUE INDEX idx_unik_penjual ON ringkasan_penjual (penjual);
-- Refresh concurrent pertama: data dasar belum berubah sejak
-- refresh penuh, jadi isi materialized view tetap sama.
REFRESH MATERIALIZED VIEW CONCURRENTLY ringkasan_penjual;
-- Siklus stale kedua: transaksi baru untuk Budi masuk...
INSERT INTO penjualan (id, tanggal, penjual, produk, jumlah, harga) VALUES
(11, DATE '2026-02-02', 'Budi', 'Kopi Gayo', 7, 85000.00);
-- ...materialized view masih stale (Budi 3 transaksi)...
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
-- ...refresh concurrent menyegarkannya tanpa mengunci pembaca...
REFRESH MATERIALIZED VIEW CONCURRENTLY ringkasan_penjual;
-- ...dan angka Budi menjadi 4 transaksi, 45 item.
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;Output yang Dihasilkan:
CREATE INDEX
REFRESH MATERIALIZED VIEW
INSERT 0 1
penjual | transaksi | total_item | total_nilai
---------+-----------+------------+-------------
Andi | 2 | 70 | 1050000.00
Budi | 3 | 38 | 2270000.00
Dewi | 3 | 23 | 1980000.00
Siti | 2 | 40 | 1475000.00
(4 rows)
REFRESH MATERIALIZED VIEW
penjual | transaksi | total_item | total_nilai
---------+-----------+------------+-------------
Andi | 2 | 70 | 1050000.00
Budi | 4 | 45 | 2865000.00
Dewi | 3 | 23 | 1980000.00
Siti | 2 | 40 | 1475000.00
(4 rows)Hasil eksekusinya memperlihatkan pola yang sudah dikenal: SELECT pertama masih menampilkan Budi 3 transaksi dan 38 item, lalu setelah REFRESH ... CONCURRENTLY angkanya menjadi 4 transaksi dan 45 item. Yang berbeda adalah jalannya — pembaca lain tetap bisa menampilkan materialized view selama proses ini berlangsung, dengan dua syarat yang melekat: hanya satu refresh yang boleh berjalan pada satu materialized view dalam satu waktu, dan refresh concurrent tidak bisa dipakai saat materialized view sedang kosong (kondisi WITH NO DATA di bawah). Refresh concurrent juga lebih lambat daripada refresh penuh karena kerja ekstra mencocokkan baris — bayar kenyamanan pembaca dengan siklus refresh yang sedikit lebih mahal.
WITH NO DATA — Struktur Saja, Isi Kosong #
Opsi WITH NO DATA memisahkan dua sisi materialized view: strukturnya (nama, kolom, tipe, index) tetap utuh, sementara isinya dibuang. Catalog mencatat keadaan itu lewat kolom relispopulated di pg_class — t berarti terisi dan siap dibaca, f berarti struktur ada tetapi isi kosong:
-- REFRESH ... WITH NO DATA membuang seluruh isi materialized view
-- dan hanya menyisakan strukturnya (kolom, tipe, index).
-- Kolom relispopulated di pg_class mencatat keadaan itu: 'f' berarti
-- isi sedang kosong dan belum bisa dibaca.
REFRESH MATERIALIZED VIEW ringkasan_penjual WITH NO DATA;
SELECT relname, relispopulated
FROM pg_class
WHERE relname IN ('penjualan', 'ringkasan_penjual')
ORDER BY relname;Output yang Dihasilkan:
REFRESH MATERIALIZED VIEW
relname | relispopulated
-------------------+----------------
penjualan | t
ringkasan_penjual | f
(2 rows)Tabel penjualan tetap t; materialized view berubah menjadi f — strukturnya masih ada, isinya tidak. Membaca materialized view dalam keadaan ini ditolak mentah-mentah:
-- Materialized view yang dibiarkan tanpa isi tidak bisa dibaca:
-- PostgreSQL menolak SELECT sampai refresh mengisinya kembali.
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;Output yang Dihasilkan:
psql:select-error.sql:5: ERROR: materialized view "ringkasan_penjual" has not been populated
HINT: Use the REFRESH MATERIALIZED VIEW command.Error materialized view "ringkasan_penjual" has not been populated beserta HINT-nya menegaskan satu-satunya jalan keluar: refresh yang mengisi. Pemulihannya persis seperti siklus pertama — REFRESH tanpa opsi apa pun — dan kolom relispopulated kembali ke t:
-- Pemulihan: REFRESH tanpa WITH NO DATA mengisi ulang materialized
-- view dari tabel dasar; relispopulated kembali 't' dan pembacaan
-- berjalan normal lagi dengan angka terbaru.
REFRESH MATERIALIZED VIEW ringkasan_penjual;
SELECT relname, relispopulated
FROM pg_class
WHERE relname IN ('penjualan', 'ringkasan_penjual')
ORDER BY relname;
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;Output yang Dihasilkan:
REFRESH MATERIALIZED VIEW
relname | relispopulated
-------------------+----------------
penjualan | t
ringkasan_penjual | t
(2 rows)
penjual | transaksi | total_item | total_nilai
---------+-----------+------------+-------------
Andi | 2 | 70 | 1050000.00
Budi | 4 | 45 | 2865000.00
Dewi | 3 | 23 | 1980000.00
Siti | 2 | 40 | 1475000.00
(4 rows)Memilih Strategi Refresh #
Ketiga jalur yang sudah diuji punya tempatnya masing-masing. Ringkasan praktisnya:
| Strategi | Penguncian pembaca | Syarat khusus | Cocok untuk |
|---|---|---|---|
REFRESH MATERIALIZED VIEW | diblokir sampai selesai | tidak ada | data kecil, jendela maintenance, data boleh kosong sejenak |
REFRESH ... CONCURRENTLY | tetap bisa membaca | index unik tanpa WHERE, view harus terisi | materialized view yang dibaca terus-menerus |
REFRESH ... WITH NO DATA | struktur tanpa isi | isi kosong, SELECT ditolak | persiapan pengisian ulang terjadwal |
Satu hal terakhir yang menentukan seberapa sering Anda perlu menjalankan semua ini: kebaruan data. Materialized view hanyalah salah satu alat di balik laporan — lapisan lain yang sama pentingnya menentukan siapa yang boleh melihat kolom dan baris mana di balik query yang sama. View biasa punya peran besar di sana, dan chapter View dan Keamanan Data membahasnya tuntas.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Bahan latihan chapter Refresh Materialized View:
-- tabel penjualan + materialized view yang sudah terisi.
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
penjual text NOT NULL,
produk text NOT NULL,
jumlah integer NOT NULL,
harga numeric(10,2) NOT NULL
);
INSERT INTO penjualan (id, tanggal, penjual, produk, jumlah, harga) VALUES
( 1, DATE '2026-01-05', 'Budi', 'Kopi Gayo', 10, 85000.00),
( 2, DATE '2026-01-08', 'Siti', 'Teh Melati', 25, 35000.00),
( 3, DATE '2026-01-12', 'Andi', 'Gula Merah', 40, 15000.00),
( 4, DATE '2026-01-15', 'Budi', 'Kopi Toraja', 8, 90000.00),
( 5, DATE '2026-01-20', 'Dewi', 'Kopi Gayo', 12, 85000.00),
( 6, DATE '2026-01-22', 'Siti', 'Teh Hijau', 15, 40000.00),
( 7, DATE '2026-01-25', 'Andi', 'Gula Merah', 30, 15000.00),
( 8, DATE '2026-01-28', 'Dewi', 'Kopi Gayo', 6, 85000.00),
( 9, DATE '2026-01-30', 'Budi', 'Teh Melati', 20, 35000.00);
CREATE MATERIALIZED VIEW ringkasan_penjual AS
SELECT penjual,
count(*) AS transaksi,
sum(jumlah) AS total_item,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY penjual;
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
isi-ulang.sql
Unduh berkas-- Pemulihan: REFRESH tanpa WITH NO DATA mengisi ulang materialized
-- view dari tabel dasar; relispopulated kembali 't' dan pembacaan
-- berjalan normal lagi dengan angka terbaru.
REFRESH MATERIALIZED VIEW ringkasan_penjual;
SELECT relname, relispopulated
FROM pg_class
WHERE relname IN ('penjualan', 'ringkasan_penjual')
ORDER BY relname;
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
refresh-concurrent-error.sql
Unduh berkas-- REFRESH ... CONCURRENTLY ditolak karena materialized view
-- belum memiliki index unik tanpa klausa WHERE.
REFRESH MATERIALIZED VIEW CONCURRENTLY ringkasan_penjual;
refresh-concurrent.sql
Unduh berkas-- Setelah index unik tersedia, refresh concurrent diterima.
-- Index unik menjadi penanda baris agar server bisa mencocokkan
-- isi lama dengan hasil query baru dan hanya mengubah bedanya.
CREATE UNIQUE INDEX idx_unik_penjual ON ringkasan_penjual (penjual);
-- Refresh concurrent pertama: data dasar belum berubah sejak
-- refresh penuh, jadi isi materialized view tetap sama.
REFRESH MATERIALIZED VIEW CONCURRENTLY ringkasan_penjual;
-- Siklus stale kedua: transaksi baru untuk Budi masuk...
INSERT INTO penjualan (id, tanggal, penjual, produk, jumlah, harga) VALUES
(11, DATE '2026-02-02', 'Budi', 'Kopi Gayo', 7, 85000.00);
-- ...materialized view masih stale (Budi 3 transaksi)...
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
-- ...refresh concurrent menyegarkannya tanpa mengunci pembaca...
REFRESH MATERIALIZED VIEW CONCURRENTLY ringkasan_penjual;
-- ...dan angka Budi menjadi 4 transaksi, 45 item.
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
select-error.sql
Unduh berkas-- Materialized view yang dibiarkan tanpa isi tidak bisa dibaca:
-- PostgreSQL menolak SELECT sampai refresh mengisinya kembali.
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
siklus-refresh.sql
Unduh berkas-- Siklus stale -> refresh -> segar dengan REFRESH penuh.
-- Tahap 1: transaksi baru masuk ke tabel dasar.
INSERT INTO penjualan (id, tanggal, penjual, produk, jumlah, harga) VALUES
(10, DATE '2026-01-31', 'Dewi', 'Kopi Toraja', 5, 90000.00);
-- Tahap 2: materialized view masih menampilkan salinan lama
-- (Dewi 2 transaksi, 18 item) -- data stale.
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
-- Tahap 3: REFRESH menjalankan ulang query definisi dan mengganti
-- seluruh isi materialized view dengan hasil terbaru.
REFRESH MATERIALIZED VIEW ringkasan_penjual;
-- Tahap 4: pembacaan berikutnya melihat angka baru
-- (Dewi kini 3 transaksi, 23 item).
SELECT penjual, transaksi, total_item, total_nilai
FROM ringkasan_penjual
ORDER BY penjual;
with-no-data.sql
Unduh berkas-- REFRESH ... WITH NO DATA membuang seluruh isi materialized view
-- dan hanya menyisakan strukturnya (kolom, tipe, index).
-- Kolom relispopulated di pg_class mencatat keadaan itu: 'f' berarti
-- isi sedang kosong dan belum bisa dibaca.
REFRESH MATERIALIZED VIEW ringkasan_penjual WITH NO DATA;
SELECT relname, relispopulated
FROM pg_class
WHERE relname IN ('penjualan', 'ringkasan_penjual')
ORDER BY relname;