Why does a filter on a BigQuery date-partitioned table sometimes scan every partition?
answer
- the decision is made before reading anything
- a value the planner cannot see yet
- OR widens the surviving set
- functions hide the partition boundary
- dry run shows pruning, and shows it failing
basics
~20 sBigQuery prunes partitions only when it can resolve the predicate on the partitioning column before execution. Comparing that column to a subquery result, ORing it with a non-partition predicate, or wrapping it in an expression BigQuery cannot map back to partition boundaries all force a full scan.
solid answer
~50 sPartition pruning happens at planning time: BigQuery must be able to turn the predicate into a set of partition boundaries before any data is read. Several query shapes defeat that. Comparing the partitioning column to a **subquery** result — `WHERE event_date = (SELECT MAX(event_date) FROM t)` — leaves the value unknown until execution, so all partitions are read. **ORing** a partition predicate with a filter on another column (`WHERE event_date = '2026-08-01' OR user_id = 'x'`) means rows anywhere could match. Wrapping the column in an expression that is not the partitioning expression, such as `WHERE CAST(event_ts AS STRING) LIKE '2026-08%'`, hides the boundary. And filtering a *different* column that merely correlates with the partition column prunes nothing at all. The fix is a literal or parameterized range predicate directly on the partitioning column; the guardrail is `require_partition_filter = TRUE` on the table. A dry run tells you immediately — pruning is reflected in the estimated bytes.
code
sql · 11 lines-- Scans every partition: bound is unknown at planning time
SELECT SUM(amount) FROM ds.events
WHERE event_date = (SELECT MAX(event_date) FROM ds.events);
-- Scans every partition: the OR branch can match anywhere
SELECT SUM(amount) FROM ds.events
WHERE event_date = '2026-08-01' OR user_id = 'u_42';
-- Prunes: constant range directly on the partitioning column
SELECT SUM(amount) FROM ds.events
WHERE event_date BETWEEN '2026-08-01' AND '2026-08-07';go deeper
Know that filtering on the partitioning column with concrete dates is what makes a query cheap, and that filtering on some other date column does nothing for cost.
Explain that pruning is a planning-time decision and name the shapes that break it: subquery-derived values, OR with a non-partition predicate, and the column wrapped in a function.
Demonstrate the diagnosis loop — dry run, INFORMATION_SCHEMA.JOBS bytes billed, rewrite — and the rewrite patterns using parameters or scripting variables for a derived bound.
Own the guardrails: required partition filters as a table convention, partition expiration, cost monitoring on billed bytes, and the review process that catches generated SQL emitting unprunable predicates.
## What pruning actually is BigQuery stores a partitioned table as a set of physical partitions plus metadata recording which partitioning value each one holds. When you submit a query, the planner examines the `WHERE` clause, tries to derive the set of partition values that could possibly satisfy it, and reads only those partitions. Everything else is never opened, and under on-demand pricing never billed. This is *static* elimination — it must be decided from the query text and table metadata alone, before a single byte is scanned. That requirement is the source of every failure mode below: if BigQuery cannot compute the surviving partition set at planning time, it must assume every partition survives. ## Shape 1 — the value comes from a subquery The classic: ```sql SELECT SUM(amount) FROM ds.events WHERE event_date = (SELECT MAX(event_date) FROM ds.events); ``` This looks like it touches one day. It does not. The scalar subquery is evaluated during execution, so at planning time the comparison value is unknown and no partition can be excluded — you scan the whole table and pay for it. The same applies to a partition column compared against a column from a joined table. The rewrite is to resolve the value out of band and inline it: run the `MAX` first and substitute a literal, use a query parameter, or use scripting to `SET` a variable and then reference it in the filter. Where the maximum is simply "the latest data", a bounded range against `CURRENT_DATE()` is usually better still, because it is a constant the planner can fold. ## Shape 2 — OR with a non-partition predicate ```sql WHERE event_date = '2026-08-01' OR user_id = 'u_42' ``` A row satisfying the second disjunct can live in any partition, so the surviving set is "all". Disjunction only prunes when *every* branch constrains the partitioning column. This is a frequent accident in generated SQL and in "optional filter" patterns like `WHERE event_date = @d OR @d IS NULL`. Split the query into a `UNION ALL` of two pruned branches, or push the optional-filter logic into the application so the emitted SQL always carries a real range. ## Shape 3 — the column is buried in an expression If the table is `PARTITION BY DATE(event_ts)`, then `WHERE DATE(event_ts) = '2026-08-01'` and `WHERE event_ts >= TIMESTAMP('2026-08-01') AND event_ts < TIMESTAMP('2026-08-02')` both prune, because BigQuery can map them onto partition boundaries. But: ```sql WHERE FORMAT_TIMESTAMP('%Y-%m', event_ts) = '2026-08' WHERE CAST(event_ts AS STRING) LIKE '2026-08%' ``` turn the partitioning value into a string first. The planner cannot invert an arbitrary function, so the boundary information is lost and everything is read. Always express the filter as a comparison or range on the partitioning column (or its declared partitioning expression) against constants. ## Shape 4 — the wrong column entirely A table partitioned by `DATE(event_ts)` and filtered on `created_at`, or on `order_id`, prunes nothing even if the two columns are perfectly correlated in practice. Correlation is not metadata. This is the most common report of "partitioning doesn't work" — the dashboard's date filter is on a different date column than the one the table is partitioned on. ## Diagnosing it A dry run is the fastest instrument: `bq query --dry_run --use_legacy_sql=false '<sql>'`, or the estimated-bytes readout in the console. Partition pruning **is** reflected in that estimate, because it is decided at planning time. So the loop is: run a dry run, see billions of bytes where you expected millions, and go find which of the four shapes above your `WHERE` clause has. (Clustering is different — block skipping happens at runtime, so a dry run does not credit it.) After the fact, `INFORMATION_SCHEMA.JOBS` gives `total_bytes_processed` and `total_bytes_billed` per job, which is how you find the expensive recurring queries rather than the one you happen to be looking at. ## Preventing it Set `require_partition_filter = TRUE` on large partitioned tables, either at creation or later with `ALTER TABLE ... SET OPTIONS`. Queries that do not carry a filter on the partitioning column then fail outright instead of quietly scanning years of data. It is a blunt instrument — it demands a filter, and cannot force that filter to be a prunable *shape* — but it converts the most expensive class of mistake into an immediate, obvious error. Pair it with `partition_expiration_days` so history that nobody queries stops existing, and with a scheduled review of the top jobs by `total_bytes_billed`.
- Does a query parameter prevent pruning the way a subquery does?No. A parameter is supplied with the job, so the planner has a concrete value and can prune normally. That makes parameterization the standard rewrite for a subquery-derived bound: compute the value in the client or in a scripting variable, pass it in, and keep the predicate a plain comparison on the partitioning column.
- How would you rewrite WHERE event_date = (SELECT MAX(event_date) FROM t) so it prunes?Resolve the bound first. Either run the MAX as a separate cheap query (it reads one column, and on a partitioned table you can bound it to recent days) and inline the result as a literal or parameter, or use BigQuery scripting: `DECLARE d DATE; SET d = (SELECT MAX(event_date) ...);` then filter `WHERE event_date = d`, which the planner sees as a constant.
- What does require_partition_filter guarantee, and what does it not?It guarantees every query carries a predicate on the partitioning column, failing the job otherwise. It does not guarantee that predicate prunes well — a filter comparing the column to a subquery satisfies the requirement while still scanning everything. Treat it as a floor against catastrophic full scans, not as a substitute for reviewing query shapes.
saying these in an interview costs you the question
- Assuming any WHERE clause mentioning a date prunes
- Thinking BigQuery prunes on a correlated but different column
- Believing pruning happens while the query runs
- Claiming SELECT with fewer columns fixes a missing partition filter
- Trusting require_partition_filter to make every query cheap