Updated August 2026. The 2023 version of this article recommended BigQuery flat-rate pricing, which Google retired on July 5, 2023. This version is written for BigQuery Editions and slot autoscaling.
Looker on BigQuery is the most common deployment we see, and the most common source of surprise invoices. Looker generates a lot of SQL, much of it from dashboards that refresh on a schedule whether anyone is watching or not, and BigQuery bills for every byte scanned or every slot-second consumed. The good news is that most of the cost is controllable from the Looker side once you understand how BigQuery charges in 2026.
Understand the pricing model you are actually on
BigQuery compute is billed one of two ways per project:
- On-demand: you pay per TiB of data scanned (list price $6.25 per TiB in US regions after the July 2023 increase; check the pricing page for your region). Cost tracks bytes, so narrow columns and good partition pruning are everything.
- Editions (Standard, Enterprise, Enterprise Plus): you pay per slot-hour for a reservation with a baseline number of slots (always on, optionally discounted with a one- or three-year commitment) and a max the reservation can autoscale up to. Cost tracks concurrency and query complexity rather than bytes. Standard has no commitment option and limited features; Enterprise adds things Looker shops tend to want, such as BI Engine and column-level security; Enterprise Plus adds compliance and cross-region features.
Flat-rate and flex slots are gone. If you still have a spreadsheet that models "unlimited queries for a fixed monthly price," throw it out. Autoscaling reservations bill in one-minute minimums per scale-up, so a dashboard that fires twenty tiles at once can spike a reservation to its max for a minute even if every query finishes in seconds.
Rough decision rule: measure your monthly bytes scanned and your peak concurrent slots from INFORMATION_SCHEMA.JOBS. If on-demand cost is consistently below what an Enterprise baseline of your p50 slot usage would cost, stay on-demand and fix scan volume. If you are scan-heavy and concurrency is smooth, an Edition with a modest baseline and a capped max is usually cheaper and, importantly, predictable.
Physical versus logical storage billing
Storage is now a real decision. Datasets bill on logical bytes by default, but you can switch a dataset to physical billing, which charges for compressed bytes (typically several times smaller) plus time-travel and fail-safe storage. For the wide, highly compressible fact tables Looker explores usually sit on, physical billing is often the cheaper option; for tables with heavy time-travel use it may not be. Check the TABLE_STORAGE view before you flip the switch.
Only select what you need
BigQuery is columnar, so the cost of a query is driven by the columns it touches. Looker already helps: an Explore only selects the fields the user picked. Where teams get hurt is in derived tables and sql_table_name expressions that SELECT * from a wide table, dragging every column into every query:
view: orders {
# Bad: every query now scans all columns of the source
# sql_table_name: (SELECT * FROM `proj.ds.orders_wide`) ;;
# Good: point at the table and let Looker project only the requested columns
sql_table_name: `proj.ds.orders` ;;
dimension: order_id {
type: number
primary_key: yes
sql: ${TABLE}.order_id ;;
}
}
Partition and cluster, and make Looker use them
Partition large fact tables by the date column users filter on, and cluster by the columns they group and filter by most (customer, status, region). Then make sure Looker's SQL can prune:
explore: orders {
# Force a date filter so partition pruning is always possible
always_filter: {
filters: [orders.created_date: "30 days"]
}
}
view: orders {
dimension_group: created {
type: time
timeframes: [raw, date, week, month, quarter, year]
# Partition column referenced directly, not wrapped in a function
sql: ${TABLE}.created_at ;;
datatype: timestamp
}
}
Two things silently defeat pruning: wrapping the partition column in a function in sql: (BigQuery cannot prune DATE(created_at) on a timestamp-partitioned table unless it is a supported expression), and CASTs introduced by mismatched datatype:. Check the bytes-processed figure in the Looker SQL tab; if a "last 7 days" query scans the whole table, pruning is broken.
Caching, datagroups and PDTs
Looker's cache is your first line of defense. Tie every explore to a datagroup whose sql_trigger is cheap and whose max_cache_age matches how often the data actually changes; a dashboard refreshed by fifty people an hour then costs one query an hour:
datagroup: orders_daily {
sql_trigger: SELECT MAX(loaded_at) FROM `proj.ds.orders` ;;
max_cache_age: "24 hours"
}
explore: orders {
persist_with: orders_daily
}
Persist expensive derived tables with the same datagroup so they rebuild once when the source changes, never on a timer:
view: customer_summary {
derived_table: {
sql: SELECT customer_id, COUNT(*) AS orders, SUM(total) AS lifetime_value
FROM `proj.ds.orders` GROUP BY customer_id ;;
datagroup_trigger: orders_daily
partition_keys: ["first_order_date"]
cluster_keys: ["customer_id"]
}
}
partition_keys and cluster_keys on the derived table mean the PDT itself is prunable, which is where a lot of Looker-on-BigQuery spend hides. Use increment_key for incremental PDTs on large append-only tables so each rebuild only processes new partitions.
Aggregate awareness
For dashboards that always show the same rollups, define aggregate_table inside the explore. Looker will transparently route matching queries to the small pre-aggregated table instead of the fact table. It is the single biggest cost lever on most instances and deserves its own tutorial.
Attribute spend to users and content
You cannot manage what you cannot attribute. Looker's System Activity History explore tells you which dashboards, users and schedules generate the most queries and runtime. On the BigQuery side, Looker sets the job's labels and the query comment with the user and dashboard, so you can join INFORMATION_SCHEMA.JOBS (bytes billed, slot milliseconds) back to Looker content. Build a weekly "top 20 most expensive dashboards" report from that join and review it; in our experience the top five are usually a scheduled dashboard nobody reads and a filter that defeats partition pruning.
A note on BI Engine
BI Engine can accelerate Looker queries, but its pricing and reservation model have changed more than once since 2023. Treat it as a per-project experiment you measure rather than a line item you budget for, and check the current pricing page before you rely on it.
Checklist
- Know whether each project is on-demand or an Edition, and why.
- Switch cold, compressible datasets to physical storage billing after checking time-travel usage.
- No
SELECT *insql_table_nameor derived tables. - Partition by date, cluster by the filter columns, and verify pruning in the SQL tab.
- Datagroup-driven caching and PDTs, never
persist_fortimers on BigQuery. - Aggregate tables for the top dashboards.
- A weekly cost-attribution report joining System Activity to
INFORMATION_SCHEMA.JOBS.
If your BigQuery bill has grown faster than your user count, our Looker health check includes a spend review that produces exactly this list for your instance. Get in touch.