In BigQuery integer range partitioning, is it possible to create non-uniform partition intervals? The use case that I have is that the date column is an integer (20201231 for example). All current queries target this date format and cannot be changed so the partition has to be on this field. Integer partitioning for this works, but only for the yearly case, eg:
PARTITION BY RANGE_BUCKET(INTEGER_DATE, GENERATE_ARRAY(20150000, 20250000, 10000))
for partitioning from 2015 to 2024. However, for other cases there are problems. Daily partitioning creates too many partitions for which there are no corresponding actual dates and easily crosses BQ's limit of 4000 partitions while not being able to span even 1 year's worth of data. For other cases (weekly, monthly etc.), the intervals are not aligned with the yearly calendar so some weeks and months will span two partitions. I tried the below (specifying a hardcoded array instead of GENERATE_ARRAY in the PARTITION BY clause) but BQ gives an error:
PARTITION BY RANGE_BUCKET(INTEGER_DATE, [20150000, 20160000, 20170000])
So, directly specifying the partition array does not work.
Does anyone know any alternatives, work arounds to this?
In BigQuery, creating non-uniform partition intervals for integer range partitioning directly is not supported, as you've noticed with the errors you've encountered. BigQuery currently only supports uniform range partitioning for integers. However, you can achieve non-uniform partitioning indirectly by leveraging additional columns or calculated fields. Here are a few workarounds:
Instead of partitioning by your integer date, consider using ingestion-time partitioning. This partitions data based on when it's loaded into BigQuery. While not directly aligned with your integer date, you can combine it with clever query filtering (e.g., using _PARTITIONTIME pseudo column) to achieve similar performance benefits.
Example UDF :
CREATE OR REPLACE FUNCTION my_dataset.custom_partition(integer_date INT64)
RETURNS INT64 AS (
CASE
WHEN integer_date >= 20230101 AND integer_date < 20240101 THEN 2023
WHEN integer_date >= 20220101 AND integer_date < 20230101 THEN 2022
-- Add more cases as needed
ELSE 0 -- Default partition
END
);
Use this UDF in your table creation:
CREATE TABLE your_partitioned_table
PARTITION BY my_dataset.custom_partition(INTEGER_DATE) AS
SELECT * FROM your_table;
Example:
CREATE TABLE your_clustered_table
PARTITION BY RANGE_BUCKET(INTEGER_DATE, GENERATE_ARRAY(20150000, 20250000, 10000))
CLUSTER BY INTEGER_DATE AS
SELECT * FROM your_table;
Important Considerations
Hybrid Partitioning with Custom Filtering:
To achieve a similar performance benefit as partitioning by integer date, use a combination of ingestion-time partitioning and filtering:
SELECT *
FROM my_dataset.my_table
WHERE _PARTITIONTIME >= TIMESTAMP("2023-01-01")
AND _PARTITIONTIME < TIMESTAMP("2024-01-01")
AND INTEGER_DATE >= 20230101 AND INTEGER_DATE < 20240101;
Ensure you choose the right partitioning strategy based on your specific use case, data distribution, and query patterns. For non-uniform partitioning, consider using a custom partitioning function or clustering to achieve the desired performance and flexibility.
Hi @ms4446 ,
Thanks for the response. The UDF example does not work for me. When trying to create the partitioned table as you mentioned, BQ gives the error: "PARTITION BY expression must be DATE, a DATE column ...".
Secondly, if however, this were successful, I assume the queries would also need to change right? The queries would have to use the UDF too, which is a problem for us since we have multiple users (internal and external) running queries using the integer date and we cannot expect them all to change that.
You're right about the limitations of using UDFs for partitioning in BigQuery. BigQuery requires the partition column to be a DATE, TIMESTAMP, or DATETIME type for time-based partitioning, or an INTEGER for range partitioning. Since using a UDF to map integers for partitioning isn't feasible, you'll need to consider alternative approaches that maintain the existing query patterns without requiring users to change their queries.
RANGE_BUCKET with GENERATE_ARRAY only supports uniform intervals, making it unsuitable for your non-uniform yearly-aligned partitioning needs.
BigQuery doesn't support directly passing a custom array to RANGE_BUCKET for partitioning.
The 4000 partition limit can be a constraint, especially with granular partitions such as daily intervals.
Workarounds and Alternatives
1. Use Integer Range Partitioning with Broader Ranges:
Create Yearly Partitions: Partition your table by broader integer ranges (e.g., by year) to reduce the number of partitions and avoid crossing the 4000 partition limit.
Example:
CREATE TABLE your_partitioned_table
PARTITION BY RANGE_BUCKET(INTEGER_DATE, GENERATE_ARRAY(20150000, 20250000, 10000)) AS
SELECT * FROM your_table;
2. Ingestion-Time Partitioning:
Combine with Integer Partitioning: Use ingestion-time partitioning to handle the data load efficiently and filter based on integer dates in queries. This avoids the complexity of changing user queries.
Example:
CREATE TABLE your_ingestion_partitioned_table
PARTITION BY _PARTITIONTIME AS
SELECT * FROM your_table;
Query Example:
SELECT *
FROM your_ingestion_partitioned_table
WHERE _PARTITIONTIME BETWEEN TIMESTAMP("2023-01-01") AND TIMESTAMP("2023-12-31")
AND INTEGER_DATE BETWEEN 20230101 AND 20231231;
3. Pre-Partitioning Data:
Create Multiple Tables: Create separate tables for different ranges of your integer dates and use table wildcard functions to query across these tables.
Example:
Query Example:
SELECT *
FROM `your_dataset.your_table_*`
WHERE _TABLE_SUFFIX BETWEEN '2015' AND '2016'
AND INTEGER_DATE BETWEEN 20150101 AND 20161231;
4. Clustering for Query Optimization:
Combine Partitioning and Clustering: Use yearly partitioning combined with clustering on INTEGER_DATE to improve performance within each partition.
Example:
CREATE TABLE your_clustered_table
PARTITION BY RANGE_BUCKET(INTEGER_DATE, GENERATE_ARRAY(20150000, 20250000, 10000))
CLUSTER BY INTEGER_DATE AS
SELECT * FROM your_table;
Important Considerations