Skip to main content

Views, Materialized Views, and Stored Procedures

What This Concept Is​

Three server-side abstraction tools that all live inside the database.

  • View: a named query. CREATE VIEW v AS SELECT .... The view does not store data; every query against it re-runs the underlying SELECT. Views are query sugar; they hide joins, filters, and projections behind a name. Some engines allow updatable views when the query is simple enough to map row-changes back onto base tables.

  • Materialized view: a named query plus its last computed result. The result is stored like a table and is queried like a table. It goes stale when underlying data changes, and is refreshed explicitly (REFRESH MATERIALIZED VIEW v) or on schedule. Some engines support REFRESH CONCURRENTLY to avoid blocking readers.

  • Stored procedure / function: a named body of SQL plus procedural logic (PL/pgSQL, T-SQL, etc.), stored in the database and invoked by CALL or SELECT function(...). Can return scalars, rows, or result sets; can contain transactions, loops, exception handling, and updates.

Why It Matters Here (connection to primary)​

Concepts 10 and 11 taught you to protect and evolve your schema. This concept adds the tools you use to publish it: stable query surfaces for consumers and precomputation for hot reads.

  • views let you ship a clean API to application teams while freely refactoring the underlying tables
  • materialized views are the most structured way to denormalize (Concept 8): the redundancy is derived from a query, not a trigger, and can be rebuilt from scratch
  • stored procedures move critical integrity or auditing logic close to the data, useful when the same rule is enforced by several services

Concrete Example​

View:

CREATE VIEW active_subscription AS
SELECT s.customer_id, s.plan_code, p.monthly_cents, s.started_on
FROM subscription s
JOIN plan p ON p.code = s.plan_code
WHERE s.ended_on IS NULL;

SELECT * FROM active_subscription WHERE customer_id = 42;

If you later reshape subscription into subscription_event, you can keep active_subscription as the stable contract.

Materialized view (for an expensive dashboard):

CREATE MATERIALIZED VIEW daily_revenue AS
SELECT date_trunc('day', placed_at)::date AS day,
SUM(total_cents) / 100.0 AS revenue
FROM orders
GROUP BY 1;

-- Refresh nightly:
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_revenue;

Queries on daily_revenue return instantly; the tradeoff is staleness bounded by the refresh cadence.

Stored function (PostgreSQL PL/pgSQL):

CREATE OR REPLACE FUNCTION charge_customer(p_customer_id BIGINT, p_cents INT)
RETURNS BIGINT AS $$
DECLARE new_order_id BIGINT;
BEGIN
IF p_cents <= 0 THEN RAISE EXCEPTION 'amount must be positive'; END IF;
INSERT INTO orders(customer_id, total_cents, currency)
VALUES (p_customer_id, p_cents, 'USD')
RETURNING id INTO new_order_id;
RETURN new_order_id;
END;
$$ LANGUAGE plpgsql;

SELECT charge_customer(42, 1500); -- returns new order id

Common Confusion / Misconception​

"A view speeds things up." No. A plain view is the same query rewritten; it has no storage. Performance comes only from materialized views, indexes, or rewriting the query.

"Materialized views stay current." Only as of the last refresh. If you want up-to-the-second correctness, use a plain view. If you want low read latency and can tolerate minutes of staleness, materialize it.

"Stored procedures are always the right place for business logic." Procedures tie logic to a specific database and are harder to test and version than application code. Use them for data-integrity rules that must survive any caller, for performance-critical sequences (batched inserts), and for clean encapsulation of audit-logging triggers. Keep general business logic in the application where it is testable.

How To Use It​

  • Use views to stabilize query interfaces across schema refactors and to simplify repeated joins.
  • Use materialized views when a specific aggregation is read many times per write and staleness is acceptable; schedule a refresh and monitor its runtime.
  • Use stored procedures / triggers to enforce invariants that cross multiple tables, to maintain denormalized counters (Concept 8), and for auditing. Document each one; they are invisible to developers reading application code.
  • Never put the only copy of a piece of business logic in a trigger. Triggers should enforce or observe, not compute business intent.

Check Yourself​

  1. When does an updatable view stop being updatable?
  2. What is the difference between REFRESH MATERIALIZED VIEW v and REFRESH MATERIALIZED VIEW CONCURRENTLY v?
  3. Name one situation where a stored procedure is the right answer and one where it is the wrong answer.

Mini Drill or Application​

Using your subscription schema from Concept 10:

  1. Write a view active_subscription as shown.
  2. Write a materialized view monthly_mrr returning one row per month with total monthly revenue, and a refresh plan.
  3. Write a stored function cancel_subscription(p_customer_id BIGINT) that sets ended_on = now() for the customer's active subscription and raises an exception if no active subscription exists.

Read This Only If Stuck​