All Products
Search
Document Center

PolarDB:Use IMCI to accelerate materialized view refreshes

Last Updated:Jul 07, 2026

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.

image

Prerequisites

  • Cluster versions:

    Oracle syntax compatibility 2.0 (minor engine version 2.0.14.10.20.0 or later)

    Note

    You 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_level parameter must be set to logical. This setting adds the required information for logical decoding to the write-ahead logging (WAL).

    Note

    You 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 SERIAL or BIGSERIAL data 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

  1. An eligible PolarDB for PostgreSQL (Compatible with Oracle) cluster.

  2. 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:

    Oracle syntax compatibility 2.0 (2.0.14.17.35.0 or later)

    For PolarDB for PostgreSQL (Compatible with Oracle) clusters with these versions, two methods are available. The following table outlines the differences.

    Comparison item

    [Recommended] Add an IMCI read-only node

    Directly use the pre-installed columnstore index extension

    Method

    You can add an IMCI read-only node manually in the console.

    No action is required. You can use the extension directly.

    Resource allocation

    The columnstore engine exclusively uses the node's resources, including all available memory.

    The columnstore engine is limited to 25% of the node's memory. The remaining memory is allocated to the row store engine.

    Business impact

    Transactional processing (TP) and analytical processing (AP) workloads are isolated on different nodes and do not affect each other.

    TP and AP workloads run on the same node and can affect each other.

    Costs

    IMCI read-only nodes incur additional charges and are billed at the same rate as regular compute nodes.

    No additional cost.

    Add an IMCI read-only node

    There are two ways to add an IMCI read-only node:

    Note

    The cluster must contain at least one read-only node. You cannot add an IMCI read-only node to a single-node cluster.

    Console
    1. Log on to the PolarDB console and select the cluster's region. You can open the Add/Remove Node wizard in one of the following ways:

      • On the Clusters page, click Add/Remove Node in the Actions column.

      • On the Basic Information page of the target cluster, click Add/Remove Node in the Database Nodes section.

    2. Select Add Read-only IMCI Node and click OK.

    3. On the cluster upgrade/downgrade page, add the IMCI read-only node and complete the payment.

      1. Click Add a Read-only IMCI Node and select the node specifications.

      2. Select a switchover time.

      3. (Optional) Review the Product Terms of Service and Service Level Agreement.

      4. Click Buy Now.

    4. After the payment is complete, return to the cluster details page and wait for the IMCI read-only node to be added. The node is ready when its status changes to Running.

    During purchase

    On the PolarDB purchase page, in the Nodes section, specify the number of IMCI Read-Only Nodes.

    Oracle syntax compatibility 2.0 (2.0.14.10.20.0 to 2.0.14.17.35.0)

    For PolarDB for PostgreSQL (Compatible with Oracle) clusters with these versions, the IMCI feature is provided as the polar_csi extension. To use IMCI, you must first create the extension in the desired database.

    Note
    • The polar_csi extension is scoped at the database level. To use IMCI in multiple databases within a cluster, you must create the polar_csi extension for each database.

    • The database account used to install the extension must be a privileged account.

    There are two ways to install the polar_csi extension:

    Console

    1. Log on to the PolarDB console. In the left-side navigation pane, click Clusters. Select the region where your cluster is located, and then click the cluster ID to open the cluster details page.

    2. In the left-side navigation pane, choose Settings and Management > Extension Management. On the Extension Management tab, select Uninstalled Extensions.

    3. In the upper-right corner of the page, select the target database. In the row for the polar_csi extension, click Install in the Actions column. In the Install Extension dialog box, select the target Database Account and click OK to install the extension in the target database.

    CLI

    Connect to the database cluster and run the following statement in a database where you have sufficient permissions to create the polar_csi extension.

    CREATE EXTENSION polar_csi;
  3. 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;
  4. In the postgres system database, install the pg_cron (scheduled tasks) extension. This extension runs tasks automatically at a specified time or interval.

    1. Switch to the database.

      \c postgres;
    2. 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.

  1. Switch to the target database (business database). This example uses testdb.

    \c testdb;
  2. 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

polar_csi.enable_query on

Allows the query to use the columnstore index.

polar_csi.cost_threshold 0

Forces the optimizer to use the columnstore index by setting the cost threshold to 0.

polar_csi.exec_parallel 6

Sets the degree of parallelism for columnstore computation. We recommend a value that 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

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.

Note

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

refresh_materialized_view_safely_using_csi

Function name. You can change it to fit your business needs.

view_name

Name of the materialized view.

schema_name

Schema in which the materialized view resides. Defaults to current_schema.

new_owner

New owner of the materialized view after it is re-created.

polar_csi.enable_query on

Allows the query to use the columnstore index.

polar_csi.cost_threshold 0

Forces the optimizer to use the columnstore index by setting the cost threshold to 0.

polar_csi.exec_parallel 6

Sets the degree of parallelism for columnstore computation. We recommend a value that does not exceed the number of CPU cores on the node.

polar_csi.memory_limit 10240

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

Note
  • Tasks can only be created in the postgres system 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_cron to 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 plain SELECT.

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.

  1. Switch to the database.

    \c postgres;
  2. Create the scheduled task. Replace the relevant parameters with your actual values.

    Note
    • Replace <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.

Note

Before you run the statement, switch to your actual business database.

SELECT customer_name, COUNT(*) FROM mv_customer_orders GROUP BY customer_name;