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:

sql buat-tabel.sql
-- 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:

text
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:

text
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:

  1. INSERT ke tabel dasar — transaksi Dewi kelima masuk; perubahan ini sama sekali tidak menyentuh materialized view.
  2. SELECT pertama — pembacaan menampilkan salinan lama: Dewi masih 2 transaksi dan 18 item. Inilah keadaan stale yang dibahas chapter sebelumnya.
  3. REFRESH MATERIALIZED VIEW ringkasan_penjual — PostgreSQL membuka definisi tersimpan, menjalankan ulang agregasinya terhadap 10 baris terbaru, dan mengganti seluruh isi fisik materialized view. Tag output REFRESH MATERIALIZED VIEW menandai penyelesaian proses penggantian ini.
  4. SELECT kedua — 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:

text
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:

text
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:

text
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:

text
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:

sql isi-ulang.sql
-- 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:

text
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:

StrategiPenguncian pembacaSyarat khususCocok untuk
REFRESH MATERIALIZED VIEWdiblokir sampai selesaitidak adadata kecil, jendela maintenance, data boleh kosong sejenak
REFRESH ... CONCURRENTLYtetap bisa membacaindex unik tanpa WHERE, view harus terisimaterialized view yang dibaca terus-menerus
REFRESH ... WITH NO DATAstruktur tanpa isiisi kosong, SELECT ditolakpersiapan 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
sql
-- 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
sql
-- 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
sql
-- REFRESH ... CONCURRENTLY ditolak karena materialized view
-- belum memiliki index unik tanpa klausa WHERE.
REFRESH MATERIALIZED VIEW CONCURRENTLY ringkasan_penjual;

refresh-concurrent.sql

Unduh berkas
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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;