はじめに
データエンジニアリングにおけるデザインパターンを扱った書籍 "Data Engineering Design Patterns" の第 8 章 Data Storage Design Patterns の感想を残していきます。
- 書籍ページ
- 無料ダウンロード
本章ではデータのストレージへの保存方法を工夫することで、分析クエリのパフォーマンス向上やストレージコスト最適化、冪等性の確保、管理の容易化を図るデザインパターンを紹介しています。
DWH を昔からやっている身からすると若干食傷気味な話題が多いのですが、知らなかったことや少し補足したいこと、昔話をしたいこともあるので、そのあたりを述べていきたいと思います。
- Backpressure Buffer(Horizontal Partitioner パターン)
- イミュータブルのありがたさ(Metadata Enhancer パターン)
- マテビューのトラウマ(Dataset Materializer パターン)
- Snowflake のメタデータを管理する FoundationDB(Manifest パターン)
- スノーフレークモデルの性能面のメリット(Normalizer パターン)
以前の 2~7 章についての感想は以下です。この書籍を読む背景や感想を残していく方針について知りたい場合は、最初の記事を参照してください。
本章で紹介されているデザインパターン
以下が本章で紹介されているデザインパターンです。(概要は私が自分の理解の元、言い直しています)
| セクション | デザインパターン | 概要 |
|---|---|---|
| Partitioning | Horizontal Partitioner | データセットをカラム値に応じて複数の行グループに分割して保存することで、読み取り性能向上や冪等性の実現を容易にする。(Ex : レンジ/リストパーティション) |
| Vertical Partitioner | データセットを可変部分と不変部分のカラムに分割した上で、不変部分は重複を排除することで保存コストの最適化や管理性向上を実現する。 | |
| Records Organization | Bucket | データセットをカーディナリティーが高いカラム値に応じて複数の行グループに分割して保存する(Ex : ハッシュパーティション、シャード) |
| Sorter | データをソートして保存する。(Ex : Redshift のソートキー、Snowflake のクラスタリング) | |
| Read Performance Optimization | Metadata Enhancer | パーティションの各カラムの最大値/最小値などの値をメタデータとして保持しておき、クエリ最適化に活用する。 |
| Dataset Materializer | データを事前に JOIN, UNION, GROUP BY することでクエリ性能を向上させる。(要はマテリアライズドビュー) | |
| Manifest | ストレージ内にある Parquet などのデータファイルの一覧を事前にリスト化しておく。(Ex : Iceberg におけるマニフェストファイル) | |
| Data Representation | Normalizer | データモデルを正規化する。 |
| Denormalizer | データモデルを非正規化する。 |
Backpressure Buffer(Horizontal Partitioner パターン)
パーティションに付きまとう問題としてスキュー(データの偏り)があります。7 章まででも何度か触れられた話題ですが、8 章ではマイクロバッチの文脈で言及があります。
マイクロバッチとは、日単位/時単位で実行するバッチとリアルタイムで処理するストリーミング処理の中間的な処理で、数秒~数分単位で頻度高くデータを処理していくアプローチになります。
マイクロバッチにおいても入力データを分割し複数のワーカープロセスで処理していくことになるのですが、例えば以下の図のように一部のワーカーに割り当てられるデータが多い(=スキューが発生している)とします。
- worker1(青)とworker3(赤)に割り当てられるデータは少ない
- worker2(黄色)に割り当てられるデータは多い
データは点線で囲った単位ごとに処理されるため、worker1(青)とworker3(赤)の処理は worker2(黄色)が完了するまで待たされるというのが本書籍で触れられているマイクロバッチにおけるスキューです。
この問題を軽減するアプローチとして Backpressure Buffer が本書籍では紹介されています。以下の図のように、他のワーカーより多いデータをバッファに退避して次の実行に処理を回すアプローチになります。
このアプローチでは worker2(黄色)のデータ処理は結局遅延するのですが、worker1(青)とworker3(赤)のデータ遅延を軽減できる(次のデータの処理に早く着手できる)というメリットがあります。
私はマイクロバッチを扱った経験が少なく、一時期 Spark Streaming を扱っていた程度なのですが、この仕組みを持っているフレームワークって何があるのでしょうか?ちょっと調べた限りだと Spark Streaming にはなさそうなのですが…
ちなみに、一般論としての Backpressure Buffer 自体はストリーミングの文脈で語られることが多いようで、以下のページでは Apache Flink における仕組みについて分かりやすく説明されています。
これ自体は TCP の輻輳制御と似た仕組みで納得感があるのですが、本書籍で紹介されているマイクロバッチの Backpressure Buffer とは以下の観点で少し方向性が違うのではないかと思いました。
- ストリーミングにおける Backpressure Buffer:データプロデューサーがスキューを生むデータを送らないように制御
- マイクロバッチにおける Backpressure Buffer:データコンシューマーがスキューを生むデータを退避
イミュータブルのありがたさ(Metadata Enhancer パターン)
Metadata Enhancer とは、テーブルを構成するある程度のサイズのデータブロックのそれぞれについて各カラムの min/max や件数、NULL 数などの統計値を持たせておき、スキャン時に不要なブロックを読み飛ばせるようにするアプローチです。
この機能は、さまざまな DBMS でいろいろな名前で呼ばれています。
- Snowflake : Micro-Partition (Partition Pruning)
- Oracle, Netezza : Zone Map
- PostgreSQL : Block Range Index (BRIN)
- SQL Server : Row Group Eliminator
このアプローチで考慮が必要な点の 1 つとして "Out-of-date statistics" が本書籍では触れられています。つまりブロックに更新が入るとブロックに付属する統計値が古くなってしまうため、定期的な更新が必要になります。
ただし、最近の DWH やストレージフォーマット(Snowflake や Iceberg)などはストレージ上のデータがイミュータブル(UPDATE されたときは新しいバージョンのブロックを作成し、古いものは論理削除される)であるため、この問題は発生しません(更新に伴う新しいブロック作成と同時に最新の統計値が取得されるため)。
DBMS の性能問題を扱うときに統計情報が現在の実データと乖離していないかという問題は常に付きまとうのですが、Snowflake ではこの問題が存在しない(ユーザーによる ANALYZE 実行が不要)のもこのイミュータブルのお陰です。
(イミュータブルの恩恵はこれ以外にもタイムトラベルやゼロコピー、変更管理など多岐に渡ります)
イミュータブルの仕組みを初めて聞いた時は少し驚きましたが、今となっては「あって当然、なければ大変」なありがたい仕組みだなと思っています。(もちろんこの仕組みがあるため、OLTP 的な更新が苦手なわけですが)
マテビューのトラウマ(Dataset Materializer パターン)
Dataset Materializer パターンとは要はマテリアライズドビュー(俗称:マテビュー)です。名前通りに受け取ればデータを実体化させているビューなのですが、それだけだと単に毎回全件洗い替えのジョブでも実現できるため、以下の 2 つの機能とともに語られることが多いです。
- 高速リフレッシュ:元テーブルの変更をマテビューに増分反映させる
- クエリリライト:元テーブルへのクエリを内部でマテビュー向けのクエリに書き換えて実行する
本書籍内では Databricks や BigQuery のマテリアライズドビューが例として挙がっています。Snowflake では Dynamic Table がマテビューとほぼ同等の機能を提供しています。(Snowflake の CREATE MATERIALIZE VIEW で作成されるものは「単一のテーブルのみをクエリ」できる=結合できないため、私は一般のマテビューとは別物と考えています)
ただし、正直言って私は過去の経験からマテビューもしくはそれと同等の機能に対して忌避感を持っています。その原因は前者の高速リフレッシュに関わる以下の問題です。
- 高速リフレッシュ不可のケースがある(本書籍でも "their incremental refreshes don’t support all SQL operations" と述べられている)
- 高速リフレッシュは増分反映を意図されているが、内部処理は非常に複雑で、実際には元テーブルのフルスキャンや中間データ量の爆発が稀によくある
些細な変更で高速リフレッシュ不可の条件を踏んだり、高速リフレッシュ処理に時間が掛かる場合のトラブルシュートが困難だったりという経験を過去にしたことがあり、若干トラウマになっています。
- 大昔に、大量のデータ集計ジョブがすべてマテビューで実現されている Oracle の大規模 DWH のチューニングをさせられたことがある
- Snowflake の Dynamic Table は、リリースされてしばらくの期間、高速リフレッシュ時の元テーブルのフルスキャンを回避できないケースがあった
まぁ、Snowflake の最近の Dynamic Table は結構まともに動いているようなので、そろそろ払拭しても良いころなのかもしれませんが。
Snowflake のメタデータを管理する FoundationDB(Manifest パターン)
Manifest パターンはオブジェクトストレージ上に配置されている Parquet などのデータファイルの一覧を事前に取得しておき、オブジェクトストレージに対するファイル一覧の取得処理を省略できる手法です。
Apache Iceberg などではマニフェストファイルにファイル一覧が記載されています。以下の記事でマニフェストファイルの中身をのぞいてみたこともあるので、興味があればどうぞ。
また、Snowflake もデータファイル(マイクロパーティション)は Amazon S3 などのオブジェクトストレージに配置されていますが、そのファイル一覧を含むメタデータは FoundationDB という分散 NoSQL で管理されていると以下の記事で紹介されています。
FoundationDB の以下の特徴が Snowflake のメタデータの管理に適しているとされています。
- 小さい read/write リクエストに対して 1 ミリ秒以下の遅延
- スキーマ進化の柔軟性
- ACID 準拠
- 値の変更監視機能(watch)
この記事は 2018 年と結構古いですが、2024 年の動画でも言及されており、継続して使用されていると考えられます。
また、以下の記事によると Snowflake の Hybrid Table も FoundationDB が裏側で動いているそうです。
FoundationDB 自体は Apple 社が買収後にオープンソース化したソフトウェアです。Apple 自身(CloudKit)や VMware でも使われているようですが、Snowflake 自身も FoundationDB の開発にかなり貢献しているようですね。
スノーフレークモデルの性能面のメリット(Normalizer パターン)
本書籍では
- 通常の第三正規系、スノーフレークスキーマ ⇒ Normalizer パターン
- One Big Table(大福帳)モデル、スタースキーマ ⇒ Denormalizer パターン
として説明されています。スノーフレークスキーマはディメンションを正規化している一方、ファクトテーブルは 1 つにまとめる(例えば注文-注文明細 ⇒ 注文ファクト)ので、正規化というと議論の余地がある気もしますが。
正規化の目的として本書籍には以下の記載があります。
(引用)
the most important goal of the Normalizer pattern is to prioritize data consistency over any eventual performance optimizations.
(DeepL による翻訳)
Normalizer パターンの最も重要な目的は、いかなる最終的なパフォーマンス最適化よりもデータの一貫性を優先することである。
データの一貫性を最重要視していることに異論はないのですが、補足としてスノーフレークスキーマの性能面のメリットについて補足したいと思います。(一般に言われる第三正規系のメリットは除きます)
-
ファクトテーブルのデータサイズ節約
One Big Table と比べるとスノーフレークスキーマのファクトテーブルはディメンションの非キー属性は含まないため、ファクトテーブルのサイズ節約を図ることができます。OLAP 的なワークロードではファクトテーブルをフルスキャンすることが多いため、データサイズ縮小はフルスキャン高速化 ⇒ クエリ性能の改善につながりやすいです。
-
サロゲートキー
DWH ではファクトとディメンションテーブルの結合キーとしてデータソース側の主キー(顧客 ID や商品番号など)ではなく DWH 側で採番した人工的な整数型キー(サロゲートキー)に置き換えられることが多いです。このサロゲートキーは大量データの結合や集計においてサイズ縮小やベクトル演算の活用により高速化をもたらします。
ちなみに、スノーフレークスキーマとスタースキーマの性能を比べた場合、スタースキーマはディメンションテーブルを非正規化しているため結合回数を削減できより高速と一般に言われますが、
- 多くのケースでは Group By Pushdown(結合処理の前にある程度集計される)の最適化がされるため、ディメンションと結合する前に件数が少なくなる
- 複数のディメンションテーブル(小さいことが多い)を先に結合してから、ファクトテーブルと結合する
となることも多く、スノーフレークスキーマが遅いのは結構限られたケースのような気がします。

