Overview

A materialized view stores the result of a SELECT on disk like a table, while a regular view reruns its query on every access. Create one when a dashboard or report reruns the same expensive aggregation and can tolerate minutes of staleness. The data stays stale until REFRESH MATERIALIZED VIEW.

Rules

  • Schedule refreshes (cron, pg_cron, or a job) or run one after bulk loads.
  • REFRESH ... CONCURRENTLY keeps the view readable during the refresh. It needs an already populated view and at least one unique index on plain columns covering all rows (no WHERE, no expressions), allows only one refresh at a time, and can be slower than a blocking refresh when most rows change.
  • PostgreSQL has no built-in incremental refresh as of version 18: every refresh recomputes the whole query. The pg_ivm extension adds incremental maintenance.
  • Index the view like a table and confirm use with EXPLAIN.
CREATE MATERIALIZED VIEW monthly_revenue AS
  SELECT date_trunc('month', created_at) AS month, sum(amount) AS revenue
  FROM orders GROUP BY 1;
CREATE UNIQUE INDEX monthly_revenue_month_idx ON monthly_revenue (month);
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_revenue;