
BigQuery Cost Control That Scales: 12 Patterns from Real Warehouses
Twelve battle-tested patterns to cut BigQuery spend 30–70% without touching data quality — partitioning, clustering, materialized views, reservations, and the FinOps loop that keeps savings from decaying.
BigQuery bills you on storage and bytes scanned (or slots). 90% of surprise bills are unbounded scans against wide, unpartitioned tables. Fix partitioning + clustering + SELECT * first, then layer materialized views, BI Engine, and right-sized reservations. Wrap it in a FinOps loop — labels, billing export, weekly review — so savings compound instead of decaying.
- Partition every fact table on event/ingest date and set require_partition_filter=true.
- Cluster on the 1–4 columns you filter or join on most — free win, no query changes.
- Kill SELECT * with a CI linter; enforce column projection at review time.
- Materialized views + BI Engine cover the top 20 dashboard queries at a flat cost.
- Attribute every dollar with query labels + billing export, and run a weekly FinOps review.
Know what you're paying for
BigQuery bills you on two axes: storage (cheap, split into active vs long-term) and compute — either on-demand ($/TB scanned) or slot-based reservations (BigQuery editions). Everything in this playbook maps to one of those two levers.
Before optimizing anything, spend one hour with INFORMATION_SCHEMA.JOBS_BY_PROJECT and your billing export. You'll almost always find that the top 10 queries account for 60–80% of spend. Fix those first; the long tail is noise.
The 12 patterns, ranked by ROI
- Partition every fact table on ingestion timestamp or event date. Turn on
require_partition_filter=trueso accidental full scans fail loudly instead of silently costing $2k. - Cluster on the 1–4 columns you filter or join on most. Zero query changes, up to 10× cheaper scans.
- Kill
SELECT *— enforce with a linter in CI. Projection is the single biggest lever on wide event tables. - Approximate aggregates (
APPROX_COUNT_DISTINCT,APPROX_QUANTILES) for dashboards. 10–100× cheaper, error < 1%. - Materialized views for the top 20 dashboard queries. Auto-refresh, transparent rewrites when the optimizer sees a match.
- BI Engine reservation for Looker Studio dashboards — sub-second queries, flat monthly cost, no per-scan surprises.
- Table sampling during development:
TABLESAMPLE SYSTEM (1 PERCENT). Saves you and the shared reservation. - Session / temp tables for exploratory work — dropped automatically, no long-term storage bill.
- Search indexes for needle-in-haystack log queries. 20× faster than
LIKE '%foo%'on TB-scale logs. - Right-size reservations — start with editions autoscaling, then buy 1-year commitments once demand stabilizes.
- Cross-region audits — egress between multi-region datasets is a silent killer. Co-locate storage with compute.
- Query labels + billing export — attribute every dollar to a team, workflow, or dashboard. Without labels, cost control is guesswork.
Partition + cluster like you mean it
Partitioning splits a table into daily (or hourly) chunks; clustering physically sorts within each partition. Together they let the optimizer skip 99% of the table for typical queries.
create table analytics.events (
event_id string,
user_id string,
event_type string,
country string,
event_ts timestamp,
payload json
)
partition by date(event_ts)
cluster by event_type, country, user_id
options (
partition_expiration_days = 400,
require_partition_filter = true
);Rules of thumb:
- Partition on the column you filter by 90% of the time (usually event date).
- Cluster keys go from most-selective to least. Don't cluster on high-cardinality free text.
- Set
partition_expiration_daysto auto-drop cold partitions if the business allows. - Turn on
require_partition_filter— you'll catch bad queries at development time, not at end-of-month.
MERGE with a partition filter in the ON clause; otherwise you rewrite the entire target.Queries you should never run
-- ❌ full scan, no filter, no projection
select * from events;
-- ❌ regex on unindexed text against TBs of logs
select * from logs where regexp_contains(message, r'timeout');
-- ❌ cross-join on a wide table
select a.*, b.* from big_a a cross join big_b b;
-- ❌ CURRENT_TIMESTAMP() in a WHERE that defeats partition pruning
select * from events where event_ts > current_timestamp() - interval 1 day;
-- ✅ scoped, projected, partition-aware
select event_id, user_id, event_type
from events
where event_date between date_sub(current_date(), interval 7 day)
and current_date()
and event_type in ('signup','purchase');The current_timestamp() anti-pattern is subtle: BigQuery can't prune partitions on non-constant expressions in some versions of the optimizer. Cast to current_date() or use a scripting variable computed once.
Materialized views + BI Engine: the flat-cost tier
Once you know your top 20 dashboard queries (from INFORMATION_SCHEMA), turn each into a materialized view. The optimizer transparently rewrites qualifying queries to hit the MV instead of the base table.
create materialized view analytics.daily_active_users
partition by activity_date
cluster by country
as
select
date(event_ts) as activity_date,
country,
approx_count_distinct(user_id) as dau
from analytics.events
where event_type in ('login','session_start')
group by 1, 2;Layer BI Engine on top for Looker Studio. It caches hot query results in memory; sub-second latency, and the monthly reservation is often cheaper than the on-demand cost it replaces.
On-demand vs editions: pick with data, not vibes
| Workload | Best fit | Why |
|---|---|---|
| Spiky, unpredictable | On-demand | Pay per TB; zero commitment. |
| Steady + predictable | Editions + autoscaling | Slot-hours are cheaper than $/TB at scale. |
| Heavy dashboards | Editions + BI Engine | Flat cost, cache hit rates near 100%. |
| ML training / batch | Editions with 1y commit | Highest discount, tolerant of queue latency. |
Rule of thumb: switch to editions once you can predict monthly TB scanned within ±30%. Start with a small baseline + generous max slots, then trim after two billing cycles.
Attribute every dollar (FinOps in a week)
- Enable the billing export to BigQuery.
- Tag every query with labels:
team,workflow,env,dashboard. dbt, Airflow, and n8n all support labels on the query API. - Build a single materialized view that joins
INFORMATION_SCHEMA.JOBS_BY_PROJECTwith billing on job ID. - Pipe it into Looker Studio with filters by team + dashboard. Ship a weekly digest to owners.
- Set a per-team budget alert at 80% of last month's baseline.
select
labels.value as team,
date(creation_time) as day,
sum(total_bytes_billed) / pow(1024,4) as tb_billed,
sum(total_slot_ms) / 1000 / 3600 as slot_hours,
count(*) as jobs
from `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT j,
unnest(labels) as labels
where labels.key = 'team'
and creation_time > timestamp_sub(current_timestamp(), interval 30 day)
group by 1, 2
order by 2 desc, 3 desc;INFORMATION_SCHEMA without a materialized cache — the metadata queries themselves cost slot-hours and will show up in your own report.The weekly FinOps loop
Cost control decays the moment attention moves elsewhere. Institutionalize a 30-minute weekly ritual:
- Top 10 by cost — the biggest queries of the week. Owner named. Fix or accept.
- Top 10 by growth — queries whose cost grew >50% week-over-week. Usually a new dashboard or a bad JOIN.
- Unlabeled spend — anything >$50 without labels. Chase the owner and label it.
- Idle reservations — reservations under 40% utilization for two weeks are ready to shrink.
- New MV candidates — repeated identical query shape from >3 sources? Materialize it.
Track the exact same five items every week for a quarter. The graph of savings vs baseline is the artifact you show leadership.
Common failure modes and how to unstick them
| Symptom | Likely cause | Fix |
|---|---|---|
| Sudden 5–10× cost spike | New dashboard scanning unpartitioned table | Force partition filter, add MV |
| MV never used by optimizer | Query shape doesn't match MV definition | Simplify query or add second MV |
| Reservation queue latency | Autoscaling max too low or slot-hungry ML job | Split ML into own reservation |
| Cross-region egress bill | Dataset in US, compute in EU | Move dataset or use scheduled transfer |
| INFORMATION_SCHEMA queries expensive | Full 6-month scan on every dashboard load | Cache to a daily MV |
How this fits your broader stack
Cost control on the warehouse is only half the picture. Pair it with the same discipline on your AI workloads and pipelines:
Frequently asked
On-demand or slots?
On-demand until you can predict monthly TB scanned within ±30%. After that, editions with autoscaling almost always beats on-demand for the same workload. Add 1-year commitments once your baseline is stable.
Do materialized views help with high-cardinality group-bys?
Yes — as long as the base table is partitioned and clustered on the same keys and the refresh cadence matches the freshness SLA. Otherwise the refresh cost eats the query savings.
How do I stop developers from accidentally scanning a whole fact table?
Set require_partition_filter=true on the table, add a CI linter (sqlfluff or a custom dbt macro) that rejects SELECT *, and give every developer read access via a service account with a per-user byte-billed quota.
Are BI Engine reservations worth it for small teams?
If a handful of Looker Studio dashboards drive most of your on-demand spend, yes — even a 2GB reservation typically pays for itself. Measure first with a two-week trial.
Building something similar?
I help teams ship production-grade AI agents, n8n workflows, and data platforms. Let's talk about what you're building.
Work with me
Islam Gamal
AI, Data & Automation Engineer. I design and ship production AI agents, n8n workflows, and cloud data platforms — with a focus on reliability, cost, and measurable business impact. Founder of Tashghil and Tek bil Arabi.
More from Islam Gamal
The Modern Data Stack in 2026: What Actually Matters (and What to Skip)
A no-hype 30-minute tour of the 2026 data stack: ingestion, warehouse, transformation, orchestration, semantic layer, reverse ETL, observability, and the AI layer on top — with concrete tool picks, cost anchors, and the anti-patterns that quietly eat budgets.
Building an LLM Cost Dashboard in BigQuery That Actually Changes Behavior
The end-to-end blueprint for tracking every LLM call in BigQuery — schema, ingestion patterns, cost attribution by team/feature/user, unit-economics dashboards, and the alerts that catch runaway spend before finance does.
Webhooks That Don't Lie: Idempotency, Signatures, Replay, and a Playbook
An opinionated production guide to receiving webhooks — signature verification, idempotency keys, store-then-process pipelines, dead-letter queues, replay UIs, provider quirks, and a local testing loop your team will actually use.
Browse every article by Islam Gamal on the author page.