レイトマテリアライゼーションとベクトル化実行の内部
列指向OLAPが集計を桁で速くする2本柱が分かります。列値の実体化を遅らせる late materialization と、SIMD で塊を処理するベクトル化実行を、内部動作から押さえられます。
- 1.early materialization は走査直後に行タプルを組み立てるが、late materialization は値を実体化せず position list(行ID/選択ベクトル)だけで処理を進め、本当に必要な列・行だけを最後に取り出す。
- 2.選択や結合で多くの行が落ちるとき、late materialization は脱落する行の列値を一切読まずに済み、I/O・復号・コピーを削減できる。フィルタの選択率が低いほど効果が大きい。
- 3.ベクトル化実行は1値ではなく1024程度のカラムバッチを1呼び出しで処理し、SIMD・分岐レス・キャッシュ局所性を活かす。position list と選択ベクトルもベクトル化カーネルとして実装され、両者は同じ列指向エンジンで噛み合う。
列値を「いつ組み立てるか」が効く
列指向ストレージはカラムごとに値を連続配置します。ここでクエリを実行するとき、ディスク上のバラバラなカラムを、いつ1行のタプルへ組み立て直す(実体化=materializeする)かが性能を大きく左右します。早く組み立てる early materialization と、できるだけ遅らせる late materialization の選択は、無駄な列値の読み出し・復号・コピーをどれだけ省けるかに直結します。本稿は late materialization と、それを支えるベクトル化実行の内部を解説します。
early materialization:走査直後にタプルを組む
素直な実装は、走査した時点で必要カラムの値を集めて行タプルを作り、以降の演算子(フィルタ・結合・集計)に1行ずつ流します。これが early materialization です。
# early materialization:先にタプルを組んでから処理
scan(orders) -> rows of (id, status, amount, region)
filter(status == 'paid') -> 残った行のタプル
aggregate(sum(amount) by region)
利点は単純さで、行が組み上がっているため後段は通常の行指向演算子をそのまま使えます。しかし問題は、フィルタで落ちる行の列値まで先に組み立ててしまう点です。status == 'paid' が全体の数%しか通さない場合でも、amount や region を全行ぶん読み・復号・コピーしてからフィルタにかけることになり、捨てる行のために費やした作業が無駄になります。列指向は「必要な列だけ読む」ことが強みなのに、early materialization は「必要かどうか決まる前に読む」ため、その強みを削ぎます。
late materialization:position list で処理を進める
late materialization は逆に、値そのものを組み立てず、行の位置情報だけで処理を進めます。中核となるのが position list(行ID列、あるいは選択ベクトル)です。フィルタは値の配列を返すのではなく、条件を満たした行の位置の集合を返します。
# late materialization:まず位置だけを絞り込む
status 列をスキャン -> 'paid' を満たす position list = [3, 7, 8, 15, ...]
# この時点で amount/region はまだ一切読んでいない
# 集計に必要な amount だけを position list で引く
amount[3], amount[7], amount[8], ... を取り出して sum
ポイントは、status 列だけを先に評価して位置を絞り、amount や region は生き残った位置の値だけを最後に取り出すことです。脱落する行の amount・region は読み出しも復号も発生しません。選択率(フィルタを通る割合)が低いほど、触らずに済む列値が増え、I/O とコピーが減ります。position list には、行IDの配列のほか、各行が生存かを 0/1 で持つビットマップ/選択ベクトルの形式があり、選択率に応じて使い分けられます。
複数条件は position list の集合演算で合成できます。A AND B は両者の position list の積(共通位置)、A OR B は和です。行IDの配列ならソート済み集合のマージ、ビットマップなら AND/OR のビット演算1命令で多数行をまとめて合成でき、これ自体がベクトル化と相性のよい操作になります。
なぜ速いのか:触らない列・行を最大化する
late materialization が効くのは、実体化の対象を「最後まで生き残った行 × 実際に出力/集計に要る列」に絞れるからです。効きどころを整理します。
- 不要な列読み出しの回避: 出力にも述語にも使われない列は最後まで読まない。フィルタで落ちる行の列値は復号すらしない。
- 圧縮表現のまま処理できる: 軽量符号化された列は、辞書IDやビットパック表現のまま述語評価でき、生き残った位置だけを最後に復号すればよい。早期に行へ展開しないぶん復号回数が減る。
- コピーとメモリ帯域の節約: タプルへ詰め替えるコピーが減り、CPU とメモリ間の帯域を節約できる。中間結果が「値の配列」ではなく「位置の集合」なので軽い。
- キャッシュ効率: 1カラムを連続走査する形が保たれるため、プリフェッチが効き、述語評価のカーネルが命令キャッシュに収まりやすい。
代償もあります。最終的に値を引くとき、position list が飛び飛びだとランダムアクセス(ギャザー)になり、連続走査より遅くなり得ます。そのため、選択率が高く(多くの行が生き残る)ギャザーする値そのものが多くなるケースや、列値を何度も参照するケースでは early materialization のほうが有利なこともあります。実エンジンはこの境界を選択率の見積もりで判断し、両者をプラン中で混在させます。
結合での late materialization
late materialization が特に効くのが結合です。結合キーだけで先にマッチング位置を求め、マッチした行ペアの位置(行ID)だけを中間結果として持ち、出力に必要な非キー列はマッチ確定後に position list で引きます。多くの行が結合で脱落する場合、脱落行の非キー列をまったく読まずに済みます。
# 結合をキー位置で先に解き、値は後から引く
build: R.key 列から ハッシュ表(key -> R側の行ID)
probe: S.key を引いてマッチした (R行ID, S行ID) のペア列を得る
# ここまで R/S の非キー列は未読
output: 必要な列だけを R行ID / S行ID で取り出して実体化
ベクトル化実行との噛み合わせ
late materialization は「何を実体化しないか」の戦略で、どう CPU を回すかは実行モデルの問題です。両者を結ぶのがベクトル化実行です。ベクトル化は1値ずつではなく、1024程度のカラムバッチを1回の呼び出しで処理し、固定費(仮想関数呼び出し・型分岐)をバッチサイズで償却します。
ここで position list と選択ベクトルは、ちょうどベクトル化カーネルの入出力として実装されます。フィルタカーネルはバッチを受け取り、生き残った位置を選択ベクトルとして返します。後段のカーネルは選択ベクトルを見て生存行だけを処理し、これが late materialization の「位置で処理を進める」を SIMD レベルで体現します。
# ベクトル化フィルタ:バッチを処理し選択ベクトルを返す
filter_eq(sel_out[], col[], value, n=1024):
k = 0
for i in 0..n: # 内側は同型配列の単純反復 → SIMD 化可能
if col[i] == value: sel_out[k++] = i
return k # 生存件数
# 後段は選択ベクトルで生存行だけを処理
sum_with_sel(sum, col[], sel[], k):
for j in 0..k: sum += col[sel[j]] # 位置で値を引いて集計
- SIMD への適合: 述語評価は同型配列の一括比較になり、1命令で複数値を比較する SIMD に載る。選択ベクトル生成も SIMD 圧縮命令で高速化できる。
- 分岐の追い出し: 生存判定がバッチ単位の選択ベクトルに集約されるため、後段カーネルの内側ループから条件分岐を減らせる。
- 遅延と償却の両立: late materialization が触る値を減らし、ベクトル化が触る値あたりの固定費を薄める。削減と効率化が直交して効く。
バッチ内で行が落ちるたびに残った値を前へ詰め直すと、コピーが嵩みます。多くのエンジンは値の配列はそのままに、選択ベクトル(生存位置の添字)だけを更新して次段へ渡します。値を動かさず位置情報だけを引き回すこの設計が、late materialization とベクトル化を同じ仕組みの上で成立させています。
late materialization の弱点は、最後に値を引くときのギャザー(ランダムアクセス)です。選択率が高いほどギャザーする値が多くなり、しかも飛び飛びの参照になるため、連続走査より遅くなり得ます。逆に選択率が低い(多くが落ちる)ほど触る値が減り、late materialization が有利です。エンジンは選択率の見積もりで early/late を切り替え、ビットマップと行ID配列も密度に応じて使い分けます。
early と late の比較
| 観点 | early materialization | late materialization |
|---|---|---|
| 実体化のタイミング | 走査直後に行タプルを組む | 出力/集計の直前まで遅らせる |
| 中間結果の形 | 値を持つ行タプル | position list / 選択ベクトル |
| 落ちる行の列読み | 先に読むため無駄が出る | 読まずに済む |
| 圧縮表現の保持 | 早期に復号・展開しがち | 圧縮のまま述語評価しやすい |
| 有利な選択率 | 高い(多くが生存) | 低い(多くが脱落) |
| 弱点 | 無駄な読み・コピー | まばらな位置のギャザー |
early=走査直後にタプル実体化、late=position list で処理を進め最後に値を引くの対比をまず即答。late が速い理由は (1) 落ちる行の列を読まない、(2) 圧縮表現のまま処理できる、(3) コピー削減。弱点はまばらな位置のギャザーで、選択率が低いほど有利。ベクトル化との関係は選択ベクトル=ベクトル化カーネルの入出力として両者が同じ列指向エンジンで噛み合う、と説明できれば十分です。
まとめ
late materializationは、列値を走査直後に行へ組み立てず、position list(行ID/選択ベクトル)だけで処理を進め、本当に必要な列・行だけを最後に実体化する戦略です。フィルタや結合で落ちる行の列値を読まず、圧縮表現のまま述語評価でき、コピーとメモリ帯域を節約します。弱点はまばらな位置のギャザーで、選択率が低いほど有利です。これを CPU 効率の面から支えるのがベクトル化実行で、選択ベクトルをカーネルの入出力として 1024 程度のバッチを SIMD で一括処理し、固定費を償却します。触る値を減らす late materializationと値あたりの固定費を薄めるベクトル化が直交して効くことが、列指向 OLAP が大量集計を桁で速くしている内部の正体です。
データベース Article
レイトマテリアライゼーションとベクトル化実行の内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
列指向
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
選択や結合で多くの行が落ちるとき、late materialization は脱落する行の列値を一切読まずに済み、I/O・復号・コピーを削減できる。フィルタの選択率が低いほど効果が大きい。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「列指向 / レイトマテリアライゼーション」に近いか確認する。
- 強みである「early materialization は走査直後に行タプルを組み立てるが、late materialization は値を実体化せず position list(行ID/選択ベクトル)だけで処理を進め、本当に必要な列・行だけを最後に取り出す。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。