Pre-Aggregation
10 full scans per dashboard load
- Every dashboard load scans 500M rows
- 30-second query time
- $5 per TB scanned, 10 queries per load
- Users stop checking the dashboard
- Dashboard reads 365-row summary table
- Millisecond query time
- Negligible scan cost
- Users check the dashboard 10x per day
Freshness is the price
| Approach | Freshness | Cost | Complexity |
|---|---|---|---|
| No pre-aggregation | Real-time (queries hit raw data) | High (full-table scans) | Low (no summary tables to maintain) |
| Nightly summary tables | T+1 (yesterday's data) | Low (365 rows per year) | Medium (ETL job to refresh) |
| Streaming aggregation | Near-real-time (seconds) | Medium (streaming infrastructure) | High (Flink/Spark Structured Streaming) |
| Materialized views (auto-refresh) | Minutes to hours | Medium (depends on platform) | Low (database handles refresh) |
Not every metric can be pre-aggregated
The average-of-averages trap
Pre-aggregation: daily_revenue_summary rolls 500M fact rows down to 365/year, storing ADDITIVE components (total_revenue, order_count) not ratios. AOV = SUM(total_revenue)/SUM(order_count) stays correct at every rollup.
| Metric | Additive? | Pre-Aggregate? | Store Instead |
|---|---|---|---|
| Revenue | Fully | Yes, SUM directly | SUM(revenue) |
| Order count | Fully | Yes, COUNT directly | COUNT(*) |
| Average order value | Non-additive | No | SUM(revenue) and COUNT(orders) separately |
| Conversion rate | Non-additive | No | SUM(conversions) and SUM(impressions) separately |
| Account balance | Semi-additive | Yes, but only per snapshot | One row per account per day, SUM across accounts only |
| Distinct users | Non-additive | Approximate only | HyperLogLog sketch (mergeable) |
Pre-computing every dimension combination
ROLLUP vs CUBE vs GROUPING SETS
| Syntax | What It Computes | Row Count |
|---|---|---|
| GROUP BY ROLLUP(year, quarter, month) | Hierarchical subtotals: month, quarter, year, grand total | Moderate (hierarchical only) |
| GROUP BY CUBE(product, region, month) | All possible combinations including subtotals | High (2^n combinations) |
| GROUP BY GROUPING SETS((product, region), (product), ()) | Only the specific combinations you list | Controlled (you choose) |
Cubes pay off on large tables with fixed filters
Picking the grain
Multiple summary levels
| Summary Level | Rows per Year | Refresh Frequency | Use Case |
|---|---|---|---|
| Hourly by product | 8,760 x products | Hourly | Real-time operations dashboard |
| Daily by product by region | 365 x products x regions | Nightly | Standard analytics dashboard |
| Monthly by region | 12 x regions | Monthly | Executive summary |
| Yearly total | 1 | Annually | Board reports |
A common mistake is building a summary table at a granularity that no consumer uses. If nobody filters by hour, an hourly summary wastes storage and refresh compute. Build summaries based on actual query patterns, not theoretical flexibility.
Full refresh vs incremental
- Delete and rebuild the entire summary table
- Simple: no state tracking
- Correct: no accumulated bugs
- Expensive: re-scans the entire fact table
- Best for: small to medium fact tables
- Only process new/changed rows since last refresh
- Complex: must track what has been processed
- Risk: bugs accumulate if logic is wrong
- Cheap: proportional to change volume, not table size
- Best for: large fact tables with append-only writes
Materialized views
| Platform | Auto-Refresh? | Incremental? | Notes |
|---|---|---|---|
| BigQuery | Yes (up to every 30 min) | Yes | Best materialized view support. Handles joins and aggregations. |
| Snowflake | Yes (automatic) | Yes | Called 'Dynamic Tables.' Specify lag time. |
| Redshift | Manual (REFRESH) | Yes (since 2023) | Must schedule refresh via Airflow or cron. |
| PostgreSQL | Manual (REFRESH) | No (full refresh only) | REFRESH MATERIALIZED VIEW CONCURRENTLY avoids locks. |
Materialized views stop at complex transformations
> The product team asks for a dashboard showing daily revenue by product category and region. The fact table has 500M rows and growing.
Pre-computing answers before anyone asks
- Category
- Data Modeling
- Duration
- 23 minutes
- Challenges
- 12 hands-on challenges
Topics covered: Why Pre-Aggregate?, Metric Types and Additivity, OLAP Cubes & Rollups, Granularity Design, Refresh Strategies for Aggregates
Lesson Sections
- Why Pre-Aggregate? (concepts: Pre-Aggregation)
10 full scans per dashboard load Take a fact table with 500 million rows and a dashboard with 10 tiles, where each tile runs a GROUP BY query that scans the entire table. That is 10 full-table scans every time someone opens the dashboard. At $5 per TB scanned (BigQuery pricing), that's real money, and at 30 seconds per query the user experience is terrible. Pre-aggregation solves this by computing the answer ahead of time. Instead of scanning 500 million rows to get daily revenue, create a daily
- Metric Types and Additivity (concepts: Metric Additivity)
Not every metric can be pre-aggregated Some metrics can be safely SUMmed across any dimension and others cannot. If you pre-aggregate a non-additive metric, the numbers will be mathematically wrong, and nobody will notice until someone reconciles against the source. The average-of-averages trap You pre-compute average order value (AOV) per day: Monday = $50, Tuesday = $100. What's the weekly AOV? NOT ($50 + $100) / 2 = $75. If Monday had 1,000 orders and Tuesday had 10 orders, the true AOV is ($
- OLAP Cubes & Rollups (concepts: OLAP Cubes)
Pre-computing every dimension combination An OLAP cube is a summary table that pre-computes aggregates for every combination of dimensions: revenue by product, by region, by product AND region, and by product AND region AND month. If you have 3 dimensions with 10 values each, the cube has 10 x 10 x 10 = 1,000 cells, and each cell stores a pre-computed SUM. Modern cloud warehouses don't use literal cubes (multidimensional arrays). They use flat summary tables with GROUP BY ROLLUP or GROUP BY CUBE
- Granularity Design (concepts: Grain Definition)
Picking the grain The granularity of your summary table determines both its size and its usefulness. A daily summary has 365 rows per year. An hourly summary has 8,760. A per-minute summary has 525,600. The finer the grain, the more flexible the queries, but the larger the table and the more it costs to refresh. Start from the finest grain that any dashboard or report needs. If no dashboard filters below daily, a daily summary is sufficient. If the operations team needs hourly metrics, you need
- Refresh Strategies for Aggregates (concepts: Pre-Aggregation)
A summary table is only useful if it stays current. The refresh strategy determines how often the summary is updated, how long it takes, and what happens when it fails. Full refresh vs incremental In production, run an incremental refresh daily and a full refresh weekly as a reconciliation check. If the full refresh produces different numbers than the incremental, you have a bug in the incremental logic, and the weekly run is what catches it. Materialized views A materialized view is a database-