TL

クエリコンパイル実行とデータ中心コード生成

大量集計でクエリを専用ネイティブコードへ落とすと、解釈オーバーヘッドが消えて桁で速くなります。HyPer 流の produce/consume モデルと分岐削減の原理を、内部動作から押さえられます。

応用クエリコンパイルコード生成HyPerLLVMproduce/consumeJIT最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.データ中心コード生成は HyPer が示した方式で、演算子ごとに produce/consume を再帰展開し、マテリアライズ不要な一連の演算子を1つのタイトなループへ融合する。LLVM などで生成したネイティブコードを実行し、Volcano の next() 連鎖と仮想関数呼び出しを除去する。
  • 2.鍵はパイプライン境界で、結合のハッシュ表構築や集計のように入力を一度ためる演算子(pipeline breaker)でループが切れる。1パイプライン内ではタプルが CPU レジスタに載ったまま複数演算子を通過し、メモリ往復と分岐が激減する。
  • 3.代償はクエリごとのコンパイル時間で、短いクエリでは実行より長くなり得る。実務はバイトコード/軽量 JIT での即時開始、最適化済みコードへの並行差し替え、コードキャッシュで費用を回収する。

解釈をやめて「クエリ専用プログラム」に変える

Volcano モデルは演算子をイテレータにして1行ずつ next() を呼びますが、大量集計では実データ演算1回の周囲を仮想関数呼び出し・型分岐・式インタプリタの固定費が何重にも取り囲み、CPU 命令の大半が「行を計算する」のではなく「行を運ぶ」ために費やされます。クエリコンパイル実行はこの解釈の層そのものを消す方式です。受け取ったプランを汎用エンジンで解釈するのではなく、そのクエリだけを実行する専用の機械語を生成して走らせます。本稿は HyPer(Neumann 2011)が確立した**データ中心コード生成(data-centric code generation)**の内部動作を、produce/consume モデルと分岐削減の観点から正確に追います。

オペレータ中心 vs データ中心

素朴なコード生成は「演算子ごとにループを書く」オペレータ中心になりがちです。Scan のループ、Filter のループ、Aggregate のループを順に並べ、各段が中間結果を配列へ書き出して次段が読む。これは Volcano の関数呼び出しを消すだけで、**段ごとのマテリアライズ(メモリ書き出し)**が残り、タプルがメモリとレジスタを往復し続けます。

HyPer の着想はここを逆転させます。中心に置くのは演算子ではなく**データ(タプル)**で、原則は「1つのタプルは、メモリへ書き戻す必要が生じるまで、できるだけ多くの演算子をレジスタ上で通り抜けさせる」です。タプルがどこまで CPU レジスタに留まれるかが性能を決めるため、コードはタプルの流れを軸に編成されます。

データの局所性(data locality)が設計原則

Volcano は演算子の局所性(各演算子のコードがまとまっている)を優先し、その代償にデータがメモリを往復します。データ中心コード生成は逆にデータの局所性を優先し、タプルをレジスタに載せ続けるためなら1つの演算子のコードが生成結果の複数箇所に分散することを許します。最適化の主語が「コード」から「データ」へ移るのが本質です。

パイプライン境界(pipeline breaker)

どこまでレジスタ上で進めるかを決めるのがパイプライン境界です。ある演算子が入力タプルを一度ためてからでないと出力できないとき、そこでパイプラインが切れます。これを pipeline breaker と呼びます。

  • pipeline breaker でない演算子: Filter、射影、式評価などは、タプルを受け取ったその場で次へ渡せる。レジスタ上の流れを止めない。
  • pipeline breaker である演算子: ハッシュ結合の build 側(ハッシュ表を作り切る)、Aggregate(全グループを集計し切る)、ソートなどは、入力を全部受け取るまで結果を出せない。ここで一旦メモリ(ハッシュ表・集計表・実体化バッファ)へ書く必要が生じる。

プランは pipeline breaker を境に複数のパイプラインへ分割されます。1つのパイプラインは「ソースから始まり、breaker でメモリへ実体化して終わる」直線で、この1パイプラインが1つの融合ループへコンパイルされます。breaker は前のパイプラインの出口(書き込み先)であると同時に、次のパイプラインの入口(走査元)になります。

# 例: SELECT sum(amount) FROM orders JOIN cust ON ...
#     WHERE orders.status='paid' GROUP BY cust.region
#
# パイプライン1: cust を走査して結合ハッシュ表を build(breaker=HashJoin.build)
# パイプライン2: orders を走査 → Filter → 結合 probe → 集計表へ加算(breaker=Aggregate)
# パイプライン3: 集計表を走査して結果を出力

produce / consume モデル

このパイプライン融合を機械的に生成する枠組みが、各演算子に与える2つのメソッド produce()consume() です。これはコードを生成するための再帰であって、実行時に呼ばれる関数ではない点に注意します(コード生成は1回、生成コードがクエリ実行時に走る)。

  • produce(): 「あなたの結果タプルを生成するコードを出力せよ」という依頼。演算子は自分のループ枠組みを書き、入力が必要なら子へ produce() を再帰委譲する。
  • consume(tuple, source): 「いま手元に1タプルが届いた。あなたの処理を施して親へ渡すコードを出力せよ」という通知。子が生成した1タプルに対する処理を、親側のコードとして埋め込む。

生成は親の produce() から始まり、子へ produce() が下りていき、最下段のソース(Scan)がループ枠を書きます。ソースは「1行得たらどうするか」を親へ問うため consume()上向きに呼び返し、各演算子の consume() が自分の処理を1タプル分のコードとして連ねます。結果として、フィルタや結合 probe や集計加算が1つのループ本体の中に縦に積層されます。

# probe 側パイプラインに対する produce/consume が織りなす生成コード(概念)
for row in scan(orders):              # Scan.produce がループ枠を出力
    if row.status == 'paid':          # Filter.consume が条件をインライン
        h = hash(row.cust_id)
        if match = probe(htable, h, row.cust_id):   # HashJoin.consume が probe をインライン
            g = group_slot(aggtable, match.region)   # Aggregate.consume が…
            g.sum += row.amount                      #   集計表へ直接加算(ここが breaker)

scan から aggtable への加算まで、演算子境界をまたぐ関数呼び出しは1つも残りませんrow のフィールドはレジスタに載ったまま Filter・Join・Aggregate を通過し、メモリに触れるのはソース読み出しとハッシュ表アクセス、breaker への書き込みだけになります。Volcano の next() 連鎖が、breaker 間の素直な for ループへ畳み込まれた形です。

push 型として現れる理由

produce/consume はソース起点で「処理した行を親のコードへ押し込む」push 型の生成になります。pull 型(上位が下位へ next() を要求)と違い、駆動の向きがデータの流れと一致するため、タプルをレジスタに載せたまま親の処理を続けて差し込め、パイプライン融合と自然にかみ合います。

なぜ分岐とメモリ往復が消えるのか

データ中心コード生成が速い理由は、削れる要素を分解すると明快です。

  • 仮想関数呼び出しの除去: Volcano は演算子の型が実行時に決まるため next() がインライン化できない間接分岐になります。コンパイル実行はクエリのプラン形状をコンパイル時に確定させるため、演算子の処理が直接コードとして展開され、間接呼び出しそのものが消えます。間接分岐が減れば分岐予測ミスとパイプラインストールも減ります。
  • 式インタプリタの除去: status='paid' のような述語を、汎用の式評価木をたどって解釈するのではなく、比較1命令へ直接コンパイルします。式ツリーの走査分岐がまるごと消えます。
  • マテリアライズの最小化: breaker と breaker の間は中間結果をメモリへ書きません。タプルがレジスタに留まるため、メモリ帯域とロード/ストア命令、キャッシュミスが大幅に減ります。
  • 死コードの消去と特殊化: 参照しない列、定数畳み込み、固定された結合キー型などが、コンパイラの最適化(LLVM の -O パス)で消去・特殊化されます。汎用エンジンでは「あらゆる場合」に備える分岐が、このクエリでは不要なら削れます。

つまり Volcano が「実データ演算の周囲を間接分岐で囲む」のに対し、生成コードは間接分岐を直接コードに、解釈を機械語に置き換えるため、CPU 命令がほぼ実データ処理だけになります。Neumann の報告では、解釈型に対して命令数・実行時間とも大きく削減されました。

LLVM でネイティブコードを生成する

HyPer は生成対象に **LLVM IR(中間表現)**を選びました。アセンブリを直書きするのではなく、移植性のある IR を吐き、LLVM の最適化器と JIT バックエンドにレジスタ割り当て・命令選択・対象 CPU 向け最適化を任せます。これにより、複雑な制御フロー(ハッシュ表の衝突処理など)は手書きの C++ ヘルパー関数を呼び、ホットなタプル処理ループだけを IR で生成する、という分担が可能になります。生成した IR と既存のランタイム関数を LLVM 上でリンクし、JIT で機械語にします。

コンパイル時間という代償

クエリごとに IR を生成し最適化・コード化するため、コンパイル時間がかかります。実行が一瞬で終わる短いクエリでは「コンパイル時間 > 実行時間」となり、解釈実行より遅くなり得ます。さらに LLVM の最適化パスは効果が大きい反面それ自体が重く、コンパイル費用の主因になります。OLTP の単純なクエリにそのまま当てると、コンパイルだけで損をします。

コンパイル費用を回収する:適応的実行

この代償への現実的な答えが適応的実行(adaptive execution)です。HyPer 系の後続実装は、同じ生成ロジックから複数の実行手段を用意し、クエリの規模に応じて切り替えます。

  • 即時開始: まず軽量なバイトコード VM か非最適化 JIT ですぐ走り始める。コンパイルの待ちをゼロにする。
  • 並行最適化と差し替え: 実行を続けながら裏で LLVM 最適化版をコンパイルし、出来上がったら実行中のパイプラインを最適化コードへ差し替える(モーフィング)。長く回るクエリだけが最適化費用を負担する。
  • コードキャッシュ: プリペアドステートメントなど形が同じクエリの生成コードを再利用し、コンパイルを1回に償却する。

これにより、「短いクエリはコンパイルせず即実行、長いクエリはコンパイルして全力」という費用対効果に応じた使い分けが自動化されます。

ベクトル化との関係

ベクトル化実行も解釈オーバーヘッドを断ちますが、断ち方が違います。

観点データ中心コンパイル(HyPer 流)ベクトル化(MonetDB/X100 流)
固定費の扱い解釈そのものを生成コードで除去バッチ処理で固定費を償却
処理単位1タプルがレジスタ上で演算子群を通過数千値のカラムバッチを型別カーネルで処理
中間結果breaker 間はレジスタ保持・非実体化演算子間でベクトルをメモリに実体化
起動コストクエリごとにコンパイル時間なし(既製カーネルを束ねる)
実装負荷高(コード生成器が必要)中(カーネル群が必要)
分岐削減間接分岐を直接コードへ展開分岐をバッチループ外へ追い出す

両者は排他ではありません。コンパイルしたコードの内側をベクトル単位で処理するハイブリッド(生成ループがバッチを回す)も実用化され、コンパイルの命令削減とベクトル化の SIMD・メモリ局所性を同時に取りに行きます。設計の主眼は列指向ストレージ列圧縮と組み合わせ、走査から集計までを CPU 効率の限界に近づけることにあります。

試験・面接で問われる要点

データ中心コード生成 = produce/consume の再帰でパイプラインを融合ループへ落とし、タプルをレジスタに載せたまま演算子群を通過させる方式と即答できるように。pipeline breaker(ハッシュ結合 build・集計・ソート)でループが切れ、breaker 間が1ループへ融合される点が核心です。速い理由は(1) 仮想関数呼び出し(間接分岐)の除去、(2) 式インタプリタの除去、(3) breaker 間のマテリアライズ最小化。代償はクエリごとのコンパイル時間で、適応的実行(即時開始+並行最適化+コードキャッシュ)で回収する、と整理できれば十分です。

まとめ

まとめ

クエリコンパイル実行は、汎用エンジンでプランを解釈する代わりにそのクエリ専用のネイティブコードを生成して解釈オーバーヘッドを消す方式です。HyPer が示したデータ中心コード生成は、演算子ごとの produce/consume をコンパイル時に再帰展開し、pipeline breaker(ハッシュ結合の build・集計・ソート)を境に分割した各パイプラインを1つの融合ループへ畳み込みます。タプルは breaker に達するまで CPU レジスタに留まり、演算子境界の関数呼び出し・式インタプリタ・中間マテリアライズが除去され、間接分岐が直接コードに置き換わるため、CPU 命令がほぼ実データ処理だけになります。生成対象に LLVM IR を使い最適化と JIT を委ねるのが典型で、クエリごとのコンパイル時間は適応的実行(即時開始・並行最適化・コードキャッシュ)で回収します。Volcano やベクトル化と並ぶ、現代の分析エンジンが集計を桁で速くする中核技術です。

データベース Article

クエリコンパイル実行とデータ中心コード生成を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

クエリコンパイル

比較で見る軸

難易度: advanced / カテゴリ: データベース / タグ数: 6

導入後に効く点

鍵はパイプライン境界で、結合のハッシュ表構築や集計のように入力を一度ためる演算子(pipeline breaker)でループが切れる。1パイプライン内ではタプルが CPU レジスタに載ったまま複数演算子を通過し、メモリ往復と分岐が激減する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
データベース
タグ数
6

判断チェックリスト

  • 自社の用途が「クエリコンパイル / コード生成」に近いか確認する。
  • 強みである「データ中心コード生成は HyPer が示した方式で、演算子ごとに produce/consume を再帰展開し、マテリアライズ不要な一連の演算子を1つのタイトなループへ融合する。LLVM などで生成したネイティブコードを実行し、Volcano の next() 連鎖と仮想関数呼び出しを除去する。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

クエリコンパイルコード生成HyPerLLVMproduce/consumeクエリコンパイルコード生成HyPer
参考: 公式情報