【テクニカル・上級編】 クエリ実行エンジン – PostgreSQL

PostgreSQLの心臓部:実行エンジンという名の「職人」と付き合う作法

PostgreSQLのアーキテクチャを語る上で、プランナが作り上げた「実行計画(Execution Plan)」を実際に動かす実行エンジン(Executor)ほど、過小評価されがちな存在はないかもしれません。

多くのエンジニアは `EXPLAIN ANALYZE` の結果を見て、プランナの選択に一喜一憂します。しかし、真のパフォーマンスチューニングを追求するなら、その計画がいかにして「Executor」という現場の職人たちに渡され、どう実行されているのかという「解釈の深淵」を知る必要があります。

今日は、PostgreSQLのクエリ実行エンジンの内部で何が起きているのか、そして現場でよくある「なぜか遅い」クエリをどう深掘りすべきか、少しマニアックな話をしましょう。

—

Executorを支配する「Tuple-at-a-time」モデル

PostgreSQLの実行エンジンは、伝統的な「Volcano Model(Iterator Model)」を採用しています。これは、各ノード(Scan, Join, Sortなど)が `ExecProcNode` を呼び出すたびに、1行(タプル)ずつ上のノードへと引き渡していく仕組みです。

このモデルの何が面白いかというと、「極めてメモリ効率が良い」反面、「関数呼び出しのオーバーヘッドが無視できない」という点です。

例えば、数百万行を処理するクエリを想像してください。このモデルでは、数百万回もの関数コールがスタックを上下することになります。これが、CPUバウンドなクエリで特定の演算子や関数がボトルネックになったとき、`perf` 等のプロファイラが「なぜか `ExecInterpExpr` や `ExecScan` に時間が溶けている」という結果を叩き出す理由です。

なぜ「プランナの意図」と「現場の挙動」が乖離するのか

プランナは統計情報を元に「最もコストが低い」道を提示しますが、Executorはあくまで「その指示に従うだけ」です。ここでトラブルシューティングの勘所となるのが、以下の3点です。

1. 期待値と実測値のギャップ(Cardinality Estimation Error)

プランナが「100行しか返ってこない」と見積もったものが、実際には100万行だった場合、Executorは Nested Loop を選択し、地獄のようなシーケンシャルスキャンを繰り返すことになります。

  • 対策: `EXPLAIN ANALYZE` で `rows` と `actual rows` を比較するのは基本中の基本。もし乖離が激しければ、`ANALYZE` ではなく、列間の相関を考慮する `CREATE STATISTICS` を検討すべきです。

2. メモリ不足による「Spill to Disk」

Hash Join や Sort を行う際、`work_mem` を超えるとデータは一時ファイル(Temp Files)に書き出されます。これはExecutorにとって、メモリ内での高速なハッシュ計算から、低速なI/Oへの強制転換を意味します。

  • 深掘りポイント: `log_temp_files` を設定し、どのクエリがディスクI/Oに逃げているかを可視化してください。`work_mem` を闇雲に上げるのではなく、そのクエリが本当に多重度を持って実行されているかを精査するのがプロの仕事です。

3. パラレルクエリの落とし穴

最近のPostgreSQLは並列処理(Parallel Query)が強力ですが、Executorは各ワーカープロセスでそれぞれ計画を実行し、最終的に `Gather` ノードで結果を結合します。

  • 注意点: 並列実行はI/O負荷とCPU負荷のバランスを劇的に変えます。たまに、並列度を上げた結果、ロック待ち(LWLock)やプロセスの立ち上げコストが嵩み、シングルスレッドよりも遅くなるという本末転倒な事態に遭遇することがあります。

—

現場で「Executor」と対話するために

もしあなたが、実行計画を眺めても解決できないパフォーマンスの問題に直面しているなら、「このクエリはExecutorにとって、どの演算が一番重たいのか?」という視点に切り替えてみてください。

  • `EXPLAIN (ANALYZE, BUFFERS)` を使い倒せ: `BUFFERS` オプションを付けると、Shared Hit/Read/Written が見えます。これを見るだけで、そのノードが物理I/Oをどれだけ叩いているか、あるいは単にキャッシュヒットしているだけなのかが一目瞭然です。
  • JITコンパイルの是非: PostgreSQL 11以降、JITが導入されました。複雑な演算を含むクエリでは劇的な改善を見せますが、逆に単純なクエリではオーバーヘッドになることもあります。`jit = off` にして実行時間が改善するようなら、クエリの複雑性を見直すサインかもしれません。

最後に

PostgreSQLの実行エンジンは、非常に堅牢で、かつ予測可能な動きをします。魔法のような最適化は期待できませんが、プランナが生成した「設計図」を忠実に、そして泥臭く実行するその姿勢は、エンジニアとして信頼に値します。

結局のところ、データベースのパフォーマンスチューニングとは、プランナという優秀な参謀の「読み」を助けるために、統計というデータを整え、Executorという現場の職人が最大限の力を発揮できる環境を整える「環境整備」に他なりません。

皆さんのクエリが、今日も効率よくタプルをフェッチし、軽快に結果を返すことを願っています。さて、次はどのクエリを最適化しに行きましょうか?

コメント

タイトルとURLをコピーしました