skip to content

When a query selects from a view, how does the query optimizer handle the view's definition, and does wrapping a query in a view add runtime cost?

level: middleimportance: must knowfreq 45%

answer

  1. view reference → definition inlined before planning
  2. pushdown, join reorder, projection pruning
  3. aggregate/DISTINCT/window/LIMIT = fence
  4. fence → inner block fully evaluated first
  5. nested views blow up plan search space

basics

~20 s

The optimizer expands (inlines) the view definition into the calling query and plans the combined statement, so predicates from outside can be pushed inside and unused work can sometimes be eliminated. The view itself adds no runtime cost. Constructs like aggregation, DISTINCT or window functions act as optimization fences that block that merging.

solid answer

~60 s

A view reference is replaced by its definition before planning — view expansion, or inlining. The optimizer then sees one query and can push the caller's predicates down into the view body, reorder joins across the boundary, and in some cases drop joins whose columns nobody selected. The abstraction is therefore usually free: the plan for a query on a view is often identical to the plan for the same query written out by hand. The important qualifier is that certain constructs create an **optimization fence**. If the view aggregates, uses DISTINCT, window functions, LIMIT, or a set operation, the outer predicate generally cannot be pushed below it without changing semantics — a filter applied before a SUM produces a different answer than one applied after. The engine must then materialise or fully evaluate the inner result before filtering, and a view that looks like a cheap lookup may scan everything. So: no fixed overhead, but a real risk that the view's shape prevents the predicate pushdown you were counting on. Read the plan rather than assuming.

code

sql · 8 lines
sql
CREATE VIEW recent_orders AS
SELECT order_id, customer_id, total, created_at
FROM orders
WHERE created_at > CURRENT_DATE - INTERVAL '30' DAY;

SELECT * FROM recent_orders WHERE customer_id = 42;
-- planned as: orders WHERE created_at > ... AND customer_id = 42
-- an index on customer_id can serve it

go deeper

for a junior

Know that the view's query is substituted into yours and planned together, so a plain view adds no cost by itself.

for a middle

Explain predicate pushdown and name the fences — aggregation, DISTINCT, window functions, LIMIT — and why a fence blocks it.

for a senior

Diagnose from the plan: check whether the predicate lands as an index condition or as a late filter above an aggregate, and watch estimation quality degrade above fences.

for a principal

Set the boundary for how much logic belongs in the view layer, knowing that fences and nesting depth turn an invisible abstraction into unpredictable plans at scale.

## Expansion, not execution When the parser meets a view name it substitutes the stored definition, producing a single query tree that mixes the caller's clauses with the view's. The optimizer then plans that combined tree. There is no separate step where the view is executed and its rows handed to the outer query — unless the optimizer decides materialisation is the best plan for reasons that would apply to an inline subquery too. The practical statement: `SELECT * FROM v WHERE x = 5` is normally planned exactly as if you had pasted the view body in as a subquery. Same access paths, same joins, same cost. That is why "views are slow" is wrong as a blanket claim. ## What the optimizer can do across the boundary **Predicate pushdown.** A filter written outside the view can be applied inside it, at the base table, so an index can serve it. This is the single most important optimization and the reason a view over a huge table can still answer a point query in microseconds. **Join reordering.** Joins from the view body and from the outer query end up in one join set the optimizer can order freely by cost. **Projection pruning.** Columns the caller never selects need not be computed or fetched, which can save expression evaluation and wide-row I/O. **Join elimination.** Some engines can drop a join entirely when the caller selects no column from that table and a foreign key with a unique target guarantees the join neither adds nor removes rows. This is what keeps wide "everything" views tolerable for narrow consumers — but it depends on declared constraints being present, so it silently stops working if they are not. ## Optimization fences A fence is any construct where moving a predicate across it would change the result. The optimizer must respect them, and each one costs you pushdown: - **Aggregation / GROUP BY.** Filtering before grouping changes which rows are aggregated. Some engines can push a predicate on a grouping *key* through, since that only removes whole groups, but a predicate on an aggregate result cannot move. - **DISTINCT.** Similar reasoning — sometimes pushable on projected columns, often not. - **Window functions.** A window value depends on the whole partition, so any filter that would remove rows from the partition alters it. Predicates essentially cannot pass. - **LIMIT / OFFSET.** Which rows survive is positional; filtering earlier changes the set entirely. - **Set operations and recursive CTEs.** Pushdown is limited and engine-dependent. - **Volatile or side-effecting functions.** The engine must not move or re-evaluate them freely. When a fence exists, the inner block is evaluated on its own and the outer predicate applies to its output. Concretely: a view aggregating daily totals across all customers, queried with `WHERE customer_id = 42`, may aggregate every customer before discarding all but one — an enormous amount of work for one row. The fix is usually to parameterise differently: filter inside the view's own logic, use an inline function or a query that groups only what is needed, or accept a materialized result for the aggregate. ## Nested views and plan size Expansion is recursive: a view on a view on a view produces one large tree. Two consequences. First, join count grows, and optimizer search space grows roughly super-linearly with it, so planning time itself becomes measurable and some engines stop exhaustive search past a threshold and fall back to heuristics — meaning a worse plan, not just slower planning. Second, each layer may add joins and columns that only exist to serve some other consumer, and unless projection pruning and join elimination fire, that work is really executed. ## How to verify Read the execution plan for the query as issued against the view. Check that the predicate you expect to be pushed appears as an index condition on the base table rather than as a filter applied late above an aggregate. Compare against the hand-written equivalent when in doubt: if plans differ, a fence or a missing constraint is the reason. Row-count estimates are also worth checking — estimation through an aggregation or a DISTINCT is much weaker, and a bad estimate above a fence propagates into bad join choices higher up. ## The judgement Views are free when they are simple projections, filters and joins. They stop being free the moment the definition contains a fence, and the failure mode is invisible in the calling SQL. That asymmetry is the actual answer to "do views cost anything": not a fixed overhead, but a loss of the optimizer's ability to specialise the query for your predicate, in exactly the cases where the query is expensive enough to matter.

  • A view aggregates order totals per customer per day. A query adds WHERE customer_id = 42 and is very slow. What is happening and what would you try?
    The GROUP BY is an optimization fence for predicates the engine cannot prove are safe to push, so it may aggregate every customer before filtering down to one. Check the plan for the filter sitting above the aggregate rather than as an index condition on the scan. Options are to make the predicate one the engine can push through the grouping key, to replace the view with a parameterised function or an inline query that groups only the wanted customer, or to precompute the aggregate.
  • Why can deeply nested views make planning itself slow, not just execution?
    Expansion is recursive, so each layer contributes its joins to one combined query. Optimizer search cost grows steeply with the number of joined relations, and past a threshold many engines abandon exhaustive search for heuristics. The result is both measurable planning time and, worse, a plan chosen without full search.

saying these in an interview costs you the question

  • Claiming a view is executed first and its rows then filtered by the outer query
  • Claiming views always add fixed overhead
  • Assuming predicates always push down through a view containing GROUP BY
  • Believing plan quality is unaffected by how many view layers are stacked
  • Thinking a view's plan can be reasoned about from its SQL text without reading the actual plan

context