A reporting query reads from a view that itself reads from three other views, and it is far slower than expected. What problems are typical of deep view-on-view chains, and how would you attack this?
answer
- expansion is recursive → one big query
- each layer adds work for someone else
- one fence anywhere kills pushdown
- estimates collapse through aggregation
- flatten the hot path; keep the stack for the tail
basics
~20 sStacked views accumulate joins and columns nobody downstream needs, and inner aggregations or DISTINCTs block predicate pushdown so the bottom of the stack scans everything. Read the expanded plan, find the fence and the joins contributing nothing, then flatten the query for that consumer.
solid answer
~60 sViews expand recursively, so a four-level stack becomes one large query. Three things typically go wrong: 1. **Accumulated work.** Each layer adds joins and columns for some other consumer. Unless projection pruning and join elimination fire — and join elimination needs declared foreign keys and unique constraints — that work is really executed. 2. **Fences kill pushdown.** One aggregation, DISTINCT or window function anywhere in the stack stops the outer predicate from reaching the base tables, so a query that looks like a point lookup scans and aggregates everything before discarding it. 3. **Estimation and planning degrade.** Row estimates through several aggregations become guesses, driving bad join orders; and with enough joined relations the optimizer stops exhaustive search and uses heuristics. Attack it by reading the actual plan, not the SQL: find where the predicate is applied, which scans return far more rows than the final result, and which joins contribute no selected column. Then flatten — write the query the report actually needs directly against base tables or against one shallow view — and, if the aggregate is genuinely reused, precompute it instead of recomputing it per read.
code
text · 7 linesFilter: (region = 'EU') actual rows=12
-> HashAggregate actual rows=1,400,000
-> Hash Join actual rows=38,000,000
-> Seq Scan on order_lines actual rows=38,000,000
-> Hash -> Seq Scan on products actual rows=90,000
-- 38M rows joined and aggregated to answer a 12-row questiongo deeper
Know that views expand into one big query and that stacking them can make a simple-looking query do a lot of work.
Identify a fence in the chain as the reason a predicate is not pushed down, and read a plan to see where the filter is applied.
Work the full diagnosis: plan with actual rows, locate the dominant layer, then flatten the hot consumer, move the fence, or restore constraints — and weigh reuse against predictability.
Govern the view layer as a shared contract: depth limits, named owners and consumers, and an explicit policy on when a hot path gets its own flattened query or a precomputed result.
## Why depth is different from complexity A single complex view is planned as one query and is usually fine. Depth is worse for a specific reason: each layer is designed without knowledge of its callers. Layer two joins in a dimension table because one report needed a label. Layer three adds a DISTINCT to protect against duplicates layer two might produce. Layer four filters. Every consumer pays for every decision made below it, and nobody can see the total from the SQL they wrote. ## The failure modes **Work nobody needs.** The classic symptom is a query selecting three columns whose plan touches nine tables. The optimizer can sometimes rescue this: projection pruning skips computing unselected expressions, and join elimination can remove a join entirely when no column from that table is selected and a foreign key to a unique key proves the join changes neither row count nor content. That proof requires the constraints to be declared. Systems that dropped foreign keys "for write performance" lose join elimination and pay for it on every read through the view stack. **Fences.** An aggregation, DISTINCT, window function, LIMIT or set operation anywhere in the chain generally prevents predicates from moving below it. The user's WHERE clause then applies to the output of that block rather than to a base-table scan, so the block is computed in full. A one-row report can cost a full aggregation of the fact table. This is the single most common cause of the "why is this simple query slow" question, and it is completely invisible at the call site. **Estimation collapse.** Selectivity estimates are reasonably good on base-table predicates and degrade through joins, aggregation and DISTINCT. Several layers in, the estimate feeding the top join can be wrong by orders of magnitude. Wrong estimates produce wrong join methods and orders — a nested loop chosen for an expected 10 rows that turns out to be 10 million — and that, not raw row count, is what turns a slow query into a stalled one. **Planning cost and search cutoff.** Expansion is recursive, so joined-relation count grows with depth. Optimizer search space grows steeply with that count, and past an engine-specific threshold most planners abandon exhaustive search for a greedy or genetic method. The consequence is not just slower planning but a plan chosen without full consideration. **Duplicate-row whack-a-mole.** Deep stacks often acquire DISTINCT at intermediate levels to hide fan-out introduced lower down. Each DISTINCT is both a correctness band-aid and a new fence, so the structure gets slower as it gets more defensive. ## How to attack it **Start from the plan, not the text.** Get the execution plan with actual row counts for the failing query. Three things to look for: where the user predicate is actually applied (a late Filter above an aggregate is the smoking gun for a fence); which scans return vastly more rows than the final result; and where estimated and actual rows diverge sharply, which localises the estimation failure. **Map the stack.** Write down each layer's definition and what it adds. Usually one or two layers explain most of the cost, and often a layer exists for a consumer that no longer exists. **Flatten for the hot consumer.** The most effective fix is usually to give the expensive report its own query — written against base tables or one shallow view — expressing exactly what it needs. This trades reuse for predictability. It is the right trade for a small number of hot paths; keep the stack for the long tail of ad-hoc consumers. **Move the fence.** If an aggregation must exist, try to arrange that the filter can be expressed on a grouping key, so the engine can push it through as group elimination. Sometimes restructuring the view to group by the column consumers filter on is enough. **Precompute deliberately.** If an inner aggregate is genuinely shared and expensive, computing it once on a schedule and reading the stored result is a different trade — staleness for cost — and should be chosen explicitly rather than arrived at by accident. **Restore the constraints.** Declared foreign keys and unique constraints are what let the optimizer eliminate joins and estimate better. In a view-heavy system they are a performance feature, not just an integrity feature. **Govern depth.** Set a limit on nesting for shared views, require a named owner and consumer list for each, and treat adding a join or a DISTINCT to a shared view as a change with blast radius across every consumer. ## Interview framing Name the three mechanisms (accumulated work, fences blocking pushdown, estimation and planning degradation), say you would read the plan with actual rows to find which one is dominant, then give the fix ladder: flatten the hot consumer, move or remove the fence, restore constraints, precompute only if genuinely shared. Mentioning the governance angle — views are a shared contract with a blast radius — is what makes it a senior answer rather than a tuning anecdote.
- How would you tell from a plan that a view layer is doing work nobody downstream needs?Look for tables scanned or joined whose columns appear nowhere in the final output and in no surviving predicate — the join exists only because a lower layer selected it. Also compare rows at the bottom scans against the final row count; a huge ratio with no filtering in between points at avoidable work. If foreign key and unique constraints are declared, some of those joins could be eliminated automatically, so their absence is worth checking too.
- Why do declared foreign keys and unique constraints matter for the performance of a view stack?Join elimination requires the optimizer to prove that removing a join changes neither the row count nor the result, which it does from a foreign key referencing a unique or primary key. Without the declarations it must execute the join even when no column from that table is selected. Constraints also improve cardinality estimation, which drives join order and method choices higher in the plan.
Each view layer is like a wrapper function written by someone who never met your caller. Individually reasonable, stacked four deep they compute a full report so you can read one number from it.
saying these in an interview costs you the question
- Blaming view overhead in general rather than identifying pushdown blockage
- Assuming the optimizer always eliminates joins whose columns are unused
- Adding DISTINCT at an intermediate layer to hide fan-out instead of fixing the join
- Reasoning about the cost from the view's SQL text instead of the actual plan
- Proposing to precompute the whole stack before finding which layer dominates