Partitioning splits an accelerated dataset into multiple physical units (files or in-memory tables) keyed by an expression evaluated per row. Queries that filter on the partitioning expression — or on a column it references — only read the partitions that can match, dramatically reducing the data scanned.
This config writes 50 separate partitions, each containing the rows whose PULocationID hashes to the same bucket. A query like WHERE PULocationID = 132 only reads the single bucket partition that could contain 132.
partition_by expression for every row and routes the row to a partition keyed by the expression's value.year then month — to combine pruning across dimensions.Partitioning is most useful when:
region = 'EU', tenant_id IN (...), created_at >= today() - INTERVAL '7 days').bucket(N, col) collapses to N partitions.partition_byLives directly under acceleration: (not under acceleration.params:). It's a list of expressions; each entry is either a plain string or a single-entry { name: expression } mapping:
Multi-entry mappings (- year: "…", month: "…" on one list item) are rejected at load time.
| Engine | Required mode: | Multi-expression | Layout |
|---|---|---|---|
arrow | memory | Yes | One Arrow MemTable per partition value. |
cayenne | file | Yes | One Vortex table per partition; catalog in a SQLite metadata file. |
An acceleration with no engine uses Cayenne on Linux and macOS, so partition_by with no engine needs a file mode, such as mode: file. Set engine: arrow to partition in memory. See Default Engine (opens in a new tab).
duckdb, sqlite, postgres, and turso accelerators do not support partition_by; configuring it on those engines is rejected at load time. Use arrow or cayenne for partitioned acceleration.
partition_by accepts any DataFusion-compatible scalar SQL expression that returns a String, integer, Boolean, or Timestamp and references exactly one column from the dataset. The most common partition transforms are:
bucket(num_buckets, column)Hashes column into num_buckets deterministic buckets.
num_buckets must be a positive integer literal ≤ 1,000,000.num_buckets — bucket(50, …) (Int64 literal) returns Int64; bucket(50::int32, …) returns Int32.num_buckets (uses ahash with a fixed seed).user_id, account_id, device_id).See the bucket reference (opens in a new tab) for the full SQL signature.
truncate(width, value)Truncates to the next-lower multiple of width. Iceberg's truncate transform.
width must be a positive Int64 literal.value may be any signed/unsigned integer, Decimal128/Decimal256, Utf8 (string), or Binary. For strings/binary, returns the first width units.value.truncate(1000, amount)), or grouping strings by prefix (truncate(2, country_code)).See the truncate reference (opens in a new tab) for examples.
date_part(unit, column) and date_trunc(unit, column)Built-in DataFusion datetime functions. Useful for time-based partitioning at year, month, day, or hour granularity.
date_part('year', col) returns the integer year (e.g. 2026).date_trunc('day', col) returns the timestamp truncated to the start of the day.YEAR(col), MONTH(col), DAY(col), etc. are aliases of date_part(...) and work identically.
:::note Filter pruning for date_part() is not yet implemented
A date_part('year', l_shipdate) partition still produces correctly-distributed partitions, but a filter like WHERE l_shipdate >= '2026-01-01' does not currently translate to a partition pruning. Queries return correct results — they just scan more partitions than necessary. If your filter is on the bare partition expression (WHERE date_part('year', l_shipdate) = 2026), the equality form does prune.
:::
column % N)A plain modulo expression also produces stable, partition-prunable buckets:
Range filters on the base column (id BETWEEN 0 AND 1000) are pruned for % partitions.
A bare column name partitions one partition per distinct value — useful for low-cardinality columns:
Pruning supports equality, IN, NOT IN, and range filters on the column.
Arrow and Cayenne accelerations accept multiple partition_by expressions. Spice partitions hierarchically — first by the leftmost expression, then by the next, and so on.
A query with WHERE region = 'EU' AND date_part('year', created_at) = 2026 prunes on both axes.
Spice attempts to translate query-time filters into a partition selection. The matrix below summarizes which filter shapes prune which partition transforms:
| Filter shape | Plain column / truncate / date_trunc / % N | bucket(N, col) |
|---|---|---|
col = X | ✓ | ✓ (filter substituted into bucket expression) |
col != X | ✓ | ✗ (no pruning) |
col IN (a, b, …) | ✓ | ✓ |
col NOT IN (a, b, …) | ✓ | ✗ (no pruning) |
col < X / <= X / > X / >= X | ✓ | ✓ only for bounded ranges (both lower and upper) |
col BETWEEN a AND b | ✓ | ✓ (when range expands to ≤ a few thousand Int32 values) |
col = a OR col = b OR … | ✓ | ✓ |
partition_expr = X (e.g. bucket(50, c) = 7) | ✓ | ✓ |
expr1_filter AND expr2_filter (composite partitions) | ✓ | ✓ |
Pruning notes:
WHERE user_id = 42 against a bucket(100, user_id) partition evaluates bucket(100, 42) once and reads only that partition.Int32 range (capped at MAX_BUCKET_ENUMERATION_I32 candidate values). Open-ended ranges (col < X with no lower bound) and non-Int32 types fall back to no pruning.date_part() filter pruning is not yet implemented for time-range filters. Equality on the partition expression still prunes; range filters on the base column do not.engine: arrow)In-memory MemTable per partition value. Partitions are rebuilt on every refresh. The simplest engine to start with for moderate datasets that fit in RAM. hash_index and sort_columns are propagated per partition.
engine: cayenne)Each partition is a separate Cayenne (Vortex) table; the partition catalog is tracked in a SQLite metadata file. Cayenne supports composite partitioning natively and is the right pick for very large datasets where Arrow would not fit in memory.
Requires mode: file.
Partitioned Cayenne tables have these limitations:
on_schema_change (opens in a new tab) to evolve logs a warning;
CDC batches with widened schemas are rejected.A refresh_mode: full refresh — and any other whole-table overwrite — replaces the contents of every existing partition, not only the ones the new rows reach. A partition the incoming rows never land in is emptied, so a row removed at the source does not stay queryable in a partition the refresh did not visit, and a refresh that returns nothing empties the table. This holds for engine: arrow and engine: cayenne alike.
partition_by after refreshSpice does not re-partition existing acceleration data. An Arrow acceleration lives in memory and is rebuilt from the source on every start, so a new partition_by takes effect on restart. A Cayenne acceleration in mode: file keeps its data and catalog on disk, so its stored layout must match partition_by when the dataset loads. A change is detected when it adds partition_by to unpartitioned data, removes it from partitioned data, or changes the partition column names.
refresh_mode: fullA file-mode Cayenne dataset with refresh_mode: full and write_mode: write_through (the default), from a source other than sink, is rebuilt with the new partition_by on the next start. Every full refresh rewrites every row from the source, so dropping the stored rows loses nothing a reload does not bring back. The runtime drops the acceleration, reloads it from the source as on a first start, and logs the change:
The rebuild happens only after the source answers a read on the schema the dataset registered with. Stored rows cannot be read under another partitioning, so while the source is unreachable, or while on_schema_change: block holds back a change to its schema, the dataset fails to load with an error that says to restore the previous partition_by or to restart once the source can be read. See Changing settings on an existing acceleration (opens in a new tab) for the other Cayenne settings that are applied on restart.
Any other file-mode Cayenne dataset, such as one with refresh_mode: append or changes, write_mode: write_back or acceleration, or a sink source, can hold rows a reload does not bring back, so Spice does not rebuild it. It fails to load when its stored data was written with a different partitioning than its current partition_by. The runtime logs one error and does not retry. The error has this form, with the values in angle brackets filled in:
The stored partitioning reads without 'partition_by' or partitioned by [<columns>], and the configured value reads no 'partition_by' or 'partition_by: [<expressions>]'.
The check compares partition column names, not expressions. An entry whose expression is a plain column is compared by the column name, even when it is written as a { name: column } mapping. Any other expression is compared by its mapping key, or by expr0, expr1, and so on when it has none. A change that keeps these names is not detected, such as bucket(10, id) to bucket(20, id) as expr0, or renaming the key of a { name: id } entry: the dataset loads, and its existing partitions keep the layout of the old expression. For a dataset that is not rebuilt automatically, follow the steps below for any change to a partition expression, not only for changes the error reports. With refresh_mode: full, a change that keeps these names is not detected either, so it is not rebuilt automatically.
To keep the existing data, restore the partition_by the data was written with, or remove partition_by if the data was written without it.
Deleting the metastore removes the catalog for every Cayenne dataset that shares it, and those datasets reload from their sources. Rows that exist only in Cayenne cannot be reloaded and are lost: rows in accelerator-only datasets (acceleration.write_mode: acceleration (opens in a new tab) or sink datasets), and write-back rows not yet written to the source.
To apply the new partition_by:
SIGTERM).cayenne.db, and every data directory it catalogs. Take the backup only after Spice has stopped.cayenne_metadata_dir on every Cayenne dataset before restarting.partition_by.A partition_by expression is rejected at startup if any of the following hold:
String, an integer (signed or unsigned), Boolean, or Timestamp.OUTER REFERENCE, UNNEST, window function, aggregate function, EXISTS, GROUPING SET, or PLACEHOLDER.col AS partition_key).For the cayenne engine, mode: must be file.
Partition events by user, collapsing millions of users into 100 stable buckets:
bucket function. Hashed Partitioning with Cayenne (opens in a new tab)bucket SQL reference (opens in a new tab)truncate SQL reference (opens in a new tab)date_part SQL reference (opens in a new tab)date_trunc SQL reference (opens in a new tab)time_partition_column for time-pruned refreshes