Articles

SQL Views vs Materialized Views: What's the Difference?

A SQL view is a saved query re-run on every read; a materialized view stores the result physically and needs refreshing. Here's when to use each.

The Lycoris Team The Lycoris Team · · 5 min read
Abstract illustration representing database tables

A view is a saved query that the database re-executes every time you select from it — it stores no data of its own, just the query definition. A materialized view stores the query’s result set physically, like a table, so reading from it doesn’t re-run the underlying query at all — until you refresh it, at which point it can go stale. The choice between them is really a choice between always-fresh-but-slower and fast-but-possibly-stale.

What a regular view is

A view wraps a query and gives it a name:

CREATE VIEW active_customers AS
SELECT id, name, email
FROM customers
WHERE status = 'active';

From then on, SELECT * FROM active_customers looks like querying a table, but under the hood the database substitutes the original query and runs it fresh — including any joins, filters, or aggregations — against the current data every single time. That means a view is always exactly as up to date as the underlying tables, with zero risk of staleness. It’s essentially a named, reusable abstraction over a query, useful for hiding complexity (a messy multi-table join becomes a simple SELECT) or restricting access (exposing only certain columns or rows to a role, without granting access to the base tables).

The cost is that a view has no independent performance benefit — if the underlying query is expensive, every read through the view pays that cost again. Indexing the view itself isn’t possible; only the base tables can be indexed, which is where B-tree, GIN, and GiST indexes actually do their work.

What a materialized view is

A materialized view runs the query once, at creation or refresh time, and physically stores the result:

CREATE MATERIALIZED VIEW active_customers_mv AS
SELECT id, name, email
FROM customers
WHERE status = 'active';

Reading from active_customers_mv reads stored rows directly — no re-execution of the underlying join or filter — so it can be dramatically faster for expensive queries, especially ones with heavy aggregation or multiple joins across large tables. Because the result is a real stored table, it can also be indexed independently of the base tables.

The tradeoff is staleness. The moment the underlying data changes, the materialized view’s stored rows no longer reflect it — until something explicitly refreshes it, which re-runs the query and rewrites the stored result. That refresh is itself a real operation with real cost, and depending on the database, it may briefly lock or block reads against the materialized view while it happens. For a deeper look at refresh mechanics and use cases, see our dedicated piece on what a materialized view is.

Comparison

ViewMaterialized view
StorageNone — just the query definitionPhysical, stored result set
FreshnessAlways currentCurrent as of last refresh
Read performanceSame as running the underlying queryFast — reads stored data directly
Can be indexedNo (only base tables can)Yes, independently
Write/refresh costNoneRefresh re-runs the query
Best forSimplifying or restricting access to always-fresh dataExpensive queries where some staleness is acceptable

Refresh strategies

How and when a materialized view refreshes is the core design decision. Options generally fall into a few patterns:

  • Manual refresh — triggered explicitly, useful for reporting views that only need to update on a known schedule.
  • Scheduled refresh — run on a timer (a nightly rebuild for a dashboard, for instance), trading freshness for predictable load.
  • Full vs incremental refresh — a full refresh reruns the entire query; an incremental refresh updates only the rows affected by recent changes, where supported, which is cheaper but more complex to reason about.

Some systems can keep a materialized view close to real time using change data capture to trigger incremental refreshes as source rows change, rather than relying on a periodic full rebuild.

Security and access control

Views have a second common use that has nothing to do with performance: restricting what a role or application can see. A view can expose only a subset of columns — hiding a salary or ssn column while still exposing the rest of an employees row — or only a subset of rows via its WHERE clause, letting a database grant access to the view without granting access to the underlying table at all. Because a view always reflects live data, this kind of access-control view never goes stale the way a materialized view would; the tradeoff of re-running the query on every read is usually a non-issue for this use case, since the point is controlled access, not raw throughput. Materialized views are rarely used this way, since their stored copy of the data would need its own separate access controls layered on top.

When to reach for which

Use a plain view when correctness matters more than raw read speed, when the underlying query is cheap, or when the goal is really about simplifying access or scoping permissions rather than performance. Reach for a materialized view when a query is expensive enough that re-running it on every read is the actual bottleneck — dashboards, reports, and aggregations over large tables are the classic case — and when the application can tolerate data being slightly behind the source of truth. If you need something between the two — cached results with fine-grained control over invalidation — that usually means building the caching logic at the application layer instead, as covered in our piece on database query optimizers and how they decide what work to do at read time.

The takeaway

A view is a saved query with no storage of its own, always fresh but never faster than the query it wraps. A materialized view stores its result physically, trading some staleness for real read-performance gains and the ability to index the result directly. Pick a view when freshness or access control is the goal; pick a materialized view when an expensive query is the bottleneck and a refresh cadence — manual, scheduled, or incremental — is an acceptable price for speed.

The Lycoris Team The Lycoris Team · · 6 min read

How to Read a Postgres EXPLAIN ANALYZE Query Plan

A step-by-step guide to running EXPLAIN ANALYZE in PostgreSQL and reading the query plan it returns — node types, costs, and where the real time went.

#Databases #SQL #Performance
The Lycoris Team The Lycoris Team · · 5 min read

What Is a Database Trigger?

A database trigger is a procedure that runs automatically on an insert, update, or delete — enforcing rules the application layer can't guarantee.

#Databases #SQL #Backend