What partition types does a BigQuery table support, and how is each declared?
answer
- three ways to slice a table
- one is not a real column at all
- arrival time versus event time
- RANGE_BUCKET over GENERATE_ARRAY
- declared once with PARTITION BY, never altered
basics
~20 sBigQuery supports three partition types: ingestion-time (the _PARTITIONTIME pseudo-column), time-unit column (a DATE, TIMESTAMP or DATETIME column at hour, day, month or year granularity), and integer range via RANGE_BUCKET. All are declared with PARTITION BY in the DDL.
solid answer
~40 sA BigQuery table can be partitioned in one of three ways, chosen once at creation with a `PARTITION BY` clause. **Ingestion-time**: BigQuery assigns each row to a partition by when it arrived, exposed through the `_PARTITIONTIME` / `_PARTITIONDATE` pseudo-columns — `PARTITION BY _PARTITIONDATE`. **Time-unit column**: you partition on a real DATE, TIMESTAMP or DATETIME column in the row, at hourly, daily, monthly or yearly granularity — `PARTITION BY DATE(event_ts)` or `PARTITION BY TIMESTAMP_TRUNC(event_ts, HOUR)`. **Integer range**: you bucket an INT64 column — `PARTITION BY RANGE_BUCKET(customer_id, GENERATE_ARRAY(0, 1000000, 10000))`. A table has at most one partitioning specification, and you cannot change it in place — you recreate the table. In practice time-unit column partitioning on the business event timestamp is the default choice, because queries filter on that column naturally.
code
sql · 11 lines-- Ingestion time, daily
CREATE TABLE ds.raw_logs (payload STRING)
PARTITION BY _PARTITIONDATE;
-- Time-unit column, hourly on a TIMESTAMP
CREATE TABLE ds.events (event_ts TIMESTAMP, user_id STRING)
PARTITION BY TIMESTAMP_TRUNC(event_ts, HOUR);
-- Integer range
CREATE TABLE ds.customers (customer_id INT64, name STRING)
PARTITION BY RANGE_BUCKET(customer_id, GENERATE_ARRAY(0, 1000000, 10000));go deeper
Be able to name the three types and write a CREATE TABLE with PARTITION BY DATE(ts). Knowing that the point is to reduce bytes scanned and billed is enough at this level.
Explain the mechanics: how each type assigns a row to a partition, what the granularity options are, where NULL and out-of-range rows land, and why the specification cannot be altered in place.
Show judgment about which axis to partition on given the real query filters, and explain why late-arriving data makes ingestion time a poor proxy for event time in business models.
Own the table-layout convention across the warehouse: which layer uses ingestion time, which uses event time, how partition expiration and required partition filters are enforced as defaults, and what the migration path is when a choice turns out wrong.
## What a partition is in BigQuery A partitioned BigQuery table is one logical table that the service splits into physical segments called partitions. Each partition holds the rows whose partitioning value falls in one bucket, and BigQuery tracks each partition's boundaries in table metadata. When a query carries a predicate on the partitioning column, the planner can decide *before running anything* which partitions can possibly contain matching rows and read only those. Under on-demand pricing you are billed for bytes read, so the partitions BigQuery skips are partitions you do not pay for. That is the whole point. The partitioning specification is chosen once, in the `CREATE TABLE` statement, and it cannot be altered afterwards — changing it means creating a new table and rewriting the data (typically `CREATE TABLE ... PARTITION BY ... AS SELECT * FROM old_table`). ## Type 1 — ingestion-time partitioning BigQuery assigns each row to a partition based on the time the row was ingested, not on anything in the row itself. The assignment is exposed by two pseudo-columns: `_PARTITIONTIME`, a TIMESTAMP truncated to the partition boundary in UTC, and `_PARTITIONDATE`, the DATE equivalent. You declare it as: ```sql CREATE TABLE ds.raw_logs (payload STRING) PARTITION BY _PARTITIONDATE; ``` Hourly, monthly and yearly ingestion-time granularity is available too, e.g. `PARTITION BY TIMESTAMP_TRUNC(_PARTITIONTIME, HOUR)`. Queries prune by filtering the pseudo-column: `WHERE _PARTITIONDATE = '2026-08-01'`. Ingestion-time partitioning suits raw landing tables where arrival time is the only time you trust, and it is what you get with append-only log dumps. Its weakness is that arrival time and event time diverge: a late-arriving event for yesterday lands in today's partition, so a business query on event date has to scan more than one partition. ## Type 2 — time-unit column partitioning You nominate a DATE, TIMESTAMP or DATETIME column that already exists in the row, and a granularity of hour, day, month or year: ```sql CREATE TABLE ds.events (event_ts TIMESTAMP, user_id STRING, amount NUMERIC) PARTITION BY DATE(event_ts); ``` For a DATE column the expression can be the column itself (`PARTITION BY event_date`) or a truncation (`PARTITION BY DATE_TRUNC(event_date, MONTH)`). For TIMESTAMP/DATETIME you use `DATE()`, `TIMESTAMP_TRUNC()` or `DATETIME_TRUNC()` with the granularity you want. This is the default recommendation: analysts filter on the business timestamp, so pruning happens without anyone thinking about it, and a late-arriving row still lands in the partition it logically belongs to. Rows whose partitioning column is NULL are collected in a dedicated `__NULL__` partition. ## Type 3 — integer range partitioning You bucket an INT64 column into fixed-width ranges using `RANGE_BUCKET` over a `GENERATE_ARRAY(start, end, interval)`: ```sql CREATE TABLE ds.customers (customer_id INT64, name STRING) PARTITION BY RANGE_BUCKET(customer_id, GENERATE_ARRAY(0, 1000000, 10000)); ``` Rows whose value falls outside `[start, end)` go to the `__UNPARTITIONED__` partition. This type is much rarer in practice. It fits a table whose dominant filter is a bounded numeric key — a customer or tenant id space you control — and it is a poor fit for an unbounded, growing id space, because the range is fixed at creation time and ids beyond `end` all pile into one place. ## Choosing between them Ask what the dominant `WHERE` clause looks like. If it filters on an event timestamp, partition by that column. If the table is a raw dump where nothing in the payload is a trustworthy time, ingestion time is honest. If the workload is keyed on a bounded integer, integer range can work. Only one axis is available per table, so the second-most-important filter column is not a partitioning candidate at all — that is what `CLUSTER BY` is for, and combining a date partition with clustering on the high-cardinality filter columns is the standard production layout. Two table options are worth setting alongside the partitioning: `partition_expiration_days`, which drops partitions automatically once they age out (a free, metadata-only delete), and `require_partition_filter = TRUE`, which rejects queries that do not filter on the partitioning column, so nobody accidentally scans five years of history.
- Can you change a table's partitioning column after it has data?No. The partitioning specification is fixed at creation. To change it you create a new table with the desired `PARTITION BY` (and `CLUSTER BY`) and rewrite the data, typically with `CREATE TABLE ... AS SELECT * FROM old_table`, then swap names. You can, however, change table options such as `partition_expiration_days` and `require_partition_filter` at any time with `ALTER TABLE ... SET OPTIONS`.
- Where do rows go when the partitioning column is NULL, or the integer falls outside the declared range?A NULL in a time-unit or integer-range partitioning column lands in the `__NULL__` partition. An integer outside the `GENERATE_ARRAY` bounds lands in `__UNPARTITIONED__`. Both are real partitions you can query and expire, but they defeat pruning for those rows, so a growing `__UNPARTITIONED__` partition is a signal your range was chosen too narrowly.
- When would you still pick ingestion-time partitioning over a column?When the payload has no trustworthy timestamp — raw exports, third-party dumps, opaque JSON — or when the operational unit really is the load batch, so that reloading a day means replacing one partition. It is also the simplest thing for append-only landing tables that a downstream job immediately reads once and transforms into event-time-partitioned models.
saying these in an interview costs you the question
- Thinking any column type can be a partition key
- Believing a table can be partitioned on two columns
- Assuming partitioning can be added later with ALTER TABLE
- Confusing ingestion time with the event timestamp in the row
- Calling PARTITION BY an index