Views — Virtual Tables
Store a query as a named database object — create, replace, and drop views, build layered reporting abstractions, control access with views, and understand when materialized views beat regular ones
Quick answer
A view is a saved query with a name. Querying it runs the underlying query, so it always shows current data. A materialized view stores the result and must be refreshed.
- Views hide complex joins behind one name that reports can reuse.
- Granting access to a view, not its tables, can hide columns or rows.
- CREATE OR REPLACE VIEW updates a view without losing its permissions.
CREATE VIEW delivered_orders AS
SELECT order_id, customer_id, total_amount
FROM orders
WHERE order_status = 'Delivered';
SELECT COUNT(*) AS delivered, ROUND(SUM(total_amount), 2) AS revenue
FROM delivered_orders;Result · 1 row
| delivered | revenue |
|---|---|
| 23 | 1209.55 |
// Part 01
What a View Is
A view is a named query stored in the database schema. Once created, it can be queried exactly like a table — you write SELECT * FROM view_name and the database executes the underlying query transparently. No data is physically stored in the view itself (for regular views). The view is simply a saved query definition that the database substitutes at query time.
Views solve three problems simultaneously: they eliminate repeated complex queries by giving them a stable name, they create a security boundary by exposing only the columns and rows a user should see, and they build an abstraction layer that shields downstream consumers from schema changes in base tables.
// Part 02
The Three Types of Views
// Part 03
Creating Your First Views
A simple aggregation view
A customer summary view
// Part 04
Querying Views Like Tables
Once a view exists, every SQL construct works on it — WHERE, GROUP BY, ORDER BY, JOINs, subqueries, CTEs. The database executes the view's underlying query and treats the result as the input for any additional clauses you add.
// Part 05
Layered Views — Building Abstractions
Views can reference other views — creating layers of abstraction where each layer builds on the previous. The base layer joins raw tables. The mid layer computes metrics. The top layer provides the final business view. Changes to the base tables propagate automatically through all layers.
🎯 Pro Tip
Limit view layering to 3 levels maximum. Deep view chains (5+ levels) become hard to debug and optimise — when a query is slow, you have to trace through multiple view definitions to understand what the database is actually computing. Flatten or materialise deep chains where performance matters.
// Part 06
Views for Security — Column and Row Masking
Views are the standard SQL mechanism for column-level and row-level security. Instead of granting access to a base table directly, grant SELECT on a view that exposes only the columns and rows a user should see. The underlying sensitive columns and filtered-out rows are completely invisible.
Column masking — hide sensitive data
Row-level security — filter which rows are visible
// Part 07
Updating Views — CREATE OR REPLACE
View definitions change when business requirements change — a new column added to a base table should appear in the view, a filter condition needs updating, or a new JOIN is required. The safest way to update a view is CREATE OR REPLACE VIEW — it updates the definition atomically without dropping and recreating, preserving any permissions granted on the view.
Dropping views safely
// Part 08
Materialized Views — Stored Snapshots
A regular view re-executes its query every time it is queried — always fresh, but always paying the computation cost. A materialized view stores the result physically on disk. Queries against it read the stored result — instant, regardless of how expensive the underlying computation is. The tradeoff: the data becomes stale the moment the base tables change, and must be explicitly refreshed.
When to materialise vs when to use a regular view
// Part 09
Updatable Views — DML Through a View
Simple views — those that select directly from one base table with no aggregation, DISTINCT, LIMIT, or complex expressions — are updatable. INSERT, UPDATE, and DELETE on the view pass through to the base table transparently. More complex views require INSTEAD OF triggers to handle DML.
Conditions that make a view non-updatable
| View contains | Updatable? | Why |
|---|---|---|
| Single base table, no aggregation | ✅ Yes | DML maps directly to the base table |
| GROUP BY or aggregate functions | ❌ No | Cannot map DML to individual base rows |
| DISTINCT | ❌ No | Deduplication prevents row-level DML mapping |
| UNION / UNION ALL | ❌ No | Multiple sources — ambiguous which table to modify |
| Subquery in SELECT list | ❌ No | Computed columns have no base column to update |
| JOIN of multiple tables | ❌ No in most DBs | PostgreSQL allows DML on one side with INSTEAD OF triggers |
| LIMIT / OFFSET | ❌ No | Cannot identify which base row to modify |
| Window functions | ❌ No | Computed from multiple rows — no single base row |
// Part 10
Views vs CTEs vs Derived Tables — When to Use Each
Views, CTEs, and derived tables all create named intermediate result sets. Choosing between them depends on scope, reuse, and persistence requirements.
| View | CTE (WITH) | Derived Table | |
|---|---|---|---|
| Scope | Database-wide — any query in any session | Current query only | Current query only, single use |
| Persistence | Stored in schema permanently | Temporary — gone after query ends | Temporary — gone after query ends |
| Reusable across sessions | ✅ Yes | ❌ No | ❌ No |
| Reusable within one query | ✅ Yes | ✅ Yes | ❌ No (once inline) |
| Can be indexed | Materialized views only | ❌ No | ❌ No |
| Access control | ✅ GRANT/REVOKE on view | ❌ No | ❌ No |
| Version-controlled | Via DDL migration scripts | Embedded in query | Embedded in query |
| Best for | Shared reporting logic, access control, API contract | Complex multi-step single query | Short single-use pre-aggregation |
🎯 Pro Tip
The decision rule: if the same query logic is needed in more than one separate query or by more than one person or application — create a view. If the logic is needed only within a single complex query — use a CTE. If it is a short one-time pre-aggregation — use a derived table inline. Views are team assets; CTEs and derived tables are query-private tools.
// Part 11
What This Looks Like at Work
You are a senior data engineer at Instacart. The analytics team runs 15 different reports — all of which need the same "delivered order with product details" JOIN across four tables. Currently every analyst copies and pastes the same 20-line JOIN query into their reports. When the schema changes (a new column, a renamed table), every one of those 15 reports breaks and needs updating manually.
🎯 Pro Tip
Version-control your view definitions just like application code. Store every CREATE OR REPLACE VIEW statement in a migrations file. When a view definition changes, the migration captures the before and after states. This makes it possible to roll back a view change if it breaks downstream reports, and gives you a full audit trail of how the view has evolved over time.
// Part 12
Interview Prep — 5 Questions With Complete Answers
A view is a named query stored in the database schema. When queried, the database executes the view's underlying query and presents the result as if it were a table. No data is physically stored in a regular view — it is purely a saved query definition. The view is transparent to the query engine: SELECT * FROM vw_store_revenue is exactly equivalent to embedding the view's full query inline.
Three primary use cases. First, abstraction and simplification: complex JOIN logic spanning multiple tables is defined once in a view and referenced by name in all downstream queries. When the schema changes, only the view definition needs updating — all queries that reference it automatically benefit. This is the most common production use of views in data engineering: a team of analysts queries clean, pre-joined views rather than raw base tables.
Second, security: views enforce column-level and row-level access control. A view can expose only a subset of columns (hiding salary, national ID, or full email) and only a subset of rows (filtering to rows belonging to a specific region or store). Granting SELECT on the view and revoking SELECT on the base table means users can only access what the view exposes. Third, API stability: when an application queries a database, the view is the stable API contract. The underlying table structure can change — columns renamed, tables split — without breaking the application, as long as the view is updated to maintain the same output column names and types.
A regular view stores only the query definition — no data is physically stored. Every time the view is queried, the database executes the underlying query against the current base table data and returns the result. The data is always current because it is computed on demand. The cost is that every query against the view pays the full computation cost of the underlying query.
A materialized view stores the result of the query physically on disk — it is a snapshot of the data at the time the view was last refreshed. Queries against a materialized view read the stored result, not the base tables — they are as fast as querying a regular indexed table. The cost is staleness: the stored result becomes out of date the moment the base tables change. Refresh must be triggered manually (REFRESH MATERIALIZED VIEW) or scheduled (via pg_cron or an external scheduler) to update the stored data.
Choose regular view when data freshness is required — financial transactions, real-time inventory, live order status. Choose materialized view when the underlying query is expensive (multi-table aggregation, complex analytics) and is queried frequently, and when slightly stale data is acceptable. A monthly revenue report that is refreshed nightly is a perfect materialized view candidate — the computation runs once at midnight, and the next day's 10,000 dashboard queries each read the stored result in milliseconds instead of recomputing a 5-second aggregation.
Yes, for simple views. A view is updatable when it meets these conditions: it selects from exactly one base table, it contains no GROUP BY or aggregate functions, no DISTINCT, no UNION, no LIMIT, no subqueries in the SELECT list, and no window functions. For updatable views, DML operations on the view are transparently passed through to the underlying base table — UPDATE vw_active_customers SET city = 'New York' WHERE customer_id = 5 executes as UPDATE customers SET city = 'New York' WHERE customer_id = 5.
WITH CHECK OPTION is an important addition to updatable views that have a WHERE filter. Without it, a user could INSERT a row through a view that does not satisfy the view's WHERE condition — the row would be inserted into the base table but immediately invisible through the view. WITH CHECK OPTION prevents this: any INSERT or UPDATE through the view must produce a row that satisfies the view's WHERE filter, or the operation fails with an error.
For complex views (those with JOINs, aggregations, or other features that make automatic DML mapping impossible), PostgreSQL supports INSTEAD OF triggers — trigger functions that intercept DML on the view and execute custom logic to update the appropriate base tables. This allows any view to appear updatable, but requires explicit implementation of the DML logic. In practice, most views that need to be updatable are deliberately kept simple to avoid the complexity of INSTEAD OF triggers.
Views implement two types of access control. Column-level security: a view can SELECT only a subset of columns from the base table, omitting sensitive columns entirely. CREATE VIEW vw_customers_public AS SELECT customer_id, first_name, city FROM customers — no email, no phone, no date of birth. When the support team is granted SELECT on vw_customers_public and has their SELECT permission on the base customers table revoked, they can query customer data but never see sensitive fields. The base table's sensitive columns are completely invisible to them.
Row-level security: a view can include a WHERE clause that limits which rows are visible. CREATE VIEW vw_my_region_orders AS SELECT * FROM orders WHERE region = current_setting('app.user_region') — each session that sets their region variable sees only their region's orders. This is how multi-tenant applications implement data isolation in PostgreSQL before row-level security policies (RLS) were available, and it remains a simple, portable approach.
The security model works by granting USAGE on the schema and SELECT on the view, while explicitly REVOKE-ing SELECT on the base tables. PostgreSQL also supports native Row Level Security (RLS) policies which are more flexible — they attach security rules directly to tables and enforce them automatically regardless of whether the user queries the table directly or through a view. For column masking, views remain the most portable approach across all SQL databases. For row filtering, both views and RLS policies are valid — RLS is preferred for complex multi-role scenarios because it cannot be bypassed by a direct table query as a view-only approach can be (if someone is accidentally granted direct table access).
The behaviour depends on the nature of the change. For additive changes (adding a new column to the base table), the view is unaffected — it continues to select its defined columns and the new column simply does not appear in the view unless the view definition is updated to include it. For destructive changes (removing or renaming a column that the view references), the view becomes invalid — querying it will fail with an error like "column does not exist" or "view definition is no longer valid."
PostgreSQL checks view validity lazily — the error appears when the view is queried, not when the underlying table is altered. Some databases (SQL Server, Oracle) track dependencies and can warn or prevent dropping a column that is referenced by a view. In PostgreSQL, you can check for stale views after a schema change by running SELECT definition FROM pg_views and parsing the definitions, or by attempting to query each view in a test environment.
This is exactly why views are valuable as an API stability layer. If you change the base table (split one large table into two, rename a column, add a column that should be computed differently) you update the view definition to maintain the same output structure. Downstream consumers — applications, reports, other views — never know the schema changed. The view absorbs the change. The migration process is: make the base table change, update the view definition (CREATE OR REPLACE VIEW or DROP + CREATE), verify the view still returns the expected output, and the downstream systems are unaffected. Without a view layer, every query that references the renamed column would need to be updated individually.
// Part 13
Errors You Will Hit — And Exactly Why They Happen
🎯 Key Takeaways
- ✓A view is a named query stored in the database schema. Querying it executes the underlying query transparently — no data is stored in a regular view, only the query definition.
- ✓Three types: regular view (always fresh, no data stored), materialized view (data stored on disk, must be refreshed, fast to query), updatable view (simple views that allow INSERT/UPDATE/DELETE through to the base table).
- ✓Views eliminate repeated complex JOIN logic. Define the join once in a view; every report queries the view by name. Schema changes update the view definition once — all downstream queries benefit.
- ✓Views enforce access control. Grant SELECT on the view, revoke SELECT on the base table. Column-level security: omit sensitive columns. Row-level security: add a WHERE clause to filter rows.
- ✓CREATE OR REPLACE VIEW updates a view definition atomically without dropping it — preserves all granted permissions. Cannot remove or reorder existing columns without DROP + CREATE.
- ✓DROP VIEW CASCADE removes the view and all dependent views. Without CASCADE, dropping a view referenced by other views raises an error.
- ✓Materialized views store query results physically. Use them when the underlying query is expensive and queried frequently and slightly stale data is acceptable. REFRESH MATERIALIZED VIEW updates the stored result.
- ✓WITH CHECK OPTION on an updatable view prevents INSERT or UPDATE from creating rows that would not be visible through the view's WHERE filter.
- ✓Views vs CTEs vs derived tables: views are database-wide and persistent — team assets. CTEs are query-scoped — single-query tools. If logic is shared across queries or sessions, create a view.
- ✓Version-control view definitions in migration scripts alongside table DDL. A view is part of the schema contract — treat changes to view definitions with the same rigour as table alterations.
What comes next
In Lesson 46, you learn Indexes — how the database finds rows without scanning every row, when to create them, which type to choose, and how to diagnose slow queries with EXPLAIN ANALYZE.
Lesson 46: Indexes→ IndexesQuick check
Does a regular view store a copy of the data?
HELP US IMPROVE
How was this lesson?
Your note is shared with the Chaduvuko team for review. Please don’t include personal or sensitive information.
Spotted a mistake or something unclear? Suggest a fix on GitHub or email us. Select the text first and it is quoted for you.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.
