Amazon Web Services ブログ

Amazon Aurora PostgreSQL の共有プランキャッシュの使用

本記事は 2026 年 1 月 20 日 に公開された「Using the shared plan cache for Amazon Aurora PostgreSQL」を翻訳したものです。

本記事では、Amazon Aurora PostgreSQL 互換エディションの共有プランキャッシュ機能により、高い同時実行性環境で汎用 SQL プランのメモリ消費を大幅に削減できることを説明します。40GB のメモリ負荷を 400MB まで削減できます。

Aurora PostgreSQL データベースクラスターが数千の同時接続を処理し、それぞれが同じプリペアドステートメントを実行している状況を想像してください。クエリ自体はシンプルなのに、メモリ使用量が数十 GB まで増加しています。何が起きているのでしょうか。これはプランの重複による隠れたコストが発生している可能性があり、共有プランキャッシュで解決できます。

PostgreSQL の汎用プランを理解する

プリペアドステートメントは、アプリケーションで (データベースとやり取りする関数やメソッドを定義する際に) 一般的に使われます 。これらのステートメントは、データベースアクセスコードやメソッドに含まれます。準備フェーズには SQL ステートメントの構造とプレースホルダーが含まれ、アプリケーションがプリペアドステートメントを実行する際に実際の値が入ります。準備フェーズでステートメントが解析、分析、書き換えられるため、実行時の解析と分析作業の繰り返しが省けます。

ソリューションに入る前に、PostgreSQL がプリペアドステートメントをどう扱うかを理解しましょう。PostgreSQL と Aurora PostgreSQL では、プリペアドステートメントは 2 種類のプランで実行できます。

  • カスタムプラン: 実行ごとに特定のパラメータ値で新しく作成され、リテラルが含まれます。
  • 汎用プラン: パラメータに依存しないプランで、実行間で再利用され、リテラルは含まれません。

デフォルトでは、PostgreSQL はこれら 2 つのプランタイプの選択にインテリジェントなアプローチを用います。

  1. プリペアドステートメントの最初の 5 回の実行ではカスタムプランを使用
  2. これらのカスタムプランの平均コストを計算
  3. 6 回目の実行で汎用プランを作成
  4. 汎用プランのコストがカスタムプランの平均コストと同等かそれ以下なら、以降の実行で使用

このアプローチは頻繁に実行されるクエリのプラン作成時間を節約できますが、多数の同時データベース接続がある環境では隠れたコストが発生します。

問題: 大規模環境でのメモリ非効率

このアプローチは個々の接続ではうまく機能しますが、多数の同時データベース接続がある環境では 2 つの大きな非効率が生じます。

  1. 不要なプラン生成: 汎用プランが使われない場合 (カスタムプランの方が効率的なため) でも、コスト比較のためにシステムは汎用プランを作成してメモリに保存します。たとえば、パーティションテーブルでは、コストがリーフパーティションごとに計算されて合計されるため、汎用プランが使われない可能性が高くなります。
  2. プランの重複: 同じクエリが数百または数千のセッションで実行される場合、各セッションが同一の汎用プランのコピーを保持し、大量のメモリ重複が発生します。

この問題を具体例で示してみましょう。

テスト環境のセットアップ

この例では、新しいセッションで各 1000 パーティションを持つテーブル t1 と t2 を作成します。次に、各ループで 1000 個の値を挿入する処理を 100 回ループし、各テーブルに 100,000 行を挿入します。最後に両方のテーブルの統計情報を更新します。

注意: 共有プランキャッシュ機能を使うには、Aurora PostgreSQL バージョン 17.6 以降、またはバージョン 16.10 以降を使用する必要があります。

-- Create partitioned tables
CREATE TABLE t1(part_key int, c1 int) PARTITION BY RANGE(part_key);
CREATE TABLE t2(part_key int, c1 int) PARTITION BY RANGE(part_key);

\pset pager

-- Generate 1000 partitions for each table (simulating large-scale partitioning)
SELECT 'CREATE TABLE t1_' || x || ' PARTITION OF t1 FOR VALUES FROM (' || x || ') TO (' || x+1 || ')'
FROM generate_series(1, 1000) x;
\gexec

SELECT 'CREATE TABLE t2_' || x || ' PARTITION OF t2 FOR VALUES FROM (' || x || ') TO (' || x+1 || ')'
FROM generate_series(1, 1000) x;
\gexec

-- Populate tables with sample data
DO
$do$
BEGIN
FOR i IN 1..100 LOOP
INSERT INTO t1 SELECT x, i FROM generate_series(1, 1000) x;
INSERT INTO t2 SELECT x, i FROM generate_series(1, 1000) x;
END LOOP;
END
$do$;

-- Update statistics for optimal query planning
ANALYZE t1, t2;

\gexec スイッチを使って、select の出力を独立した SQL ステートメントとして実行できます。\pset pager で psql pager を無効にすると、テーブルパーティション作成時に何度も Enter を押す必要がなくなります。

メモリ消費の観察

Session 1 で、次のプリペアドステートメントを作成して実行します。

-- Create a prepared statement with a simple join
PREPARE p2(int, int) AS 
SELECT sum(t1.c1) 
FROM t1, t2 
WHERE t1.part_key = t2.part_key 
AND t1.c1 = $1 
AND t1.part_key = $2;

-- Execute 6 times to trigger generic plan creation
EXECUTE p2(1, 4); -- Execution 1: Custom plan
EXECUTE p2(1, 4); -- Execution 2: Custom plan
EXECUTE p2(1, 4); -- Execution 3: Custom plan
EXECUTE p2(1, 4); -- Execution 4: Custom plan
EXECUTE p2(1, 4); -- Execution 5: Custom plan
EXECUTE p2(1, 4); -- Execution 6: Generic plan created

次に、メモリ消費を確認します。

-- Check memory usage for cached plans
SELECT name, ident, pg_size_pretty(total_bytes) as size
FROM pg_backend_memory_contexts 
WHERE name = 'CachedPlan';
-[ RECORD 1 ]-+---------------------------------------
name | CachedPlan
ident | prepare p2(int, int) as +
| select sum(t1.c1) +
| from t1, t2 +
| where t1.part_key = t2.part_key and +
| t1.c1 = $1 and t1.part_key = $2;
size | 4161 kB

このテストでは、汎用プランが約 4MB を消費し、プリペアドステートメントが解放されるか接続が終了するまでメモリに残ることがわかります。

重複の問題

次に、別のセッション (Session 2) で同じプリペアドステートメントを実行します。

-- Session 2: Using the same prepared statement
PREPARE p2(int, int) AS 
SELECT sum(t1.c1) 
FROM t1, t2 
WHERE t1.part_key = t2.part_key 
AND t1.c1 = $1 
AND t1.part_key = $2;

-- Execute 6 times
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);

-- Check memory usage
SELECT name, ident, pg_size_pretty(total_bytes) as size
FROM pg_backend_memory_contexts 
WHERE name = 'CachedPlan';

-[ RECORD 1 ]-+---------------------------------------
name | CachedPlan
ident | prepare p2(int, int) as +
| select sum(t1.c1) +
| from t1, t2 +
| where t1.part_key = t2.part_key and +
| t1.c1 = $1 and t1.part_key = $2; 
size | 4161 kB

Session 2 も全く同じ汎用プランで 4MB を消費しています!

乗算効果

この重複は、プリペアドステートメントを実行するすべてのセッションで発生します。影響を計算してみましょう。

  • 1 プリペアドステートメント × 100 接続 × 4MB = 400MB のメモリ
  • 100 種類のプリペアドステートメント × 100 接続 × 4MB = 40GB のメモリ

この大量のメモリ消費は、セッションが同一の汎用プランのコピーを保存しているにもかかわらず発生します。多数の同時データベース接続がある環境では、利用可能なメモリをすぐに使い果たし、より大きく高価なインスタンスタイプを使わざるを得なくなります。

ソリューション: Aurora PostgreSQL 共有キャッシュプラン

Aurora PostgreSQL は共有キャッシュプラン (SPC) でこの問題を解決します。各汎用プランのコピーを 1 つだけ保持し、複数セッションが使えるようにします。プランキャッシュのパフォーマンス上の利点を維持しながら、メモリ消費を大幅に削減します。

共有プランキャッシュ (SPC) は、クラスターまたはインスタンスパラメータグループで有効にできます。

apg_shared_plan_cache.enable = ON

apg_shared_plan_cache.enable は動的パラメータであるため、変更を有効にするためにインスタンスを再起動する必要はありません。

SPC は動的ハッシュテーブルとして実装され、セッション間で共有されます。キャッシュ内のエントリ数は apg_shared_plan_cache.max で制御できます。次のパラメータでエントリの最小サイズと最大サイズも制御できます。

apg_shared_plan_cache.min_size_per_entry
apg_shared_plan_cache.max_size_per_entry

共有プランキャッシュの動作デモ

共有プランキャッシュを有効にして、先ほどの実験を繰り返してみましょう。

Session 1 (最初の接続):

-- Create and execute the same prepared statement
PREPARE p2(int, int) AS 
SELECT sum(t1.c1) 
FROM t1, t2 
WHERE t1.part_key = t2.part_key 
AND t1.c1 = $1 
AND t1.part_key = $2;

-- Execute 6 times
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);

-- Check memory usage
SELECT name, ident, pg_size_pretty(total_bytes) as size
FROM pg_backend_memory_contexts 
WHERE name = 'CachedPlan';

最初のセッションは、ローカルメモリに 4MB のプランが表示されます (共有キャッシュに入力するために必要)。

Session 2 (後続の接続):

-- Create the same prepared statement
PREPARE p2(int, int) AS 
SELECT sum(t1.c1) 
FROM t1, t2 
WHERE t1.part_key = t2.part_key 
AND t1.c1 = $1 
AND t1.part_key = $2;

-- Execute 6 times
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);
EXECUTE p2(1, 4);

-- Check memory usage
SELECT name, ident, pg_size_pretty(total_bytes) as size
FROM pg_backend_memory_contexts 
WHERE name = 'CachedPlan';
(0 rows)

ローカルプランストレージなし! 2 番目のセッションは共有プランキャッシュを使っています。

キャッシュ使用状況の監視

次の SQL を実行して、キャッシュに保存された個々の共有プランが受け取ったキャッシュヒット数を表示します。各ヒットは、セッションメモリで重複する必要がなかったプランを表します。

-- View shared plan cache statistics
SELECT cache_key, query, hits
FROM apg_shared_plan_cache();
-[ RECORD 1 ]-------------------------------------
cache_key | -5127257242415815179
query | prepare p2(int, int) as +
| select sum(t1.c1) +
| from t1, t2 +
| where t1.part_key = t2.part_key and +
| t1.c1 = $1 and t1.part_key = $2;
hits | 2

クリーンアップ:

-- clear the cache
SELECT * FROM apg_shared_plan_cache_reset();

-- drop the tables
DROP TABLE t1;
DROP TABLE t2;

パフォーマンスへの影響

100 接続で 100 種類のプリペアドステートメントを使う先のシナリオ例では、40GB の重複プランストレージからわずか 400MB の共有キャッシュに変換されました。以下のスクリーンショットは、apg_shared_plan_cache.enable = off で 100 接続で 100 種類のプリペアドステートメント (上記の例から使用) を使って pgbench でテストを実行したインスタンスから取得した Freeable Memory CloudWatch メトリックのグラフです。02:05 から 02:10 の間に、FreeableMemory が約 40GB 減少しています。これは予想される重複プランストレージのフットプリントと一致します。共有プランキャッシュを有効にして同じテストを再度実行すると、メモリへの影響は大幅に削減され、40GB ではなくわずかなメモリしか必要としませんでした。

CloudWatch memory utilization graph showing freeable memory and a 56.8% memory dip running many similar queries without the shared plan cache and only a 7% drop when using it

この削減により、次のことが可能になります。

  • 同じワークロードをより小さいインスタンスで実行し、AWS コストを大幅に削減
  • メモリ制限に達することなくより多くの同時接続をサポート
  • トラフィックスパイク時のメモリ不足エラーを回避

ベストプラクティス

この機能は次の場合に特に有益です。

  • アプリケーションが数百または数千のデータベース接続を維持している
  • プリペアドステートメントを多用している
  • クエリにパーティションテーブルや複雑な演算子 (join や共通テーブル式など) が含まれ、大きなプランが生成される
  • バックエンドプロセスからの高いメモリ使用量が観察される
  • ワークロードに、パラメータ化されたクエリを使用した反復的なクエリパターンがある

共有キャッシュプランは大きな利点を提供しますが、次のシナリオには適さない場合があります。

  • 一意性が高いアドホッククエリを用いるワークロード
  • プリペアドステートメントをほとんど再利用しないアプリケーション
  • 同時接続が少ない環境

まとめ

本記事では、Aurora PostgreSQL で共有キャッシュプランを有効にする方法を説明しました。多数の同時データベースセッションでプリペアドステートメントを使用する際に、同じ汎用クエリプランがメモリに重複して保存されるのを防げることを示しました。

セッション間で冗長なプランストレージを削除することで、より小さいインスタンスでより多くの接続を実行でき、運用の複雑さとコストの両方を削減できます。さまざまなプランタイプの詳細については PostgreSQL ドキュメントの the prepare statement を、空きメモリ測定の詳細については Amazon CloudWatch metrics for Amazon Aurora を参照してください。

著者について

Souvik Bhattacherjee

Souvik Bhattacherjee

Souvik は AWS の Senior Software Engineer で、Aurora PostgreSQL データベースのクエリ処理機能の向上に取り組んでいます注。データベース/HPC 業界で 8 年以上の経験があり、データベースシステムと高性能コンピューティングシステムに関連するトピックに貢献してきました。

Jungkook Lee

Jungkook Lee

Jungkook は AWS の Senior Software Development Engineer で、Aurora PostgreSQL のパフォーマンス向上と機能拡張に注力するチームをリードしています。データベースシステムと分散コンピューティングアーキテクチャで 10 年以上の経験があり、クエリ最適化とデータベースパフォーマンスを専門としています。

Stephen Wood

Stephen Wood

Stephen は AWS の Senior Specialist Database Solutions Architect です。Amazon RDS PostgreSQL、Amazon Aurora PostgreSQL、Amazon Aurora DSQL を専門としています。過去 24 年間、さまざまなタイプの企業でデータベースシステムに関わっており、常に新しいデータベーステクノロジーに取り組むことを楽しんでいます。


翻訳は Technical Account Manager の石渡が担当しました。