Which conditions let a materialized aggregate answer a warehouse query written against the fact table?
answer
- the optimizer has to prove equivalence
- coarser can roll up, finer cannot
- filter columns must exist in the aggregate
- its own WHERE clause must subsume the query's
- failure is silent, so read the plan
basics
~20 sThe aggregate's grain must be at least as coarse as the query's and derivable from it, every column the query filters or groups on must exist in the aggregate, its joins and filters must subsume the query's, its measures must be re-aggregatable, and it must be fresh enough for the engine to trust.
solid answer
~50 sAutomatic rewrite is a **containment proof**, and it succeeds only when every part holds. The aggregate's grain must be **at least as coarse** as what the query needs and the query's grain must be derivable by re-aggregating it — day rolls up to month, but an hourly question cannot be answered from a daily aggregate. Every column the query **groups by or filters on** must exist in the aggregate, or its rows cannot be selected. The aggregate's own `WHERE` clause and join set must **subsume** the query's, so it cannot be missing rows the query needs. The measures must be re-aggregatable, which rules out stored averages and distinct counts. Finally the engine must consider it **fresh enough** under its staleness policy. If any condition fails the query silently reads the fact table — same result, ten or a hundred times the cost — so verify from the plan that the aggregate is what was actually scanned.
code
sql · 15 lines-- Aggregate definition
SELECT CAST(order_ts AS DATE) AS order_day,
country,
sum(amount) AS sum_amount,
count(*) AS row_count
FROM fact_orders
GROUP BY 1, 2;
-- Eligible: coarser grain, columns present, additive measures
SELECT country, sum(amount) FROM fact_orders
WHERE order_ts >= DATE '2025-01-01' GROUP BY country;
-- Not eligible: device_type is not stored in the aggregate
SELECT country, sum(amount) FROM fact_orders
WHERE device_type = 'mobile' GROUP BY country;go deeper
Know that some engines can transparently answer a query from a pre-computed aggregate, and that this only works when the aggregate already contains the columns and the level of detail being asked for.
Explain the conditions: grain subsumption in the coarser direction only, grouping and filter columns present, the aggregate's own predicate subsuming the query's, and measures that can be recombined.
Show that failure is silent and expensive, and describe how you verify from the plan and from query history that the aggregate is actually being hit, plus how a load falling behind flips a query's cost.
Own the choice between transparent rewrite and named rollup tables the BI layer targets directly, and the monitoring that turns a silent rewrite regression into an alert rather than an invoice surprise.
## What rewrite is Automatic query rewrite means the user writes a query against the fact table and the optimizer substitutes a pre-computed aggregate, transparently, because it can prove the substitution returns identical rows. It is attractive because consumers — BI tools, notebooks, ad-hoc analysts — need no knowledge of which aggregates exist, and you can add or drop aggregates without editing any query. The price is that eligibility is invisible: when the proof fails, nothing errors, the query just costs what it would have cost without the aggregate. ## The conditions the optimizer must satisfy **Grain subsumption.** The aggregate's grouping keys must be at least as coarse as the query's requirement, and the query's grain must be derivable from the aggregate's by further grouping. Day to month works because a day maps to exactly one month; month to day does not, because the aggregate has destroyed the information. Functional dependencies matter here: an engine can only exploit day-to-month if it knows the derivation, which usually means the query applies a recognised truncation to a column the aggregate stores. **Column availability for grouping and filtering.** Every column the query groups by must be present. Every column the query filters on must also be present, because the rows to keep have to be selectable *after* aggregation. This is the condition people forget: adding `WHERE device_type = 'mobile'` to a dashboard query silently disqualifies an aggregate that does not carry `device_type`, and the only symptom is that the bill goes up. **Predicate and join containment.** If the aggregate itself was defined with a filter — say only completed orders, or only the last 90 days — it can serve a query only when the query's predicate is at least as restrictive within that region. An aggregate over 90 days cannot answer a two-year question. Likewise, the joins the aggregate materialized must match or subsume the joins in the query; if the query joins a dimension the aggregate never touched, the engine can only rewrite when that join is provably lossless and the needed key is present. **Measure re-aggregatability.** The query's aggregate functions must be derivable by combining the stored measures — sums from sums, counts from counts, averages from a stored sum and count. A stored average or a stored distinct count blocks rewrite at any coarser grain. **Freshness.** The engine must decide the aggregate is current enough. Systems differ in policy: some refuse rewrite once the aggregate is behind its base tables at all, some accept a declared staleness tolerance, some transparently combine the aggregate with unmerged recent data. The important operational fact is that a large load can flip a query from cheap to expensive without anyone changing the query. ## Verifying rather than hoping Because every failure mode is silent, treat rewrite as something you measure: - Read the query plan and confirm the scanned relation is the aggregate, not the fact table. - Compare bytes scanned or compute time against the same query with the aggregate dropped or disabled. - Track hit counts per aggregate over time from query history; a rewrite that was working can stop working after a schema change, a new filter in a dashboard, or a refresh falling behind. ## Transparent rewrite versus pointing consumers at the rollup The alternative is to skip rewrite entirely and have the BI layer query the aggregate table by name. Compare honestly: - **Rewrite** keeps one logical model, survives adding and removing aggregates, and helps ad-hoc queries you never anticipated. But eligibility is fragile and invisible, and cost becomes hard to predict. - **Explicit rollup tables** give predictable, auditable cost and no surprises, at the price of consumers hard-coding a physical choice, and of every new question needing someone to know which table to hit. A common production compromise: define aggregates for rewrite where the engine supports it, but expose the important ones through named views so critical dashboards have a guaranteed path, and monitor hit rates so silent regressions surface as a cost alert rather than a quarterly invoice surprise.
- A dashboard query was being rewritten onto an aggregate and stopped after someone added a filter. What happened?The new filter almost certainly references a column the aggregate does not store. Rows have already been collapsed, so the engine cannot select the matching subset and abandons the rewrite, falling back to the fact table with no error. Either add the column to the aggregate's grouping keys — accepting the row-count increase — or build a second aggregate for that consumer if the cardinality makes it worthwhile.
- Why can a daily aggregate serve a monthly query but not an hourly one?Re-aggregation only moves toward coarser grain. Each day maps to exactly one month, so summing days gives the month. An hour is a subdivision of a day and the aggregate has already discarded intra-day detail, so no combination of stored rows can reconstruct it. Answering the hourly question requires either the fact table or a separate hourly aggregate.
- How would you prove in production that an aggregate is actually being used?Read the executed plan and confirm the aggregate is the scanned relation, then compare bytes scanned or compute time against the same query run without it. Beyond the one-off check, aggregate query history by target relation to track hit counts per aggregate over time, and alert when a previously-hit aggregate drops to zero hits — that is how silent rewrite regressions get caught early.
saying these in an interview costs you the question
- Believes rewrite works whenever an aggregate exists on the same table
- Thinks a daily aggregate can answer an hourly question
- Forgets that filter columns must be present, not just grouping columns
- Expects an error when rewrite fails instead of a silent cost increase
- Assumes a stale aggregate is used anyway with slightly old numbers