A partitioned materialized view is an advanced materialized view feature offered by PolarDB for PostgreSQL. It automatically creates a matching partitioned materialized view on top of a partitioned base table, enables per-partition incremental refresh, and significantly improves refresh efficiency.
Overview
Partitioned materialized views support two partition alignment modes:
-
Proportional alignment: source-table partitions and materialized-view partitions have a one-to-one correspondence. Each source partition maps to one child materialized view, and a refresh only touches the partitions whose data has changed. Best suited to cases where the source partitioning granularity matches the query granularity of the materialized view.
-
Rollup alignment: the materialized view rolls up along a time dimension, merging data from multiple source partitions into a single materialized-view partition. For example, the source table is partitioned by day while the materialized view aggregates by month. The system inspects the
IMMUTABLEaggregate expressions in the defining query (such asLEFT(date_str, 6)) and detects the rollup relationship automatically.
Both modes share the same interface. The system decides which alignment to apply by analyzing the defining query.
Scope of application
Extensions
Run the following SQL statement to install the polar_ivm extension:
CREATE EXTENSION IF NOT EXISTS polar_ivm;
Partition-type requirements
|
Partition type |
Support status |
|
RANGE partitioning |
Supported |
|
LIST partitioning |
Not supported |
|
HASH partitioning |
Not supported |
|
Multi-column partition key |
Not supported (single column only) |
Defining-query requirements
-
Partition key must appear in GROUP BY: for an aggregate query, the partition key (or its rollup expression) must be listed in the
GROUP BYclause. -
Partitioned table cannot appear in a subquery: the partitioned table must not appear inside a
WHEREsubquery, aFROMsubquery, or a CTE. -
Partitioned table cannot be on the nullable side of a JOIN: for a
LEFT JOIN, the partitioned table must be on the left; for aRIGHT JOIN, it must be on the right. -
Partitioned table cannot be referenced more than once: the same partitioned table must not appear in the query multiple times.
-
Other materialized views cannot be referenced: the defining query must not reference any existing materialized view.
-
Volatile functions are not allowed: non-immutable functions such as
CURRENT_DATEorrandom()must not be used.
Incremental refresh mechanism
Partitioned materialized views achieve intelligent incremental refresh through version tracking:
|
Scenario |
Refresh behavior |
|
Partition data unchanged |
The partition is skipped (unless |
|
Partition data changed |
Only that partition is refreshed. |
|
Other joined tables change |
All partitions are refreshed. |
|
Partitions added to or dropped from the source table |
The structure is synced automatically at refresh time, and any newly added partition is refreshed. |
Interface reference
Partitioned materialized views are managed through three stored procedures exposed under the polar_matview schema: create_partition_mv to create, refresh_partition_mv to refresh, and drop_partition_mv to drop. All three stored procedures must be invoked with the CALL statement.
Create a partitioned materialized view
Call the polar_matview.create_partition_mv stored procedure to create a partitioned materialized view:
CALL polar_matview.create_partition_mv(
mv_schema => 'public',
mv_name => 'my_mv',
ref_partition_table => 'public.orders'::regclass::oid,
view_def => 'SELECT order_date, COUNT(*) AS cnt FROM public.orders GROUP BY order_date',
use_imci => FALSE
);
Parameter description:
|
Parameter |
Type |
Default |
Description |
|
|
NAME |
Required |
Schema that contains the materialized view. |
|
|
NAME |
Required |
Name of the materialized view. |
|
|
OID |
Required |
OID of the source partitioned table. Obtain it with |
|
|
TEXT |
Required |
The |
|
|
BOOLEAN |
FALSE |
Whether to use the in-memory column index (IMCI) to accelerate creation and refresh. |
Alignment detection: the system determines the alignment mode from the query in view_def. If the partition-key expression in GROUP BY matches the source partition key exactly, proportional alignment is used. If GROUP BY uses an IMMUTABLE aggregate expression such as LEFT(col, 6), rollup alignment is detected automatically.
Rollup alignment uses the first partition-key-bearing expression in GROUP BY as the partition key of the materialized view. Reorder your view definition so that the desired partition expression comes first.
Refresh a partitioned materialized view
Call the polar_matview.refresh_partition_mv stored procedure to refresh a partitioned materialized view:
CALL polar_matview.refresh_partition_mv(
mv_schema => 'public',
mv_name => 'my_mv',
use_imci => FALSE,
force_refresh => FALSE
);
Parameter description:
|
Parameter |
Type |
Default |
Description |
|
|
NAME |
Required |
Schema that contains the materialized view. |
|
|
NAME |
Required |
Name of the materialized view. |
|
|
BOOLEAN |
FALSE |
Whether to use the in-memory column index (IMCI) to accelerate the refresh. |
|
|
BOOLEAN |
FALSE |
When set to |
Refresh behavior:
-
By default, a refresh syncs any partition structure changes on the source table (added or dropped partitions) and then only refreshes partitions whose data version has changed.
-
When
force_refresh=TRUEis set, the version check is skipped and all partitions are refreshed.
Drop a partitioned materialized view
Call the polar_matview.drop_partition_mv stored procedure to drop a partitioned materialized view together with all its child partitioned materialized views, and to clean up the related metadata:
CALL polar_matview.drop_partition_mv(
mv_schema => 'public',
mv_name => 'my_mv'
);
Parameter description:
|
Parameter |
Type |
Default |
Description |
|
|
NAME |
Required |
Schema that contains the materialized view. |
|
|
NAME |
Required |
Name of the materialized view. |
Examples
Proportional alignment example
The source table is partitioned by day and the materialized view is also partitioned by day, so source partitions and materialized-view partitions correspond one to one.
-- Create the source partitioned table (RANGE partitioned on the single column order_date)
CREATE TABLE orders (
order_id BIGSERIAL,
order_date DATE NOT NULL,
amount NUMERIC(12, 2)
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_20260101 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-01-02');
CREATE TABLE orders_20260102 PARTITION OF orders
FOR VALUES FROM ('2026-01-02') TO ('2026-01-03');
-- Create a proportionally aligned partitioned materialized view: GROUP BY uses the partition key order_date itself
CALL polar_matview.create_partition_mv(
mv_schema => 'public',
mv_name => 'orders_daily_mv',
ref_partition_table => 'public.orders'::regclass::oid,
view_def => 'SELECT order_date, COUNT(*) AS cnt, SUM(amount) AS total FROM public.orders GROUP BY order_date',
use_imci => FALSE
);
-- Refresh the partitioned materialized view (only partitions with changed data are refreshed)
CALL polar_matview.refresh_partition_mv(
mv_schema => 'public',
mv_name => 'orders_daily_mv'
);
Rollup alignment example
The source table is partitioned at a fine granularity while the materialized view aggregates at a coarser granularity, so multiple source partitions merge into a single materialized-view partition.
Example: TEXT partition key with LEFT() rollup (daily source partitions to monthly materialized view)
-- ========== 1. Create the daily-partitioned source table (partition key is TEXT in 'YYYYMMDD' format) ==========
DROP SCHEMA IF EXISTS report CASCADE;
CREATE SCHEMA report;
CREATE TABLE report.daily_data (
log_date TEXT NOT NULL, -- Format: 'YYYYMMDD', for example, '20240105'
value INTEGER
) PARTITION BY RANGE (log_date);
-- Each source partition corresponds to one day
CREATE TABLE report.daily_data_20240105 PARTITION OF report.daily_data
FOR VALUES FROM ('20240105') TO ('20240106');
CREATE TABLE report.daily_data_20240115 PARTITION OF report.daily_data
FOR VALUES FROM ('20240115') TO ('20240116');
CREATE TABLE report.daily_data_20240125 PARTITION OF report.daily_data
FOR VALUES FROM ('20240125') TO ('20240126');
CREATE TABLE report.daily_data_20240205 PARTITION OF report.daily_data
FOR VALUES FROM ('20240205') TO ('20240206');
CREATE TABLE report.daily_data_20240215 PARTITION OF report.daily_data
FOR VALUES FROM ('20240215') TO ('20240216');
CREATE TABLE report.daily_data_20240305 PARTITION OF report.daily_data
FOR VALUES FROM ('20240305') TO ('20240306');
INSERT INTO report.daily_data VALUES
('20240105', 20), ('20240115', 20), ('20240125', 20), -- 2024-01 total 60
('20240205', 40), ('20240215', 50), -- 2024-02 total 90
('20240305', 60); -- 2024-03 total 60
-- ========== 2. Create the monthly rollup materialized view ==========
-- GROUP BY uses LEFT(log_date, 6) to roll the partition key up to year-month; the system detects rollup alignment automatically
-- The 6 daily source partitions are merged into 3 monthly materialized-view partitions
CALL polar_matview.create_partition_mv(
mv_schema => 'report',
mv_name => 'mv_monthly_summary',
ref_partition_table => 'report.daily_data'::regclass::oid,
view_def => 'SELECT LEFT(log_date, 6) AS month, SUM(value) AS total FROM report.daily_data GROUP BY LEFT(log_date, 6)',
create_no_data => TRUE
);
CALL polar_matview.refresh_partition_mv(
mv_schema => 'report',
mv_name => 'mv_monthly_summary'
);
-- ========== 3. Query the result ==========
SELECT * FROM report.mv_monthly_summary ORDER BY month;
-- month | total
-- --------+-------
-- 202401 | 60 -- merged from three daily partitions 20240105/20240115/20240125
-- 202402 | 90 -- merged from two daily partitions 20240205/20240215
-- 202403 | 60 -- corresponds to one daily partition 20240305
-- ========== 4. Incremental refresh (only partitions with changed data are refreshed) ==========
INSERT INTO report.daily_data VALUES ('20240215', 10);
CALL polar_matview.refresh_partition_mv(
mv_schema => 'report',
mv_name => 'mv_monthly_summary'
);
-- Version tracking detects that only the 2024-02 rollup partition (which contains 20240215) needs to be refreshed;
-- the 2024-01 and 2024-03 partitions remain unchanged and are skipped
-- ========== 5. Refresh again after adding a new daily partition ==========
CREATE TABLE report.daily_data_20240315 PARTITION OF report.daily_data
FOR VALUES FROM ('20240315') TO ('20240316');
INSERT INTO report.daily_data VALUES ('20240315', 30);
CALL polar_matview.refresh_partition_mv(
mv_schema => 'report',
mv_name => 'mv_monthly_summary'
);
-- The system syncs the newly added partition structure from the source table automatically and rolls the 20240315 data up into the 2024-03 partition
SELECT * FROM report.mv_monthly_summary ORDER BY month;
-- month | total
-- --------+-------
-- 202401 | 60
-- 202402 | 100 -- becomes 100 after appending the 10 for 20240215
-- 202403 | 90 -- becomes 90 after adding the 30 for 20240315
A rollup expression must be an IMMUTABLE function. The built-in PostgreSQL functions date_trunc() and to_char() are STABLE (they depend on session parameters) and therefore cannot be used directly as a partition key. If the source-table partition key is of type DATE and you need to roll up by month, change the partition key to TEXT (for example, in 'YYYYMMDD' format) and use LEFT() to roll up, or wrap your own IMMUTABLE date-truncation function.