

# Aurora PostgreSQL で REPLICA IDENTITY FULL のパフォーマンスの問題を回避する
<a name="PostgreSQL.ReplicaIdentityFull"></a>

PostgreSQL 論理レプリケーションでは、サブスクライバーが更新または削除する正しい行を見つけることができるように、発行された各テーブルに*レプリカ ID* が必要です。デフォルトでは、プライマリキーがレプリカ ID として機能します。テーブルにプライマリキーまたは適切な一意のインデックスがない場合、レプリカ ID を `FULL` に設定することで、PostgreSQL は行全体をキーとして使用します。

`REPLICA IDENTITY FULL` はプライマリキーなしでテーブルをレプリケートするという即時の問題を解決しますが、パブリッシャーとサブスクライバーの両方で重大なパフォーマンスの問題が発生する可能性があります。これらの影響を理解することは、ブルー/グリーンデプロイなど、内部で論理レプリケーションに依存する機能を含め、Aurora PostgreSQL で論理レプリケーションを使用するすべての人にとって重要です。

## REPLICA IDENTITY FULL が問題を引き起こす理由
<a name="PostgreSQL.ReplicaIdentityFull.WhyProblems"></a>

### パブリッシャーの WAL ボリュームの増加
<a name="PostgreSQL.ReplicaIdentityFull.WALVolume"></a>

`REPLICA IDENTITY` 設定は、更新または削除された行を識別するために PostgreSQL がログ先行書き込み (WAL) に書き込む情報を制御します。デフォルトのレプリカ ID (プライマリキー) では、キー列のみが古い行 ID として記録されます。`FULL` を使用すると、PostgreSQL は `UPDATE` および `DELETE` のたびに、*すべて*の列の古い値を記録します。これにはいくつかの影響があります。
+ **WAL のサイズが大幅に増加します。**更新の場合、各列の古い値と新しい値の両方が記録されるため、各 WAL レコードのサイズは約 2 倍になります。テーブルに [TOAST](https://www.postgresql.org/docs/current/storage-toast.html) を使用して保存された大きな値が含まれている場合、更新によって変更されていない場合でも TOAST 化された値を取得して WAL に書き込む必要があるため、増加がさらに大きくなる可能性があります。
+ **パブリッシャーの I/O および CPU 使用率が高くなっています。**追加の WAL 書き込みは、特に書き込み負荷の高いワークロードの場合、より多くのディスク I/O 帯域幅と CPU サイクルを消費します。
+ **さらに多くのデータがサブスクライバーに送信されます。**パブリッシャーは、ネットワーク経由でより大きな WAL レコードを各サブスクライバーに送信する必要があり、その結果、帯域幅の消費量が増加します。

### サブスクライバーでの行検索の遅延
<a name="PostgreSQL.ReplicaIdentityFull.SlowLookups"></a>

サブスクライバーが `UPDATE` または `DELETE` ログレコードを受信すると、テーブルのローカルコピーで一致する行を見つける必要があります。`REPLICA IDENTITY FULL` を使用すると、サブスクライバーは古い行イメージの*すべて*の列値に一致する行を検索します。

PostgreSQL がこの検索を実行する方法は、PostgreSQL メジャーバージョンによって異なります。
+ **PostgreSQL 16 より前:** テーブルにプライマリキーがなく、明示的に設定されたレプリカ ID インデックスがない場合、サブスクライバーは `UPDATE` または `DELETE` オペレーションごとにテーブル全体のシーケンシャルスキャンを実行します。大きなテーブルでは、これにより適用パフォーマンスが極めて低下します。
+ **PostgreSQL 16 以降:** サブスクライバーは、そのインデックスがレプリカ ID として明示的に設定されていない場合でも、行検索に Btree インデックスまたはハッシュインデックスを使用できます。ただし、サブスクライバーはどのインデックスが最も効率的かを評価しません。[バージョン 16 以降](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=89e46da5e)、PostgreSQL は検出した[最初の適切なインデックス](https://www.postgresql.org/docs/18/logical-replication-publication.html#LOGICAL-REPLICATION-PUBLICATION-REPLICA-IDENTITY)を選択し、ユーザーはこの選択を制御できません。選択したインデックスの選択性が低い場合 (ブール値列やステータス列のインデックスなど)、行の検索はシーケンシャルスキャンとほぼ同じくらい遅くなる可能性があります。このため、`REPLICA IDENTITY FULL` を使用した暗黙的なインデックス選択に依存することは信頼性が低く、推奨される設定ではなくフォールバックと考える必要があります。

### REPLICA IDENTITY FULL がレプリケーション遅延を引き起こす方法
<a name="PostgreSQL.ReplicaIdentityFull.ReplicationLag"></a>

上記の 2 つの問題、つまりパブリッシャーの WAL が大きいこととサブスクライバーの行検索が遅いことが組み合わさって、レプリケーションの遅延が発生します。

デフォルトでは、PostgreSQL の論理レプリケーションは、サブスクリプションごとに単一の*適用ワーカー*プロセスを使用して、パブリッシャーから変更を受け取り、サブスクライバーのテーブルに適用します。適用ワーカーは、変更をコミット順に、一度に 1 行ずつ連続して処理します。つまり、サブスクライバーのスループットは、個々の変更を適用する速度によって制限されます。

適切なインデックスのないテーブルに `REPLICA IDENTITY FULL` が設定されている場合、`UPDATE` および `DELETE` のたびに、一致する行を見つけるためにテーブル全体のシーケンシャルスキャンが必要です。テーブルに数百万行ある場合、これらの各オペレーションには数秒以上かかることがあります。その結果、連鎖的な問題が発生します。

1. **パブリッシャーは、サブスクライバーが適用できるよりも速いペースで変更を生成します。**パブリッシャーの書き込みワークロードは通常の速度で継続されますが、サブスクライバーの適用ワーカーは、各行の検索においてシーケンシャルスキャンまたは選択性の低いインデックスによってボトルネックが発生しています。

1. **WAL はパブリッシャーに蓄積され、ストレージを使い果たす可能性があります。**PostgreSQL は、サブスクライバーが適用したことを確認するまで WAL セグメントを再利用することはできません。サブスクライバーがさらに遅れると、パブリッシャーは WAL をディスクに蓄積します。Aurora PostgreSQL では、これは CloudWatch で `OldestReplicationSlotLag` が増加する形で現れます。重大なケースでは、使用可能なすべてのストレージを消費し、パブリッシャーが書き込みの受け入れを停止する可能性があります。

1. **遅延は自己強化的です。**サブスクライバーが遅れると、サブスクライバーのテーブルはレプリケートされた挿入によって肥大化し、各シーケンシャルスキャンはさらに遅くなります。介入しない場合、遅延は制限なしで大きくなります。

この問題は、頻繁に `UPDATE` または `DELETE` オペレーションを受け取るテーブルでは特に深刻です。`INSERT` オペレーションは、サブスクライバーの行検索を必要としないため、影響を受けません。

**注記**  
PostgreSQL 16 以降、適用ワーカーは大規模なストリーミングトランザクションに並列適用を使用できるようになり、スループットの向上に役立ちます。ただし、インデックスを使用しない `REPLICA IDENTITY FULL` では、基本的な行検索のボトルネックは残ります。これは、個々の行を特定するためにスキャンが必要となるためです。

### ブルー/グリーンデプロイへの影響
<a name="PostgreSQL.ReplicaIdentityFull.BlueGreen"></a>

Amazon Aurora のブルー/グリーンデプロイでは、内部的に論理レプリケーションを使用して、データベースごとに 1 つのサブスクリプションを設定することで、グリーン環境とブルー環境の同期を維持します。グリーン環境の論理的なレプリケーション*適用プロセス*は、シングルスレッドです。単一の適用ワーカープロセスは、ブルー環境からすべての変更を受け取り、コミット順に一度に 1 つずつ適用します。ブルー/グリーンレプリケーションパスに並列適用はありません。

このシングルスレッド設計は、グリーン環境がブルー環境に対応する能力が、適用ワーカーが個々の変更を処理できる速度に完全に依存することを意味します。テーブルがプライマリキーまたは適切なインデックスなしで `REPLICA IDENTITY FULL` を使用する場合、適用ワーカーへの影響は PostgreSQL バージョンによって異なります。16 より前のバージョンでは、それらのテーブルに対するすべての `UPDATE` および `DELETE` で、適用ワーカーはテーブル全体のシーケンシャルスキャンを実行して一致する行を見つける必要がありました。バージョン 16 以降では、PostgreSQL は[適切なインデックス](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=89e46da5e)が利用可能であればそれを使用しますが、対象となるインデックスが存在しない場合は、適用ワーカーはシーケンシャルスキャンにフォールバックします。適用ワーカーが 1 行の大きなテーブルをスキャンしている間、すべてのテーブルにわたるその他の保留中の変更はすべてキューに入れられて待機します。

ブルー/グリーンデプロイへの影響は重大です。
+ **レプリケーションの遅延が継続的に増加します。**ブルー環境が単一の適用ワーカーが処理できるよりも速く書き込みトラフィックを生成する場合、グリーン環境はさらに遅れをとるようになります。適用ワーカーはシングルスレッドであるため、キャッチアップを並列化する方法はありません。
+ **スイッチオーバーはブロックできます。**ブルー/グリーンスイッチオーバーでは、グリーン環境をブルー環境と完全に同期する必要があります。レプリケーション遅延が大きすぎる場合、タイムアウト期間内にスイッチオーバーが完了しない場合があります。
+ **グリーン環境がいつまでもキャッチアップできない可能性があります。**`REPLICA IDENTITY FULL` を使用して、インデックスのない大きなテーブルで書き込み負荷の高いワークロードを実行する場合、適用レートが非常に遅くなり、グリーン環境が永続的に遅れる可能性があるため、最初にレプリカ ID 設定を解決しない限り、スイッチオーバーができない可能性があります。
+ **WAL はブルー環境に蓄積されます。**グリーン環境が遅れている間、ブルー環境はレプリケーションスロットの WAL セグメントを保持します。これにより、ブルー (本番稼働) 環境でのストレージ使用量が増加し、本番稼働のパフォーマンスに影響する可能性があります。

これらの問題を回避するために、ブルー/グリーンデプロイを作成する*前に*、すべてのテーブルにプライマリキー、または `ALTER TABLE ... REPLICA IDENTITY USING INDEX` を使用してレプリカ ID として明示的に設定された適切な一意のインデックスが存在することを確認します。PostgreSQL 16 以降では、サブスクライバーが選択性の低いインデックスを選択したり、シーケンシャルスキャンにフォールバックしたりする可能性があるため、暗黙的なインデックス選択を伴う `REPLICA IDENTITY FULL` に依存しないでください。代表的な書き込みワークロードを使用してデプロイをテストし、グリーン環境が維持できることを確認します。

ブルー/グリーンデプロイの制限事項の詳細については、「[Amazon Aurora のブルー/グリーンデプロイの制限と考慮事項](blue-green-deployments-considerations.md)」を参照してください。ベストプラクティスについては、[Aurora PostgreSQL のブルー/グリーンデプロイのベストプラクティス](blue-green-deployments-best-practices.md#blue-green-deployments-best-practices-postgres)を参照してください。

## REPLICA IDENTITY FULL を使用してテーブルを識別する方法
<a name="PostgreSQL.ReplicaIdentityFull.Identify"></a>

次のクエリを実行して、`REPLICA IDENTITY FULL` を持つすべてのテーブルを検索します。

```
SELECT n.nspname AS schema, c.relname AS table_name, c.relreplident
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
  AND c.relreplident = 'f'
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY n.nspname, c.relname;
```

`relreplident` 列の値は次のとおりです。
+ `d` - デフォルト (プライマリキー)
+ `n` - なし
+ `f` - full (行全体)
+ `i` - 特定のインデックス

## 回避策とベストプラクティス
<a name="PostgreSQL.ReplicaIdentityFull.Workarounds"></a>

### 可能な限りプライマリキーを追加する
<a name="PostgreSQL.ReplicaIdentityFull.AddPrimaryKey"></a>

最も効果的な解決策は、プライマリキーがないテーブルにプライマリキーを追加することです。プライマリキーが存在する場合、PostgreSQL はそれをデフォルトのレプリカ ID として使用します。これにより、サブスクライバーでの効率的な行検索が可能になり、パブリッシャーの WAL のオーバーヘッドが最小限に抑えられます。

```
ALTER TABLE my_table ADD COLUMN id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY;
```

**重要**  
このステートメントは、デフォルト値の式が揮発性の `nextval()` を使用しているため、`ACCESS EXCLUSIVE` ロックを取得し、テーブル全体を書き換えます。テーブルへのすべての読み取りと書き込みは、書き換えの間ブロックされます。大きなテーブルの場合、これは大幅なダウンタイムを引き起こす可能性があります。この変更はメンテナンスウィンドウ中に計画するか、最初に列を null 許容として作成し、その後、制約を別のステップでバックフィルして追加するなどの代替アプローチを検討してください。

アプリケーションの制約によりプライマリキーを追加できない場合は、`NOT NULL` 列のセットに一意のインデックスを追加し、それをレプリカ ID として設定することを検討してください。

```
CREATE UNIQUE INDEX my_table_replica_idx ON my_table (col1, col2);
ALTER TABLE my_table REPLICA IDENTITY USING INDEX my_table_replica_idx;
```

**注記**  
インデックスの構築中に書き込みがブロックされないようにするには、[https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY](https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY) 句 `CREATE UNIQUE INDEX CONCURRENTLY my_table_replica_idx ON my_table (col1, col2);` を使用します。

**注記**  
レプリカ ID に使用されるインデックスは一意でなければならず、部分的でなく、遅延可能でない、`NOT NULL` 制約のある列のみを含める必要があります。

### 暗黙的なインデックス選択に依存しない (PostgreSQL 16 以降)
<a name="PostgreSQL.ReplicaIdentityFull.SubscriberIndexes"></a>

PostgreSQL 16 以降、サブスクライバーの適用ワーカーは、レプリカ ID が `FULL` に設定されている場合、レプリカ ID として明示的に設定されていない場合でも、行検索に Btree インデックスまたはハッシュインデックスを使用できます。これにより、場合によってはシーケンシャルスキャンが防止されますが、この暗黙的な動作に依存することは、以下の理由からアンチパターンと言えます。
+ **どのインデックスが選択されるか制御できません。**PostgreSQL は、最も選択的または効率的なインデックスではなく、カタログ順で検出される最初の適格なインデックスを選択します。テーブルに複数の適格なインデックスがある場合、選択したインデックスの選択性が低くなり、検索パフォーマンスが低下する可能性があります。
+ **この動作は脆弱です。**インデックスを追加、削除、または再構築すると、適用ワーカーが使用するインデックスが変更され、レプリケーションで予期しないパフォーマンスのリグレッションが発生する可能性があります。
+ **根本的な問題を覆い隠します。**プライマリキーまたは明示的なレプリカ ID を持たないテーブルは、論理レプリケーションにおいて本質的にリスクがあります。暗黙的なインデックス選択に依存することは、問題を解決するのではなく、問題を先送りするだけです。

代わりに、レプリケートされたすべてのテーブルのレプリカ ID を明示的に設定します。
+ **最適なオプション:** プライマリキーを追加します。これは、最も信頼性が高く効率的なレプリカ ID です。
+ **代替方法:** `ALTER TABLE ... REPLICA IDENTITY USING INDEX` を使用して、`NOT NULL` 列のみを持つ部分的でない、遅延制約でもない、特定の一意インデックスを指定します。これにより、行の識別に使用される列を明示的に制御できます。

`REPLICA IDENTITY FULL` は、どちらのオプションも実現できないテーブルのみに使用してください。また、パフォーマンスは直接制御できない要因に依存することを理解します。

### レプリケーション遅延のモニタリング
<a name="PostgreSQL.ReplicaIdentityFull.MonitorLag"></a>

`REPLICA IDENTITY FULL` を使用する場合は、レプリケーションの遅延を注意深くモニタリングして、サブスクライバーのスローダウンが深刻化する前に検出します。

**パブリッシャー側**で、現在の WAL 位置とサブスクライバーが確認した位置の間の遅延を確認します。

```
SELECT slot_name, confirmed_flush_lsn, pg_current_wal_lsn(),
       (pg_current_wal_lsn() - confirmed_flush_lsn) AS lag_bytes
FROM pg_replication_slots
WHERE slot_type = 'logical';
```

`lag_bytes` の値が着実に増加している場合、サブスクライバーが遅れていることを示しています。`pg_stat_replication_slots` ビューには、各レプリケーションスロットの使用状況に関する追加の統計が表示されます。

**サブスクライバー側**では、`pg_stat_subscription` ビューには、最後に受信および報告された WAL ロケーションを含む各適用ワーカーの状態が表示されます。

```
SELECT subname, received_lsn, latest_end_lsn,
       last_msg_send_time, last_msg_receipt_time
FROM pg_stat_subscription;
```

**注記**  
PostgreSQL 16 以降では、`worker_type` を選択することで、メインの適用ワーカーと並列適用ワーカーを区別することもできます。

`received_lsn` と `latest_end_lsn` の間に大きなギャップがある場合、または `last_msg_send_time` のタイムスタンプが古い場合は、適用ワーカーが処理に追いついていないことを示している可能性があります。`pg_stat_subscription_stats` ビューでは、遅延の原因となる可能性のあるエラーや競合も追跡されます。

**Aurora PostgreSQL** では、元も遅延しているレプリケーションスロットの遅延をバイト単位で追跡する CloudWatch メトリクス `OldestReplicationSlotLag` をモニタリングすることもできます。値の増加は、レプリケーション遅延の早期警告サインです。詳細については、 を参照してください。[Aurora PostgreSQL 論理レプリケーションの書き込みスルーキャッシュと論理スロットのモニタリング](AuroraPostgreSQL.Replication.Logical-monitoring.md)

**適用中に最適でないインデックスを使用している可能性のあるテーブルを確認する**

サブスクライバーでは、適用ワーカーが過剰なヒープ読み取りを実行しているテーブルを特定できます。これは、適用中にテーブルに行検索の効率的なインデックスがないことを示している可能性があります。サブスクライバーに対して次のクエリを実行します。

```
SELECT relname, heap_blks_read, heap_blks_hit,
       idx_blks_read, idx_blks_hit,
       heap_blks_read + heap_blks_hit AS total_heap_access
FROM pg_statio_user_tables
WHERE heap_blks_read > 0
ORDER BY heap_blks_read DESC
LIMIT 10;
```

`heap_blks_read` に比べて `idx_blks_read` の値が高いテーブルは、適用ワーカーが `UPDATE` および `DELETE` オペレーションのために行検索する際に、効率的なインデックスを使用していないことを示している可能性があります。これは、`REPLICA IDENTITY FULL` を使用している場合に発生するレプリケーション遅延の一般的な原因です。

**注記**  
このクエリを実行するには、サブスクライバーで [https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-COUNTS](https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-COUNTS) パラメータが有効になっている必要があります。このパラメータはデフォルトでオンになっています。

### REPLICA IDENTITY FULL が必要かどうかを評価する
<a name="PostgreSQL.ReplicaIdentityFull.Evaluate"></a>

`REPLICA IDENTITY FULL` を設定する前に、本当に必要かどうかを検討してください。これを使用する一般的な理由は次のとおりです。
+ テーブルにプライマリキーまたは一意のインデックスがありません。
+ 変更データキャプチャ (CDC) コンシューマーには、変更前の行の完全なイメージが必要です。
+ これらの列を変更しない更新の場合、レプリケーションイベントに TOAST 化された列値を含める必要があります。

プライマリキーがないことが唯一の理由がであれば、プライマリキーの追加がほとんどの場合良い方法です。CDC の完全な前イメージが必要な場合は、CDC コンシューマーが外部で状態を維持することで完全な行を再構築できるかどうかを検討してください。これにより、`REPLICA IDENTITY FULL` の WAL とサブスクライバーのオーバーヘッドを回避できます。

## レコメンデーションの概要
<a name="PostgreSQL.ReplicaIdentityFull.Summary"></a>


| シナリオ | 推奨事項 | 
| --- | --- | 
| テーブルにプライマリキーがある | デフォルトのレプリカ ID を使用する (アクションは不要) | 
| テーブルに一意の NOT NULL インデックスがある | ALTER TABLE ... REPLICA IDENTITY USING INDEX を使用して、そのインデックスをレプリカ ID として設定する | 
| テーブルに適切なキーがない (PostgreSQL 16 以降) | プライマリキーまたは一意のインデックスを追加します。暗黙的なインデックス選択での REPLICA IDENTITY FULL の使用は信頼性が低く、最後の手段にする必要がある | 
| テーブルに適切なキーがない (PostgreSQL 16 より前) | プライマリキーまたは一意のインデックスを追加する。可能であれば REPLICA IDENTITY FULL は避ける | 
| 大きな列/TOAST 化された列を含む書き込み負荷の高いワークロード | WAL ボリューム増幅のため REPLICA IDENTITY FULL を回避する | 