Skip to content
Views and Materialized Views
step 1/4

Reading — step 1 of 4

Learn

~3 min readViews, Triggers, Performance

A view is a saved query — a virtual table whose contents are computed on demand. Use them to encapsulate complex queries, restrict access, or provide a stable API over an evolving schema.

Basic view

CREATE VIEW active_users AS
SELECT id, name, email
FROM users
WHERE deleted_at IS NULL;

-- Use it like a table:
SELECT * FROM active_users WHERE email LIKE '%@example.com';

Under the hood, the DB inlines the view's definition — same as if you'd written the full query.

Drop and replace:

DROP VIEW IF EXISTS active_users;
CREATE OR REPLACE VIEW active_users AS ...   -- Postgres

Why use views

1. Encapsulation. A complex 3-table join becomes one name:

CREATE VIEW order_summary AS
SELECT u.name, o.id AS order_id, p.title, oi.quantity
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
JOIN users u ON u.id = o.user_id;

Queries using order_summary don't know (or care) about the underlying schema. Refactor without breaking consumers.

2. Security. Grant SELECT on a view that hides sensitive columns:

CREATE VIEW public_users AS SELECT id, name, country FROM users;  -- no email/SSN
GRANT SELECT ON public_users TO reporting_role;

3. Stable API. When the schema changes, update the view. Consumers don't need to update.

Updatable views

In Postgres and MySQL, simple views are automatically updatable:

UPDATE active_users SET name = 'Ada L.' WHERE id = 1;
-- Translates to UPDATE on the underlying table.

Views with joins, GROUP BY, or DISTINCT are NOT updatable. Use INSTEAD OF triggers (Postgres) for fancier behavior.

Materialized views

A materialized view stores the result. Faster reads, stale until refreshed:

-- Postgres syntax:
CREATE MATERIALIZED VIEW daily_stats AS
SELECT date, COUNT(*) AS events, SUM(amount) AS total
FROM events
GROUP BY date;

REFRESH MATERIALIZED VIEW daily_stats;     -- recompute
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_stats;  -- without blocking reads

Use for expensive aggregates that don't need to be live (yesterday's sales, monthly summaries).

SQLite doesn't have materialized views — work around with regular tables refreshed by triggers or app code.

Common patterns

Hide soft-deletes:

CREATE VIEW users AS SELECT * FROM users_all WHERE deleted_at IS NULL;

Double-naming is a smell — usually you put a users view over a users_table or use a column predicate.

Compatibility shim during a migration:

-- Old code references 'username'. New schema uses 'handle'.
CREATE VIEW old_users AS SELECT id, handle AS username, email FROM users;

Aggregation API:

CREATE VIEW daily_active_users AS
SELECT date, COUNT(DISTINCT user_id) AS dau
FROM logins
GROUP BY date;

When NOT to use

  • Performance-critical queries — views can hide costs. Use the raw query and EXPLAIN it.
  • Heavy joins — the optimizer doesn't always inline well. Test with EXPLAIN.
  • Trivial wrappers — "SELECT * FROM users WHERE id = ?" is not a view.

Views vs CTEs vs subqueries

  • View — persisted in schema, reusable, lives in DB metadata
  • CTE — defined per query, scoped to one statement
  • Subquery — inline, anonymous

Views for cross-query reuse. CTEs for within-query clarity. Subqueries for one-shot use.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…