行指向ストレージと列指向ストレージ
同じ表でも物理レイアウトを変えるだけで集計が桁違いに速くなる理由が分かります。行指向と列指向の違いを、圧縮・スキャン・ベクトル化実行まで原理から押さえられます。
- 1.行指向は1行分の全カラムを連続配置し、列指向は同一カラムの値を連続配置する。この物理レイアウトの差が圧縮効率・スキャン量・OLTP/OLAP 適性をすべて決める。
- 2.列指向は同型・低カーディナリティの値が並ぶため RLE や辞書符号化が効き、必要カラムだけ読むため I/O が激減する。代わりに1行の挿入・更新は全カラムのブロックを触るため不利。
- 3.列指向の本領はベクトル化実行で、1行ずつではなくカラムの配列をまとめて処理し SIMD と CPU キャッシュを活かす。これが OLAP の集計を桁で速くする中核。
同じ表でも「並べ方」で性能が変わる
リレーショナルな表は論理的には行と列の二次元ですが、ディスクやメモリは一次元の連続バイト列です。二次元の表を一次元に どう写すか――この物理レイアウトの選択が、圧縮率・スキャン量・OLTP/OLAP 適性を根本から決めます。代表的な選択肢が 行指向(row-oriented / NSM) と 列指向(column-oriented / DSM) です。
例として orders(id, user_id, amount, status) という4カラムの表を考えます。
行指向(NSM: N-ary Storage Model)
ブロック内: [id1,user1,amount1,status1][id2,user2,amount2,status2]...
└─ 1行が連続 ─┘
列指向(DSM: Decomposition Storage Model)
id列: [id1, id2, id3, ...]
user_id列:[user1, user2, user3, ...]
amount列: [amount1, amount2, amount3, ...]
status列: [status1, status2, status3, ...]
行指向は「1行の全カラム」が物理的に隣り合います。列指向は「1カラムの全行の値」が隣り合います。どちらも同じデータですが、後段の処理特性がまったく変わります。行をまたいで同じカラムを縦に走査するのが集計、1行の全カラムを横に取り出すのが業務処理――という対比を念頭に置くと理解が早いです。
スキャン量:読まないカラムは読まない
SELECT SUM(amount) FROM orders WHERE status = 'paid' のような集計を考えます。必要なのは amount と status の2カラムだけです。
- 行指向: 1行が連続配置されているため、
amountを読むにはidやuser_idも含む行全体がストレージブロックに同居します。ブロック単位の I/O では、関心のないカラムも巻き込んで読むことになります。 - 列指向:
amount列とstatus列のブロックだけを読めばよく、id・user_id列のブロックには一切触れません。これを 射影プッシュダウン(projection pushdown) と呼びます。
カラム数が N で、集計が k カラムだけを使うとき、列指向が読む I/O 量は素朴には k / N の割合まで減ります。広い表(N が大きい)で少数カラムを集計する OLAP では、この差が支配的です。逆に「1行の全カラムが欲しい」点取得は、列指向だと N 個のブロックを別々に読んで再構成する必要があり不利になります。
行指向の正式名称は NSM、列指向は DSM です。両者の中間として、ブロック内を行で区切りつつブロック内部はカラムごとにまとめる PAX(Partition Attributes Across) もあります。Parquet や ORC は巨大ファイルを「行グループ(row group)」に分け、その中をカラム単位で格納する PAX 系のレイアウトで、列スキャンの利点とブロック局所性を両立させています。
圧縮効率:同型の値が並ぶ強み
列指向の二つ目の武器が圧縮です。圧縮は 似たデータが近くにあるほどよく効くという性質を持ち、列指向は「同じカラムの値=同じ型・近い分布」を連続配置するため、行指向より格段に有利です。
- RLE(Run-Length Encoding):
statusのように取りうる値が少ないカラムをソートしておくとpaid,paid,paid,...と同値が連続し、(paid, 1000回)のように畳める。 - 辞書符号化(dictionary encoding): 低カーディナリティのカラムで、実値を小さな整数 ID に置き換え、ID 列を格納する。
{tokyo,osaka,...}を{0,1,...}にすると、4バイト整数どころか数ビットで足りる。 - ビットパッキング / フレーム・オブ・リファレンス: 整数列の値域が狭ければ、必要ビット数だけに詰める。連番に近い
idは差分(delta)化してから詰めるとさらに縮む。
辞書や RLE で符号化した列は、復号せずに圧縮されたまま述語評価できることがあります。status = 'paid' を辞書 ID 0 への等値比較に変換すれば、整数比較で済みます。圧縮はストレージ削減だけでなく、I/O と CPU の双方を減らす点が本質です。行指向では1行内に型の異なる値が混在するため、ここまで強い符号化は効きません。
| 手法 | 効く条件 | 代表カラム |
|---|---|---|
| RLE | ソート済みで同値が連続する低カーディナリティ列 | status, flag, 区分コード |
| 辞書符号化 | 取りうる値の種類が少ない(低カーディナリティ) | 国名, カテゴリ, enum |
| ビットパッキング / delta | 値域が狭い、または連番に近い整数列 | id, 連番キー, タイムスタンプ |
OLTP と OLAP:なぜ適性が分かれるか
ここまでの性質が、そのまま OLTP / OLAP の適性差になります。
| 観点 | 行指向(NSM) | 列指向(DSM) |
|---|---|---|
| 1行の全カラム取得 | 1ブロックで完結し高速 | N 個の列ブロックを読み再構成、不利 |
| 少数カラムの大量集計 | 不要カラムも巻き込み I/O 過多 | 必要列だけ読み I/O が激減 |
| 1行の挿入・更新 | 1ブロックに追記・更新、得意 | 全カラムのブロックを触り不得意 |
| 圧縮効率 | 型混在で効きにくい | 同型・近い分布で強く効く |
| 代表用途 | OLTP(業務トランザクション) | OLAP(分析・集計) |
列指向で INSERT が不得意なのは、1行を足すだけで N 個のカラム領域すべてに値を追記し、各々の符号化・圧縮を更新する必要があるためです。さらに行の途中更新は圧縮ブロックの再構成を招きます。そこで分析系エンジンは更新を 追記中心で扱い、変更を別領域に溜めて後でまとめて反映する設計を採ります。この「追記して後で畳む」発想は LSM-Tree と同根で、ランダム更新を避けてシーケンシャルに均すという点で共通しています。点更新の局所性が要る OLTP では B+Tree 系の行指向ストレージ が依然として主役です。
ベクトル化実行:列指向の本領
列指向の真価は、ストレージ削減以上に 実行モデルにあります。従来の行指向エンジンは「1行を取り、式を評価し、次の行へ」という 火山(Volcano)モデルで、行ごとに next() を呼びます。これは1値あたりの関数呼び出しや分岐のオーバーヘッドが大きく、CPU が遊びます。
列指向では値が型のそろった配列として並ぶため、1行ずつではなくカラムの一塊(バッチ、典型的には1000〜数千値)をまとめて処理できます。これが ベクトル化実行(vectorized execution) です。
スカラ実行(行ごと):
for row in rows: out[row] = a[row] + b[row] # 1値ごとに呼び出し・分岐
ベクトル化実行(バッチ単位):
add(out_vec, a_vec, b_vec, n=1024) # 配列をまとめて1関数で処理
ここで効くのが次の3点です。
- SIMD: 連続した同型配列は、1命令で複数要素を同時計算する SIMD 命令に乗せやすい。
amount列の合計は8値・16値を一度に足し込める。 - CPU キャッシュと予測: 同型値の連続アクセスはキャッシュ効率がよく、分岐がループ外に出るため分岐予測も当たりやすい。
- 解釈オーバーヘッドの償却: 関数呼び出し1回でバッチ全体を処理するため、1値あたりの固定費が
1 / バッチサイズに薄まる。
JIT で式をネイティブコードへコンパイルする方式もありますが、ベクトル化は事前コンパイル済みの型別カーネルを束ねるだけで近い効果を、より単純な実装で得られます。クエリオプティマイザ が選んだ実行計画を、この列バッチのパイプラインで流すのが現代の分析エンジンの基本形です。
列指向では、まず status 列を評価して合格行のビットマップを作り、そのビットが立った位置だけを後段の amount 列から取り出す、という順序が取れます。これを 遅延マテリアライズ(late materialization) と呼びます。行を早く組み立てず、必要になるまでカラムのまま持ち回ることで、捨てる行のために他カラムを読む無駄を省けます。
列指向が勝つのは「広い表・少数カラム・大量行の集計」という条件がそろったときです。狭い表で全カラムを使う、あるいは1〜数行をキーで取り出す処理では、列ブロックの再構成コストが効いて行指向に負けます。さらに高頻度の点更新は列指向の苦手分野です。レイアウトは ワークロードに対して選ぶものであり、万能の正解ではありません。
NSM=行指向、DSM=列指向、PAX=ハイブリッド(Parquet/ORC)の対応をまず即答できるように。列指向が速い理由は (1) 必要カラムだけ読む射影プッシュダウン、(2) 同型値の連続による高い圧縮率、(3) ベクトル化実行による SIMD・キャッシュ活用の3点セットで説明します。代償は 点取得・点更新の弱さで、ゆえに OLTP は行指向、OLAP は列指向という棲み分けになる、という流れが定番です。
まとめ
行指向(NSM)は1行の全カラムを連続配置して点取得・点更新に強く、列指向(DSM)は同一カラムの値を連続配置して大量集計に強い、という非対称な関係です。列指向の優位は 不要カラムを読まない射影プッシュダウン、同型値が並ぶことによる強い圧縮、そして カラム配列をまとめて処理するベクトル化実行(SIMD・キャッシュ・解釈費の償却)から生まれます。代償として1行の取得・更新は不利になるため、OLTP は行指向、OLAP は列指向と ワークロードに合わせて選ぶのが原則です。Parquet/ORC の PAX レイアウトは両者の中間で、データウェアハウスの分析基盤を支えています。
データベース Article
行指向ストレージと列指向ストレージを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
列指向
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
列指向は同型・低カーディナリティの値が並ぶため RLE や辞書符号化が効き、必要カラムだけ読むため I/O が激減する。代わりに1行の挿入・更新は全カラムのブロックを触るため不利。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「列指向 / 行指向」に近いか確認する。
- 強みである「行指向は1行分の全カラムを連続配置し、列指向は同一カラムの値を連続配置する。この物理レイアウトの差が圧縮効率・スキャン量・OLTP/OLAP 適性をすべて決める。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。