TOASTはPostgreSQLで8KBのページサイズを超える行データを処理するために導入され、大きな列を別の物理ストレージに圧縮する役割を果たします。論理レプリケーションがWALを介して変更をストリーム配信する際、REPLICA IDENTITY設定が古いタプル値のどれが含まれるかを決定します。デフォルトのREPLICA IDENTITY DEFAULTは主キーのみを送信しますが、REPLICA IDENTITY FULLは完全な古い行の画像を送信します。
テーブルに約2KBを超えるJSONBまたはTEXT列があり、それらがTOASTに圧縮されている場合、非TOAST列のみを変更するUPDATE操作は、WALレコードの外部TOAST値を取得できない可能性があります。この論理デコーディングプロセスは、I/Oを削減するために変更されていないTOASTポインタをスキップし、競合解決中にNULLまたは欠落した値を受け取る原因となります。
REPLICA IDENTITY FULLに切り替えると、PostgreSQLはコミット中に外部ストレージから全てのTOAST値を明示的に取得し、WALレコードに完全な古いタプルを含めることを強制します。これにより、UPSERT操作のデータの完全性が保障されますが、WAL量が300-500%増加するため、広いJSONBテーブルにとっては非常に大きな負担となります。これは、すべてのUPDATEが行のフルプレイイメージをログに記録する必要があるからです。
ある金融取引プラットフォームが、規制報告のために主要なPostgreSQL 15クラスターからデータウェアハウスに注文書のスナップショットをレプリケートする必要がありました。market_dataテーブルは、楽器識別子と、深さを持つ情報を含む大きなJSONBペイロード(10-50KB)を格納していました。レプリケーションは、REPLICA IDENTITY DEFAULT(主キーのみ)を使用してpglogicalで行われました。ウェアハウス側のETLプロセスは、監査ログのデルタ変更を計算するために古いJSONB値を必要としていたため、ゆっくり変化する次元テーブルを維持するためにUPSERT操作を実行しようとしました。
高ボリュームの取引期間中に、注文書が頻繁に更新される一方でJSONBペイロードが変更されない場合、論理レプリケーションストリームは主キーと新しいタプルデータのみを含むUPDATEレコードを送信しました。TOASTされた古いJSONB値は、UPDATE文がupdated_atタイムスタンプ列のみを変更するため、変更セットに含まれませんでした。ETLプロセスは、事前の更新状態のJSONBにアクセスできず、監査トレイルの正確な価格変動デルタを計算できなくなり、MiFID IIのコンプライアンス要件に違反しました。
解決策1: REPLICA IDENTITY FULLへの切り替え このアプローチでは、パブリッシャーが各UPDATEごとに完全な古い行の画像をWALに書き込むことを強制します。TOASTストレージからフルのJSONBコンテンツを含みます。長所には、データの完全性の保証および構造変更を必要としない簡単な実装が含まれていました。しかし、短所は非常に大きく、50KBペイロードの場合、WAL生成量が約400%増加し、主ストレージの空き容量が危険にさらされ、ウェアハウスへのネットワーク遅延が増加するリスクがありました。1秒あたり10,000件の更新を処理するテーブルにとって、これは本番の安定性にはリスクが高すぎると見なされました。
解決策2: 専用の履歴テーブルを使用したアプリケーションレベルのジャーナリング
チームは、更新前に古いJSONB値を別のmarket_data_historyテーブルにコピーするトリガーを主要なテーブルに作成することを検討しました。長所は、論理レプリケーションがこの履歴テーブルを別にレプリケートでき、TOASTの省略問題を回避でき、主テーブルのWALフットプリントを小さく保てたことです。短所には、プライマリでの二重書き込みのオーバーヘッド(トランザクションの遅延を増加させる)、2倍の速度で増加する追加のストレージ要件、およびトランザクションIDとタイムスタンプを使用して履歴レコードを主テーブルの変更と相関させるためのETLロジックの複雑さが含まれていました。
解決策3: JSONBのハッシュを含むカバーインデックスを使用したREPLICA IDENTITYの使用
この戦略は、md5(jsonb_column::text)に基づく関数インデックスを作成し、そのハッシュを複合REPLICA IDENTITYインデックスに含めるというものでした。長所には、JSONBコンテンツの変更がWAL内のハッシュの変化を通じて検出可能で、ペイロード全体を送信する必要がないことが含まれていました。短所には、実際の古い値を取得できない(そのハッシュのみ)、および高回転テーブルのインデックス維持オーバーヘッドが含まれており、規制要件を満たすために必要な正確な事前更新状態を示すには不十分です。
チームは解決策2(アプリケーションレベルのジャーナリング)を選択しましたが、修正を加えました。PostgreSQLのバージョン14以上で利用可能なJSONBの部分更新最適化を活用し、完全な古い行の代わりに変更されたパス(diff)のみを保存するBEFORE UPDATEトリガーを実装しました。これにより、履歴テーブルの成長が軽減され、必要なすべての事前イメージデータが入手できるようになりました。主テーブルのWAL膨張を避けるために、主テーブルにはREPLICA IDENTITY DEFAULTを維持し、ETLが監査再構築のために履歴テーブルに対して結合するように指示しました。
レプリケーションストリームのサイズは安定しており、主ストレージへの圧力を防ぎました。ETLプロセスは、現在の行の状態と履歴テーブルから保存された差分を統合することに成功し、完全な監査トレイルの再構築を実現しました。規制コンプライアンスは、主ストレージが15%の増加で達成され(REPLICA IDENTITY FULLの場合は400%)、トランザクションスループットへの影響は最小限に抑えられました。
PostgreSQLの論理デコーディングがTOAST値を省略するのはなぜか、列が変更されていても?
多くの候補者は、任意のUPDATEが自動的にすべてのTOASTされた値をWALに取得するものだと仮定します。しかし、PostgreSQLは実際にデータを変更するためにダータムを読み込むときにのみ「タプルのunTOAST」を実行します。UPDATEが異なる列を変更する場合(例:SET updated_at = NOW())であり、JSONB列をそのターゲットリストやWHERE句で参照しない場合、TOASTポインタは変更されず、外部ストレージにもアクセスされません。このため、WALレコードにはディスク上のタプルとそのポインタのみが含まれ、実際のデータは含まれません。論理デコーディングはWALからタプルを再構築するため、古いバージョンのヒープやTOASTテーブルにアクセスしないため、省略された値は変更ストリーム内でNULLとして表示されます。
REPLICA IDENTITY FULLはHOT(ヒープオンリータプル)更新とどのように相互作用するのか?
候補者はしばしば、REPLICA IDENTITY FULLがテーブルにおけるHOT更新を無効にすることを見落とします。HOT更新は、インデックス付きの列が変更されない場合に、PostgreSQLが同じデータページ内で行バージョンをチェーンできるようにします。REPLICA IDENTITY FULLがアクティブな場合、すべてのUPDATEがレプリケーションのために完全な古い行の画像を記録する必要があり、システムは全内容で行を一意に特定する必要があります。これにより、論理レプリケーションは完全なタプル比較データを必要とし、インデックスの更新を強制し、インデックス付きでない列を変更しても、多くの行バージョンのすべてのUPDATEで行インデックスが追加され、HOT最適化が壊れます。したがって、この設定のテーブルは、インデックスの膨張やI/Oの増加を経験し、高回転テーブルにとって重要なトレードオフとなります。
TOAST圧縮とPostgreSQLのWAL圧縮の違いは何であり、それらは論理レプリケーション中にどのように相互作用しますか?
この質問は、深いシステム知識と表面的な理解を区別します。TOAST圧縮は、LZ4またはPGLZを使用して大きな列を外部テーブルに保存する前に行サイズを削減します。WAL圧縮(wal_compression=lz4で有効)は、クラッシュリカバリの効率のためにWALに書き込まれた完全なページ画像を圧縮します。しかしREPLICA IDENTITY FULLが使用される場合、論理デコーディングに送信される古いタプルデータは、ストレージのためにWALレコードが圧縮される前に抽出されます。したがって、論理デコーダは、(取得された場合)圧縮されないTOASTデータを受け取り、一方で物理的なWALファイルはフルページ画像の一部として圧縮された状態で保存され、ネットワーク帯域幅とディスクI/Oに異なる影響を与えます。