Refreshing a PostgreSQL materialized view can be slow when you process massive amounts of data, such as billions of records. Low data freshness impacts the efficiency of business intelligence (BI) analysis and reporting. The In-Memory Column Index (IMCI) feature in PolarDB for PostgreSQL can dramatically reduce the time required to refresh a materialized view. This improves data freshness and boosts the performance of your BI and reporting workloads.
Solution overview
IMCI is an analytics acceleration engine provided by PolarDB for PostgreSQL. It creates a columnstore index for a row-store table and automatically maintains consistency between the row-store data and the columnstore index. When executing complex aggregations or joins, the database can use IMCI for computation, delivering performance that far exceeds traditional row-store scans.
The core of this solution is to create an IMCI for the base tables of the materialized view. This accelerates the creation and refresh of the materialized view.
Prerequisites
Cluster version:
PostgreSQL 16 (minor engine version 2.0.16.8.3.0 or later)
PostgreSQL 14 (minor engine version 2.0.14.10.20.0 or later)
NoteYou can view the minor engine version in the console or using the
SHOW polardb_version;statement. If your cluster does not meet the minor engine version requirement, you must upgrade the minor engine version.The source table must have a primary key. The primary key column must be included when you create the columnstore index.
The
wal_levelparameter must be set tological. This setting adds the information required for logical encoding to the write-ahead logging (WAL).NoteYou can set the wal_level parameter in the console. For more information, see Set the wal_level parameter. The cluster restarts after this parameter is modified. Plan your business operations carefully before you make this change.
Notes
You can create only one columnstore index for each table.
You cannot modify a columnstore index. To add columns to a columnstore index, you must rebuild the index.
Preparations
Prepare the environment
Prepare a PolarDB for PostgreSQL cluster that meets the prerequisites.
Enable IMCI. The method for enabling it varies based on the minor engine version of your PolarDB for PostgreSQL cluster:
Install the pg_hint_plan extension in your business system. This extension lets you adjust execution plans using hints in special comments.
CREATE EXTENSION pg_hint_plan;Create the pg_cron (scheduled task) extension in the
postgressystem database. This extension lets you automatically run tasks at specified times or intervals.Switch to the database.
\c postgres;Install the extension.
CREATE EXTENSION pg_cron;
Prepare the data
In your business system, create the customers and orders tables, create an IMCI for them, and insert test data.
Switch to your business system. This example uses
testdb.\c testdb;Create tables and insert data.
-- Create the customers table and an IMCI. CREATE TABLE customers ( customer_id SERIAL PRIMARY KEY, customer_name VARCHAR(100), email VARCHAR(100) ); CREATE INDEX idx_customers_csi ON customers USING csi; -- Create the orders table and an IMCI. CREATE TABLE orders ( order_id SERIAL PRIMARY KEY, order_date DATE, amount DECIMAL(10, 2), customer_id INT REFERENCES customers(customer_id) ); CREATE INDEX idx_orders_csi ON orders USING csi; -- Insert data into the customers table. INSERT INTO customers (customer_name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com'), ('Charlie', 'charlie@example.com'); -- Insert data into the orders table. INSERT INTO orders (order_date, amount, customer_id) VALUES ('2025-06-01', 200.00, 1), ('2025-06-02', 150.00, 2), ('2025-06-03', 300.00, 1), ('2025-06-04', 100.00, 3);
Create the materialized view
When you create the materialized view, use hints to force the query optimizer to use the columnstore index for computation.
/*+ SET(polar_csi.enable_query on) SET(polar_csi.cost_threshold 0) SET(polar_csi.exec_parallel 6) SET(polar_csi.memory_limit 10240) */CREATE MATERIALIZED VIEW mv_customer_orders AS
SELECT
c.customer_name AS customer_name,
o.order_date AS order_date,
o.amount AS amount
FROM
orders o
JOIN
customers c ON o.customer_id = c.customer_id;Hint parameters
Parameter | Description |
| Allows queries to use the columnstore index. |
| Sets the cost threshold to 0 to force the use of the columnstore index. |
| Sets the degree of parallelism for columnstore computation. We recommend that this value does not exceed the number of CPU cores on the node. |
| Sets the available memory for computation in MB. |
Refresh the materialized view
Create a function to refresh the materialized view
Encapsulate the refresh process in a function. We recommend that you use the following function for refreshes because it can safely replace the old view while preserving its indexes and ownership.
The following function is for reference only. Although it ensures a safe switchover, you must fully test it before you use it in a production environment.
-- view_name is the name of the materialized view, schema_name is the schema where the materialized view resides (default is current_schema), and new_owner is the owner of the newly created materialized view.
CREATE OR REPLACE FUNCTION refresh_materialized_view_safely_using_csi(
view_name TEXT,
schema_name TEXT DEFAULT NULL,
new_owner TEXT DEFAULT NULL
)
RETURNS BOOL
LANGUAGE plpgsql
AS $$
DECLARE
view_definition TEXT;
new_view_name TEXT;
old_view_name TEXT;
index_record RECORD;
index_creation_sql TEXT;
explain_result TEXT;
target_schema TEXT;
qualified_old_name TEXT;
qualified_new_name TEXT;
current_owner TEXT;
grant_record RECORD;
BEGIN
-- Determine the target schema (use the input parameter or the current schema).
IF schema_name IS NULL THEN
target_schema := current_schema();
ELSE
target_schema := schema_name;
END IF;
-- Construct the fully qualified table name.
qualified_old_name := format('%I.%I', target_schema, view_name);
qualified_new_name := format('%I.%I', target_schema, view_name || '_new');
RAISE NOTICE 'Operating in schema: %', target_schema;
-- Verify that the materialized view exists.
IF NOT EXISTS (
SELECT 1 FROM pg_matviews
WHERE matviewname = view_name
AND schemaname = target_schema
) THEN
RAISE EXCEPTION 'Materialized view "%" does not exist in schema "%"', view_name, target_schema;
END IF;
-- Get the definition and current owner of the materialized view.
SELECT m.definition, p.rolname INTO view_definition, current_owner
FROM pg_matviews m
JOIN pg_class c ON m.matviewname = c.relname AND m.schemaname = target_schema
JOIN pg_roles p ON c.relowner = p.oid
WHERE m.matviewname = view_name
AND m.schemaname = target_schema;
IF view_definition IS NULL THEN
RAISE EXCEPTION 'Failed to retrieve definition for materialized view "%"', view_name;
END IF;
-- Set the names for the new and old views.
old_view_name := view_name;
new_view_name := view_name || '_new';
-- Performance parameters for IMCI.
SET LOCAL polar_csi.cost_threshold = 0;
-- Print the query plan.
RAISE NOTICE 'Query plan for materialized view refresh:';
FOR explain_result IN EXECUTE format('/*+ SET(polar_csi.enable_query on) */ EXPLAIN CREATE MATERIALIZED VIEW %s AS %s', qualified_new_name, view_definition) LOOP
RAISE NOTICE '%', explain_result;
END LOOP;
BEGIN
-- Create the new materialized view.
EXECUTE format('/*+ SET(polar_csi.enable_query on) */ CREATE MATERIALIZED VIEW %s AS %s', qualified_new_name, view_definition);
-- If a new owner is specified, set the owner.
IF new_owner IS NOT NULL THEN
-- Verify that the user exists.
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = new_owner) THEN
RAISE EXCEPTION 'Role "%" does not exist', new_owner;
END IF;
EXECUTE format('ALTER MATERIALIZED VIEW %s OWNER TO %I', qualified_new_name, new_owner);
RAISE NOTICE 'Changed owner from "%" to "%"', current_owner, new_owner;
END IF;
-- Copy all indexes from the old view to the new view.
FOR index_record IN
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = old_view_name
AND schemaname = target_schema
LOOP
-- Replace the old view name with the new view name.
index_creation_sql := regexp_replace(
index_record.indexdef,
' ON ' || target_schema || '.' || old_view_name || ' ',
' ON ' || target_schema || '.' || new_view_name || ' ',
'i'
);
-- Handle the special case of UNIQUE indexes.
index_creation_sql := regexp_replace(
index_creation_sql,
'INDEX ' || index_record.indexname || ' ON',
'INDEX ' || index_record.indexname || '_new ON',
'i'
);
RAISE NOTICE 'Creating index: %', index_creation_sql;
EXECUTE index_creation_sql;
END LOOP;
-- Copy permissions from the old view.
RAISE NOTICE 'Restoring permissions to new view %.%', target_schema, new_view_name;
FOR grant_record IN
SELECT
(acl).grantee::regrole::text AS grantee,
(acl).privilege_type
FROM
pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
CROSS JOIN aclexplode(c.relacl) AS acl
WHERE
n.nspname = target_schema
AND c.relname = old_view_name
LOOP
CONTINUE WHEN grant_record.grantee IS NULL;
-- RAISE NOTICE 'Granting % ON %.% TO %',
-- grant_record.privilege_type, target_schema, new_view_name, grant_record.grantee;
EXECUTE format(
'GRANT %s ON %I.%I TO %s',
grant_record.privilege_type,
target_schema,
new_view_name,
quote_ident(grant_record.grantee)
);
END LOOP;
-- Drop the old materialized view.
EXECUTE format('DROP MATERIALIZED VIEW %s', qualified_old_name);
-- Rename the new materialized view to the original name.
EXECUTE format('ALTER MATERIALIZED VIEW %s RENAME TO %I', qualified_new_name, old_view_name);
-- Rename the indexes (remove the _new suffix).
FOR index_record IN
SELECT indexname
FROM pg_indexes
WHERE tablename = old_view_name
AND schemaname = target_schema
LOOP
IF position('_new' in index_record.indexname) > 0 THEN
EXECUTE format(
'ALTER INDEX %I.%I RENAME TO %I',
target_schema,
index_record.indexname,
replace(index_record.indexname, '_new', '')
);
END IF;
END LOOP;
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Failed to refresh materialized view: %', SQLERRM;
RETURN FALSE;
END;
END;
$$;Parameters
Parameter | Description |
| The function name. You can change it as needed. |
| The name of the materialized view. |
| The schema where the materialized view resides. The default is `current_schema`. |
| The new owner of the newly created materialized view. |
| Allows queries to use the columnstore index. |
| Sets the cost threshold to 0 to force the use of the columnstore index. |
| Sets the degree of parallelism for columnstore computation. We recommend that this value does not exceed the number of CPU cores on the node. |
| Sets the available memory for computation in MB. |
Perform the refresh
Manual refresh
When needed, you can manually call the function to perform a refresh. Replace mv_customer_orders with the name of your materialized view.
SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders');Scheduled refresh using pg_cron
You can create tasks only in the
postgressystem database, and you must use a privileged account.You can specify an owner for the rebuilt materialized view to prevent regular users from being unable to read the view after it is created by a privileged account. To modify other permission settings, you must adjust the refresh function defined earlier.
When you use
pg_cronfor scheduled refreshes, make sure the task execution interval is longer than the actual refresh time of the materialized view to prevent tasks from stacking up. Because a refresh involves writing data, it is usually much slower than a simpleSELECTstatement.
Create a scheduled task
Switch to the postgres system database. In pg_cron, specify parameters such as the task name, interval, and the operation to perform. For more information, see pg_cron (scheduled task) extension.
Switch to the database.
\c postgres;Create a scheduled task. Replace the parameters with your actual values.
NoteReplace
<mv_name>with the name of your materialized view.Replace
<database_name>with the name of your business system.Replace
<schema_name>with the name of your schema.Replace
<user_name>with your actual user name.
Syntax
SELECT cron.schedule_in_database( 'refresh_mv_customer_orders', -- Task name (customizable) '*/5 * * * *', -- Cron expression, for example, run every 5 minutes $$SELECT refresh_materialized_view_safely_using_csi('<mv_name>', '<schema_name>', '<user_name>')$$, '<database_name>' );Example
SELECT cron.schedule_in_database( 'refresh_mv_customer_orders', -- Task name (customizable) '*/5 * * * *', -- Cron expression, for example, run every 5 minutes $$SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders', 'public', 'polarpg')$$, 'testdb' );
View configured scheduled tasks
Run the following SQL statement to view configured scheduled tasks.
SELECT * FROM cron.job;The following result is returned:
jobid | schedule | command | nodename | nodeport | database | username | active | jobname
-------+-------------+----------------------------------------------------------------------------------------------+----------+----------+----------+----------+--------+----------------------------
1 | */5 * * * * | SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders', 'public', 'polarpg') | /data/. | 3000 | testdb | polarpg | t | refresh_mv_customer_orders
(1 row)Delete a scheduled task
If you no longer need the scheduled refresh, you can run the following SQL statement to delete the task.
SELECT cron.unschedule('refresh_my_materialized_view');View scheduled task execution details
You can run the following SQL statement to view the execution details of scheduled tasks.
SELECT * FROM cron.job_run_details;The following is returned:
jobid | runid | job_pid | database | username | command | status | return_message | start_time | end_time
-------+-------+---------+----------+----------+----------------------------------------------------------------------------------------------+-----------+----------------+-------------------------------+-------------------------------
1 | 1 | 76537 | testdb | polarpg | SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders', 'public', 'polarpg') | succeeded | 1 row | 2025-08-27 08:35:00.007231+00 | 2025-08-27 08:35:00.024946+00
(1 rows)Query the materialized view
Run the following SQL statement to query the materialized view. Replace mv_customer_orders with the name of your materialized view.
Before you run the query, make sure that you have switched to your business database.
SELECT customer_name, COUNT(*) FROM mv_customer_orders GROUP BY customer_name;




