Refreshing a PostgreSQL materialized view over billions of records can take minutes to hours, making it impractical for business intelligence (BI) analysis and reporting that require fresh data. The In-Memory Column Index (IMCI) feature in PolarDB for PostgreSQL dramatically reduces refresh time by pushing the underlying query through a column store engine instead of a row-store scan.
This guide covers how to enable IMCI, create a materialized view that uses it, and automate refreshes with pg_cron.
When to use this approach
Use IMCI-accelerated refreshes when:
Your base tables contain billions of records
The materialized view query involves complex aggregations or multi-table joins
You need data refreshed on a schedule (every few minutes to hours) rather than in real time
For small tables or simple queries, a standard REFRESH MATERIALIZED VIEW statement is sufficient.
How it works
IMCI creates a columnstore index alongside a row-store table and keeps both in sync automatically. When the database executes a query that benefits from column-oriented access—such as aggregations or joins over large datasets—it routes computation through the column store engine.
Creating an IMCI on the base tables of a materialized view accelerates both the initial view creation and subsequent refreshes.
Prerequisites
Before you begin, ensure that you have:
A PolarDB for PostgreSQL cluster running one of the following versions:
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
A source table with a primary key (required for IMCI; the primary key column must be included in the columnstore index)
The
wal_levelparameter set tological
To check your minor engine version, run SHOW polardb_version; or view it in the console. To upgrade, see Version management.Set wal_level in the console. See Set the wal_level parameter. The cluster restarts after this change—plan accordingly.Usage notes
Each table supports only one columnstore index.
Columnstore indexes cannot be modified. To add columns, rebuild the index.
Set up the environment
Step 1: Enable IMCI
The method for enabling IMCI depends on your minor engine version.
Step 2: Install supporting extensions
Install the pg_hint_plan extension, which lets you control the query optimizer using hints in SQL comments:
CREATE EXTENSION pg_hint_plan;Install the pg_cron extension in the postgres system database for scheduled refresh tasks:
\c postgres;
CREATE EXTENSION pg_cron;Step 3: Prepare the base tables
Switch to your business database and create the base tables with columnstore indexes.
This example uses a testdb database with customers and orders tables:
\c testdb;
-- Create the customers table with a 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 with a 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 sample data
INSERT INTO customers (customer_name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com'),
('Charlie', 'charlie@example.com');
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
Use query hints to force the optimizer to route the view query 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 |
|---|---|
polar_csi.enable_query on | Allows the query to use the columnstore index |
polar_csi.cost_threshold 0 | Forces the optimizer to choose the columnstore index by setting the cost threshold to 0 |
polar_csi.exec_parallel 6 | Sets the degree of parallelism for column store computation. We recommend that this value does not exceed the number of CPU cores on the node. |
polar_csi.memory_limit 10240 | Sets the memory available for computation, in MB |
Refresh the materialized view
About the refresh strategy
The refresh function described below uses a full-rebuild strategy: it creates a new materialized view, copies indexes and permissions from the old one, drops the old view, and renames the new one. This ensures IMCI hints are applied consistently on every refresh.
Create the refresh function
This function is provided as a reference implementation. Test it thoroughly in a staging environment before using it in production.
-- Parameters:
-- view_name: Name of the materialized view to refresh
-- schema_name: Schema containing the materialized view (defaults to current_schema)
-- new_owner: Owner to assign to the newly created view (optional)
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;
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;
$$;Function parameters
| Parameter | Default | Description |
|---|---|---|
view_name | — | Name of the materialized view |
schema_name | current_schema | Schema where the materialized view resides |
new_owner | — | Owner to assign to the rebuilt view. Specify this when a privileged account runs the refresh so that regular users retain read access |
Refresh manually
Call the function directly when needed. Replace mv_customer_orders with your materialized view name:
SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders');Schedule automatic refreshes with pg_cron
Scheduled tasks must be created in the postgres system database using a privileged account.Specify new_owner when a privileged account runs the scheduled refresh, so that regular users can still read the rebuilt view.Set the task interval longer than the actual refresh duration. Because a refresh writes data, it is much slower than a SELECT. If a new task starts before the previous one finishes, tasks stack up and can degrade performance.Create a scheduled task
Switch to the postgres database, then create the task. Replace the placeholders with your actual values:
| Placeholder | Description |
|---|---|
<mv_name> | Name of your materialized view |
<database_name> | Name of your business database |
<schema_name> | Name of your schema |
<user_name> | Owner to assign to the rebuilt view |
\c postgres;
SELECT cron.schedule_in_database(
'refresh_mv_customer_orders', -- Task name (customizable)
'*/5 * * * *', -- Cron expression (every 5 minutes in this example)
$$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',
'*/5 * * * *',
$$SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders', 'public', 'polarpg')$$,
'testdb'
);View configured tasks
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)View task execution details
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)Delete a scheduled task
SELECT cron.unschedule('refresh_my_materialized_view');Query the materialized view
Before running the query, switch to your business database. Replace mv_customer_orders with your materialized view name:
SELECT customer_name, COUNT(*) FROM mv_customer_orders GROUP BY customer_name;




