When you process massive data (for example, at the billion-row scale), refreshing a PostgreSQL materialized view becomes extremely slow. Stale data hurts BI analysis and reporting efficiency. The In-Memory Column Index (IMCI) feature in PolarDB for PostgreSQL (Compatible with Oracle) significantly reduces materialized view refresh time, which improves data freshness and speeds up BI analysis and reporting.
Solution overview
IMCI is the analytical acceleration engine provided by PolarDB for PostgreSQL (Compatible with Oracle). It can build a columnstore index on top of a row-store table and automatically keeps the row-store data and the columnstore index in sync. When you run complex aggregations or join queries, the database can compute the result using the columnstore index, which delivers performance far beyond a traditional row-store scan.
The core idea of this solution is to build a columnstore index on the base tables that back a materialized view, so that both the initial creation and subsequent refreshes of the view are accelerated.
Prerequisites
-
Cluster versions:
Oracle syntax compatibility 2.0 (minor engine version 2.0.14.10.20.0 or later)
NoteYou can view the minor engine version in the console or by running the
SHOW polardb_version;statement. If the minor engine version does not meet the requirements, upgrade the minor engine version. -
The
wal_levelparameter must be set tological. This setting adds the required information for logical decoding to the write-ahead logging (WAL).NoteYou can set the wal_level parameter in the console. Modifying this parameter restarts the cluster. Plan your business operations accordingly and proceed with caution.
-
The source table must have a primary key, and the primary key column must be included when you create the columnstore index. Using a
SERIALorBIGSERIALdata type for the primary key is recommended, as it significantly improves data synchronization efficiency. -
You can create only one columnstore index for each table.
Usage notes
Each table can have only one columnstore index.
Columnstore indexes cannot be modified. To add columns to a columnstore index, rebuild the index.
Preparation
Prepare the environment
An eligible PolarDB for PostgreSQL (Compatible with Oracle) cluster.
Enable the columnstore index feature.
The method for enabling IMCI varies depending on the minor engine version of your PolarDB for PostgreSQL (Compatible with Oracle) cluster:
In your target database (business database), install the pg_hint_plan extension. This extension lets you use special comment-style hints to adjust query plans that have already been chosen.
CREATE EXTENSION pg_hint_plan;In the
postgressystem database, install the pg_cron (scheduled tasks) extension. This extension runs tasks automatically at a specified time or interval.Switch to the database.
\c postgres;Install the extension.
CREATE EXTENSION pg_cron;
Prepare the data
In your target database (business database), create the customers table and the orders table, build columnstore indexes on them, and insert test data.
Switch to the target database (business database). This example uses
testdb.\c testdb;Create the tables and insert the data.
-- Create the customers table and its columnstore index. 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 its columnstore index. 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 compute the view through the columnstore index.
/*+ 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 the query to use the columnstore index. |
| Forces the optimizer to use the columnstore index by setting the cost threshold to 0. |
| Sets the degree of parallelism for columnstore computation. We recommend a value that does not exceed the number of CPU cores on the node. |
| Sets the memory available for computation, in MB. |
Refresh the materialized view
Create the refresh function
The refresh process is wrapped in a function. We recommend the following function because it swaps in the new view safely while preserving its indexes and ownership.
The following function is provided for reference. It guarantees safe view swap-in, but you must still test it thoroughly in your own environment before using it in production.
-- view_name is the name of the materialized view. schema_name is the schema in which the view resides (defaults to current_schema). new_owner is the owner assigned to the newly created 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 fully qualified table names.
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;
-- Retrieve 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 old and new views.
old_view_name := view_name;
new_view_name := view_name || '_new';
-- IMCI performance parameters.
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 view permissions.
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 |
| Function name. You can change it to fit your business needs. |
| Name of the materialized view. |
| Schema in which the materialized view resides. Defaults to current_schema. |
| New owner of the materialized view after it is re-created. |
| Allows the query to use the columnstore index. |
| Forces the optimizer to use the columnstore index by setting the cost threshold to 0. |
| Sets the degree of parallelism for columnstore computation. We recommend a value that does not exceed the number of CPU cores on the node. |
| Sets the memory available for computation, in MB. |
Execute the refresh
Refresh manually
When your business requires it, call the function manually to trigger a refresh. Replace the name in the call with your actual materialized view name. This example uses mv_customer_orders.
SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders');Schedule refreshes with pg_cron
Tasks can only be created in the
postgressystem database, and only a privileged account can create them.You can specify the owner for the rebuilt materialized view so that regular users can still read the view after it is re-created by a privileged account. If you need to adjust other permission-related settings, modify the refresh function defined above.
When you use
pg_cronto schedule refreshes, make sure the task interval is strictly longer than the actual refresh duration; otherwise tasks pile up. Because a refresh writes data, it is typically much slower than a plainSELECT.
Create a scheduled task
Switch to the postgres system database and use pg_cron to specify the task name, interval, and operation. For more information, see pg_cron (scheduled tasks) extension.
Switch to the database.
\c postgres;Create the scheduled task. Replace the relevant parameters with your actual values.
NoteReplace
<mv_name>with the actual materialized view name.Replace
<database_name>with the actual business database name.Replace
<schema_name>with the actual schema name.Replace
<user_name>with the 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 scheduled tasks
Run the following SQL statement to view the scheduled tasks that have been configured.
SELECT * FROM cron.job;Expected output:
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 scheduled refreshes, run the following SQL statement to delete the scheduled task.
SELECT cron.unschedule('refresh_my_materialized_view');View task execution details
Run the following SQL statement to view the execution details of scheduled tasks.
SELECT * FROM cron.job_run_details;Expected output:
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 the name with your actual materialized view name. This example uses mv_customer_orders.
Before you run the statement, switch to your actual business database.
SELECT customer_name, COUNT(*) FROM mv_customer_orders GROUP BY customer_name;