✍️

データ指向アプリケーションデザインの第3章を読んだので全部まとめた

2025/01/01に公開

列ストレージと圧縮

列ストレージと圧縮が相性が良い理由

  • 同じカラムには似たような値が連続することが多い
    たとえば、user_id カラムにはユーザーID、status_code カラムには同じようなステータスコード(200、404 など)が多数格納されます。
    「同じ型・類似の値が並びやすい」ため、ランレングス圧縮 (RLE) や辞書圧縮 (Dictionary Encoding) などが効果的に働きます。

  • 必要なカラムだけを読み込める
    列ごとにデータを格納しているため、テーブルの一部のカラムだけにアクセスするような分析処理では不要なデータを読まずに済みます。さらに、読み込み対象のカラムは圧縮された状態で保存されているため、ディスクI/Oやメモリ転送量を大幅に削減できます。

代表的な圧縮手法

(1) ランレングス圧縮 (Run-Length Encoding, RLE)

  • 概要: 同じ値が連続している区間を「値」と「連続する回数」のペアで表現する圧縮方式。
    例: [A, A, A, B, B, A][(A, 3), (B, 2), (A, 1)]
  • 特徴:
    • 同じ値が長く連続するほど圧縮率が高い。
    • 値のバリエーションが大きく、短い連続が多い場合はあまり効果が期待できない。

(2) 辞書圧縮 (Dictionary Encoding)

  • 概要: 現れうる値を辞書化し、辞書のインデックス(ID) で実際の値を置き換えることで圧縮する。
    例: ["apple", "banana", "apple"]
    辞書:
    1: "apple"
    2: "banana"
    
    実データ: [1, 2, 1]
  • 特徴:
    • 文字列など値のサイズが大きい場合に特に有効
    • 値の種類(ユニークな値の数)が多いと圧縮率は下がる。

(3) ビットパッキング (Bit-Packing)

  • 概要: 連続する値が取りうる範囲に応じて必要最小限のビット数だけで各値を表現する手法。
    例: 値が 0〜15 (4ビットで表現可能) のとき、8ビットではなく4ビット単位で格納する。
  • 特徴:
    • 整数やフラグのように値の幅が限られている場合、空間を節約できる。
    • 動的にビット数を調整できる実装では、多少のオーバーヘッドが増える可能性もある。

(4) デルタ圧縮 (Delta Encoding)

  • 概要: 順番に並んだ値同士の「差分」だけを格納する圧縮方式。
    例: [100, 102, 103, 103, 105] → 基準値(100) と差分 [0, +2, +1, 0, +2]
  • 特徴:
    • 並んでいる値が連続的に変化する場合に効果的
    • 値の変動が激しい場合はあまり効かないこともある。

圧縮後のデータ読み込み・扱いの工夫

  • 圧縮データのまま演算する「レイクハウス」や「列指向データベース」
    列指向データベースの多くは、圧縮したままのデータに対してフィルタリングや集計などを行えるように工夫されています。
    たとえば RLE 圧縮された列に対して「ある値が含まれる行はどれか?」を判定する場合、解凍せずに圧縮形式を利用して走査を最小化できる場合があります。

  • 圧縮形式の選択
    データの特性、クエリパターンなどに合わせて、最適な圧縮形式を選びます。実運用では、複数の圧縮手法をカラムごとに使い分けるケースも一般的です。

要点まとめ

  1. 列ストレージは同種の値が連続するため圧縮効率が高い。
  2. 代表的な圧縮手法には、ランレングス圧縮、辞書圧縮、ビットパッキング、デルタ圧縮などがある。
  3. 圧縮したままデータにアクセスできる仕組みによって、ディスクI/Oやメモリアクセスを減らし、クエリ性能を高められる

列ストレージを選択する大きな理由のひとつは、まさにこの「圧縮効果」とそれに伴う「高速クエリ」の実現です。データ分析やアドホックなクエリにおいては特に有効で、実際に多くの分析基盤やデータウェアハウスで採用されています。

列ストレージとソート順序

なぜ列ストレージにソートが重要なのか

  • 圧縮効率の向上
    ソート済みの列では、同じ値や近い値がまとまりやすくなるため、RLE(ランレングス圧縮)やデルタ圧縮などの手法がより効果を発揮します。
  • 検索・フィルタリングの高速化
    特定のカラムを絞り込む(たとえば「売上日が2024年のものだけ取得」など)場合、ソート済みの列であればブロックスキップや二分探索が可能になり、全データを走査せずに済みます。
    (ソートされていないと、どこに目的のデータがあるか分からず、フルスキャンが必要になる。)

列指向DBでの典型的なアプローチ

  • クラスタリングキー(ソートキー)の指定
    列ストレージを採用しているデータベース(Amazon Redshift、Snowflake、Vertica など)では、テーブル作成時に「どの列をソート順序の鍵にするか」指定できる。
    • 集計やフィルタに頻出する列をソートすることで、読み込み性能圧縮効率の向上が見込まれる。
  • 複数列をソートキーにできる
    たとえば (date, user_id) のように複数の列で並び替えると、「特定の日付かつ特定ユーザー」のクエリがさらに高速になる可能性がある。

ソートと圧縮の相乗効果

(1) ランレングス圧縮(RLE)との相性

  • ソート→連続した同じ値が固まる
    例:性別やステータスなど有限の離散的な値がある列をソートすると、「Male」「Male」「Male」…「Female」「Female」… といった形でまとまりやすくなり、RLE圧縮の効率が高まる。

(2) デルタ圧縮との相性

  • 近い値が固まる→差分(デルタ)が小さい
    数値カラムや日付カラムをソートすると、隣接する値同士の差分が小さくなる。差分ベースで圧縮するデルタ圧縮が非常に効率的になる。

(3) その他の圧縮手法

  • 辞書圧縮やビットパッキングなども、ソートによって値が近い(または同じ)ものが連続する状況を作り出すため、圧縮時の辞書サイズ削減・ビット幅の最適化に寄与しやすい。

ソート順序を活かしたクエリの最適化

(1) 範囲クエリの高速化

  • 日付や時刻などの範囲指定
    例: WHERE date BETWEEN '2024-01-01' AND '2024-12-31'
    ソートされた日付列があれば、該当する範囲のブロックだけスキャンすればよくなる。

(2) ブロックスキップや索引(インデックス)との組み合わせ

  • 列ストアにおいては、行全体ではなくカラム単位で「どのブロックにどの値が含まれるか」などのメタデータを持つ場合がある(“Zone Map” や “Min/Max Index” などとも呼ばれる)。
    • ソートされた列は最小値・最大値が一定の範囲で固まるため、クエリ時に不要なブロックを丸ごと読み飛ばす(スキップする)ことが容易になる。

(3) グループ化・集計の高速化

  • ソート順序に基づいてデータがまとまっていれば、グループバイ処理が効率的に行われる
    例: GROUP BY status_code
    事前に status_code でソートされていれば、データ読み込み時に連続する要素をまとめて集計できる。

ソート順序を選ぶ際の注意点

  1. 頻繁にフィルタリング・集計される列を優先してソートキーに設定する。
  2. ソートコストとのトレードオフを意識する。
    • データの挿入や更新が頻繁にある場合、都度ソートを維持するためのコストがかかる。
  3. 複数列のソートキーの順番にも注意する。
    • 例: (date, user_id)(user_id, date) のどちらを先にするかで、どんなクエリが速くなるか変わる。

要点まとめ

  1. 列ストレージでソートすると、圧縮効率が大幅に高まり、ディスクI/Oやメモリ使用量が削減できる。
  2. ソートされたカラムを利用すると、範囲クエリ・グループバイ処理・インデックススキップが高速化する。
  3. ソートキーの選択は、クエリの特徴と更新頻度のトレードオフを踏まえて行うことが重要。

列指向データベースの強みは「必要なカラムだけ効率よく読み込む」だけでなく、「特定のカラムをソートし圧縮しやすい」という点も大きいです。ソート順序が適切に設定されると、分析系のワークロードにおいて非常に高いパフォーマンスを発揮します。

列ストレージと書き込み

列指向ストレージが書き込みに不向きな理由

  1. 物理構造が列ベース

    • 行ごとに1カ所へ追記していく行指向ストレージと違い、列ストレージはカラムごとに別々のファイルや領域にデータを格納します。
    • 単一のレコード(行)を書き込む場合、すべてのカラムファイルを一斉に更新する必要があるため、オーバーヘッドが大きくなりがちです。
  2. ソート+圧縮との相性

    • 列指向DBでは読み込み性能を最大化するため、よく使われるカラムをソートした状態で格納し、圧縮効率を高めています。
    • 新規行をそのまま書き込むと、既存のソート順や圧縮形式を崩すことになるので、そのまま上書き・追記はしにくい構造です。
  3. 典型的な使用用途

    • 列ストレージは、分析やOLAP(Online Analytical Processing)用途で大量データをまとめて読み込むのに向いています。
    • トランザクションベースの小さな書き込み(OLTP)を頻繁に行うワークロードにはあまり適しません。

代表的な書き込み戦略

(1) バッチ挿入(マイクロバッチ)

  • 概要
    新規データをある程度**まとまった単位(バッチ)**で列ストレージに取り込む方式。
    • 例: 1時間や1日の終わりなどにまとめてロードする。
  • メリット
    • まとめて書き込むことでI/Oを削減し、ソートや圧縮の再処理を一度に済ませられる。
    • 事前にデータを集約・変換しておけば、本体への書き込みが効率的。
  • デメリット
    • リアルタイム性が下がる。バッチ間隔が長いほど、データの反映が遅れる。

(2) デルタストア (Delta Store) やステージング領域

  • 概要
    列指向の本体(メインストア)は基本的に読み取り専用に近い形で運用し、新規データは一時的に「行ストレージ」や「専用の書き込みバッファ」に格納しておく。
    • 一定量たまったらマージ(ソート・圧縮して列ストレージへ統合)。
  • メリット
    • こまめな行単位の書き込みを行指向の形で一時保管できるため、書き込みのスループットを向上できる。
    • 読み取り時には、メインストア(列)+デルタストア(行)を合わせて参照することで、最新データも取得可能。
  • デメリット
    • デルタストアが肥大化すると、クエリ時にメインストア+デルタストアを結合する処理コストが増える。
    • 定期的に**マージ処理(コンパクション)**を行い、本体に統合する必要がある。

(3) ミニバッチ+ログストレージ

  • 概要
    新規書き込みはすべてログ(WAL: Write-Ahead Log 的な仕組み) に記録して即時に確定とし、読み込み主体の処理でログをまとめて列ストレージに再編する。
    • 「ログ」と「本体列ストア」の2段構成。
  • メリット
    • 書き込みはログへの sequential write が中心なので速度・安全性を確保しやすい。
    • 列ストアへの取り込みタイミングや手法を柔軟に制御できる。
  • デメリット
    • ログ領域と列ストアで同じデータを二重管理するため、ストレージ容量が増える可能性。
    • ログからのコンパクションや再配置の負荷をどう分散させるかが課題。

書き込み時の主な工夫・ポイント

  1. ソート・圧縮の再処理をバッチ化

    • 書き込み単位が小さすぎると、たびたびソートや圧縮をやり直すことになり非効率。
    • ある程度まとまった単位でまとめて再ソート、再圧縮すると、列ストレージのメリットを損なわずに済む。
  2. クエリとの整合性確保

    • OLTPではなくOLAP用途が多いとはいえ、分析クエリに最新データを含めたいケースもある。
    • そのため、メインの列ストアとデルタストア(あるいはログ)の両方をクエリ時に取り込む仕組みが必要。
  3. 読み取りパフォーマンスとのバランス

    • 小さい書き込みを即座に列ストレージに反映すると、ソートや圧縮が乱れ、読み取り性能が落ちるリスクがある。
    • 書き込み負荷と読み取り性能のトレードオフを意識して、適切なフロー(たとえば数時間ごとのバッチや夜間バッチ)を設計する。

要点まとめ

  1. 列ストレージは「書き込み」よりも「読み込み・分析」に最適化されており、小さな行単位の書き込みは不向き。
  2. 代表的な書き込み戦略として、
    • バッチ挿入(マイクロバッチ)
    • デルタストア+マージ
    • ログ+ミニバッチ
      などが採用される。
  3. ソート&圧縮効率を高めつつ、新規データへのアクセスも保証するために、バッチ・マージ・再圧縮のタイミングが重要。

列指向データベースは、高速な分析クエリや大規模集計の場面で威力を発揮します。その一方で、書き込みワークロードは工夫が求められるため、リアルタイム処理を要求する部分は行指向ストアやログベースの仕組みを組み合わせる、あるいは一定周期ごとにデータを列ストレージに再配置するといった設計がよく行われます。

列ストレージとデータキューブ

列ストレージと集計処理の関係

  • 列指向ストレージは主に分析(OLAP)用のクエリにおいて、集計やグループ化などが高速に行えるように最適化されています。
  • 大規模なデータに対して集計クエリを頻繁に実行する場合、以下のようなプリコンピュート(事前計算)の仕組みを活用することで、さらに高速化を図ることができます。

データキューブ (Data Cube)

2.1 データキューブとは

  • 多次元集計 (OLAP) を効率的に行うためのデータ構造
    たとえば「売上」という指標を、日付(time)、店舗(store)、商品カテゴリ(category)といった複数の次元(dimension)ごとに集計するイメージです。
  • 各次元の粒度(例: 年・月・日、店舗別・地域別、カテゴリ別・商品別 等)ごとに部分的に集計した結果を格納し、クエリの際に再計算を避けて素早く回答できるようにするのが狙いです。

2.2 ロールアップ (Roll-up) とドリルダウン (Drill-down)

  • ロールアップ: 粒度を粗くして、より大きなくくりで集計する
    • 例: 日次 → 月次 → 年次 のように集約レベルを大きくする
  • ドリルダウン: 粒度を細かくして、詳細レベルまで掘り下げる
    • 例: 国全体の売上 → 各地域 → 各店舗 → 個々の商品
  • データキューブを用いると、このような多段階の集計を必要に応じて事前計算または部分的に計算済みとして持つことができるため、集計クエリを高速に処理できます。

2.3 列ストレージとの親和性

  • 列指向DBでは同じカラムが連続して格納・圧縮されているため、集計クエリ(SUM、COUNT、AVG など)は必要な列だけ効率よく読込み&演算できます。
  • データキューブとして集計を格納しておくと、さらに最小限の追加演算で済むため、高速に集計結果を得られます。
  • ただし、扱う次元や粒度が増えるほどデータキューブのサイズも大きくなりがちという注意点があります。

マテリアライズドビュー (Materialized View)

3.1 マテリアライズドビューとは

  • ビュー(クエリ結果)の実体を、あらかじめ物理的に保存しておく仕組みです。
    通常のビューはクエリのたびに再計算が必要ですが、マテリアライズドビューでは結果データをキャッシュ的に保持します。
  • 例:
    CREATE MATERIALIZED VIEW monthly_sales AS
      SELECT
        store_id,
        DATE_TRUNC('month', sale_date) AS sale_month,
        SUM(amount) AS total_amount
      FROM sales
      GROUP BY store_id, DATE_TRUNC('month', sale_date);
    
    こうすると、monthly_sales へのクエリは、毎回すべての sales テーブルを読んで集計しなくても済むようになります。

3.2 メリットとデメリット

  • メリット
    • 重たい集計クエリを事前計算しておくため、読み取りが圧倒的に速くなる。
    • BIツール等から同様の集計クエリが何度も呼ばれる場合、大きなパフォーマンス向上が期待できる。
  • デメリット
    • 更新コスト: 元データが変わるたびに、マテリアライズドビューを更新(リフレッシュ)する必要がある。
    • ストレージ消費: 実体として保存するため、テーブルのレプリカを持つのと同様の容量がかかる場合がある。

3.3 列ストレージでの扱い

  • マテリアライズドビューも、列ストレージ上で圧縮やソートされると大規模データに対する集計/フィルタリングがさらに速くなる場合があります。
  • 多くの分析系データベース(Amazon Redshift、Snowflake など)がマテリアライズドビューをサポートしており、自動的・部分的に更新(インクリメンタルリフレッシュ)する仕組みを提供していることもあります。

データキューブとマテリアライズドビューの使い分け

  1. 目的
    • データキューブ: 多次元の切り口で自由にロールアップ/ドリルダウンしたいOLAP向け。高度にプリコンピュートされた多次元集計構造が欲しい場合。
    • マテリアライズドビュー: ある特定のクエリ(ビュー)に対してよく使う集計結果をキャッシュしたい場合。表計算やBIダッシュボードで決まりきった集計を繰り返す状況。
  2. 柔軟性 vs. オーバーヘッド
    • データキューブ: 大規模で多次元の事前集計を持つため、柔軟に分析できる反面、維持コスト(サイズ・更新処理)は大きくなる。
    • マテリアライズドビュー: 一部のクエリや集計だけピンポイントで事前計算するので、比較的オーバーヘッドは抑えやすい。
  3. 更新のしやすさ
    • いずれも、元データ変更時の更新処理をどう行うかが重要。
    • 定期バッチ or インクリメンタル更新を検討し、クエリ性能と新鮮性のバランスを考慮する必要がある。

要点まとめ

  1. データキューブ: 多次元の集計をプリコンピュートしておき、ロールアップ/ドリルダウンを高速化する仕組み。OLAP基盤での大量かつ複雑な集計に有効だが、更新・保管コストも大きい。
  2. マテリアライズドビュー: 特定の集計クエリ結果をあらかじめ実体化してストレージに置き、頻出クエリの再計算を省く。更新の仕組み(フルリフレッシュ・インクリメンタル)をどう設計するかが鍵。
  3. 列ストレージとの組み合わせ: もともと集計クエリに強い列指向DBを使いつつ、これらを組み合わせると、さらに大規模分析を高速に行えるようになる。

特に分析系ワークロードでは、「頻出する集計クエリを事前計算して高速化する」アプローチは非常に有効です。データ規模・クエリ頻度・更新頻度を考慮しながら、データキューブやマテリアライズドビューを適切に活用することで、列指向ストレージを最大限に活かした分析基盤を構築できます。

Discussion