パイプラインブレーカとブロッキングオペレータ
ソートやハッシュ構築でクエリのメモリが跳ね上がる理由が分かります。実行計画を分断するブロッキングオペレータの正体と、確保メモリ・並列スケジューリングへの影響を原理から押さえられます。
- 1.パイプラインブレーカ(ブロッキングオペレータ)は、入力を全部読み終えるまで1行も出力できない演算子。ソート・ハッシュ集約・ハッシュ結合の構築側・ウィンドウ関数などが該当し、ここで実行計画のパイプラインが分断される。
- 2.ブロッキングオペレータは入力全体(または片側全体)を状態としてメモリに溜める必要があり、ここがクエリのピークメモリと作業メモリ(work_mem)確保の主因になる。溢れればディスクへスピルし I/O 律速に転じる。
- 3.パイプライン境界はスケジューリングの単位でもある。下流パイプラインは上流の完了を待つため逐次実行になり、並列化やインターリーブの効きどころ・メモリ予算の割り当てがこの境界で決まる。
パイプラインを分断する演算子とは
クエリの物理プランは演算子の木ですが、実行の観点ではもう一段粗い単位――パイプラインで捉えると、メモリとスケジューリングの挙動が一気に見通せます。パイプラインとは、行が止まらず流れ続ける一連の演算子の区間のことです。Scan が読んだ行が Filter を通り、そのまま結合の探索側を抜け、出力へ――この間どの演算子も「次の1行」を即座に上へ渡せるなら、それは1本のパイプラインです。
この流れを断ち切る演算子をパイプラインブレーカ(pipeline breaker)、あるいは**ブロッキングオペレータ(blocking operator)**と呼びます。定義は明快で、入力を最後の1行まで読み終えるまで、出力の最初の1行を生成できない演算子です。途中まで読んだだけでは答えが確定しないため、行を上へ流せず、そこでパイプラインが分断されます。
-- パイプライン境界の例(Sort がブレーカ)
[ パイプライン2 ] Sort.next() → ... 上流へ
▲ ここで分断(Sort は全入力を読むまで何も返せない)
[ パイプライン1 ] Scan → Filter → Sort へ全行を投入
なぜ全入力が要るのかは演算子ごとに理由が違いますが、効果は共通です。入力全体(または結合の片側全体)を状態としてメモリに保持する必要が生じ、それがクエリのピークメモリとスケジューリングの両方を支配します。本稿はこの構造を、メモリ確保と実行スケジューリングの観点から掘り下げます。実行モデルそのものの違いはクエリ実行モデルを参照してください。
なぜ「全入力」が必要になるのか
代表的なブロッキングオペレータが、なぜ最後の行まで待つのかを整理します。
- ソート(
ORDER BY・マージ結合の前処理): 最小値が入力のどこに現れるか事前に分からないため、全行を見るまで先頭の出力を確定できない。全行を整列バッファに溜める必要がある。 - ハッシュ集約(
GROUP BY・DISTINCT): あるグループの集計値は、そのキーを持つ最後の行を読むまで確定しない。全グループ分の集約状態をハッシュテーブルに保持する。 - ハッシュ結合の構築側(build): 探索(probe)を始める前に、片側全体からハッシュテーブルを作り切る必要がある。構築側は完全にブロッキングで、探索側はそのテーブルが完成して初めて流れ出す(ハッシュ結合のGrace/Hybrid)。
- ウィンドウ関数・
UNION(重複排除)・一部の集合演算: パーティション全体やソート順が確定しないと値を出せない。
対照的に、**ノンブロッキング(パイプライン可能)**な演算子は、Scan・Filter・式評価・ネステッドループ結合の探索側・ハッシュ結合の探索側など、「1行入れば1行(または0行)出せる」もので、状態をほとんど溜めません。
ハッシュ結合はよく「ブロッキング」と一括りにされますが、正確には構築側だけが完全ブロッキングで、探索側はパイプライン可能です。つまりハッシュ結合は構築側パイプラインの終端であると同時に、探索側パイプラインの途中の演算子でもあります。マージ結合は両入力のソートがブレーカで、ネステッドループ結合は(内側を都度走査するなら)どちらもブロックしません。「結合=ブロッキング」と丸暗記せず、演算子のどの入力が全件を要求するかで考えるのが正しい捉え方です。
メモリ確保の主因はブレーカである
ブロッキングオペレータが溜める状態こそが、クエリのピークメモリを決めます。PostgreSQL の work_mem、他システムの作業メモリやソート領域は、この状態を載せるための予算です。重要なのは、この予算が演算子(ノード)単位で確保される点です。
-- 同一クエリ内に複数のブレーカがあると予算は積み上がる
Hash Join
├─ Hash (build) ← work_mem 1 つぶん
└─ Sort (probe 側入力) ← work_mem もう 1 つぶん
ピークメモリ ≒ 同時に生存するブレーカ状態の合計
つまりプラン内に複数のブロッキングオペレータがあり、それらの状態が**同時に生存(オーバーラップ)**するなら、消費メモリはおおむねその合計になります。並列実行ではワーカーごとにこの予算が掛かるため、並列度 × ノード数 × work_mem のオーダーで膨らみ得ます。「work_mem を上げたら同時実行でメモリが枯渇した」という典型的事故の根はここにあります。
状態が予算に収まらなければ、ブレーカは中間データを一時ファイルへ書き出す**スピル(spill)**に移行します。スピルした瞬間からそのオペレータは CPU 律速ではなく I/O 律速になり、何回データを読み書きするかが速度を決めます。ソートなら外部ソートの多方向マージ、ハッシュ集約・ハッシュ結合ならパーティション分割と再帰処理に切り替わります(原理は外部ソートとスピルを参照)。
ブロッキングオペレータは入力全体を溜めるため、その地点でデータが必ずマテリアライズ(具現化)されます。前段の見積もり――特に行数の見積もり――が外れると、ここで想定外のメモリ確保やスピルが一気に顕在化します。プランの最初のブレーカは、推定誤差が初めて『現実のメモリ消費』として現れる場所だと考えると、チューニングの当たりが付けやすくなります。
パイプライン境界はスケジューリングの単位
ブレーカがもたらすもう一つの帰結がスケジューリングです。パイプライン境界は、実行エンジンが「ここまでを一気に流す」と決める単位そのものになります。
ブロッキングオペレータの下流(出力側)のパイプラインは、上流(入力側)のパイプラインが完全に終わるまで開始できません。ソートが全行を整列し終えるまで、その上のマージ結合は1行も処理できない。ハッシュ結合の構築が終わるまで、探索側パイプラインは走り出せない。ブレーカは逐次化(直列化)のポイントなのです。
-- 実行の時間軸(→ が時間。P1 が終わってから P2 が動く)
P1: Scan→Filter→Build/Sort =======■(完了)
P2: ======> (ここで初めて開始)
この性質から、スケジューラの設計上いくつかの帰結が生まれます。
- 並列化の境界: 1本のパイプライン内は素直に並列化(パーティション並列・データ並列)できますが、ブレーカをまたぐと「全ワーカーの上流完了を待ち合わせる」**同期点(バリア)**が入ります。並列の効きは概ねパイプライン単位で、ブレーカが多いほど待ち合わせが増えます。
- メモリ予算の配分: 同時に生きるパイプラインが少ないほど、各ブレーカへ割けるメモリが増えスピルを避けやすい。逆に多数のブレーカが同時生存するプランは、限られた予算を分け合い全員が中途半端にスピルしがちです。実行順をブレーカ境界で区切れることは、メモリ予算をいつ・どこへ振るかを決める足場になります。
- 構築の先行スケジューリング: ハッシュ結合では、探索側の前に構築側を先に走らせ切る必要があります。複数結合が連なるプランで「どの構築を先に作るか」「いくつ同時に構築状態を持つか」は、ピークメモリと直結する設計判断です。
複数ハッシュ結合の形は、同時にメモリ上に存在する構築側ハッシュテーブルの数を左右します。左深(left-deep)の木では、各結合の構築側を順に消費していくため、設計次第で生存する構築テーブルを少なく保てます。ブッシー(bushy)な木は並列性が上がる一方、複数の構築側が同時生存しやすくピークメモリが増えがちです。結合順の探索(結合順序の列挙)は、コストだけでなく『同時に生きるブレーカ状態の量』も意識して評価されます。
実行計画でブレーカを読む
実行計画上、ブロッキングオペレータは特徴的なノードとして現れます。PostgreSQL の EXPLAIN ANALYZE を例に取ると、見分け方と危険信号は次の通りです。
| ノード | ブロッキング性 | メモリ/スピルの読みどころ |
|---|---|---|
| Sort | 完全ブロッキング | Sort Method が quicksort なら in-memory、external merge ならスピル+Disk 使用量 |
| Hash / Hash Join | 構築側のみブロッキング | Hash ノードの Buckets/Batches。Batches > 1 はスピル |
| HashAggregate | 完全ブロッキング | Memory Usage と Disk Usage。Disk が出れば溢れている |
| WindowAgg | パーティション単位でブロッキング | ソート/区切りが必要な場合は前段 Sort のスピルに注意 |
| Materialize | 明示的ブロッキング | 意図的に中間結果を溜める。再走査の節約か、スピル要因かを見る |
実行計画を読むときの勘所は、ノードを上から眺めて「最初に行をブロックするのはどこか」を特定することです。そこがパイプラインの分断点であり、メモリ確保とスピルが起きる第一容疑者です。external merge・Batches > 1・Disk Usage のいずれかが出ていれば、そのブレーカで作業メモリを超過しています。対処は二択で、work_mem を増やして in-memory に収めるか、前段で行数を減らして溜める量自体を小さくするか。前者は同時実行でのメモリ枯渇と裏腹なので、まずは行数見積もりの精度(カーディナリティ推定)を疑うのが定石です。
パイプラインブレーカ=入力を全件読むまで1行も出せない演算子(ソート・ハッシュ集約・ハッシュ結合の構築側・ウィンドウ関数など)を即答できるように。効果は二つ。(1) メモリ:入力全体を状態として溜めるためクエリのピークメモリと作業メモリ確保の主因になり、溢れればスピルで I/O 律速。(2) スケジューリング:下流パイプラインは上流ブレーカの完了を待つ逐次化点となり、並列化のバリアとメモリ配分の境界を作る。ハッシュ結合は構築側のみブロッキングで探索側はパイプライン可能、という非対称性が頻出。
まとめ
- **パイプラインブレーカ(ブロッキングオペレータ)**は、入力を全件読み終えるまで出力を1行も生成できない演算子。ソート・ハッシュ集約・
DISTINCT・ハッシュ結合の構築側・ウィンドウ関数などが該当し、実行計画のパイプラインをそこで分断する。 - メモリ面では、ブレーカが入力全体(結合では片側全体)を状態として溜めるため、これがクエリのピークメモリと作業メモリ(
work_mem)確保の主因。同時生存するブレーカ状態の合計がピークを決め、並列度・ノード数で掛け算的に膨らむ。予算を超えればスピルし I/O 律速に転じる。 - スケジューリング面では、パイプライン境界が逐次化点となり、下流は上流ブレーカの完了を待つ。これが並列化のバリア、メモリ予算配分の単位、構築の先行スケジューリングの基準になる。
- 実行計画では
Sort・Hash・HashAggregate等がブレーカで、external merge・Batches > 1・Disk Usageがスピルの合図。work_mem調整と行数見積もりの改善が効く。スピルの内部機構は外部ソートとスピル、実行モデル全体はクエリ実行モデルを参照。
データベース Article
パイプラインブレーカとブロッキングオペレータを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
クエリ実行
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
ブロッキングオペレータは入力全体(または片側全体)を状態としてメモリに溜める必要があり、ここがクエリのピークメモリと作業メモリ(work_mem)確保の主因になる。溢れればディスクへスピルし I/O 律速に転じる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「クエリ実行 / 実行計画」に近いか確認する。
- 強みである「パイプラインブレーカ(ブロッキングオペレータ)は、入力を全部読み終えるまで1行も出力できない演算子。ソート・ハッシュ集約・ハッシュ結合の構築側・ウィンドウ関数などが該当し、ここで実行計画のパイプラインが分断される。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。