Interview Prep/GCP & Cloud

Advanced13 min read

BigQuery Deep Dive: Performance and Cost

How BigQuery actually runs a query, why partitioning and clustering matter more than anything else, and how to answer the cost-optimisation questions that always come up.

BigQuery is a serverless, columnar, distributed warehouse. There are no clusters to size. That shifts the whole optimisation conversation from 'how much hardware' to 'how few bytes can I touch'.

How a query actually runs

  1. Data lives in Colossus (distributed storage) in Capacitor columnar format — not on the compute nodes
  2. Compute runs as slots, BigQuery's unit of CPU. A query is broken into stages executed across many slots
  3. Stages exchange data through an in-memory shuffle tier
  4. Because storage and compute are separate, only the columns and partitions you reference are read

Tip — The one-liner that shows you get it

'BigQuery bills on bytes scanned, and it is columnar — so SELECT * on a wide table is the single most expensive mistake you can make, even with a LIMIT 10. LIMIT does not reduce bytes scanned.'

Partitioning and clustering — the two big levers

Table partitioned by event_date, clustered by cell_site_id2026-03-01site 1000–1009site 1010–1019site 1020–1029site 1030–10392026-03-02site 1000–1009site 1010–1019site 1020–1029site 1030–10392026-03-03site 1000–1009site 1010–1019site 1020–1029site 1030–10392026-03-04site 1000–1009site 1010–1019site 1020–1029site 1030–1039WHERE event_date = '2026-03-03' → scans ONE partition (pruning)AND cell_site_id = 1015 → inside it, jumps to ONE block (clustering)
Partitioning eliminates whole date slices. Clustering sorts data inside each partition so BigQuery can skip blocks.
PartitioningClustering
What it doesPhysically splits the table into segmentsSorts data within each partition
Typical columnDate/timestamp (event_date), integer range, or ingestion timeHigh-cardinality filter/join keys (cell_site_id, subscriber_id)
LimitUp to 4,000 partitions per tableUp to 4 columns, order matters
Cost effectPrunes whole partitions — huge, predictable savingsSkips blocks — savings vary with data layout
Best forTime-range filters, cheap partition-level reloadsSelective equality filters and joins
create table core.network_kpi_daily (
  event_date    date       not null,
  cell_site_id  string     not null,
  subscriber_id string,
  total_gb      numeric,
  drop_rate     float64
)
partition by event_date
cluster by cell_site_id, subscriber_id
options (
  require_partition_filter = true,   -- stops accidental full-table scans
  partition_expiration_days = 400
);

Watch out — require_partition_filter is your cost guardrail

It forces every query to filter on the partition column, so nobody can accidentally scan five years of data. Naming this unprompted is a strong signal that you have owned a real BigQuery bill.

Cost optimisation checklist

  • Select only needed columns — never SELECT * on wide tables
  • Always filter on the partition column and enforce it with require_partition_filter
  • Materialise repeated heavy aggregations into summary tables instead of recomputing per dashboard
  • Use incremental models so you rebuild one day, not the whole history
  • Set expirations on partitions and staging datasets so junk data stops accruing storage cost
  • Use `--dry_run` / the UI byte estimate in CI to catch expensive queries before they merge
  • Choose the right pricing — on-demand for spiky/unpredictable, editions/slot reservations for steady heavy load
  • Avoid `SELECT DISTINCT` on huge sets when APPROX_COUNT_DISTINCT is good enough

Loading data efficiently

MethodCostLatencyUse when
Batch load from GCSFreeMinutesStandard ELT — the default choice
Storage Write APIPaidSecondsStreaming ingestion; exactly-once with stream offsets
Legacy streaming insertsPaid, pricierSecondsLegacy only — prefer Storage Write API
External / BigLake tablesQuery-timen/aQuery data in GCS without loading it

Practice questions

QA dashboard query costs far too much. How do you bring the cost down?show

Work top-down: reduce bytes scanned first, then reduce how often the scan happens.

  • Check the bytes scanned in the query plan — that is the bill, not the runtime
  • Trim to the columns actually used; wide SELECT * is usually most of the cost
  • Verify the partition filter is present and pruning — a filter on a non-partition column does nothing for cost
  • Add clustering on the frequent equality filters
  • Pre-aggregate into a daily summary table so the dashboard reads thousands of rows, not billions
  • Consider a materialized view or BI Engine for repeated dashboard traffic

Likely follow-up: How would you stop this from recurring across the team?

QWhat are slots, and what happens when you run out?show

A slot is a unit of compute. Queries are split into stages, and stages consume slots in parallel.

  • On-demand — you pay per byte scanned, with a large shared slot pool and no capacity guarantee
  • Editions / reservations — you buy a fixed slot capacity, pay for it regardless of use, and get predictable performance
  • When capacity is exhausted, queries queue rather than fail — the symptom is variable latency, not errors
  • Use reservations with assignments to stop an ad-hoc analyst query from starving production ETL
QHow do you handle updates and deletes in BigQuery?show

BigQuery is optimised for append and bulk rewrite, not row-level OLTP updates. Use MERGE for upserts and partition overwrites for reloads.

  • MERGE — the standard upsert, ideal for SCD Type 2 and idempotent streaming sinks
  • Partition overwrite — for a daily reload, replace the whole partition; simple, cheap, idempotent
  • Avoid many small UPDATE/DELETE statements — each rewrites storage blocks and they hit quota limits
  • Deletes on a partitioned table are far cheaper when they align to whole partitions
QWhat is the difference between a view, a materialized view, and a table?show

They trade freshness against cost and speed.

  • View — stored SQL, re-executed every time; no storage cost, no speed benefit
  • Materialized view — precomputed and incrementally refreshed by BigQuery; fast and automatically used to rewrite matching queries, but supports limited SQL
  • Table (built by dbt) — full control, any SQL, any refresh schedule; you own the freshness
  • For a layered dbt architecture, most core/mart models are tables or incremental tables precisely because you control when and how they refresh