億行規模などの大量のデータを処理する場合、PostgreSQL のマテリアライズドビューのリフレッシュは非常に遅くなります。古いデータは、BI 分析やレポート作成の効率を損ないます。PolarDB for PostgreSQL のインメモリ列指向インデックス (IMCI) 機能は、マテリアライズドビューのリフレッシュ時間を大幅に短縮しことで、データの鮮度を向上させ、BI 分析とレポート作成を高速化します。
ソリューション概要
IMCI は、PolarDB for PostgreSQL が提供する分析高速化エンジンです。行ストアテーブルの上に列ストアインデックスを構築し、行ストアデータと列ストアインデックスを自動的に同期させることができます。複雑な集約や JOIN クエリを実行する際、データベースは列ストアインデックスを使用して結果を計算できるため、従来の行ストアスキャンをはるかに超えるパフォーマンスを実現します。
このソリューションの核となる考え方は、マテリアライズドビューを支えるベーステーブルに列ストアインデックスを構築し、ビューの初期作成と後続のリフレッシュの両方を高速化することです。
前提条件
-
クラスターのバージョン:
-
PostgreSQL 14 (マイナーエンジンバージョン 2.0.14.10.20.0 以降)
-
PostgreSQL 15 (マイナーエンジンバージョン 2.0.15.15.7.0 以降)
-
PostgreSQL 16 (マイナーエンジンバージョン 2.0.16.8.3.0 以降)
-
PostgreSQL 17 (マイナーエンジンバージョン 2.0.17.7.5.0 以降)
説明マイナーエンジンバージョンは、コンソールで表示するか、
SHOW polardb_version;文を実行して確認できます。マイナーエンジンバージョンが要件を満たしていない場合は、マイナーエンジンバージョンをアップグレードしてください。 -
-
wal_levelパラメーターをlogicalに設定する必要があります。この設定により、論理デコーディングに必要な情報が先行書き込みログ (WAL) に追加されます。説明コンソールで wal_level パラメーターを設定できます。このパラメーターを変更するとクラスターが再起動します。ビジネス運用に応じて計画を立て、注意して進めてください。
-
ソーステーブルにはプライマリキーが必要であり、列ストアインデックスを作成する際にはプライマリキー列を含める必要があります。プライマリキーに
SERIALまたはBIGSERIALデータ型を使用すると、データ同期効率が大幅に向上するため推奨されます。 -
テーブルごとに作成できる列ストアインデックスは 1 つだけです。
注意事項
各テーブルに設定できる列ストアインデックスは 1 つだけです。
列ストアインデックスは変更できません。列ストアインデックスに列を追加するには、インデックスを再構築する必要があります。
事前準備
環境の準備
対象の PolarDB for PostgreSQL クラスター。
列ストアインデックス機能を有効にします。
IMCI を有効にする方法は、ご利用の PolarDB for PostgreSQL クラスターのマイナーエンジンバージョンによって異なります。
ターゲットデータベース (ビジネスデータベース) に、pg_hint_plan 拡張をインストールします。この拡張を使用すると、特別なコメント形式のヒントを使用して、すでに選択されているクエリプランを調整できます。
CREATE EXTENSION pg_hint_plan;postgresシステムデータベースに、pg_cron (定期タスク) 拡張をインストールします。この拡張は、指定した時間または間隔でタスクを自動的に実行します。データベースに切り替えます。
\c postgres;拡張をインストールします。
CREATE EXTENSION pg_cron;
データの準備
ターゲットデータベース (ビジネスデータベース) で、customers テーブルと orders テーブルを作成し、それらに列ストアインデックスを構築して、テストデータを挿入します。
ターゲットデータベース (ビジネスデータベース) に切り替えます。この例では
testdbを使用します。\c testdb;テーブルを作成し、データを挿入します。
-- customers テーブルとその列ストアインデックスを作成します。 CREATE TABLE customers ( customer_id SERIAL PRIMARY KEY, customer_name VARCHAR(100), email VARCHAR(100) ); CREATE INDEX idx_customers_csi ON customers USING csi; -- orders テーブルとその列ストアインデックスを作成します。 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; -- customers テーブルにデータを挿入します。 INSERT INTO customers (customer_name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com'), ('Charlie', 'charlie@example.com'); -- orders テーブルにデータを挿入します。 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);
マテリアライズドビューの作成
マテリアライズドビューを作成する際に、ヒントを使用してクエリオプティマイザーに列ストアインデックス経由でビューを計算させます。
/*+ SET(polar_csi.enable_query on) SET(polar_csi.cost_threshold 0) SET(polar_csi.max_parallel_workers 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;ヒントパラメーター
パラメーター | 説明 |
| クエリが列ストアインデックスを使用できるようにします。 |
| コストのしきい値を 0 に設定することで、オプティマイザーに列ストアインデックスを強制的に使用させます。 |
| 列ストア計算の並列度を設定します。ノードの CPU コア数を超えない値を推奨します。 |
| 計算に使用できるメモリを MB 単位で設定します。 |
polar_csi.max_parallel_workers パラメーターは、以前のカーネルバージョンでは polar_csi.exec_parallel という名前でした。polar_csi.max_parallel_workers をサポートしていないカーネルバージョンでは、代わりに polar_csi.exec_parallel を使用してください。
-
PostgreSQL 14:
-
バージョン 2.0.14.20.42.0 以前では
polar_csi.exec_parallelを使用します。 -
バージョン 2.0.14.20.43.0 以降では
polar_csi.max_parallel_workersを使用します。
-
-
PostgreSQL 16:
-
バージョン 2.0.16.11.15.0 以前では
polar_csi.exec_parallelを使用します。 -
バージョン 2.0.16.13.16.0 以降では
polar_csi.max_parallel_workersを使用します。
-
マテリアライズドビューのリフレッシュ
リフレッシュ関数の作成
リフレッシュプロセスは関数にラップされます。以下の関数は、インデックスと所有権を保持しながら新しいビューを安全に交換するため、推奨されます。
以下の関数は参照用に提供されています。安全なビューの交換を保証しますが、本番環境で使用する前に、ご自身の環境で十分にテストする必要があります。
-- view_name はマテリアライズドビューの名前です。schema_name はビューが存在するスキーマです (デフォルトは current_schema)。new_owner は新しく作成されたビューに割り当てられるオーナーです。
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
-- ターゲットスキーマを決定します (入力パラメーターまたは現在のスキーマを使用)。
IF schema_name IS NULL THEN
target_schema := current_schema();
ELSE
target_schema := schema_name;
END IF;
-- 完全修飾テーブル名を構築します。
qualified_old_name := format('%I.%I', target_schema, view_name);
qualified_new_name := format('%I.%I', target_schema, view_name || '_new');
RAISE NOTICE 'スキーマで操作しています: %', target_schema;
-- マテリアライズドビューが存在することを確認します。
IF NOT EXISTS (
SELECT 1 FROM pg_matviews
WHERE matviewname = view_name
AND schemaname = target_schema
) THEN
RAISE EXCEPTION 'マテリアライズドビュー "%" はスキーマ "%" に存在しません', view_name, target_schema;
END IF;
-- マテリアライズドビューの定義と現在のオーナーを取得します。
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 'マテリアライズドビュー "%" の定義の取得に失敗しました', view_name;
END IF;
-- 古いビューと新しいビューの名前を設定します。
old_view_name := view_name;
new_view_name := view_name || '_new';
-- IMCI パフォーマンスパラメーター。
SET LOCAL polar_csi.cost_threshold = 0;
-- クエリプランを出力します。
RAISE NOTICE 'マテリアライズドビューリフレッシュのクエリプラン:';
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
-- 新しいマテリアライズドビューを作成します。
EXECUTE format('/*+ SET(polar_csi.enable_query on) */ CREATE MATERIALIZED VIEW %s AS %s', qualified_new_name, view_definition);
-- 新しいオーナーが指定されている場合は、オーナーを設定します。
IF new_owner IS NOT NULL THEN
-- ユーザーが存在することを確認します。
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = new_owner) THEN
RAISE EXCEPTION 'ロール "%" は存在しません', new_owner;
END IF;
EXECUTE format('ALTER MATERIALIZED VIEW %s OWNER TO %I', qualified_new_name, new_owner);
RAISE NOTICE 'オーナーを "%" から "%" に変更しました', current_owner, new_owner;
END IF;
-- 古いビューから新しいビューにすべてのインデックスをコピーします。
FOR index_record IN
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = old_view_name
AND schemaname = target_schema
LOOP
-- 古いビュー名を新しいビュー名に置き換えます。
index_creation_sql := regexp_replace(
index_record.indexdef,
' ON ' || target_schema || '.' || old_view_name || ' ',
' ON ' || target_schema || '.' || new_view_name || ' ',
'i'
);
-- UNIQUE インデックスの特殊なケースを処理します。
index_creation_sql := regexp_replace(
index_creation_sql,
'INDEX ' || index_record.indexname || ' ON',
'INDEX ' || index_record.indexname || '_new ON',
'i'
);
RAISE NOTICE 'インデックスを作成しています: %', index_creation_sql;
EXECUTE index_creation_sql;
END LOOP;
-- ビューの権限をコピーします。
RAISE NOTICE '新しいビュー %.% に権限を復元しています', 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;
-- 古いマテリアライズドビューを削除します。
EXECUTE format('DROP MATERIALIZED VIEW %s', qualified_old_name);
-- 新しいマテリアライズドビューを元の名前に変更します。
EXECUTE format('ALTER MATERIALIZED VIEW %s RENAME TO %I', qualified_new_name, old_view_name);
-- インデックスの名前を変更します (_new サフィックスを削除)。
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 'マテリアライズドビューのリフレッシュに失敗しました: %', SQLERRM;
RETURN FALSE;
END;
END;
$$;パラメーター
パラメーター | 説明 |
| 関数名。ビジネスニーズに合わせて変更できます。 |
| マテリアライズドビューの名前。 |
| マテリアライズドビューが存在するスキーマ。デフォルトは current_schema です。 |
| 再作成後のマテリアライズドビューの新しいオーナー。 |
| クエリが列ストアインデックスを使用できるようにします。 |
| コストのしきい値を 0 に設定することで、オプティマイザーに列ストアインデックスを強制的に使用させます。 |
| 列ストア計算の並列度を設定します。ノードの CPU コア数を超えない値を推奨します。 |
| 計算に使用できるメモリを MB 単位で設定します。 |
polar_csi.max_parallel_workers パラメーターは、以前のカーネルバージョンでは polar_csi.exec_parallel という名前でした。polar_csi.max_parallel_workers をサポートしていないカーネルバージョンでは、代わりに polar_csi.exec_parallel を使用してください。
-
PostgreSQL 14:
-
バージョン 2.0.14.20.42.0 以前では
polar_csi.exec_parallelを使用します。 -
バージョン 2.0.14.20.43.0 以降では
polar_csi.max_parallel_workersを使用します。
-
-
PostgreSQL 16:
-
バージョン 2.0.16.11.15.0 以前では
polar_csi.exec_parallelを使用します。 -
バージョン 2.0.16.13.16.0 以降では
polar_csi.max_parallel_workersを使用します。
-
リフレッシュの実行
手動でのリフレッシュ
ビジネスで必要なときに、関数を手動で呼び出してリフレッシュをトリガーします。呼び出しの名前を実際のマテリアライズドビュー名に置き換えます。この例では mv_customer_orders を使用します。
SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders');pg_cron を使用したリフレッシュのスケジュール設定
タスクは
postgresシステムデータベースでのみ作成でき、特権アカウントのみが作成できます。再構築されたマテリアライズドビューのオーナーを指定できるため、特権アカウントによって再作成された後も一般ユーザーはビューを読み取ることができます。他の権限関連の設定を調整する必要がある場合は、上記で定義したリフレッシュ関数を変更してください。
pg_cronを使用してリフレッシュをスケジュールする場合、タスクの間隔が実際のリフレッシュ時間よりも厳密に長くなるようにしてください。そうしないと、タスクが溜まってしまいます。リフレッシュはデータを書き込むため、通常は単純なSELECTよりもはるかに遅くなります。
定期タスクの作成
postgres システムデータベースに切り替え、pg_cron を使用してタスク名、間隔、および操作を指定します。詳細については、「pg_cron (定期タスク) 拡張」をご参照ください。
データベースに切り替えます。
\c postgres;定期タスクを作成します。関連するパラメーターを実際の値に置き換えてください。
説明<mv_name>を実際のマテリアライズドビュー名に置き換えます。<database_name>を実際のビジネスデータベース名に置き換えます。<schema_name>を実際のスキーマ名に置き換えます。<user_name>を実際のユーザー名に置き換えます。
構文
SELECT cron.schedule_in_database( 'refresh_mv_customer_orders', -- タスク名 (カスタマイズ可能) '*/5 * * * *', -- Cron 式、例: 5 分ごとに実行 $$SELECT refresh_materialized_view_safely_using_csi('<mv_name>', '<schema_name>', '<user_name>')$$, '<database_name>' );例
SELECT cron.schedule_in_database( 'refresh_mv_customer_orders', -- タスク名 (カスタマイズ可能) '*/5 * * * *', -- Cron 式、例: 5 分ごとに実行 $$SELECT refresh_materialized_view_safely_using_csi('mv_customer_orders', 'public', 'polarpg')$$, 'testdb' );
定期タスクの表示
次の SQL 文を実行して、設定されている定期タスクを表示します。
SELECT * FROM cron.job;期待される出力:
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)定期タスクの削除
定期的なリフレッシュが不要になった場合は、次の SQL 文を実行して定期タスクを削除します。
SELECT cron.unschedule('refresh_my_materialized_view');タスク実行詳細の表示
次の SQL 文を実行して、定期タスクの実行詳細を表示します。
SELECT * FROM cron.job_run_details;期待される出力:
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)マテリアライズドビューのクエリ
次の SQL ステートメントを実行して、マテリアライズドビューをクエリします。名前を実際のマテリアライズドビュー名に置き換えてください。この例では、mv_customer_orders を使用します。
文を実行する前に、実際のビジネスデータベースに切り替えてください。
SELECT customer_name, COUNT(*) FROM mv_customer_orders GROUP BY customer_name;