Views, materialized views and derived data
Should this dashboard use a materialized view?
The idea in 30 seconds
A materialized view trades freshness and refresh work for fast reads. Before approving one, pin down how stale the numbers may be, what each refresh costs now and at ten times the data, whether a refresh blocks readers, and what happens when a refresh fails. The answers decide between a materialized view, a better index, or a summary table updated as data arrives.
Helps to know first: Aggregation and grouping, Joins and relational querying
On this page
The situation
The sales dashboard shows revenue by month and category. It runs the report on every page load and takes almost two seconds. An AI assistant opened a pull request to fix it.
Review taskCache the revenue report
Pull request: “Dashboard is slow. Precompute the report in a materialized view and refresh it every minute from cron.”
-- migration
CREATE MATERIALIZED VIEW monthly_revenue_mv AS
SELECT date_trunc('month', o.ordered_at) AS month, p.category,
sum(i.quantity * i.unit_price) AS revenue
FROM orders o
JOIN order_items i ON i.order_id = o.id
JOIN products p ON p.id = i.product_id
GROUP BY 1, 2;And the schedule:
# crontab
* * * * * psql -c "REFRESH MATERIALIZED VIEW monthly_revenue_mv"Would you approve it? Write the review you would leave.
Your call
Model review
Request changes. A materialized view fits this report well, but the refresh as written will make the dashboard freeze regularly and fail silently.
1. Every refresh blocks the dashboard. A plain refresh takes an ACCESS EXCLUSIVE lock, which blocks SELECT. On a copy with a million orders, the refresh took 1.65 seconds and a dashboard query arriving during it waited 1.4 seconds. With a refresh every minute, every user eventually hits that freeze. Use REFRESH MATERIALIZED VIEW CONCURRENTLY, which requires a unique index:
CREATE UNIQUE INDEX ON monthly_revenue_mv (month, category);With that, readers in our test waited 1 ms during a refresh.
2. Every minute, the whole report is recomputed. Each refresh runs the full query: about 1.65 seconds of database work per minute today, for 72 rows that mostly do not change. It grows with the order tables, not with how many orders changed. If a refresh ever takes longer than the interval, refreshes queue behind each other, because only one can run at a time against a materialized view.
3. Ask how fresh the dashboard needs to be. If finance reads it daily, refresh hourly or nightly. If it must be up to the minute, recomputing the whole year every minute is the wrong tool: keep a summary table that each new order adds to, so the work is proportional to new data, not all data.
4. A failed refresh is invisible. If the cron job fails (a lock timeout, a deploy, a full disk), the materialized view keeps serving the last good result, and nobody knows the numbers stopped moving. Log each refresh, alert when the last success is too old, and show “data as of …” on the dashboard.
Compare it with yours. Did your review catch the main problems?
How to decide
Prefer a plain view or an index first, unless the query is expensive even when written well. A view costs nothing to keep correct; check the query plan before adding a cache.
Prefer a materialized view when the result is small, expensive to compute, read far more often than the data changes, and allowed to be minutes or hours old.
Prefer a summary table updated as data arrives when the numbers must be close to live, or a full recompute would take longer than the freshness you need.
Whatever you choose, make freshness visible. Record when each refresh succeeded, alert when it is late, and show users the time the data is from.
Check yourself
What two changes would you make to the pull request’s refresh, at minimum?
Show answer
Add a unique index on
(month, category)and refreshCONCURRENTLY, so readers are not blocked; and record and alert on refresh success, so a broken refresh is noticed.Why does refreshing every minute get worse as the business grows, even if the report has only 72 rows?
Show answer
A refresh reruns the full query over all orders, so its cost grows with the order tables. Once it takes longer than the interval, refreshes queue up, because only one can run at a time.
When is a summary table a better choice than a materialized view?
Show answer
When the numbers must be nearly live, or a full recompute is too slow for the freshness required. A summary table updated as each order arrives does work proportional to the new data.
Finished reading?
Saved in this browser only.
Sources
Where each claim on this page comes from.
- 1.PostgreSQL documentation: REFRESH MATERIALIZED VIEWPrimary· Parameters: CONCURRENTLY; Notes
CONCURRENTLY needs a unique index and a populated view; only one refresh runs at a time against a materialized view.
- 2.PostgreSQL documentation: 13.3 Explicit LockingPrimary· §13.3.1 Table-Level Locks
A refresh without CONCURRENTLY takes ACCESS EXCLUSIVE, which blocks SELECT.
Go further
What to read next, and why it is worth the time.
- 1.PostgreSQL documentation: 39.3 Materialized ViewsPrimary· §39.3
A worked sales-summary example, including a unique index, and a clear note on when stale data is acceptable.
Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.