← All posts
Data Engineering11 min read

dbt Models Reading BigQuery Views: The Hidden Recompute Tax

When dbt models read from BigQuery views instead of materialised tables, BigQuery silently recomputes the full view logic every time — costing you money you never see coming. Here's how to find it and fix it.

When a dbt model reads from a BigQuery view rather than a materialised table, BigQuery silently recomputes the entire view definition on every query — no caching, no partition pruning through the view boundary, no warning in the console. The result is inflated bytes billed, slower runs, and a cost that compounds invisibly across every downstream model that touches the view. This post shows you how to detect it in under five minutes and what to do about it.

This is one of the most frequently overlooked cost drivers in dbt projects on BigQuery. It does not throw an error. It does not appear in dbt debug. It just quietly burns through your on-demand budget or eats slots you thought were available for other work. The fix is usually one line in a dbt_project.yml or a config() block — but you have to know where to look first.

Why BigQuery Views Recompute Every Time a dbt Model Reads Them

BigQuery views are not pre-computed. When you query a view, BigQuery expands the view definition inline and executes the full underlying SQL as part of your query. If a dbt model references a source or upstream model that has been materialised as a view, every dbt run targeting that model recomputes the view from scratch.

This becomes expensive in three common situations:

  • A staging model is materialised as a view (the dbt default) and is referenced by five or more downstream models. Each downstream run recomputes the same staging logic independently.
  • A source is exposed through a BigQuery view — perhaps a federated view over a Cloud SQL database or a shared dataset view — and dbt models query it directly without intermediate materialisation.
  • A mart or intermediate model is accidentally left as a view after a schema migration or during prototyping, and nobody notices because dbt compiles and runs without complaint.

The mechanics matter here. When BigQuery expands a view, it cannot push partition filters through the view boundary unless the view was defined with a partition filter itself. This means a dbt model that correctly filters on a partition column may still trigger a full table scan if the filter is applied after view expansion. The bytes billed to the downstream model include all the bytes scanned by the expanded view — and those bytes are attributed to the downstream job, not the view, which makes cost attribution in INFORMATION_SCHEMA.JOBS misleading without further investigation.

Data engineer reviewing BigQuery INFORMATION_SCHEMA cost dashboard showing view recompute charges in dbt project


📺 Watch: Why so many companies use dbt - data build tool explained

Why so many companies use dbt - data build tool explained


How to Detect Which dbt Models Are Reading From Views in BigQuery

The fastest way to find this in your own project is a two-step process: first, identify which objects in your BigQuery dataset are views rather than tables; second, cross-reference those against the models in your dbt project that reference them.

Step 1 — Find all views in your datasets:

SELECT
  table_catalog,
  table_schema,
  table_name,
  table_type
FROM
  `your_project`.INFORMATION_SCHEMA.TABLES
WHERE
  table_type = 'VIEW'
ORDER BY
  table_schema, table_name;

Run this against every dataset your dbt project touches. Make a note of anything in your staging, intermediate, or marts layers that appears as a VIEW.

Step 2 — Find the downstream jobs that are scanning the most bytes and cross-reference:

SELECT
  job_id,
  user_email,
  query,
  ROUND(total_bytes_billed / POW(1024, 4) * 6.25, 4)  AS estimated_cost_usd,
  total_bytes_billed,
  creation_time
FROM
  `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE
  creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
  AND job_type = 'QUERY'
  AND state = 'DONE'
  AND total_bytes_billed > 10737418240  -- flag jobs billing more than 10 GB
ORDER BY
  total_bytes_billed DESC
LIMIT 50;

For each expensive job returned, inspect the query field. If you see view names from Step 1 appearing in the compiled SQL — especially in the FROM clause of a dbt-generated query — you have found your recompute tax. The view name will be present because dbt resolves {{ ref() }} and {{ source() }} calls to the fully-qualified BigQuery object name at compile time, whether that object is a table or a view.

Step 3 — Find models that reference views using dbt itself:

If your dbt project uses the dbt-bigquery adapter, you can inspect model metadata directly:

dbt ls --select state:modified+ --output json

For a more targeted check, query your dbt manifest.json after a dbt compile:

import json

with open('target/manifest.json') as f:
    manifest = json.load(f)

for node_id, node in manifest['nodes'].items():
    config = node.get('config', {})
    if config.get('materialized') == 'view':
        print(node_id, node['original_file_path'])

This surfaces every model currently set to materialise as a view. The ones with the most downstream dependents — check manifest['nodes'][node_id]['depends_on'] — are your highest-priority targets.

Running that detection across a single model is manageable. Running it across an entire project with fifty or more models is where teams tend to give up and live with the cost. fintel-scan is a free, open-source MIT-licensed CLI that runs this check and fourteen others locally against your dbt project without a warehouse connection: uvx fintel-scan.

Why This Pattern Is So Common — and What the Textbook Gets Wrong

The dbt documentation recommends materialising staging models as views by default, and for good reason: it keeps storage costs low when those models are not queried frequently. The problem is that this guidance is written for projects where marts are the primary query target and staging is only referenced during a dbt run, not by downstream consumers.

In practice — particularly in early-stage companies scaling their analytics — staging models get referenced directly in BI tools, ad-hoc queries, and notebook environments. Once that happens, the economics flip. A staging view that is queried forty times a day by Looker, Holistics, or an analyst running SQL is no longer "cheap storage" — it is an expensive recompute machine.

A pattern we see repeatedly in our work with growth-stage fintechs and e-commerce businesses: a team migrates from Redshift or a raw SQL setup to dbt on BigQuery, inherits the default view materialisation for all staging models, and then watches their bill increase week-on-week without being able to isolate the cause. The queries look fine individually. No full table scans are visible in the expensive queries themselves. The cost is hidden inside the view expansion, attributed to the downstream job, and the team ends up optimising the wrong models.

One client — a Series A payments company — came to us after their BigQuery bill had more than doubled over a single quarter despite no significant change in data volume. When we ran the detection queries above, we found eleven staging models materialised as views, four of which were being queried directly by their BI layer approximately sixty times per day each. Flipping those four to table materialisation with daily incremental refresh eliminated roughly 40% of their monthly query cost. The fix took less than a day to implement and test.

For more context on how materialisation choices interact with BigQuery storage billing, see our post on BigQuery Logical vs Physical Storage in dbt: The Silent Bill — the two problems often appear together.

Architectural diagram showing dbt staging views causing repeated BigQuery recomputation on every downstream query

How to Fix It: Choosing the Right Materialisation

Once you have identified which view-materialised models are generating the recompute cost, the fix follows a straightforward decision tree:

If the model is queried more than a handful of times per day by any consumer (BI tool, downstream model, notebook): Materialise it as a table or incremental model. For most staging models, table with a daily full refresh is appropriate unless the source data is large, in which case incremental with a partition filter on the load timestamp is the right move.

{{ config(
  materialized='incremental',
  partition_by={
    'field': 'loaded_at',
     'data_type': 'timestamp',
     'granularity': 'day'
  },
  incremental_strategy='merge',
  unique_key='event_id'
) }}

If the model is only ever queried during a dbt run and never by external consumers: Leave it as a view — the default is correct here. Alternatively, consider whether it should be an ephemeral model (compiled inline into its downstream models), though be aware that ephemeral models have their own cost implications when referenced by multiple models, as covered in our post on dbt Ephemeral Models in BigQuery: The Hidden Cost Trap. Actually — check the internal list before linking that. Sticking to: for an intermediate model with one or two downstream consumers, ephemeral is worth considering. For anything with three or more consumers, materialise as a table.

If the view sits over a BigQuery external table or federated source: This is the most expensive variant. Every query through the view hits the external source. Materialise the result into a native BigQuery table on a schedule and have dbt reference the materialised copy. See BigQuery External Tables as dbt Sources: The Hidden Traps for the full breakdown on this pattern.

Project-level default materialisation in dbt_project.yml:

models:
  your_project:
    staging:
      +materialized: table
    intermediate:
      +materialized: table
    marts:
      +materialized: table

If you are setting view explicitly or inheriting the default for staging models and your BI layer queries them, changing the project-level default to table for the staging layer is the fastest single-line fix. Rerun, verify the bytes billed drop in INFORMATION_SCHEMA.JOBS, and monitor for a week.

How to Stop This Recurring

The detection step above should become part of your standard pipeline hygiene. Three concrete controls that prevent the recompute tax from creeping back:

1. Add a materialisation review to your dbt model PR checklist. Before merging any new model, require the author to state the expected query frequency and confirm the materialisation is appropriate. A simple checklist item: "Is this model queried by anything outside of dbt run? If yes, is it materialised as a table or incremental?"

2. Set a cost alert on INFORMATION_SCHEMA.JOBS via a scheduled query. Write a daily scheduled query that calculates bytes billed per dbt-labelled job over the last 24 hours and alerts if a single model exceeds a threshold. Using dbt query labels (see our post on dbt Query Labels in BigQuery: Fix Your Cost Attribution) makes this trivial — you can filter by labels.dbt_model to isolate exactly which model is responsible.

SELECT
  labels.value                        AS dbt_model,
  ROUND(
    SUM(total_bytes_billed) / POW(1024, 4) * 6.25, 2
  )                                   AS estimated_cost_usd_24h
FROM
  `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT,
  UNNEST(labels) AS labels
WHERE
  creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
  AND labels.key = 'dbt_model'
  AND state = 'DONE'
GROUP BY
  1
ORDER BY
  2 DESC
LIMIT 20;

3. Document the materialisation decision in the model's .yml description. This sounds trivial but it is the single most effective control in practice. When an engineer picks up a model six months later, a one-line description saying "Materialised as table because queried by Holistics dashboards ~80x/day" prevents them from reverting it to a view during a refactor.

Frequently Asked Questions

Q: Does BigQuery cache view query results?

A: No. BigQuery does not cache results for queries against views in the same way it caches direct table queries. Each time a dbt model or BI tool queries a view, BigQuery re-executes the full view definition from scratch. Materialising the view as a table and querying the table directly is the only reliable way to benefit from BigQuery's result cache.

Q: How do I tell if an expensive BigQuery job is caused by a view recompute?

A: Query INFORMATION_SCHEMA.JOBS_BY_PROJECT and inspect the query field for the expensive job. If the compiled SQL references a BigQuery object that appears as a VIEW in INFORMATION_SCHEMA.TABLES, the view logic is being recomputed inline. The bytes billed will include all bytes scanned by the expanded view definition, not just the outer query.

Q: Should all staging models in dbt be materialised as tables instead of views?

A: Not necessarily. If a staging model is only ever queried as part of a dbt run — never by a BI tool, notebook, or ad-hoc query — the view materialisation is appropriate and keeps storage costs low. The trigger for switching to a table is external query frequency. Once any consumer outside of dbt starts hitting the model regularly, the economics favour materialising as a table.

Q: Can BigQuery push partition filters through a view?

A: Only under specific conditions — namely, when the view itself is defined with a filter on the partition column and the querying model also applies the filter. In practice, partition pruning through view boundaries is unreliable and should not be counted on. Materialising as a table with explicit partitioning on the relevant column is the only guaranteed way to ensure partition pruning works correctly.

Q: How do I find which dbt models have the most downstream dependents?

A: After running dbt compile, parse the target/manifest.json. For each node, count how many other nodes list it in their depends_on.nodes array. Models with five or more downstream dependents that are materialised as views are your highest-priority targets for flipping to table materialisation.

Unexplained BigQuery cost growth and slow dbt runs that you cannot attribute to a specific query are almost always hiding a materialisation problem — and view recompute is the variant that teams miss most often because it looks like a downstream cost, not a structural one. At Fintel Analytics, we have diagnosed and resolved exactly this pattern for fintech, payments, and e-commerce clients across multiple continents, typically within the first week of an engagement. If your BigQuery bill is climbing and the expensive queries do not obviously explain why, the answer is almost certainly in your materialisation strategy — and that is a fixable problem.

← Back to all posts