--- title: Index Partitioning description: Partition ParadeDB indexes to accelerate filter and JOIN queries canonical: https://www.paradedb.com/docs/reference/indexing/partition-by --- `partition_by` is currently a Beta feature under active development. Syntax and internal planner behaviors may change in future releases. By default, a ParadeDB index does not partition rows across segments. By using the `partition_by` option in `CREATE INDEX`, you can partition the index space along one or more columns: ```sql CREATE INDEX items_idx ON items USING paradedb (id, tenant_id, description, created_at) WITH ( partition_by = 'tenant_id', target_segment_count = 16 ); ``` Partitioning an index provides two key benefits: - Accelerating search and filter queries - Accelerating joins You can also partition along multiple columns by specifying a comma-separated list: ```sql CREATE INDEX events_idx ON events USING paradedb (id, organization_id, created_at, message) WITH ( partition_by = 'organization_id, created_at', target_segment_count = 32 ); ``` Because ParadeDB partitions rows multi-dimensionally using a kd-tree, the order in which columns are specified in `partition_by` is not important. Because [`target_segment_count`](/reference/indexing/create-index#index-partitioning-beta) controls the total number of segments, that segment budget is divided across all partition columns. For most workloads, partitioning on 1 or 2 high-selectivity columns gives the best balance between query performance and partitioning overhead. Partition boundaries are established during `CREATE INDEX` or `REINDEX` and are not yet rebalanced as new data is inserted. Query performance against partitioned indexes will gradually decline as writes accumulate. Running `REINDEX` restores optimal partition boundaries. Incremental maintenance of partitioned indexes across ongoing writes is being actively worked on. --- ## Column Selection Guidance Choosing the right column(s) for `partition_by` depends on your query patterns and join requirements. | Workload / Primary Pattern | Recommended `partition_by` Column | Expected Impact | | :--------------------------------------------------- | :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | | Multi-Tenant SaaS (queries filter by tenant) | Tenant ID (e.g. `tenant_id`, `org_id`) | Pruning: Queries filter on `tenant_id = ?`, bypassing non-matching segments entirely to reduce I/O and cache churn. | | Time-Series / Event Logs (queries filter by recency) | Timestamp / Date (e.g. `created_at`, `event_time`) | Pruning: Range filters (`created_at >= ...`) skip older or non-matching segments. | | Large-Scale Joins (e.g. `orders` JOIN `items`) | Equi-join key on both tables (e.g. `orders.item_id` and `items.id`) | Co-Partitioned Joins: Joins and downstream operations (aggregates, Top-K) execute worker-locally without inter-worker data exchange. | | Multi-Tenant with Joins | Shared tenant key on both tables (e.g. `orders.tenant_id` and `items.tenant_id`) | Dual Benefit: Segment pruning for single-tenant searches and worker-local joins within a tenant. | | Multiple Query Filters | Multiple columns (e.g. `tenant_id, created_at`) | Multi-Dimensional Pruning: Enables pruning on tenant equality, timestamp ranges, or both, with segment granularity divided among the columns. | ### Requirements for Index Partitioning Columns specified in `partition_by` must meet the following constraints: 1. Single-valued only: Multi-valued types (such as arrays `TEXT[]`, `INT[]` and JSON/JSONB fields) cannot be used. 2. Columnar indexed: The column must be columnar indexed. Scalar types (integers, floats, booleans, dates, timestamps, UUIDs) are columnar indexed by default. 3. Tokenizer: If a text column is used in `partition_by`, it must use the [`literal`](/reference/tokenizers/available-tokenizers/literal) tokenizer (e.g. `(tenant_code::pdb.literal)`). 4. Low-cardinality columns: Columns with few distinct values (booleans, enums) cap total partitions to their cardinality. To reach `target_segment_count`, pair them with Postgres's internal `ctid` column (e.g. `partition_by = 'is_active, ctid'`). Only use `ctid` in this specific case: it usually cannot prune on its own and should not be used alone or with high-cardinality columns. --- ## Performance Tuning By default, `CREATE INDEX` sets `target_segment_count` equal to the number of CPUs on the system. When using `partition_by`, we recommend setting `target_segment_count` to 2–4X the CPU core count (or parallel worker count). Over-partitioning creates narrower value ranges per segment, allowing queries to prune more non-matching data and helping the planner align partition boundaries across tables during parallel joins. However, creating too many segments increases per-segment scan and metadata overhead. Sizing `target_segment_count` to 2–4X CPU cores strikes a practical balance between pruning granularity and resource usage. ```sql -- Example: On an 8-core machine, over-partition by 4X (target_segment_count = 32) CREATE INDEX users_idx ON users USING paradedb (id, name) WITH (partition_by = 'id', target_segment_count = 32); CREATE INDEX posts_idx ON posts USING paradedb (id, owner_user_id, title) WITH (partition_by = 'owner_user_id', target_segment_count = 32); ``` For more details on tuning worker parallelism and segment counts, see [Read Throughput](/operate/performance-tuning/reads#adjusting-target-segment-count). Runtime planner settings for partitioned joins are listed in the [Configuration Reference](/reference/configuration#query-planning).