Rollups
Pre-aggregated Parquet files that DuckDB serves when a query matches. The warehouse runs only a miss.
Updated
A Rollup is a stored pre-aggregation. It computes selected measures over chosen dimensions at a time grain. It saves the result as Parquet files.
When a query matches the stored fields, datahashi sends it to embedded DuckDB. DuckDB reads Parquet files from ENGINE_ROLLUP_STORE_DIR. A full hit does not touch the warehouse. A rollup with live_tail still reads the newest period from the warehouse.
flowchart TD
Q["Semantic Query<br/>revenue by month and region"] --> Router
Router{"Pre-agg router<br/>dimensions, measures and grains stored?<br/>caller row filter fits?"}
Router -->|"Match"| Duck["DuckDB storage<br/>reads Parquet from ENGINE_ROLLUP_STORE_DIR"]
Router -->|"Miss"| QR["Query Runner<br/>dispatches SQL to the warehouse"]
Routing follows two rules:
- The engine uses a rollup only when the query groups by stored dimensions and reads stored measures. A metric counts when every measure it reads is stored.
- On any mismatch, the engine sends the query to the warehouse. Routing never fails the query.
Declare a rollup
Declare rollups in the rollups: block of the model YAML:
rollups:
- name: monthly_order_summary
entity: Order
dimensions:
- status
- { name: order_date, grain: day }
- customer.region # Joined dimension through relationship
measures:
- revenue
- order_count
- customer_id_approx # count_distinct_approx
sketches:
- customer_id_approx # Enables re-aggregation at coarser grains
refresh:
every: 1h
when_changed: updated_at # Only rebuilds if newest timestamp moved
build_range:
dimension: order_date
days: 365 # Stores the last 365 days of data
partition:
by: month
refresh_last: 2 # Only re-scans the newest 2 months
compact: quarter # Merge ended periods into a coarser file
live_tail: true # Reads newest period from the warehouse
sort_by:
- order_date
- customer.region
Features
1. Grain derivability
When a rollup stores a time dimension at day, it can answer day, week, month, quarter, year, and fiscal grains. DuckDB re-aggregates daily buckets into the requested grain.
2. Live tail
History is static. The current month still changes. With live_tail: true:
- Historical periods come from Parquet files.
- The open period comes from the warehouse through Query Runner.
- DuckDB unions both sets into one result.
Answers stay current without a full rebuild on every change.
3. Sketches for non-additive measures
sum, count, min, and max re-aggregate across dimensions. Exact count_distinct does not. count_distinct_approx and percentiles (median, p90, p95, p99) do not, unless you sketch them.
List those measures under sketches:. A sketch is a HyperLogLog (about 1.6% error) or exact value counts for a percentile. A daily rollup can then answer a coarser grain. The sketched measure must also appear in measures. Its agg must be count_distinct_approx or a percentile.
4. Partitioning and compaction
partition.byisday,week,month,quarter, oryear. One Parquet file per period. A scheduled refresh re-reads the newestrefresh_lastperiods.partition.compactmerges ended periods into a coarser file. It is not a root key of the rollup.partition.append: truereads only rows since the last refresh. It cannot be combined withlive_tail. Every stored measure must be additive or listed insketches.
5. Freshness markers
A blind rebuild of a large rollup wastes compute.
when_changed: updated_at. The engine runsSELECT max(updated_at)on the warehouse. If the value did not move, it skips the rebuild.when_changed_sql. Custom SQL, for exampleSELECT max(batch_id) FROM etl_meta.refresh: { manual: true }has no schedule. CallPOST /v1/hooks/rollups/rebuildon the control plane, with a hook token, at the end of ETL. The engine does not serve that route.
6. Joined dimensions
A rollup can store a dimension of another entity, such as customer.region on an Order rollup. The path must be one chain of many_to_one or one_to_one relationships. The rollup then serves queries that join that entity.
7. Join rollups
A query can mix measures from two entities. Example: revenue from Order and tickets from Ticket by customer.region. You do not need one rollup that holds both.
When each entity has a fresh rollup with the same shared dimensions and joins, the engine reads one rollup per entity. It joins the results on the grouped dimensions. Live queries work the same way.
- Declare one rollup per entity. Do not merge entities into one rollup.
- It is all or nothing. If any entity has no matching rollup, the whole query runs live. One statement cannot mix DuckDB and the warehouse.
- A query with
count_distinctor percentile measures across entities never uses this path. A rollup withlive_tailnever uses it either.
Inspection
Every query response includes diagnostic metadata:
- On a hit:
meta.rollupis{"used": "monthly_order_summary"}. A live-tail hit also sets"live_tail": true. When several rollups were joined,joinedlists them. - On a miss:
meta.rollup.missedis a list of{rollup, reason}. Reasons includegrain_not_derivable,outside_build_range, androw_security.
Next: Data roles and row-level security · PostgreSQL wire protocol.