Views in SQL

Imagine a payroll analyst who needs to see, every single day, each department's average salary and headcount. But this analyst must never see individual employee salaries. Writing the same five-line aggregate query over and over is error-prone. Giving raw table access would also violate confidentiality. SQL solves this with a view: a named, stored query that behaves like a table. It computes its rows on demand from underlying data. The analyst queries `DeptSummary` as if it were a real table. The database quietly rewrites that query in terms of the actual `Employee` table. Views matter because real database schemas are rarely designed for the convenience of every application. A schema optimized for transactional correctness is normalized and sprawling across a dozen tables. Such a schema is often hostile to reporting, dashboards, and access control. Views let a database administrator present a simplified, restricted, or reorganized external view of the data. This is done without duplicating storage or breaking the underlying schema. This is precisely the "external level" of the classic three-schema architecture: physical storage, logical schema, and the view layer that different users see. This article explains how views are defined and expanded during query processing. It covers how the optimizer treats them. It explains when a view can be materialized (physically stored and incrementally maintained) rather than recomputed on every query. It also covers the subtle rules governing when a view is updatable. This means knowing when writes through the view can be unambiguously translated back into writes on the base tables. It builds on Aggregation and the GROUP BY Clause and Set Operations in SQL, since view bodies routinely use both. It also sets up the reasoning needed for Triggers and Stored Procedures and the Query Processing Overview of how declarative SQL becomes an execution plan. By the end, you should be able to define views correctly. You should be able to predict exactly when a view can accept `INSERT`/`UPDATE`/`DELETE`. You should be able to reason about the performance tradeoff between virtual and materialized views. You should also be able to avoid the classic pitfalls. These include the security hole created by naive view updates, or the staleness bugs of poorly maintained materialized views.

Key takeaways
  • A view is a named query; querying it is equivalent to substituting its definition wherever it is referenced — this substitution is called view expansion.
  • Virtual views store no data and are recomputed at query time; materialized views store results physically and must be refreshed as base data changes.
  • A view is updatable only if every row of the view maps back to exactly one row of exactly one base table, unambiguously, and the update makes sense there.
  • `WITH CHECK OPTION` prevents a classic bug: updates through a view silently making rows disappear from that view.
  • Materialized view maintenance can be incremental (compute a delta) or full (recompute from scratch); incremental maintenance is the key efficiency lever.
  • Views underlie schema evolution, fine-grained access control, query simplification, and are the conceptual seed for query rewriting and materialized-view-based optimization in modern query processors.
The core insight

A view is nothing more (and nothing less) than a stored query name — it has no independent existence until you either expand it symbolically at query time or materialize it physically as data.

The idea

A view is a virtual table defined by a SQL query over one or more base tables (or other views). Syntactically: ```sql CREATE VIEW DeptSummary AS SELECT dept_id, AVG(salary) AS avg_salary, COUNT() AS headcount FROM Employee GROUP BY dept_id; ``` Once created, `DeptSummary` can appear in `FROM` clauses, joins, and subqueries exactly like a table: `SELECT FROM DeptSummary WHERE headcount > 10`. Crucially, the database does not compute and store `DeptSummary` at creation time (unless it is explicitly a materialized view). Instead, the view's defining query is retained as metadata in the system catalog. When a query references the view, the query processor performs view expansion: it textually (or, more precisely, at the level of the internal query representation — the relational-algebra or parse-tree form) substitutes the view's defining query for every occurrence of the view name, then optimizes the resulting composite query as a whole. This distinguishes two categories sharply. A virtual view (the default in standard SQL, sometimes just called "a view") has zero storage cost and is always perfectly up to date, because it is recomputed — conceptually, though not always literally — every time it is queried. A materialized view (`CREATE MATERIALIZED VIEW` in PostgreSQL/Oracle, or an "indexed view" in SQL Server) is a view whose result is physically stored, like a table, complete with its own indexes. Materialized views trade storage and maintenance overhead for query speed: reading a materialized view is a simple table scan/index lookup, whereas reading a virtual view means re-executing potentially expensive joins and aggregations every time. A third concept, updatability, asks: if a user issues `UPDATE DeptSummary SET avg_salary = 50000 WHERE dept_id = 3`, does that even make sense? Updating a view means updating data that is derived, so the DBMS must be able to translate the requested change into an unambiguous change on the base table(s). Some views (simple projections/selections of a single table, retaining the primary key) support this cleanly; views involving aggregation, `DISTINCT`, joins, or set operations generally do not, because the mapping from view rows back to base rows is either lossy, many-to-one, or undefined.

How it works

View expansion is the mechanism that makes virtual views work, and it is best understood as a rewriting step performed before (or during) query optimization. Given a query `Q` that references view `V`, and `V`'s stored definition `def(V)`, the processor: 1. Parses `Q` into an internal representation (typically a relational-algebra expression tree or an annotated parse tree). 2. For every leaf node in that tree that is a reference to view `V`, replaces the leaf with the (also parsed) expression tree of `def(V)`, renaming attributes/aliases as needed to avoid collisions. 3. Repeats this recursively if `def(V)` itself references other views (views can be nested/stacked). 4. Hands the fully expanded, view-free expression tree to the query optimizer, which treats it as an ordinary query over base tables — free to reorder joins, push down selections, choose indexes, etc., across the former view boundary. This last point matters: expansion happens before optimization, not after, precisely so the optimizer isn't blocked from optimizing across view boundaries. If a user writes `SELECT FROM DeptSummary WHERE dept_id = 3`, naive execution would compute the aggregate over all* departments and then filter — wasteful. But after expansion, the optimizer sees a single query with both the `GROUP BY` and the `WHERE dept_id = 3` predicate, and (subject to correctness of predicate pushdown past aggregation, which holds when the predicate is on a grouping column) can push the selection below the aggregation, filtering rows before grouping.

View updates work by an analogous but stricter mechanism: the processor must translate an `INSERT`/`UPDATE`/`DELETE` on the view into one on the base table(s). SQL standard and most systems apply a syntactic test for whether a view is updatable, roughly: ``` A view V, defined as SELECT ... FROM T [WHERE ...], is UPDATABLE if: 1. V's FROM clause references exactly one updatable table or view (no joins). 2. V's SELECT list contains no aggregate functions, no GROUP BY, no HAVING. 3. V's SELECT list contains no DISTINCT. 4. V's SELECT list contains no set operations (UNION, INTERSECT, EXCEPT), unless it is UNION ALL of updatable views on disjoint conditions (implementation-dependent). 5. Every column of T that lacks a default value and does not allow NULL is either exposed in V's SELECT list, or is not required for the specific DML operation (e.g., not required for UPDATE of other columns). 6. Any subquery in the WHERE clause does not reference the same table T that is being updated (to avoid re-entrant ambiguity), depending on implementation. ``` If these hold, the DBMS rewrites `UPDATE V SET c = expr WHERE p` into `UPDATE T SET c = expr WHERE p AND view_condition`, where `view_condition` is `V`'s own `WHERE` clause translated to reference `T`. If they fail — say `V` joins two tables, or aggregates — the view is not updatable, and any DML against it raises an error (or, in some systems, must be redirected through an `INSTEAD OF` trigger that the DBA writes by hand, explicitly encoding the intended semantics). A second refinement is `WITH CHECK OPTION`: ```sql CREATE VIEW ActiveEmployee AS SELECT FROM Employee WHERE status = 'ACTIVE' WITH CHECK OPTION; ``` Without `CHECK OPTION`, a user could `UPDATE ActiveEmployee SET status = 'RETIRED' WHERE emp_id = 42`; this update succeeds on the base table, but the resulting row no longer satisfies* the view's `WHERE` clause and vanishes from `ActiveEmployee` immediately after being modified through it — a classic surprise. `WITH CHECK OPTION` makes the DBMS reject any insert/update that would produce a row failing the view's own predicate, closing this hole.

Materialized view maintenance is a distinct algorithmic problem: once `MV` is materialized, how does the system keep it consistent as base tables change? Two families of strategy exist:

  • Full recomputation: drop and rebuild `MV` from scratch by re-running its defining query. Simple, always correct, but O(size of the full re-evaluation)O(\text{size of the full re-evaluation}) — potentially very expensive for large base tables, and typically done on a schedule (nightly `REFRESH MATERIALIZED VIEW`) rather than on every write.
  • Incremental view maintenance (IVM): given a delta ΔT\Delta T (the set of inserted/deleted/updated rows) applied to base table TT, compute a corresponding delta ΔMV\Delta MV to apply to the materialized view, without re-scanning all of TT. For example, if MV=σp(T)MV = \sigma_p(T) (a selection), then ΔMV=σp(ΔT)\Delta MV = \sigma_p(\Delta T) — trivially incremental. For joins, Δ(R⋈S)=(ΔR⋈S)∪(R⋈ΔS)∪(ΔR⋈ΔS)\Delta(R \bowtie S) = (\Delta R \bowtie S) \cup (R \bowtie \Delta S) \cup (\Delta R \bowtie \Delta S), letting the system touch only the changed rows and their join partners, not the entire relations. For aggregates like `SUM`/`COUNT`, incremental maintenance keeps per-group running totals and adjusts them by the delta's contribution — but `AVG` requires maintaining both `SUM` and `COUNT` internally to update correctly, and `MIN`/`MAX` are the hard case: removing the current minimum forces a re-scan of that group unless auxiliary structures (like a sorted index) are kept.

```pseudocode // Full re-evaluation refresh function REFRESH_FULL(MV): result = EVALUATE(def(MV)) // execute the defining query fresh MV.storage = result MV.indexes = REBUILD_INDEXES(result) // Incremental view maintenance for MV = R JOIN S function REFRESH_INCREMENTAL_JOIN(MV, deltaR_insert, deltaR_delete, deltaS_insert, deltaS_delete): // Apply deletions first, then insertions, to avoid double counting if deltaR_delete is not empty: MV.remove( JOIN(deltaR_delete, S) ) if deltaS_delete is not empty: MV.remove( JOIN(R, deltaS_delete) ) if deltaR_insert is not empty: MV.add( JOIN(deltaR_insert, S) ) if deltaS_insert is not empty: MV.add( JOIN(R, deltaS_insert) ) // Note: JOIN(deltaR_insert, deltaS_insert) is naturally included because // by the time deltaS_insert is applied, S already reflects deltaR's effect // if updates are interleaved correctly; production systems order this // carefully to avoid missing or double-counting the delta-delta term. // Incremental maintenance for MV = SELECT g, SUM(x), COUNT(*) FROM T GROUP BY g function REFRESH_INCREMENTAL_AGG(MV, deltaT_insert, deltaT_delete): for each row r in deltaT_delete: group = MV.lookup(r.g) group.sum -= r.x group.count -= 1 if group.count == 0: MV.remove(group) // group vanished entirely for each row r in deltaT_insert: group = MV.lookup(r.g) if group does not exist: MV.insert(g = r.g, sum = r.x, count = 1) else: group.sum += r.x group.count += 1 ``` The engine chooses which strategy per view (and sometimes hybridizes: incremental for small deltas, periodic full refresh to bound drift/fragmentation), and this choice is itself informed by the query-processing machinery covered in the Query Processing Overview.

Figure
From SQL text to expanded query plan, and the materialization fork
User query SELECT * FROM DeptSummary WHERE headcount > 10 System catalog DeptSummary → GROUP BY query over Employee view expansion Expanded (view-free) algebra tree σ (headcount>10) γ dept_id; AVG,COUNT Employee optimizer may push σ below γ Virtual view path re-execute plan every query always fresh, no extra storage cost ≈ cost(base query) Materialized view path stored + indexed like a table read cost ≈ O(index lookup) needs refresh: full or incremental

Complexity & correctness

View expansion cost. Expansion itself is cheap: it is a tree substitution operation, O(n)O(n) in the size of the query's internal representation (number of nodes), because each reference to a view is replaced once, and nesting depth is bounded by the number of distinct views in the schema (cyclic view definitions are disallowed by the catalog, so expansion always terminates). The real cost is whatever the optimizer subsequently does with the larger expanded tree — join-order search, which is the exponential/NP-hard part of query optimization discussed under Query Processing Overview — but that cost is identical to writing the expanded query by hand; views add no asymptotic overhead of their own beyond the linear substitution.

Correctness of expansion. The substitution is correct by a simple equational argument: a view is defined as an equation, V≡def(V)V \equiv \text{def}(V), over the relational algebra. Relational algebra respects referential transparency — replacing any subexpression by an equal one yields an equal result — so substituting def(V)\text{def}(V) for every occurrence of VV produces an expression provably equivalent to the original query, for any database instance. This is why the optimizer is free to push predicates, reorder joins, and apply any algebraic rewrite rule across the former view boundary: the boundary was never semantically meaningful, only syntactically convenient. This is also why nested/stacked views (a view defined over other views) work correctly by simple induction on nesting depth: expand the innermost view first, then treat the result as an ordinary base-table reference for the next level.

Correctness of updatable-view translation. The syntactic criteria for updatability exist precisely to guarantee a unique, well-defined inverse mapping from a view row to a base row. Consider why each restriction is necessary:

  • No joins in an updatable view: if `V = R ⋈ S`, then inserting a new tuple into `V` would require simultaneously inventing consistent new tuples for both `R` and `S` — there is no canonical way to split one row into two without extra information the system doesn't have. (Some systems relax this for very restricted join shapes, like a view over a single "primary" table and a lookup table joined on a foreign key, but general joins are excluded.)
  • No aggregation: `AVG(salary) = 50000` for a department is consistent with infinitely many underlying salary distributions; `UPDATE DeptSummary SET avg_salary = 50000` has no unique inverse image on `Employee`. Aggregation is inherently lossy (many base states map to one view state), which is exactly what makes it useful for reporting and exactly what makes it non-invertible.
  • No `DISTINCT`: duplicate elimination is lossy in the same sense — if two identical rows collapse to one in the view, deleting "that" view row is ambiguous (delete one underlying copy? both?).
  • `WHERE` clause becomes part of the translated predicate, and `CHECK OPTION` closes the "disappearing row" loophole: without it, the system permits an update that is locally valid on the base table but violates the implicit contract "this view only shows rows satisfying pp" — a correctness property of the view's interface, not the base data.

Formally, updatability corresponds to whether the view's defining query is invertible as a partial function on database instances restricted to the relevant tuple: does there exist a well-defined function mapping a modification of VV's extension to a unique modification of TT's extension that induces exactly that change (and no other) in VV? Selections and simple projections that retain a key are invertible in this sense; aggregation, `DISTINCT`, and general joins are not, and the SQL standard's syntactic rules are a conservative (sound but not complete) approximation of this semantic property — they never permit an unsafe update, but they do sometimes forbid updates that a human could argue are "obviously fine" in special cases, trading completeness for a simple, checkable rule.

Materialized view maintenance complexity. Full recomputation costs exactly the cost of evaluating def(MV)\text{def}(MV) from scratch — call this C(def)C(\text{def}), dominated by the largest join/scan in the query, typically O(Nlog⁡N)O(N \log N) for a sort-merge join over NN rows or O(N)O(N) expected for a hash join. Incremental maintenance costs are proportional to the size of the delta, not the size of the base tables: for a selection, O(∣ΔT∣)O(|\Delta T|); for a join, O(∣ΔR∣⋅∣S∣+∣R∣⋅∣ΔS∣)O(|\Delta R| \cdot |S| + |R| \cdot |\Delta S|) in the naive nested-loop form, but typically O((∣ΔR∣+∣ΔS∣)log⁡(∣R∣+∣S∣))O((|\Delta R| + |\Delta S|) \log(|R|+|S|)) using indexes to look up join partners for just the delta rows — a substantial win when ∣ΔT∣≪∣T∣|\Delta T| \ll |T|, which is the common case in an OLTP system with a large stable table and a small daily batch of changes. The correctness of the incremental join formula

Δ(R⋈S)=(ΔR⋈S)∪(R⋈ΔS)∪(ΔR⋈ΔS)\Delta(R \bowtie S) = (\Delta R \bowtie S) \cup (R \bowtie \Delta S) \cup (\Delta R \bowtie \Delta S)

follows directly from bilinearity of the join operator over set union/difference, and production systems must apply deletions before insertions (as in the pseudocode above) to avoid transiently counting a row twice or dropping the delta-delta cross term — a genuine and well-known implementation subtlety.

Tradeoffs, stated honestly. Materialized views speed up reads at the cost of (a) storage, (b) write amplification (every base update may trigger maintenance work on every dependent materialized view), and (c) potential staleness if refresh is deferred/asynchronous — a materialized view is a form of denormalization and inherits denormalization's classic consistency-vs-performance tension. Virtual views have zero staleness and zero storage cost but zero speedup; they are purely a logical/security convenience, not a performance one, unless the optimizer's rewrites happen to produce a cheap plan anyway.

Worked example

Consider a simplified schema: ``` Employee(emp_id, name, dept_id, salary, status) Department(dept_id, dept_name, budget) ``` Sample data:

emp_idnamedept_idsalarystatus
1Ana1090000ACTIVE
2Ben1070000ACTIVE
3Cid2060000ACTIVE
4Dee2080000RETIRED
5Eli3055000ACTIVE

Define two views: ```sql CREATE VIEW ActiveEmployee AS SELECT emp_id, name, dept_id, salary FROM Employee WHERE status = 'ACTIVE' WITH CHECK OPTION; CREATE MATERIALIZED VIEW DeptSummary AS SELECT dept_id, AVG(salary) AS avg_salary, COUNT(*) AS headcount FROM Employee GROUP BY dept_id; ```

Step 1 — updatability check on `ActiveEmployee`. Single base table, no aggregation, no `DISTINCT`, no joins, primary key `emp_id` retained → updatable. A user runs: ```sql UPDATE ActiveEmployee SET salary = 95000 WHERE emp_id = 1; ``` This translates to `UPDATE Employee SET salary = 95000 WHERE emp_id = 1 AND status = 'ACTIVE'`, which succeeds (Ana is active). Now the user tries: ```sql UPDATE ActiveEmployee SET status = 'RETIRED' WHERE emp_id = 2; ``` Without `CHECK OPTION`, this would silently succeed on `Employee` and Ben would vanish from `ActiveEmployee`. With `CHECK OPTION`, the DBMS evaluates whether the post-update row satisfies `status = 'ACTIVE'` — it doesn't — and rejects the statement with an error, exactly as designed.

Step 2 — expansion for a query. ```sql SELECT name FROM ActiveEmployee WHERE dept_id = 10; ``` Expands to: ```sql SELECT name FROM Employee WHERE status = 'ACTIVE' AND dept_id = 10; ``` Result: `{Ana, Ben}` (Ana and Ben are both active, dept 10). Cid, Dee, Eli are excluded by either the department filter or the status filter — correctly, since the view's `WHERE` and the query's `WHERE` are conjoined by expansion, exactly as the algebra predicts.

Step 3 — materialized view initial state. `DeptSummary` (materialized, over all employees regardless of status, per its own definition):

dept_idavg_salaryheadcount
10800002
20700002
30550001

Step 4 — incremental maintenance on insert. A new employee is hired: ```sql INSERT INTO Employee VALUES (6, 'Fay', 10, 100000, 'ACTIVE'); ``` The delta is ΔTinsert={(6,Fay,10,100000,ACTIVE)}\Delta T_{\text{insert}} = \{(6,\text{Fay},10,100000,\text{ACTIVE})\}. Running `REFRESH_INCREMENTAL_AGG`:

  • Group `dept_id = 10` exists: `sum` was 90000+70000=16000090000+70000=160000, becomes 160000+100000=260000160000+100000=260000; `count` was 2, becomes 3.
  • New `avg_salary = 260000/3 ≈ 86666.67`.

Updated materialized view, touching only one row, without rescanning departments 20 or 30:

dept_idavg_salaryheadcount
1086666.673
20700002
30550001

This demonstrates the efficiency claim concretely: the refresh touched O(1)O(1) existing group plus the one new row, rather than re-scanning all six employee rows and recomputing every group from scratch — the payoff of incremental maintenance grows with table size while the delta stays small.

Common pitfalls & traps

PitfallWhy it happensConsequence
Assuming any view is updatableForgetting the join/aggregate/`DISTINCT` restrictions`UPDATE`/`INSERT` statement rejected at runtime, or (in permissive systems) silently ambiguous behavior
Omitting `WITH CHECK OPTION` on a filtering viewNot realizing an update can move a row outside the view's own predicateRows "disappear" from the view immediately after being updated through it — a data-integrity surprise, not a bug in the DBMS
Treating a materialized view as always freshConfusing virtual views (always current) with materialized views (only as current as the last refresh)Stale reads, especially dangerous if the materialized view feeds a security/authorization decision
Deep view stacking with expensive `SELECT *` at every layerConvenience of building views on views without checking the fully expanded planOptimizer may still handle it, but debugging performance requires reading the expanded plan, not the surface SQL
Forgetting that view definitions freeze column names/types, not underlying schema changesAdding/dropping/renaming a base-table column after the view exists`CREATE VIEW ... AS SELECT ` views can break or silently return stale column sets depending on system (`SELECT ` is expanded once, at creation, in most systems)
Applying deletions and insertions out of order during incremental maintenanceNaively coding delta application without orderingDouble counting or dropped delta-delta join terms, producing a materialized view inconsistent with a full recompute

Pioneers & history

The view concept traces directly to the foundational work on the relational model and the three-schema architecture (external, conceptual, internal), which formally separates what an application sees from how data is physically stored — views are the primary mechanism implementing the external/logical boundary in every SQL system since System R.

Origins · 1 minEdgar F. Codd and how this was discovered
EF
Edgar F. Codd
1923–2003 · IBM San Jose Research Laboratory, inventor of the relational model

Codd's 1970 paper "A Relational Model of Data for Large Shared Data Banks" introduced relations, normalization, and — crucially for views — the idea that a query language operating purely on relational algebra could define derived relations indistinguishable in kind from stored ones. This equivalence between a stored table and a "virtual" derived table is the entire conceptual basis of the SQL view. Codd received the Turing Award in 1981 for this body of work.

History
1970
— Codd publishes the relational model, establishing relations and derived relations as the same kind of object.
1976
— the ANSI/SPARC three-schema architecture formalizes external, conceptual, and internal views of data.
1976–1979
— IBM's System R prototype implements `CREATE VIEW` and confronts the view-update problem directly.
1986
— SQL becomes an ANSI standard, codifying updatable-view rules close to their modern form.
1998–2000
s — Oracle, SQL Server, and PostgreSQL popularize materialized/indexed views with incremental maintenance for OLAP workloads.

Where this matters

Views are load-bearing infrastructure across nearly every production database system. Business intelligence and reporting tools (Tableau, Looker, Power BI) almost never query raw normalized tables directly; they query curated views or materialized views that pre-join and pre-aggregate data into report-friendly shapes, hiding schema complexity from analysts. Row-level and column-level security in systems like PostgreSQL, Oracle, and SQL Server is frequently implemented via views combined with grants — a user is given access only to a filtering view (`WHERE tenant_id = current_user_tenant()`), never to the base table, so multi-tenant SaaS applications rely on exactly the `WITH CHECK OPTION` reasoning discussed above to keep tenants from reading or corrupting each other's data. Data warehouses and OLAP cubes are, at their core, elaborate materialized views: star-schema fact tables are pre-aggregated along common dimensions (time, region, product) precisely because recomputing these aggregates from raw transactional logs at query time would be prohibitively slow — this is the same incremental-maintenance machinery scaled up, often called "summary tables" or "rollups" in warehouse literature. Modern cloud data warehouses (Snowflake, BigQuery, Redshift) expose materialized views as a first-class performance feature with automatic incremental refresh and even automatic query rewriting, where the optimizer detects that a user's ad hoc query could be partially answered from an existing materialized view and substitutes it in — a direct descendant of the view-expansion mechanism, run in reverse. Database migration and schema-evolution tools also use views as a compatibility shim, letting old application code keep querying a stable view interface while the underlying physical schema is refactored beneath it — the same separation of concerns that motivates studying view expansion alongside the broader Query Processing Overview and the procedural logic layered on top in Triggers and Stored Procedures.

Ask the tutor: Can you walk through why a view with `UNION ALL` of two updatable sub-views is sometimes considered updatable, while one with plain `UNION` never is?

What exactly is the difference between a view and a table?

A table stores its own rows physically; a view stores only a query definition in the catalog and computes rows by executing that query (or, for materialized views, by reading a physically stored, periodically refreshed copy). Querying a virtual view is functionally identical to substituting its definition inline wherever the view name appears — this is view expansion. Structurally, both can appear in `FROM` clauses, be joined, filtered, and aliased identically; SQL deliberately makes them syntactically indistinguishable to callers. The difference only becomes operationally visible around writes (updatability rules differ sharply) and performance (a table read is a direct scan; a virtual view read re-executes its defining query, possibly involving joins and aggregation).

Can I create an index on a view?

Not on an ordinary virtual view, since it has no physical storage to index — there is nothing to build a B+-tree over. You can, however, create indexes on a materialized view (sometimes called an "indexed view" in SQL Server), because materializing the view produces an actual stored table-like structure that supports the same indexing machinery as a base table. This is one of the main reasons to materialize a view in the first place: not just avoiding recomputation, but enabling fast lookups (e.g., `WHERE dept_id = 10`) against a large derived result that would otherwise require scanning and re-aggregating raw data on every query.

Why can't a view involving a JOIN be updated in general?

Updating a view means translating a change to the view's rows into a change to the base tables' rows, and this translation must be unique and well-defined. If a view joins tables R and S, a single view row is built from one row of R combined with one row of S; inserting a new view row would require simultaneously inventing consistent, correctly-linked new rows in both R and S, and there is generally no unique way to do that (which foreign key values should the new rows share? what about other columns not shown in the view?). Because the mapping from view row to (R-row, S-row) pair is not invertible in general, SQL disallows updates through multi-table-join views by default; specific engines allow narrow exceptions with `INSTEAD OF` triggers that a developer writes explicitly to define the intended semantics.

Is a materialized view guaranteed to be correct, or just fast?

It is guaranteed correct at the moment of its last refresh, assuming the refresh algorithm (full or incremental) is implemented correctly; between refreshes, it can be stale relative to the base tables if writes have occurred since. Whether staleness matters depends entirely on the application: a nightly sales-summary dashboard tolerates a day of staleness happily, while a materialized view feeding an access-control decision generally cannot. Some systems support synchronous materialized views that are updated transactionally alongside the base table (paying a write-time cost on every base update), while others default to asynchronous, scheduled, or on-demand refresh — always check which mode is configured before assuming freshness.

What does "view expansion" actually look like inside the query optimizer?

Concretely, the query is parsed into an algebraic expression tree (or an annotated abstract syntax tree); wherever a leaf node refers to a view name, that leaf is replaced by the parsed expression tree of the view's own defining query, with attribute names/aliases renamed as needed to avoid collisions with the outer query. This is repeated recursively for nested views until no view references remain, producing a single tree over base tables only. That fully expanded tree is exactly what the cost-based optimizer receives, so it can apply standard rewrite rules — predicate pushdown, join reordering, projection pruning — across what used to be a view boundary, since the substitution preserves semantic equivalence exactly.

How do systems handle SELECT * inside a view definition if the base table's schema changes later?

Most systems resolve `SELECT ` <strong>at view-creation time</strong>, expanding it immediately into the explicit list of columns that existed in the base table at that moment, and storing that explicit list in the catalog rather than a literal ``. Consequently, adding a new column to the base table afterward does not automatically appear in the view's output; dropping or renaming a column the view depends on typically breaks the view (some systems mark it invalid and require `ALTER VIEW`/`CREATE OR REPLACE VIEW` to fix it). This is a frequent source of confusion, since developers often expect views to "just track" the base schema the way a fresh `SELECT *` on the table would.

Do views help performance at all, or are they purely for convenience and security?

A plain virtual view provides no inherent speedup; it is expanded and optimized exactly as if you had written the underlying query yourself, so its benefit is organizational — simplifying SQL, enforcing consistent business logic, and enabling access control — not computational. Performance gains come specifically from materializing a view (storing and indexing its result) or from an optimizer that performs automatic query rewriting to substitute a matching materialized view into an unrelated query. So the honest answer is: views are a logical abstraction first, and only become a performance tool once paired with materialization or a rewrite-capable optimizer.

Before this

Where this leads