数十億行規模の大量のデータを処理する場合、PostgreSQL のマテリアライズドビューのリフレッシュは非常に遅くなります。古いデータは BI 分析やレポート作成の効率を低下させます。PolarDB for PostgreSQL (Compatible with Oracle) のインメモリ列指向インデックス (IMCI) 機能は、マテリアライズドビューのリフレッシュ時間を大幅に短縮しことで、データの鮮度を向上させ、BI 分析とレポート作成を高速化します。
ソリューション概要
IMCI は PolarDB for PostgreSQL (Compatible with Oracle) が提供する分析高速化エンジンです。行ストアテーブル上に列ストアインデックスを構築し、行ストアデータと列ストアインデックスを自動的に同期させることができます。複雑な集約や結合クエリを実行する際、データベースは列ストアインデックスを使用して結果を計算できるため、従来の行ストアスキャンをはるかに超えるパフォーマンスを実現します。
このソリューションの中核となる考え方は、マテリアライズドビューを支えるベーステーブルに列ストアインデックスを構築することで、ビューの初回作成と後続のリフレッシュの両方を高速化することです。
前提条件
-
クラスターのバージョン:
Oracle 構文互換性 2.0 (マイナーエンジンバージョン 2.0.14.10.20.0 以降)
説明コンソールでマイナーエンジンバージョンを表示するか、
SHOW polardb_version;文を実行して確認できます。マイナーエンジンバージョンが要件を満たさない場合は、マイナーエンジンバージョンをアップグレードしてください。 -
wal_levelパラメーターをlogicalに設定する必要があります。この設定により、先行書き込みログ (WAL) に論理デコーディングに必要な情報が追加されます。説明コンソールで wal_level パラメーターを設定できます。このパラメーターを変更するとクラスターが再起動します。ご自身のビジネス運用に合わせて計画し、注意して操作を進めてください。
-
ソーステーブルにはプライマリキーが必要であり、列ストアインデックスを作成する際にはプライマリキー列を含める必要があります。プライマリキーに
SERIALまたはBIGSERIALデータ型を使用すると、データ同期の効率が大幅に向上するため推奨されます。 -
各テーブルに作成できる列ストアインデックスは 1 つだけです。
注意事項
各テーブルに作成できる列ストアインデックスは 1 つだけです。
列ストアインデックスは変更できません。列ストアインデックスに列を追加するには、インデックスを再構築する必要があります。
事前準備
環境の準備
要件を満たす PolarDB for PostgreSQL (Compatible with Oracle) クラスター。
列ストアインデックス機能を有効にします。
IMCI を有効にする方法は、ご利用の PolarDB for PostgreSQL (Compatible with Oracle) クラスターのマイナーエンジンバージョンによって異なります。
対象のデータベース (ビジネスデータベース) に、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.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;ヒントパラメーター
パラメーター | 説明 |
| クエリが列ストアインデックスを使用できるようにします。 |
| コストのしきい値を 0 に設定することで、オプティマイザーに列ストアインデックスの使用を強制します。 |
| 列ストア計算の並列度を設定します。ノードの CPU コア数を超えない値を推奨します。 |
| 計算に使用できるメモリを MB 単位で設定します。 |
マテリアライズドビューのリフレッシュ
リフレッシュ関数の作成
リフレッシュ処理を関数として定義します。以下の関数は、インデックスと所有権を保持しながら新しいビューを安全に入れ替えるため、推奨されます。
以下の関数は参照用です。安全なビューの入れ替えを保証しますが、本番環境で使用する前に、ご自身の環境で十分にテストする必要があります。
-- 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 'Operating in schema: %', target_schema;
-- マテリアライズドビューが存在することを確認します。
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;
-- マテリアライズドビューの定義と現在の所有者を取得します。
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;
-- 古いビューと新しいビューの名前を設定します。
old_view_name := view_name;
new_view_name := view_name || '_new';
-- IMCI パフォーマンスパラメーター。
SET LOCAL polar_csi.cost_threshold = 0;
-- クエリプランを出力します。
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
-- 新しいマテリアライズドビューを作成します。
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 '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;
-- 古いビューから新しいビューにすべてのインデックスをコピーします。
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 'Creating index: %', index_creation_sql;
EXECUTE index_creation_sql;
END LOOP;
-- ビューの権限をコピーします。
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;
-- 古いマテリアライズドビューを削除します。
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 'Failed to refresh materialized view: %', SQLERRM;
RETURN FALSE;
END;
END;
$$;パラメーター
パラメーター | 説明 |
| 関数名。ビジネスニーズに合わせて変更できます。 |
| マテリアライズドビューの名前。 |
| マテリアライズドビューが存在するスキーマ。デフォルトは current_schema です。 |
| 再作成後のマテリアライズドビューの新しい所有者。 |
| クエリが列ストアインデックスを使用できるようにします。 |
| コストのしきい値を 0 に設定することで、オプティマイザーに列ストアインデックスの使用を強制します。 |
| 列ストア計算の並列度を設定します。ノードの CPU コア数を超えない値を推奨します。 |
| 計算に使用できるメモリを MB 単位で設定します。 |
リフレッシュの実行
手動でのリフレッシュ
ビジネスで必要な場合、関数を手動で呼び出してリフレッシュをトリガーします。呼び出しの際の名前を実際のマテリアライズドビュー名に置き換えてください。この例では 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;