Start by ensuring queries actually prune partitions and use clustering effectively. Key patterns: use direct partition column predicates (no wrapping), select only needed columns, avoid non-deterministic expressions on the partition key, and precompute heavy work with materialized views or denormalized tables.Examples and concrete changes:1) Table structure (recommended)sql
CREATE TABLE project.dataset.events
PARTITION BY DATE(event_date) -- explicit date column
CLUSTER BY user_id;
2) Partition-filter rewrite — avoid wrapping partition columnBad (scans whole table):sql
SELECT user_id, COUNT(*) FROM events
WHERE DATE(event_timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY user_id;
Good (prunes partitions):sql
SELECT user_id, COUNT(*) FROM events
WHERE event_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY) AND CURRENT_DATE()
GROUP BY user_id;
Or use ingestion-time pseudo-column:sql
WHERE _PARTITIONDATE BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY) AND CURRENT_DATE()
3) Avoid non-deterministic or casting on partition columnBad: WHERE FORMAT_DATE('%Y-%m-%d', event_date) = '2025-11-01'Good: WHERE event_date = '2025-11-01'4) Column pruning — select only what you needBad:sql
SELECT * FROM events WHERE event_date = '2025-11-01'
Good:sql
SELECT user_id, event_type, revenue FROM events WHERE event_date = '2025-11-01'
5) Use clustering filters to reduce scanned bytesWhen clustering by user_id, include equality or range filters on user_id in WHERE to exploit clustering:sql
WHERE event_date BETWEEN ... AND ... AND user_id IN ('u1','u2'...)
6) Materialized view for repeated aggregates (last-30d)Use MV to pre-aggregate expensive joins/aggregations:sql
CREATE MATERIALIZED VIEW dataset.mv_user_daily AS
SELECT user_id, event_date, COUNT(*) AS cnt, SUM(revenue) AS rev
FROM dataset.events
GROUP BY user_id, event_date;
Querying the MV reads far less data:sql
SELECT user_id, SUM(cnt) FROM dataset.mv_user_daily
WHERE event_date BETWEEN ... AND ... GROUP BY user_id;
7) Denormalization / ETL precomputeIf analysts frequently join user profile data, maintain a denormalized table or daily snapshot:- ETL job writes events_enriched (partitioned by date) joining user attributes at ingest time.This moves join cost off ad-hoc queries and reduces scanned bytes.8) Other tips- Use CLUSTER BY on multiple high-selectivity columns if access patterns warrant.- Consider partition expiration to keep storage small.- Use APPROX_* functions for faster, cheaper cardinality estimates.- Monitor bytes scanned with INFORMATION_SCHEMA.JOBS_BY_PROJECT and query plan.Reasoning: partition predicates that are sargable allow BigQuery to skip files; clustering reduces scanned columns within partitions when filters target clustered keys; selecting fewer columns reduces columnar reads; materialized views/denormalized tables trade storage/ETL cost for much lower query scan cost on common queries.