Partitioned tables

BigQuery · Practical · 14 min

Partitioning is the main tool for cutting both cost and latency on large BigQuery tables.

A partitioned table is split into segments by the value of one column, and queries that filter on that column read only the matching partitions. On-demand, that directly lowers bytes billed, and the dry-run estimate reflects it.

Partitioning options:

  • Time-unit column: PARTITION BY DATE(event_ts), or TIMESTAMP_TRUNC(event_ts, HOUR), DATE_TRUNC(d, MONTH).
  • Ingestion time: PARTITION BY _PARTITIONDATE, based on when rows arrived.
  • Integer range: PARTITION BY RANGE_BUCKET(customer_id, GENERATE_ARRAY(0, 1000000, 10000)).

Useful options:

  • require_partition_filter = TRUE rejects queries that don't filter on the partition column, preventing accidental full scans.
  • partition_expiration_days = 400 drops old partitions automatically.

Pruning needs a filter BigQuery can evaluate against the partition column, e.g. WHERE DATE(event_ts) BETWEEN '2025-06-01' AND '2025-06-07' or a constant expression. Filters that hide the column behind complex expressions or depend on a subquery may not prune. Check the estimate.

A table can have at most 10,000 partitions, and each partition should hold a meaningful amount of data. Daily partitions on a small table only add overhead; use monthly, or rely on clustering alone.

Older datasets often use date-sharded tables (events_20250601, events_20250602...) queried with a wildcard table events_* and _TABLE_SUFFIX. GA4 exports still do. Partitioned tables are preferred for new designs: one schema, less metadata, better performance.

Example

CREATE TABLE web.events (
  event_ts   TIMESTAMP NOT NULL,
  user_id    STRING,
  event_name STRING,
  country    STRING
)
PARTITION BY DATE(event_ts)
OPTIONS (
  require_partition_filter = TRUE,
  partition_expiration_days = 400
);

-- Reads one week of partitions only
SELECT event_name, COUNT(*) AS n
FROM web.events
WHERE DATE(event_ts) BETWEEN '2025-06-01' AND '2025-06-07'
GROUP BY event_name;

-- Rejected: no filter on the partition column
-- SELECT COUNT(*) FROM web.events;   -- error: cannot query without a partition filter

-- Legacy date-sharded tables use a wildcard and _TABLE_SUFFIX
SELECT COUNT(*) FROM `proj.analytics_123.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20250601' AND '20250607';

Remember

  • Partition by a date/timestamp, ingestion time or integer range
  • Filters on the partition column prune data and lower cost
  • require_partition_filter prevents accidental full scans
  • Prefer partitioned tables over date-sharded tables

Watch out for

  • Partitioning a small table by day, creating thousands of tiny partitions with no benefit.

Check yourself

  1. A table is partitioned by DATE(created_at). Which query reads the least data?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.cloud.google.com/bigquery/docs/partitioned-tables