Partitioning
Table partitioning is a technique used to manage very large tables — such as fact tables — by dividing them into smaller pieces called partitions. This approach can significantly improve query performance, as the Greengage DB optimizer can target only the relevant partitions instead of scanning the entire table.
Partitioning is defined at table creation by specifying a partition key: one or more columns used to determine how the data is divided across partitions.
Introduction to table partitioning
Table partitioning overview
Partitioning and table distribution serve different purposes in Greengage DB. Partitioning divides a large table into smaller pieces to improve query performance and simplify maintenance tasks such as archiving or dropping old data. Distribution, on the other hand, determines how data (in both partitioned and non-partitioned tables) is spread across segments to support parallel query execution. Partitioning does not affect data distribution across segments.
Tables are partitioned at creation time using the PARTITION BY
clause, optionally followed by SUBPARTITION BY
for multi-level partitioning.
Partitioning creates a top-level (or parent) table with one or more levels of sub-tables (or child tables).
Internally, Greengage DB creates an inheritance relationship between the top-level table and its underlying partitions, similar to the functionality of the INHERITS
clause.
Greengage DB uses the partitioning criteria defined during table creation to generate individual partitions.
Each partition is created with a distinct CHECK
constraint that specifies the range of data it can contain.
The query optimizer uses these constraints to determine which table partitions to scan to satisfy a given query predicate.
The following example shows a single-level partitioned sales
table with monthly partitions for the first quarter of 2025.
Each partition corresponds to a specific date range defined by the partitioning rules:
sales ├─── sales_1_prt_jan_2025 (date >= '2025-01-01' AND date < '2025-02-01') ├─── sales_1_prt_feb_2025 (date >= '2025-02-01' AND date < '2025-03-01') └─── sales_1_prt_mar_2025 (date >= '2025-03-01' AND date < '2025-04-01')
The following example illustrates a two-level partitioned sales
table.
Each monthly partition is further subpartitioned by the region
column, dividing the data into the asia
and europe
subpartitions:
sales ├─── sales_1_prt_jan_2025 (date >= '2025-01-01' AND date < '2025-02-01') │ ├─── sales_1_prt_jan_2025_2_prt_asia (region = 'Asia') │ └─── sales_1_prt_jan_2025_2_prt_europe (region = 'Europe') ├─── sales_1_prt_feb_2025 (date >= '2025-02-01' AND date < '2025-03-01') │ ├─── sales_1_prt_feb_2025_2_prt_asia (region = 'Asia') │ └─── sales_1_prt_feb_2025_2_prt_europe (region = 'Europe') └─── sales_1_prt_mar_2025 (date >= '2025-03-01' AND date < '2025-04-01') ├─── sales_1_prt_mar_2025_2_prt_asia (region = 'Asia') └─── sales_1_prt_mar_2025_2_prt_europe (region = 'Europe')
A partitioned table can have a maximum of 32767 partitions for each partition level.
To insert data into a partitioned table, you typically target either the root partitioned table (sales
in the example above) or one of its leaf child partitions (for example, sales_1_prt_jan_2025_2_prt_asia
and sales_1_prt_jan_2025_2_prt_europe
).
Keep the following in mind when inserting data:
-
You can insert data into the root table (for example,
sales
) created with theCREATE TABLE
command. Greengage DB automatically routes the data to the correct child partition based on the partitioning rules. -
You can insert data directly into a leaf partition (for example,
sales_1_prt_jan_2025_2_prt_europe
) as long as the data fits that partition’s criteria. -
Inserting data into a non-leaf or intermediate partition (for example,
sales_1_prt_jan_2025
) is not allowed and causes an error.
Note that the top-level and intermediate tables — such as sales
, sales_1_prt_jan_2025
, sales_1_prt_feb_2025
, and sales_1_prt_mar_2025
— remain empty after inserting the data.
The best practice for loading data into partitioned tables is using partition exchange.
Decide whether to use partitioning
Use partitioning if all or most of these points can be applied to a table:
-
There is a large table of facts.
Large fact tables with millions of rows are good candidates for partitioning. In contrast, smaller tables with only thousands of rows or fewer are unlikely to benefit significantly from partitioning.
-
You are not satisfied with the current query performance.
Apply partitioning only when query performance on the table does not meet the required response times.
-
There is a column that allows the table to be divided into approximately equal-sized partitions.
Choose a partition key that results in partitions with roughly the same number of rows. The more evenly the table is divided into small chunks, the greater the performance benefits you can get from partitioning. For example, dividing a table into 10 evenly sized partitions can improve query performance by up to 10x — assuming the partition key is used in query predicates (see the following item below).
-
Most queries targeted for optimization use the partition key in their predicates.
Partitioning improves performance only if the query optimizer can select partitions based on the query predicates. If a query scans all partitions, it may perform worse than querying a non-partitioned table. Make sure that your query plans contain partition elimination.
-
There are business requirements for retaining historical data.
Partitioning is well-suited for managing time-based data retention. For example, if only the last 12 months of data need to be kept, you can drop the oldest partition and load new data into a new one.
Avoid creating more partitions than necessary. Excessive partitioning can negatively impact system operations such as vacuuming, segment recovery, cluster expansion, disk usage checks, and others.
Syntax overview
To define the partitioning strategy, use the PARTITION BY
clause in the CREATE TABLE command.
To partition a table:
-
Decide on the partition design: date range, numeric range, or list of values.
-
Select one on more columns to use as the partition key.
-
Decide on the number of partitioning levels needed. For example, you can create a range-partitioned table by month and then subpartition each monthly partition by sales region.
The simplified syntax of the CREATE TABLE
command with single-level partitioning is as follows:
CREATE TABLE <table_name> (
<column_definitions>
)
[ PARTITION BY <partition_type> (<column>)
(
<partition_spec>
)]
The main arguments are:
-
partition_type
— specifies the type of partitioning:LIST
(based on a list of values) orRANGE
(based on a numeric or date range). -
column
— defines the column used for partitioning. -
partition_spec
— defines the individual partitions to be created. Common clauses include:-
START
,END
, andEVERY
— used for defining range partitions. -
VALUES
— specifies the values for list partitions. -
DEFAULT PARTITION
— captures any rows that do not match the specified partition criteria; applicable to all partition types. Note that the query optimizer always scans the default partition, which may slow down the overall scan time if this partition contains data.
-
-
If a partitioned table contains a primary key or a column with a
UNIQUE
constraint, these columns must be included in the partition key. -
Tables with the
DISTRIBUTED REPLICATED
distribution policy cannot be partitioned.
Range partitioning
Range partitioning divides data based on numeric or date/timestamp ranges.
Use the PARTITION BY RANGE
clause to specify the partition key column, then define partition intervals using the START
and END
keywords.
The INCLUSIVE
and EXCLUSIVE
modifiers control whether the boundary values are included in the range.
By default, START
values are inclusive, and END
values are exclusive.
There are two ways to define range partitions — automatic and manual.
Generate partitions automatically
To generate partitions automatically, use the START
and END
clauses to specify the boundaries of the entire range.
Then, specify the partition size with the EVERY
clause:
-
For date ranges, this clause requires the interval unit such as
day
,month
, oryear
. Example:EVERY (INTERVAL '1 month')
. -
For numeric ranges, provide a numeric step value. Example:
EVERY (100)
.
DBMS automatically creates the necessary partitions by dividing the entire range into equal intervals based on the specified increment.
PARTITION BY RANGE (<column>)
(
START ([<datatype>] '<start_value>') [INCLUSIVE | EXCLUSIVE]
END ([<datatype>] '<end_value>') [INCLUSIVE | EXCLUSIVE] ]
EVERY ([<datatype>] [<number> | INTERVAL] '<interval_value>') ]
);
Define partitions manually
An alternative approach to defining a partitioned table is to specify each partition individually.
In this case, provide a comma-separated list of partition definitions within the parentheses following the PARTITION BY RANGE
clause.
Each partition definition begins with the PARTITION <partition_name>
clause, where <partition_name>
is a custom identifier used in the partition’s full name instead of an automatically generated number.
Following the partition name, use the standard START
and END
clauses to define the range boundaries for each partition.
Note that only the last partition requires the END
value; it is optional for all others.
PARTITION BY RANGE (<column>)
(
PARTITION <partition_name>
START ([<datatype>] '<start_value>') [INCLUSIVE | EXCLUSIVE]
END ([<datatype>] '<end_value>') [INCLUSIVE | EXCLUSIVE],
[PARTITION <partition_name> ...]
);
List partitioning
List partitioning divides data based on a predefined set of values for the partition key.
Use the PARTITION BY LIST
clause to specify the partition key column, followed by a comma-separated list of partition definitions enclosed in parentheses.
Each partition definition follows this template:
PARTITION <partition_name> VALUES (<list_value>[, ...])
where:
-
<partition_name>
is a custom identifier used instead of an automatically generated number in the full partition name; -
<list_value>
represents one or more partition key values assigned to that partition.
Multi-level partitioning
Multi-level partitioning combines range and/or list partitioning across multiple levels. This lets you divide first-level partitions into second-level subpartitions, which can be divided further if needed.
Use the PARTITION BY
clause (with RANGE
or LIST
) to define the partition key at the first level.
For all subsequent levels, use the SUBPARTITION BY
clause (also with RANGE
or LIST
).
Subpartition definitions follow the same format as partitions, except the keyword SUBPARTITION
is used instead of PARTITION
.
There are two ways to define subpartitions — manually or with a subpartition template.
Manual definition of subpartitions
In this approach, every partition and subpartition is defined explicitly.
First, specify all partition keys using the PARTITION BY
and SUBPARTITION BY
clauses.
Then, list the partition and subpartition specifications.
The hierarchy of partitions and subpartitions is represented by nested parentheses, reflecting their level in the partitioning structure.
CREATE TABLE <table_name> (
<column_definitions>
)
[ PARTITION BY <partition_type> (<column>)
[ SUBPARTITION BY <partition_type> (<column1>) ]
[ SUBPARTITION BY <partition_type> (<column2>) ]
[...]
( <partition_spec>
[ ( <subpartition_spec_column1>
[ ( <subpartition_spec_column2>
[...] ) ] ) ],
[ <partition_spec>
[ ( <subpartition_spec_column1>
[ ( <subpartition_spec_column2>
[...] ) ] ) ], ]
[...]
) ]
Subpartition templates
In this approach, subpartitions for each hierarchy level are defined once using the SUBPARTITION TEMPLATE
clause, placed immediately after the corresponding SUBPARTITION BY
clause.
Using a subpartition template ensures that all partitions share a consistent structure, including any partitions added in the future.
CREATE TABLE <table_name> (
<column_definitions>
)
[ PARTITION BY <partition_type> (<column>)
{ [ SUBPARTITION BY <partition_type> (<column1>)
SUBPARTITION TEMPLATE ( <template_spec> ) ]
[ SUBPARTITION BY partition_type (<column2>)
SUBPARTITION TEMPLATE ( <template_spec> ) ]
[...] }
( <partition_spec> ) ]
Create a partitioned table
Date range partitioning
The command below creates a range-partitioned table with monthly partitions for Q1 2025 and includes a default partition for dates beyond these partitions:
CREATE TABLE sales
(
id INT,
date DATE,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
(
START (DATE '2025-01-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
EVERY (INTERVAL '1 month'),
DEFAULT PARTITION other_dates
);
The following command creates the same partitioned sales
table but defines each partition manually:
CREATE TABLE sales
(
id INT,
date DATE,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
(
PARTITION jan_2025 START (DATE '2025-01-01') INCLUSIVE,
PARTITION feb_2025 START (DATE '2025-02-01') INCLUSIVE,
PARTITION mar_2025 START (DATE '2025-03-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE,
DEFAULT PARTITION other_dates
);
Numeric range partitioning
The following command creates a range-partitioned sales
table based on the amount
column:
CREATE TABLE sales
(
id INT,
date DATE,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (amount)
(
START (0)
END (1000)
EVERY (250),
DEFAULT PARTITION out_of_range
);
This command creates the sales
table with manually defined range partitions:
CREATE TABLE sales
(
id INT,
date DATE,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (amount)
(
PARTITION low_amounts START (0),
PARTITION medium_amounts START (100),
PARTITION high_amounts START (500)
END (1000),
DEFAULT PARTITION out_of_range);
List partitioning
The SQL statement below creates the sales
table partitioned by region
using list partitioning.
It defines explicit partitions for Asia
and Europe
, along with a default partition for all other regions:
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY LIST (region)
(
PARTITION asia VALUES ('Asia'),
PARTITION europe VALUES ('Europe'),
DEFAULT PARTITION other_regions
);
Multi-level partitioning
This command creates a two-level partitioned sales
table: it uses automatic range partitioning by date
at the first level and a SUBPARTITION TEMPLATE
to define list subpartitions by region
at the second level:
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe'),
DEFAULT SUBPARTITION other_regions
)
(
START (DATE '2025-01-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
EVERY (INTERVAL '1 month'),
DEFAULT PARTITION other_dates
);
The following command creates a two-level partitioned sales
table with manually defined monthly partitions by date, each containing list subpartitions by region
:
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
(
PARTITION jan_2025 START (DATE '2025-01-01') INCLUSIVE (
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe'),
DEFAULT SUBPARTITION other_regions
),
PARTITION feb_2025 START (DATE '2025-02-01') INCLUSIVE (
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe'),
DEFAULT SUBPARTITION other_regions
),
PARTITION mar_2025 START (DATE '2025-03-01') INCLUSIVE (
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe'),
DEFAULT SUBPARTITION other_regions
),
DEFAULT PARTITION other_dates (
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe'),
DEFAULT SUBPARTITION other_regions
)
);
The command below creates the sales
table with three levels of partitioning:
-
automatic range partitioning by
date
; -
list subpartitioning by
region
; -
range subpartitioning by
amount
.
SUBPARTITION TEMPLATE
is used at both subpartition levels.
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe'),
DEFAULT SUBPARTITION other_regions
)
SUBPARTITION BY RANGE (amount)
SUBPARTITION TEMPLATE
(
START (0)
END (1000)
EVERY (100),
DEFAULT SUBPARTITION out_of_range
)
(
START (DATE '2025-01-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
EVERY (INTERVAL '1 month'),
DEFAULT PARTITION other_dates
);
Partition an existing table
Table partitioning can be defined only at creation. To partition an existing table, follow the steps below:
-
Create a new partitioned table:
CREATE TABLE sales_partitioned ( LIKE sales ) WITH (appendoptimized = true) PARTITION BY RANGE (date) ( START (DATE '2025-01-01') INCLUSIVE END (DATE '2025-04-01') EXCLUSIVE EVERY (INTERVAL '1 month'), DEFAULT PARTITION other_dates );
-
Load the original table data into the new table:
INSERT INTO sales_partitioned SELECT * FROM sales;
-
Drop the original table:
DROP TABLE sales;
-
Rename the new table with the original table’s name:
ALTER TABLE sales_partitioned RENAME TO sales;
After creating a new table, you need to reapply any table permissions. Learn more in Roles and privileges.
View partitioning information
The partitioned table below serves as an example for demonstrating how to view partitioning information.
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe'),
DEFAULT SUBPARTITION other_regions
)
(
START (DATE '2025-01-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
EVERY (INTERVAL '1 month'),
DEFAULT PARTITION other_dates
);
psql meta-commands
The \d+
meta-command below shows information about the sales
table:
\d+ sales
The Child tables
section of the output lists the child tables associated with the root table:
Append-Only Table "public.sales" Column | Type | Modifiers | Storage | Stats target | Description --------+---------------+-----------+----------+--------------+------------- id | integer | | plain | | date | date | | plain | | region | text | | extended | | amount | numeric(10,2) | | main | | Compression Type: None Compression Level: 0 Block Size: 32768 Checksum: t Child tables: sales_1_prt_2, sales_1_prt_3, sales_1_prt_4, sales_1_prt_other_dates Distributed by: (id) Partition by: (date) Options: appendonly=true
You can execute the same command for a child table to see its child tables used for subpartitioning:
\d+ sales_1_prt_2
In addition to the Child tables
section, the output also includes an Inherits
section indicating that this table inherits columns and constraints of the sales
table:
Append-Only Table "public.sales_1_prt_2" Column | Type | Modifiers | Storage | Stats target | Description --------+---------------+-----------+----------+--------------+------------- id | integer | | plain | | date | date | | plain | | region | text | | extended | | amount | numeric(10,2) | | main | | Compression Type: None Compression Level: 0 Block Size: 32768 Checksum: t Check constraints: "sales_1_prt_2_check" CHECK (date >= '2025-01-01'::date AND date < '2025-02-01'::date) Inherits: sales Child tables: sales_1_prt_2_2_prt_asia, sales_1_prt_2_2_prt_europe, sales_1_prt_2_2_prt_other_regions Distributed by: (id) Partition by: (region) Options: appendonly=true
SQL commands
To get information on partitioned tables in the current database, use one of the ways described below.
pg_catalog.pg_partitions
To show the partition design of the specified table, run the SQL query against the pg_catalog.pg_partitions
system view:
SELECT partitiontablename,
partitionname,
partitiontype,
partitionlevel,
partitionrank
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'sales';
The result might look as follows:
partitiontablename | partitionname | partitiontype | partitionlevel | partitionrank ---------------------------------------------+---------------+---------------+----------------+--------------- sales_1_prt_2 | | range | 0 | 1 sales_1_prt_3 | | range | 0 | 2 sales_1_prt_4 | | range | 0 | 3 sales_1_prt_other_dates | other_dates | range | 0 | sales_1_prt_2_2_prt_asia | asia | list | 1 | sales_1_prt_2_2_prt_europe | europe | list | 1 | sales_1_prt_2_2_prt_other_regions | other_regions | list | 1 | sales_1_prt_3_2_prt_asia | asia | list | 1 | sales_1_prt_3_2_prt_europe | europe | list | 1 | sales_1_prt_3_2_prt_other_regions | other_regions | list | 1 | sales_1_prt_4_2_prt_asia | asia | list | 1 | sales_1_prt_4_2_prt_europe | europe | list | 1 | sales_1_prt_4_2_prt_other_regions | other_regions | list | 1 | sales_1_prt_other_dates_2_prt_asia | asia | list | 1 | sales_1_prt_other_dates_2_prt_europe | europe | list | 1 | sales_1_prt_other_dates_2_prt_other_regions | other_regions | list | 1 | (16 rows)
The table contains the following columns:
-
partitiontablename
— a name of the table that is used to access the partition directly in DML commands; -
partitionname
— a partition name that is given at creation time and can be referred to in theALTER TABLE
commands; -
partitiontype
— a partition type; -
partitionlevel
— a partition level in the hierarchy (starting with0
for partitions at the first level); -
partitionrank
— a rank of the partition compared to other partitions of the same level (starting with1
). Defined only for range partitions.
You can also use the partitionboundary
column to get partition specifications.
pg_catalog.pg_partition_templates
To show all subpartitions created with a subpartition template, run the SQL query against the pg_catalog.pg_partition_templates
system view:
SELECT schemaname,
tablename,
partitionname,
partitiontype,
partitionlevel,
partitionrank
FROM pg_catalog.pg_partition_templates;
The result should look like this:
schemaname | tablename | partitionname | partitiontype | partitionlevel | partitionrank ------------+-----------+---------------+---------------+----------------+--------------- public | sales | asia | list | 1 | public | sales | europe | list | 1 | public | sales | other_regions | list | 1 | (3 rows)
pg_catalog.pg_partition_columns
To show all partition key columns, get information from the pg_catalog.pg_partition_columns
system view:
SELECT *
FROM pg_catalog.pg_partition_columns;
The result should look like this:
schemaname | tablename | columnname | partitionlevel | position_in_partition_key ------------+-----------+------------+----------------+--------------------------- public | sales | date | 0 | 1 public | sales | region | 1 | 1 public | sales | region | 1 | 1 (3 rows)
pg_catalog.pg_partition
To display the partition levels along with the corresponding tables, use the pg_catalog.pg_partition
system catalog table:
SELECT parent_table.relname,
partition_info.parkind,
partition_info.parlevel,
partition_info.paristemplate
FROM pg_catalog.pg_partition AS partition_info
LEFT JOIN pg_class AS parent_table ON parent_table.oid = partition_info.parrelid;
The result might look like this:
relname | parkind | parlevel | paristemplate ---------+---------+----------+--------------- sales | l | 1 | f sales | l | 1 | t sales | r | 0 | f (3 rows)
Maintain partitioned tables
Add a default partition
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe')
)
(
START (DATE '2025-01-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
EVERY (INTERVAL '1 month')
);
You can add a default partition to a partition design with the ALTER TABLE
command:
ALTER TABLE sales
ADD DEFAULT PARTITION other_dates;
If your partition design is multi-level, each level in the hierarchy must have a default partition:
ALTER TABLE sales
ALTER PARTITION FOR (RANK (1))
ADD DEFAULT PARTITION other_regions;
ALTER TABLE sales
ALTER PARTITION FOR (RANK (2))
ADD DEFAULT PARTITION other_regions;
ALTER TABLE sales
ALTER PARTITION FOR (RANK (3))
ADD DEFAULT PARTITION other_regions;
You can get the ranks of partitions in the pg_catalog.pg_partitions system view.
To check that the default partitions are added, execute the following query:
SELECT partitiontablename,
partitionname,
partitiontype,
partitionlevel,
partitionrank
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'sales';
The output should include the other_dates
and other_regions
partitions:
partitiontablename | partitionname | partitiontype | partitionlevel | partitionrank --------------------------------------+---------------+---------------+----------------+--------------- sales_1_prt_1 | | range | 0 | 1 sales_1_prt_2 | | range | 0 | 2 sales_1_prt_3 | | range | 0 | 3 sales_1_prt_other_dates | other_dates | range | 0 | sales_1_prt_1_2_prt_asia | asia | list | 1 | sales_1_prt_1_2_prt_europe | europe | list | 1 | sales_1_prt_1_2_prt_other_regions | other_regions | list | 1 | sales_1_prt_2_2_prt_asia | asia | list | 1 | sales_1_prt_2_2_prt_europe | europe | list | 1 | sales_1_prt_2_2_prt_other_regions | other_regions | list | 1 | sales_1_prt_3_2_prt_asia | asia | list | 1 | sales_1_prt_3_2_prt_europe | europe | list | 1 | sales_1_prt_3_2_prt_other_regions | other_regions | list | 1 | sales_1_prt_other_dates_2_prt_asia | asia | list | 1 | sales_1_prt_other_dates_2_prt_europe | europe | list | 1 | (15 rows)
Add a new partition
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe')
)
(
PARTITION jan_2025 START (DATE '2025-01-01') INCLUSIVE ,
PARTITION feb_2025 START (DATE '2025-02-01') INCLUSIVE ,
PARTITION mar_2025 START (DATE '2025-03-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
);
You can add a partition to a partition design with the ALTER TABLE
command.
If the original partition design includes subpartitions defined by a subpartition template, the newly added partition is subpartitioned according to this template.
The following command adds a new range partition for April 2025 to the sales
table:
ALTER TABLE sales
ADD PARTITION apr_2025 START (DATE '2025-04-01')
END (DATE '2025-05-01') EXCLUSIVE;
When you add a subpartition to an existing partition, you can specify the partition to alter:
ALTER TABLE sales
ALTER PARTITION FOR (RANK (4))
ADD PARTITION africa VALUES ('Africa');
To check that new partitions are added, execute the following query:
SELECT partitiontablename,
partitionname,
partitiontype,
partitionlevel,
partitionrank
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'sales';
The result includes the sales_1_prt_apr_2025
and sales_1_prt_apr_2025_2_prt_africa
partitions:
partitiontablename | partitionname | partitiontype | partitionlevel | partitionrank -----------------------------------+---------------+---------------+----------------+--------------- sales_1_prt_jan_2025 | jan_2025 | range | 0 | 1 sales_1_prt_feb_2025 | feb_2025 | range | 0 | 2 sales_1_prt_mar_2025 | mar_2025 | range | 0 | 3 sales_1_prt_apr_2025 | apr_2025 | range | 0 | 4 sales_1_prt_apr_2025_2_prt_asia | asia | list | 1 | sales_1_prt_apr_2025_2_prt_europe | europe | list | 1 | sales_1_prt_apr_2025_2_prt_africa | africa | list | 1 | sales_1_prt_feb_2025_2_prt_asia | asia | list | 1 | sales_1_prt_feb_2025_2_prt_europe | europe | list | 1 | sales_1_prt_jan_2025_2_prt_asia | asia | list | 1 | sales_1_prt_jan_2025_2_prt_europe | europe | list | 1 | sales_1_prt_mar_2025_2_prt_asia | asia | list | 1 | sales_1_prt_mar_2025_2_prt_europe | europe | list | 1 | (13 rows)
You cannot add a partition to a partition design that has a default partition. You must split the default partition to add a partition. Learn more in Split the default partition.
Split a partition
Split the specified partition
CREATE TABLE sales
(
id INT,
date DATE,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
(
PARTITION jan_2025 START (DATE '2025-01-01') INCLUSIVE ,
PARTITION feb_2025 START (DATE '2025-02-01') INCLUSIVE ,
PARTITION mar_2025 START (DATE '2025-03-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
);
Splitting a partition divides an existing partition into two new ones.
You can split a partition using the ALTER TABLE
command.
The split value you specify goes into the latter partition.
Note that only the lowest-level partitions — the ones that actually contain data — can be split. For single-level partitioning, you can split both range and list partitions, while for multi-level partitioning — only range partitions.
Execute the following command to split a monthly partition into two: the first for dates March 1–15 and the second for March 16–31:
ALTER TABLE sales
SPLIT PARTITION FOR ('2025-03-01')
AT ('2025-03-16')
INTO (PARTITION mar_1to15_2025, PARTITION mar_16to31_2025);
To see the updated partition design of the table, run the following SQL query:
SELECT partitionname,
partitionboundary
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'sales';
The result looks like this:
partitionname | partitionboundary -----------------+-------------------------------------------------------------------------------------------------------- jan_2025 | PARTITION jan_2025 START ('2025-01-01'::date) END ('2025-02-01'::date) WITH (appendonly='true') feb_2025 | PARTITION feb_2025 START ('2025-02-01'::date) END ('2025-03-01'::date) WITH (appendonly='true') mar_1to15_2025 | PARTITION mar_1to15_2025 START ('2025-03-01'::date) END ('2025-03-16'::date) WITH (appendonly='true') mar_16to31_2025 | PARTITION mar_16to31_2025 START ('2025-03-16'::date) END ('2025-04-01'::date) WITH (appendonly='true') (4 rows)
Split the default partition
CREATE TABLE sales
(
id INT,
date DATE,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
(
PARTITION jan_2025 START (DATE '2025-01-01') INCLUSIVE ,
PARTITION feb_2025 START (DATE '2025-02-01') INCLUSIVE ,
PARTITION mar_2025 START (DATE '2025-03-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE,
DEFAULT PARTITION other_dates
);
To split the default partition, use the SPLIT DEFAULT PARTITION
clause:
ALTER TABLE sales
SPLIT DEFAULT PARTITION
START ('2025-04-01') INCLUSIVE
END ('2025-05-01') EXCLUSIVE
INTO (PARTITION apr_2025, DEFAULT PARTITION);
To see the updated partition design, run the following SQL query:
SELECT partitionname,
partitionboundary
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'sales';
The result should look like this:
partitionname | partitionboundary ---------------+------------------------------------------------------------------------------------------------- jan_2025 | PARTITION jan_2025 START ('2025-01-01'::date) END ('2025-02-01'::date) WITH (appendonly='true') feb_2025 | PARTITION feb_2025 START ('2025-02-01'::date) END ('2025-03-01'::date) WITH (appendonly='true') mar_2025 | PARTITION mar_2025 START ('2025-03-01'::date) END ('2025-04-01'::date) WITH (appendonly='true') apr_2025 | PARTITION apr_2025 START ('2025-04-01'::date) END ('2025-05-01'::date) WITH (appendonly='true') other_dates | DEFAULT PARTITION other_dates WITH (appendonly='true') (5 rows)
Modify a subpartition template
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe')
)
(
PARTITION jan_2025 START (DATE '2025-01-01') INCLUSIVE ,
PARTITION feb_2025 START (DATE '2025-02-01') INCLUSIVE ,
PARTITION mar_2025 START (DATE '2025-03-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
);
Use the SET SUBPARTITION TEMPLATE
clause to modify the subpartition template of a partitioned table.
Partitions added after you set a new subpartition template have the new partition design.
Existing partitions are not modified.
The following example alters the subpartition template of the sales
table:
ALTER TABLE sales
SET SUBPARTITION TEMPLATE
(SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe'),
SUBPARTITION africa VALUES ('Africa'),
DEFAULT SUBPARTITION other_regions);
When you add a new date-range partition, it should include the new regional list subpartition for Africa
:
ALTER TABLE sales
ADD PARTITION apr_2025 START (DATE '2025-04-01')
END (DATE '2025-05-01') EXCLUSIVE;
To see the updated partition design, run the following SQL query:
SELECT partitiontablename,
partitionboundary
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'sales';
The result should include the subpartitions asia
, europe
, africa
, and a default partition named other_regions
:
partitiontablename | partitionboundary ------------------------------------------+------------------------------------------------------------------------------------------------- sales_1_prt_jan_2025 | PARTITION jan_2025 START ('2025-01-01'::date) END ('2025-02-01'::date) WITH (appendonly='true') sales_1_prt_feb_2025 | PARTITION feb_2025 START ('2025-02-01'::date) END ('2025-03-01'::date) WITH (appendonly='true') sales_1_prt_mar_2025 | PARTITION mar_2025 START ('2025-03-01'::date) END ('2025-04-01'::date) WITH (appendonly='true') sales_1_prt_apr_2025 | PARTITION apr_2025 START ('2025-04-01'::date) END ('2025-05-01'::date) sales_1_prt_apr_2025_2_prt_asia | SUBPARTITION asia VALUES('Asia') sales_1_prt_apr_2025_2_prt_europe | SUBPARTITION europe VALUES('Europe') sales_1_prt_apr_2025_2_prt_africa | SUBPARTITION africa VALUES('Africa') sales_1_prt_apr_2025_2_prt_other_regions | DEFAULT SUBPARTITION other_regions sales_1_prt_feb_2025_2_prt_asia | SUBPARTITION asia VALUES('Asia') WITH (appendonly='true') sales_1_prt_feb_2025_2_prt_europe | SUBPARTITION europe VALUES('Europe') WITH (appendonly='true') sales_1_prt_jan_2025_2_prt_asia | SUBPARTITION asia VALUES('Asia') WITH (appendonly='true') sales_1_prt_jan_2025_2_prt_europe | SUBPARTITION europe VALUES('Europe') WITH (appendonly='true') sales_1_prt_mar_2025_2_prt_asia | SUBPARTITION asia VALUES('Asia') WITH (appendonly='true') sales_1_prt_mar_2025_2_prt_europe | SUBPARTITION europe VALUES('Europe') WITH (appendonly='true') (14 rows)
Exchange a partition
CREATE TABLE sales
(
id INT,
date DATE,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
(
PARTITION jan_2025 START (DATE '2025-01-01') INCLUSIVE ,
PARTITION feb_2025 START (DATE '2025-02-01') INCLUSIVE ,
PARTITION mar_2025 START (DATE '2025-03-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE,
DEFAULT PARTITION other_dates
);
Partition exchange enables you to swap a table partition with another table with the same structure, commonly called a staging table. After the exchange, the partition contains the data from the staging table, which holds the original partition data.
This feature is useful for data loading, as inserting data directly into partitioned tables can be inefficient. Additionally, partition exchange can be used to change the storage type of existing partitions.
To demonstrate partition exchange, first add sample data to the sales
table:
INSERT INTO sales (id, date, amount)
SELECT gs.id,
DATE '2025-01-01' + (gs.id % 59),
round((random() * 1000)::NUMERIC, 2)
FROM generate_series(1, 40000) AS gs(id);
Check that a table corresponding to the mar_2025
partition does not contain data:
SELECT count(*)
FROM sales_1_prt_mar_2025;
Create a table with the same structure as the partitioned table but with another storage type:
CREATE TABLE stg_mar_2025
(
LIKE sales
)
WITH (appendoptimized = false);
Insert sample data to the table:
INSERT INTO stg_mar_2025 (id, date, amount)
SELECT gs.id,
DATE '2025-03-01' + (gs.id % 31),
round((random() * 1000)::NUMERIC, 2)
FROM generate_series(1, 20000) AS gs(id);
To exchange a partition, use the ALTER TABLE
command with the EXCHANGE PARTITION
clause:
ALTER TABLE sales
EXCHANGE PARTITION FOR (DATE '2025-03-01')
WITH TABLE stg_mar_2025 WITH VALIDATION;
Check that a table corresponding to the mar_2025
partition now contains data:
SELECT count(*)
FROM sales_1_prt_mar_2025;
The result should look like this:
count ------- 20000
Finally, check that the storage type of the partition has changed as well:
\dt+ sales*
The result should show that the storage type for sales_1_prt_mar_2025
is heap
:
List of relations Schema | Name | Type | Owner | Storage | Size | Description --------+-------------------------+-------+---------+-------------+---------+------------- public | sales | table | gpadmin | append only | 160 kB | public | sales_1_prt_feb_2025 | table | gpadmin | append only | 803 kB | public | sales_1_prt_jan_2025 | table | gpadmin | append only | 854 kB | public | sales_1_prt_mar_2025 | table | gpadmin | heap | 1280 kB | public | sales_1_prt_other_dates | table | gpadmin | append only | 160 kB | (5 rows)
Before exchanging the default partition, make sure the data in the staging table is valid and does not belong to any child partitions.
Otherwise, queries that use GPORCA may return wrong results or cause data corruption after UPDATE
or DELETE
operations.
Rename a partition
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe')
)
(
START (DATE '2025-01-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
EVERY (INTERVAL '1 month')
);
This command renames the top-level parent table:
ALTER TABLE sales
RENAME TO globalsales;
Note that it also automatically updates the prefixes of all its child table names.
To change a partition name, use the ALTER TABLE
command with the RENAME PARTITION
clause:
ALTER TABLE globalsales
RENAME PARTITION FOR ('2025-01-01') TO jan_2025;
ALTER TABLE globalsales
RENAME PARTITION FOR ('2025-02-01') TO feb_2025;
ALTER TABLE globalsales
RENAME PARTITION FOR ('2025-03-01') TO mar_2025;
To check new partition names, use the following query:
SELECT partitiontablename,
partitionname,
partitiontype,
partitionlevel,
partitionrank
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'globalsales';
The result should look like this:
partitiontablename | partitionname | partitiontype | partitionlevel | partitionrank -----------------------------------------+---------------+---------------+----------------+--------------- globalsales_1_prt_jan_2025 | jan_2025 | range | 0 | 1 globalsales_1_prt_feb_2025 | feb_2025 | range | 0 | 2 globalsales_1_prt_mar_2025 | mar_2025 | range | 0 | 3 globalsales_1_prt_feb_2025_2_prt_asia | asia | list | 1 | globalsales_1_prt_feb_2025_2_prt_europe | europe | list | 1 | globalsales_1_prt_jan_2025_2_prt_asia | asia | list | 1 | globalsales_1_prt_jan_2025_2_prt_europe | europe | list | 1 | globalsales_1_prt_mar_2025_2_prt_asia | asia | list | 1 | globalsales_1_prt_mar_2025_2_prt_europe | europe | list | 1 |
Truncate a partition
CREATE TABLE sales
(
id INT,
date DATE,
region TEXT,
amount DECIMAL(10, 2)
)
WITH (appendoptimized = true)
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
SUBPARTITION asia VALUES ('Asia'),
SUBPARTITION europe VALUES ('Europe')
)
(
PARTITION jan_2025 START (DATE '2025-01-01') INCLUSIVE ,
PARTITION feb_2025 START (DATE '2025-02-01') INCLUSIVE ,
PARTITION mar_2025 START (DATE '2025-03-01') INCLUSIVE
END (DATE '2025-04-01') EXCLUSIVE
);
To delete all data from the specified partition without dropping the partition itself, use the ALTER TABLE
command with the TRUNCATE PARTITION
clause.
Insert sample data to the table:
INSERT INTO sales (id, date, region, amount)
SELECT gs.id,
DATE '2025-01-01' + (gs.id % 90),
CASE WHEN gs.id % 2 = 0 THEN 'Asia' ELSE 'Europe' END,
round((random() * 1000)::NUMERIC, 2)
FROM generate_series(1, 40000) AS gs(id);
View information about the available partitions:
SELECT partitiontablename,
partitionname,
partitionrank
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'sales';
The result should look like this:
partitiontablename | partitionname | partitionrank -----------------------------------+---------------+--------------- sales_1_prt_jan_2025 | jan_2025 | 1 sales_1_prt_feb_2025 | feb_2025 | 2 sales_1_prt_mar_2025 | mar_2025 | 3 sales_1_prt_feb_2025_2_prt_asia | asia | sales_1_prt_feb_2025_2_prt_europe | europe | sales_1_prt_jan_2025_2_prt_asia | asia | sales_1_prt_jan_2025_2_prt_europe | europe | sales_1_prt_mar_2025_2_prt_asia | asia | sales_1_prt_mar_2025_2_prt_europe | europe | (9 rows)
Check a row count in the sales_1_prt_jan_2025
table corresponding to an intermediate partition:
SELECT count(*)
FROM sales_1_prt_jan_2025;
The result might look like this:
count ------- 13794
Then, check that the sales_1_prt_feb_2025_2_prt_asia
table corresponding to a leaf partition also contains data:
SELECT count(*)
FROM sales_1_prt_feb_2025_2_prt_asia;
To truncate the intermediate jan_2025
partition, use the following command:
ALTER TABLE sales
TRUNCATE PARTITION jan_2025;
To truncate the asia
subpartition in the feb_2025
partition, use the following SQL request:
ALTER TABLE sales
ALTER PARTITION feb_2025
TRUNCATE PARTITION asia;
Then, again use the SELECT count(*)
command to check that a row count is 0
for both partitions.
Drop a partition
To drop the specified partition, use the ALTER TABLE
command with the DROP PARTITION
clause.
The following example drops the jan_2025
partition from the sales
table created in the Truncate a partition section:
ALTER TABLE sales
DROP PARTITION jan_2025;
Check that the partition does not exist anymore by querying pg_partitions
:
SELECT partitiontablename,
partitionname,
partitionrank
FROM pg_catalog.pg_partitions
WHERE schemaname = 'public'
AND tablename = 'sales';
The result should look as follows:
partitiontablename | partitionname | partitionrank -----------------------------------+---------------+--------------- sales_1_prt_feb_2025 | feb_2025 | 1 sales_1_prt_mar_2025 | mar_2025 | 2 sales_1_prt_feb_2025_2_prt_asia | asia | sales_1_prt_feb_2025_2_prt_europe | europe | sales_1_prt_mar_2025_2_prt_asia | asia | sales_1_prt_mar_2025_2_prt_europe | europe | (6 rows)