🎓 レベル:発展 | 重要度:A(必須)
📎 前提:正規化と非正規化のトレードオフ・インデックスと実行計画 | 関連:ファイルフォーマットとシリアライゼーション・データウェアハウス
要点(BLUF)
- データの物理的な並べ方には**行指向(1行分を連続して置く)と列指向(1列分を連続して置く)**がある。
- 分析クエリは「多数の行の、少数の列を集計」する。列指向なら必要な列だけ読めばよく、I/Oが激減する。さらに同じ列=同種の値が並ぶので圧縮がよく効く。
- 一方、1行まるごと読み書きするOLTP(更新系)は行指向が有利。OLAPは列指向、OLTPは行指向が原則。DuckDB・BigQuery・Redshift・Parquetはみな列指向。
概念 ── 同じ表、違う並べ方
論理的には同じテーブルでも、ディスク上の並べ方が違います。
flowchart TB
subgraph ROW["行指向(OLTP向け)"]
R["行1:1,Sato,East / 行2:2,Tanaka,West / 行3:3,Ito,East"]
end
subgraph COL["列指向(OLAP向け)"]
C1["id列: 1,2,3"]
C2["name列: Sato,Tanaka,Ito"]
C3["region列: East,West,East"]
end
行指向は「id, name, regionを1セットで連続」配置。列指向は「id だけ連続、name だけ連続、region だけ連続」配置です。
仕組み ── なぜ分析で速いか
「全顧客の amount 合計」を求めるとき:
- 行指向:1行に全列が混ざって置かれているため、
amountを集めるには全行(=全列)を読み込み、各行からamountだけ取り出す。不要な列のI/Oが大半。 - 列指向:
amount列だけが連続して置かれているので、その列のブロックだけ読めば終わり。他列のI/Oはゼロ。
flowchart LR
Q["SUM(amount)を計算"] --> Rd["行指向:全列を読む(無駄なI/O大)"]
Q --> Cd["列指向:amount列だけ読む(I/O最小)"]
実際、ファイルフォーマットとシリアライゼーション のラボでは、Parquet(列指向)から amount 列だけを読み出し、他列を触らずに合計(24975000)を得ています。「必要な列だけI/O」が列指向の第一の武器です。
第二の武器が圧縮。同じ列には同種の値(同じ型・似た範囲・繰り返し)が並ぶため、辞書エンコードやランレングス圧縮が劇的に効きます(CSV比でしばしば数倍小さい)。読むバイト数自体が減るので、さらに速くなります。
設計の勘所 ── ベクトル化と述語プッシュダウン
- ベクトル化実行:列がメモリに連続するため、CPUは1値ずつでなく**まとめて(SIMD)**処理でき、キャッシュ効率も高い。
- 述語プッシュダウン/統計:列ブロックごとに最小・最大値を持ち、「この範囲に該当行なし」と分かればブロックごとスキップ(→ ファイルフォーマットとシリアライゼーション)。
- 不利な点:1行の挿入・更新は全列に分散書き込みが要り、行指向より高コスト。だから更新系には向かない。
なぜそうするか ── クエリ形状に物理を合わせる
なぜOLAPで列指向を選ぶのか。分析クエリの形(少数列×多数行)に物理レイアウトを一致させると、読むデータ量そのものが最小化されるからです。インデックス(→ インデックスと実行計画)は「少数行を素早く特定」する道具ですが、分析は「大量行を全部なめて集計」するのでインデックスが効きにくい。そこで読む量を列で削る列指向が主役になります。OLTPとOLAPでストレージ方式が分かれるのは、クエリの形が正反対だからです。
⚠️ よくある落とし穴
- 列指向DBに1行ずつ大量INSERT → 極端に遅い。まとめて一括ロード(バルクロード)が前提。
- 列指向なら何でも速いと誤解 →
SELECT *(全列)では列指向の利点が消える。必要な列だけSELECTして初めて効く。 - 行指向DWHで重い集計を回し続ける → 列指向DWH/Parquetへ移すだけで桁違いに速くなる場合がある。
対応ラボ
列だけ読むI/O削減と圧縮の実測は ファイルフォーマットとシリアライゼーション の 03_sql_optimization.py(Parquetからamount列のみ読込・合計確認済み)。
関連
- 物理形式の具体(Parquet等)は ファイルフォーマットとシリアライゼーション
- 索引による別アプローチは インデックスと実行計画、非正規化との関係は 正規化と非正規化のトレードオフ
- これを使う分析基盤は データウェアハウス