Guide to Creating and Managing BigQuery Partitioned Tables for Cost Control
Learn how to create time-unit and integer-range partitioned tables in BigQuery, enable require_partition_filter, and verify partition pruning to lower query costs and avoid full table scans.
18 Aug 2025, 01:16 UTC

In Google BigQuery, query cost is directly proportional to the number of bytes scanned. When a table grows beyond a few gigabytes, a query that scans the entire table can become expensive and slow. Partitioning solves this by physically dividing the table into segments based on a column, allowing BigQuery to skip irrelevant segments when a filter matches the partition column.
Desired Outcome
Create a partitioned table that reduces scanned bytes, enforce mandatory partition filters to avoid accidental full scans, and verify that partition pruning works as expected.
Prerequisites
- A Google Cloud project with the BigQuery API enabled.
- A column suitable for partitioning: DATE, DATETIME, TIMESTAMP, or INT64.
- BigQuery Data Editor role (roles/bigquery.dataEditor) on the target dataset.
- The bq command‑line tool installed and authenticated, or access to the BigQuery console.
Procedure
1. Create a time‑unit partitioned table
Time‑unit partitioning is ideal for event logs or any data with a timestamp. The example below partitions by the DATE extracted from a TIMESTAMP column.
CREATE TABLE `PROJECT_ID.DATASET_ID.event_logs` ( event_id STRING, event_timestamp TIMESTAMP, payload STRING ) PARTITION BY DATE(event_timestamp);
Run the statement in the BigQuery console or with the bq CLI:
bq query --use_legacy_sql=false ' CREATE TABLE `PROJECT_ID.DATASET_ID.event_logs` ( event_id STRING, event_timestamp TIMESTAMP, payload STRING ) PARTITION BY DATE(event_timestamp); '
2. Create an integer‑range partitioned table
When data is keyed by a numeric identifier, integer‑range partitioning lets you define start, end, and interval values.
CREATE TABLE `PROJECT_ID.DATASET_ID.user_data` ( user_id INT64, signup_date DATE ) PARTITION BY RANGE(user_id) ( START 0, END 1000000, INTERVAL 1000 );
Each partition holds 1,000 user IDs, enabling the engine to jump directly to the relevant range when a query filters on user_id.
3. Enable the require_partition_filter guardrail
To prevent costly full‑table scans, set the table option require_partition_filter to true. This forces every query to include a filter on the partition column.
CREATE TABLE `PROJECT_ID.DATASET_ID.secure_logs` ( log_id STRING, log_date DATE ) PARTITION BY log_date OPTIONS ( require_partition_filter = true );
Attempting a query without a filter will return an error similar to:
Error: Query failed because no partitioning filter was found for the require_partition_filter option.
4. Verify partition pruning
After running a query, inspect the execution details to confirm that only the expected partitions were read.
- Execute a filtered query, for example:
SELECT COUNT(*) FROM `PROJECT_ID.DATASET_ID.event_logs` WHERE DATE(event_timestamp) = '2024-09-01';
DESCRIBE TABLE `PROJECT_ID.DATASET_ID.event_logs`;
The output will show a partition column with type DATE and the partitioning mode.
Expected Checks
- The table creation statement completes without error.
- Queries that include a filter on the partition column return results and show reduced bytes processed.
- Queries omitting the partition filter on a table with require_partition_filter = true return an error.
- The DESCRIBE output lists the correct partitioning column and type.
Recovery Options (Rollback)
Creating a table changes state; if you need to undo the operation, drop the table and recreate it with a different schema.
DROP TABLE `PROJECT_ID.DATASET_ID.event_logs`;
Warning: Dropping a table permanently deletes all data. Ensure you have a backup or export if the data is needed.
After dropping, you can rerun the CREATE statement with adjusted options (e.g., a different partition column or interval).
Limitations and Practical Tips
- Maximum of 4,000 partitions per table. Exceeding this limit leads to metadata overhead and can degrade performance.
- The partitioning column cannot be altered after table creation; you must migrate data to a new table with the desired scheme.
- Ingestion‑time partitioning creates a hidden _PARTITIONTIME column; it cannot be used as a regular column in queries.
- When using integer‑range partitioning, ensure the START, END, and INTERVAL values accommodate the full range of your data; values outside the range are placed in the UNPARTITIONED partition.
- To monitor partition usage over time, query the INFORMATION_SCHEMA.PARTITIONS view:
SELECT partition_id, total_rows FROM `PROJECT_ID.DATASET_ID.INFORMATION_SCHEMA.PARTITIONS` WHERE table_name = 'event_logs';
Combining Partitioning with Clustering
Within each partition, you can cluster on one or more columns to further improve performance for high‑cardinality filters.
CREATE TABLE `PROJECT_ID.DATASET_ID.clustered_logs` ( event_id STRING, event_timestamp TIMESTAMP, user_id INT64, payload STRING ) PARTITION BY DATE(event_timestamp) CLUSTER BY user_id;
Clustering does not affect the number of partitions but sorts data inside each partition, which can reduce bytes processed when queries filter on the clustered column.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.