Views & Materialized Views
Definition
A view is a saved, named
SELECTquery that behaves like a virtual table - queried like any table, but always reflects live data, computed fresh on every access. A materialized view is similar but physically stores its result set on disk, requiring an explicit refresh to stay current.
Creating a View
CREATE VIEW active_users AS
SELECT id, name, email
FROM users
WHERE is_active = true;
SELECT * FROM active_users WHERE name LIKE 'A%'; -- query a view exactly like a tableViews run their underlying query every time
There’s no caching - a view is essentially a stored, reusable
SELECTstatement. Performance is identical to running the underlying query directly.
Why Use Views
Common reasons to reach for a view
- Simplify complex queries: hide a multi-join, multi-CTE query behind a simple
SELECT * FROM my_view- Security/access control: expose only specific columns/rows to certain users, hiding sensitive columns entirely (e.g. a view that excludes a
password_hashcolumn)- Backward compatibility: if a table’s structure changes, a view can preserve the old interface for existing application code
- Consistency: define business logic (e.g. “active user” =
is_active = true AND last_login > now() - interval '90 days') once, reuse everywhere
Updating Through a View
UPDATE active_users SET email = 'new@example.com' WHERE id = 1;Not all views are updatable
Simple views (single table, no aggregation, no
DISTINCT, noGROUP BY) are automatically updatable. Views involving joins, aggregates, orGROUP BYare NOT automatically updatable - use anINSTEAD OFtrigger (see 15-Triggers) or a rule to define custom update behavior for those.
Modifying & Dropping Views
CREATE OR REPLACE VIEW active_users AS
SELECT id, name, email, created_at
FROM users
WHERE is_active = true;
DROP VIEW active_users;
DROP VIEW IF EXISTS active_users CASCADE; -- also drops anything depending on this view
CREATE OR REPLACE VIEWcan't change column names/types/orderTo add, remove, or reorder columns, you must
DROP VIEWfirst, thenCREATE VIEWfresh - a straightREPLACEonly allows appending new columns at the end.
Materialized Views
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT date_trunc('month', order_date) AS month, SUM(amount) AS total
FROM orders
GROUP BY date_trunc('month', order_date);
SELECT * FROM monthly_sales; -- fast - reads pre-computed, stored dataRefreshing
REFRESH MATERIALIZED VIEW monthly_sales; -- locks the view during refresh, blocks reads
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales; -- allows reads during refresh, requires a unique index
CONCURRENTLYrequires a unique index on the materialized viewWithout at least one
UNIQUEindex defined on it,REFRESH ... CONCURRENTLYfails - add one withCREATE UNIQUE INDEX ON monthly_sales (month)before using this option.
View vs Materialized View
graph TD A["View"] --> A1["Always live data"] A --> A2["No storage cost"] A --> A3["Slower for expensive underlying queries (recomputed every time)"] B["Materialized View"] --> B1["Data as of last REFRESH (can go stale)"] B --> B2["Uses disk storage"] B --> B3["Fast reads (pre-computed)"]
| View | Materialized View | |
|---|---|---|
| Data freshness | Always current | As of last REFRESH |
| Read speed | Same as underlying query | Fast - pre-computed |
| Storage | None (just stores the query definition) | Physically stored on disk |
| Best for | Simplifying/securing everyday queries | Expensive aggregations/reports queried often, where slightly stale data is acceptable |
Scheduling refreshes
Materialized views don’t refresh automatically - schedule
REFRESH MATERIALIZED VIEWvia a cron job,pg_cronextension, or application-triggered job for dashboards/reports that can tolerate being minutes or hours old.
Inspecting Views
\dv -- list all views (psql meta-command)
\d+ active_users -- view definition and details
SELECT view_definition FROM information_schema.views WHERE table_name = 'active_users';