Availability: Pre-aggregates are a Beta feature available on Enterprise plans only.
Managed vs external at a glance
Managed pre-aggregates are the default. Use external pre-aggregates when you already maintain warehouse rollups (for example, BigQuery materialized views that refresh incrementally per partition) and don’t want Lightdash to rebuild the same data on your warehouse.
How it works
- Declare the external pre-aggregate in your dbt YAML with a
tablekey pointing at the warehouse relation you’ll build. - Get the column contract with the CLI. Lightdash generates the exact column names, types, and materialization SQL your table must conform to.
- Build the table in your warehouse using any orchestration you like — dbt models, Airflow DAGs, scheduled queries.
- Verify the built table matches the contract with the same CLI command, and wire the check into CI.
- Deploy the YAML change.
1. Declare the pre-aggregate
Add atable key on the pre-aggregate. Its value is a warehouse relation reference, injected verbatim into the generated SQL — quote and qualify it exactly as your warehouse expects.
- dbt v1.9 and earlier
- dbt v1.10+ and Fusion
- Lightdash YAML
dimensions, metrics, filters, time_dimension, granularity — is the same as a managed pre-aggregate and drives matching identically. The materialization-only keys refresh, sorts, max_rows, and materialization_role are silently ignored on an external definition; you own the build, so Lightdash cannot honor them.
2. Get the column contract
Your table must conform to a column contract that Lightdash generates from the pre-aggregate definition. Thelightdash pre-aggregate-check-external CLI command prints the expected columns and the exact materialization SQL for each external pre-aggregate.
lightdash pre-aggregate-check-external runs fully locally. It compiles your dbt project and reads warehouse credentials and dialect from your active profiles.yml target — no Lightdash server call is made, so it works before the project is ever deployed.- The expected column contract — column name, type, and role (dimension, time dimension + grain, metric, or metric component)
- An actual column check against the declared table when it exists (or a warning that the table is missing)
- The materialization SQL — the exact SELECT Lightdash expects to serve from
- Average metrics decompose into components. An
averagemetric stores as<fieldId>__sumand<fieldId>__countcolumns, and the materialization SQL emitsSUM(expr)andCOUNT(expr). You never store an average value directly; Lightdash re-computes the average from the components at query time. - Joins are baked in at materialization time. If your pre-aggregate lists dimensions from joined tables (for example,
customers.country), the generated SQL includes the join. Your external table stores the joined-dimension columns as flat columns — no join happens at serve time.
3. Build the table
Any orchestration works. The CLI’s--json flag is designed for scripting the build itself — for example, wrapping every generated SELECT in a CREATE OR REPLACE TABLE:
bq query, snowsql, psql, etc.). More commonly, teams register the generated SQL as a dbt model or a warehouse-native materialized view and refresh it on their existing orchestration cadence.
4. Verify the table matches
Re-run the check with--fail-on-mismatch after the build. This exits with code 1 on any column drift, so you can wire it into CI next to the table build:
--clear-cachematters right after a rebuild. BigQuery and Snowflake result caches key on query text and can return the pre-rebuild schema if you don’t invalidate them.--fail-on-mismatchgives exit 1 on drift — pair it with--allin CI to catch every external definition in one run.- On mismatch the output lists per-column
✗(missing / type_mismatch) alongside the rebuild SQL.
5. Deploy
Deploy the YAML change withlightdash deploy (or your CI deploy) as normal. External pre-aggregates take effect on the next compile — there is no materialization job to wait for.
What Lightdash owns vs what you own
External pre-aggregates split responsibility explicitly: Lightdash owns:- Matching — same rules as managed pre-aggregates (field coverage, granularity, filter compatibility, non-additive metric rejection, smallest-wins)
- Compiling the served query against your table in your warehouse’s dialect
- Re-truncating the stored time dimension for coarser-grain queries (a day-grain table serves week, month, quarter, year)
- Re-aggregating
__sumand__countcomponents intoaveragemetrics at query time - Applying
sql_filter, model required filters, and required attributes at serve time — the same access control the warehouse path enforces - Falling back to the warehouse per query if serving errors (missing table, missing column, type mismatch)
- Recording hits and misses in pre-aggregate analytics and the dashboard audit
- Building the table with the exact column names and types Lightdash generates
- Refreshing the table on whatever cadence and orchestration you choose
- Keeping the table complete — missing rows produce silently incomplete results
- Applying the pre-aggregate’s
filtersat build time — Lightdash does not re-apply them at serve time on external tables - Access — every credential that queries Lightdash must be able to read the table, including any per-user warehouse credentials
Current limitations
External pre-aggregates are opt-in per pre-aggregate. Definitions without atable key continue to run as managed pre-aggregates with no change in behavior.
External pre-aggregates:
- Do not check the table’s existence or schema at deploy time. Use
lightdash pre-aggregate-check-external(locally and in CI) to catch drift. - Do not check freshness — Lightdash trusts that the table is current.
- Silently ignore the materialization-only keys (
refresh,sorts,max_rows,materialization_role) on the definition. - Do not show a dedicated “external” indicator in the Project Settings pre-aggregates UI. Hits and misses still show up in analytics and dashboard audits.