When you partition a table by time, querying or enforcing uniqueness on a non-time column becomes a problem: without a way to span all subpartitions with a single index, every query must scan every subpartition, and cross-partition unique constraints are impossible. Global indexes solve both problems by letting a single index cover all subpartitions of a partitioned table.
Supported versions
PolarDB for PostgreSQL clusters running PostgreSQL 14 with minor engine version 2.0.14.9.15.0 or later.
Check your minor engine version in the console or by running:
SHOW polardb_version;If the version does not meet the requirement, upgrade the minor engine version.
How global indexes work
A partitioned table splits data into independent subpartitions based on a partition key. A local index creates one index per subpartition. A global index creates a single index that spans all subpartitions.
A common partitioning pattern is time-based: use a timestamp column as the partition key, create new subpartitions weekly or monthly, and archive old ones to reduce operations and maintenance (O&M) costs. In this pattern, the partition key is a time column — not the primary identifier. This causes two problems:
No partition pruning on non-partition key queries: Querying by
idforces the database to scan all subpartitions, because it cannot determine which one contains the data.No cross-partition uniqueness: A local UNIQUE index cannot enforce uniqueness across the entire partitioned table.
Global index vs. local index
| Dimension | Global index | Local index |
|---|---|---|
| Index scope | Covers the entire partitioned table | One index per subpartition |
| Query on non-partition key | Single index scan | Scans all subpartitions |
| UNIQUE constraint on non-partition key | Supported | Not supported |
| ATTACH/DETACH subpartitions | Supported | Supported |
| Expression indexes | Not supported | Supported |
| Index on partition key columns | Not supported | Supported |
| Manageability | More complex (spans all subpartitions) | Simpler (scoped to each subpartition) |
Limitations
Specify the
GLOBALkeyword inCREATE INDEXto create a global index. Without it, a local index is created by default.Global indexes support concurrent creation using the
CONCURRENTLYkeyword.Global indexes are only supported on partitioned tables — not on non-partitioned tables or on child tables that contain subpartitions.
Global indexes do not support expression indexes.
Global indexes cannot be created on the partition key columns of a partitioned table.
You can still
ATTACHorDETACHsubpartitions from a partitioned table that has a global index.
Syntax
CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] name ] ON [ ONLY ] table_name [ USING method ]
( { column_name | ( expression ) } [ COLLATE collation ] [ opclass [ ( opclass_parameter = value [, ... ] ) ] ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] )
[ INCLUDE ( column_name [, ...] ) ]
[ WITH ( storage_parameter [= value] [, ... ] ) ]
[ GLOBAL/LOCAL ]
[ TABLESPACE tablespace_name ]
[ WHERE predicate ]Examples
Accelerate non-partition key queries
Step 1: Create a partitioned table.
CREATE TABLE partition_range (
id INT,
a INT,
b INT,
created_date TIMESTAMP WITHOUT TIME ZONE
) PARTITION BY RANGE (created_date);
CREATE TABLE partition_range_part01 PARTITION OF partition_range FOR VALUES FROM (MINVALUE) TO ('2020-01-01 00:00:00');
CREATE TABLE partition_range_part02 PARTITION OF partition_range FOR VALUES FROM ('2020-01-01 00:00:00') TO ('2020-02-01 00:00:00');
CREATE TABLE partition_range_part03 PARTITION OF partition_range FOR VALUES FROM ('2020-02-01 00:00:00') TO ('2020-03-01 00:00:00');Step 2: Query without an index.
EXPLAIN (COSTS OFF) SELECT * FROM partition_range WHERE id = 1;All subpartitions are scanned because partition pruning cannot apply to a non-partition key:
QUERY PLAN
------------------------------------------------------------
Append
-> Seq Scan on partition_range_part01 partition_range_1
Filter: (id = 1)
-> Seq Scan on partition_range_part02 partition_range_2
Filter: (id = 1)
-> Seq Scan on partition_range_part03 partition_range_3
Filter: (id = 1)
(7 rows)Step 3: Add a local index and query again.
CREATE INDEX partition_range_idx_local ON partition_range(id);
EXPLAIN (COSTS OFF) SELECT * FROM partition_range WHERE id = 1;All subpartition indexes are still scanned, because local indexes are created on each subpartition individually:
QUERY PLAN
--------------------------------------------------------------------------------------------------
Append
-> Index Scan using partition_range_part01_id_idx on partition_range_part01 partition_range_1
Index Cond: (id = 1)
-> Index Scan using partition_range_part02_id_idx on partition_range_part02 partition_range_2
Index Cond: (id = 1)
-> Index Scan using partition_range_part03_id_idx on partition_range_part03 partition_range_3
Index Cond: (id = 1)
(7 rows)Step 4: Add a global index and query again.
CREATE INDEX partition_range_idx_global ON partition_range(id) GLOBAL;
EXPLAIN (COSTS OFF) SELECT * FROM partition_range WHERE id = 1;The database uses the global index to locate the target subpartition directly — 7 plan nodes reduced to 2:
QUERY PLAN
-----------------------------------------------------------------------
Global Index Scan using partition_range_idx_global on partition_range
Index Cond: (id = 1)
(2 rows)Enforce a unique constraint on a non-partition key
The partition key for partition_range is created_date, but the column that requires a unique constraint is id.
In native PostgreSQL, creating a UNIQUE index on a non-partition key column fails:
CREATE UNIQUE INDEX partition_range_id_unique_idx ON partition_range(id);
ERROR: unique constraint on partitioned table must include all partitioning columns
DETAIL: UNIQUE constraint on table "partition_range" lacks column "created_date" which is part of the partition key.PolarDB for PostgreSQL removes this restriction. By default, a UNIQUE constraint on a non-partition key is automatically enforced as a global constraint backed by a global index. This behavior is controlled by the polar_pk_in_non_partition_column_mode parameter:
| Value | Behavior |
|---|---|
global_index (default) | UNIQUE constraint on a non-partition key is converted to a global constraint |
none | Same behavior as native PostgreSQL — UNIQUE on a non-partition key fails with an error |
To create the unique global index explicitly:
CREATE UNIQUE INDEX partition_range_id_unique_idx ON partition_range(id) GLOBAL;Performance benchmark
The following results are from pgbench with a scale factor of 80,000, comparing a standard table, a partitioned table with local indexes, and a partitioned table with global indexes. All examples use single-row point queries on a non-partition key column.
Point query performance (TPS)
| Configuration | Concurrency 1 | Concurrency 32 | Concurrency 64 | Concurrency 1 (PS) | Concurrency 32 (PS) | Concurrency 64 (PS) |
|---|---|---|---|---|---|---|
| Standard table | 27,732 | 494,433 | 430,848 | 53,935 | 985,880 | 886,882 |
| Partitioned table + local index | 367 | 4,155 | 3,688 | 856 | 8,742 | 6,790 |
| Partitioned table + global index | 19,006 | 308,128 | 262,941 | 45,090 | 820,924 | 731,557 |
PS = Prepared Statement
TPC-B performance (TPS)
TPC-B includes both point queries and Data Manipulation Language (DML) statements.
| Configuration | Concurrency 1 | Concurrency 32 | Concurrency 64 | Concurrency 1 (PS) | Concurrency 32 (PS) | Concurrency 64 (PS) |
|---|---|---|---|---|---|---|
| Standard table | 1,115 | 51,025 | 60,409 | 4,822 | 90,312 | 100,802 |
| Partitioned table + local index | 271 | 2,903 | 2,524 | 550 | 5,276 | 4,237 |
| Partitioned table + global index | Not supported | 4,334 | 69,040 | 75,232 | — | — |
Summary
Global indexes improve point query and DML performance on partitioned tables by an order of magnitude compared to local indexes.