データウェアハウスはビジネスインテリジェンス(BI)、特に分析を支えるためのデータマネジメントツールである。データウェアハウスは専らクエリと分析を行うことを目的とし、多くの場合大量の履歴データを含んでいる。 - Oracle.com
introduction
この記事はRalph KimballとMargy RossによるThe Data Warehouse Toolkitの要約です。この本は私の上司から勧められたものですが、私のデータウェアハウスの知識を一新するものでした。キャリアの早い段階でこれを読まなかったことが信じられないです。
データの分野で働く人全てにこの本をお勧めします。データアナリスト、エンジニア、サイエンティストなど職種にかかわらず分析ニーズを支えるデータウェアハウスの構築を任されることがあるかもしれません。
この本は以下の常用な3つの原則を示しています。
- Simple : 簡単な構造で、理解しやすいこと
- Fast: クエリが高速に返ってくること
- Flexible: あらゆる角度、分類から分析できること
Overview
- Fact and Dimension Tables
- Keys and Normalization
- Other Fact Table Best Practices
- Other Dimension Table Best Practices
1. Fact and Dimension Tables
計算を含むデータはファクトテーブルで、制約、グループ、ラベルを含むデータはディメンションテーブルで扱う。 - The Data Warehouse Toolkit
Fact Tables
小売業のケースを例に挙げます。販売のトランザクションは以下のように扱われます。
+------------+---------+--------+-----+--------+
| date | store | item | qty | amount |
+------------+---------+--------+-----+--------+
| 2021-01-01 | Store 1 | Gloves | 2 | 20 |
| 2021-01-02 | Store 2 | Hammer | 1 | 28 |
+------------+---------+--------+-----+--------+
これはFact Tableと呼ばれ、以下を含みます
- Dimensions:
date
,store
,item
- Measures:
qty
,amount
Table Grain
Grainとは1行に取り込まれるデータ粒度のレベルを著します。この場合は特定の店舗における1つのトランザクションになります。
Grain Do's and Don'ts
- Fact Tableを設計する早期からGrainを定義するべき
Grainの定義はその後全ての設計に影響するため、非常に重要なステップです。Grainを選択する際は手戻りを防ぐため、必ずステークホルダーから同意を得ます。 - 最小レベルのGrainを保存する
ストレージの削減や、高レベルでの分析のためにより高いレベルでのGrainを集計する必要があるかもしれません。これは問題ありませんが、柔軟な分析を可能にするためにより下位のレベルのGrainになっているデータを別のテーブルで保持していることを確認してください。 - Do not mix the grain
異なるGrainでFactを保持すると、Measureを集計する際にエラーが発生する場合があります。このエラーは特定のフィルタを適用するようにユーザーを教育すれば回避可能ですが、「簡単に使用できる」という原則に違反します。
Do not store different grains in the same table:
+------------+---------+-------------+-----+--------+
| date | store | item | qty | amount |
+------------+---------+-------------+-----+--------+
| 2021-01-01 | Store 1 | <all_items> | 3 | 28 | <-- rollup
| 2021-01-01 | Store 1 | Gloves | 2 | 20 | <-- component
| 2021-01-01 | Store 1 | Hammer | 1 | 8 | <-- component
+------------+---------+-------------+-----+--------+
Dimension tables
先ほどのデータにcountry
とitem_cat
の情報を追加してみましょう。一つの方法はFact tableに新しい列を追加することです。
+------+-----------+---------+-----------+--------+-----+--------+
| date | country | store | item_cat | item | qty | amount |
+------+-----------+---------+-----------+--------+-----+--------+
| ... | Singapore | Store 1 | Materials | Gloves | 2 | 20 |
| ... | Thailand | Store 2 | Tools | Hammer | 1 | 28 |
+------+-----------+---------+-----------+--------+-----+--------+
しかし、この方法はスケーラブルではありません。Fact tableは数百万行から数十億行まで達することもあります。列を追加するとストレージ容量が指数的に増加します。
これらの属性はそれぞれのDimensionと1対1の関係になるため、個別のDimension Tableを作成することが適切です。
Fact Table
+------------+---------+--------+-----+--------+
| date | store | item | qty | amount |
+------------+---------+--------+-----+--------+
| 2021-01-01 | Store 1 | Gloves | 2 | 20 |
| 2021-01-02 | Store 2 | Hammer | 1 | 28 |
+------------+---------+--------+-----+--------+
Dimension Tables
+---------+-----------+
| store | country |
+---------+-----------+
| Store 1 | Singapore |
| Store 2 | Thailand |
+---------+-----------+
+--------+-----------+
| item | item_cat |
+--------+-----------+
| Gloves | Materials |
| Hammer | Tools |
+--------+-----------+
Star Schema
Fact Tableの周りにDimensionを並べると、有名なStar Schemaがで効き上がります。Star Schemaは実用性と使いやすさから人気があります。
Querying a Star Schema
ユーザーがこれらのDimensionを出力に含めたい場合は、SQLに簡単なjoin句を含めるだけで実現可能です。
select
f_txn.*
d_store.country_name
from
f_txn
left join
d_store
on d_store.store = f_txn.store
where
d_store.country_name = 'Singapore'
2. Keys and Normalization
Natural Keys
Natural Keyはデータウェアハウスの外で運用システムで使用されるユニークキーです。通常ビジネス上の意味を持つような長いコードになります。以下で使用しているキー(例:Store 1
,Gloves
)はNatural Keyに分類されます。
Fact Table
+------------+------------+-----------+-----+--------+
| date (FK) | store (FK) | item (FK) | qty | amount |
+------------+------------+-----------+-----+--------+
| 2021-01-01 | Store 1 | Gloves | 2 | 20 |
| 2021-01-01 | Store 2 | Hammer | 1 | 28 |
+------------+------------+-----------+-----+--------+
Dimension Tables
+------------+--------------+
| store (PK) | country_name |
+------------+--------------+
| Store 1 | Singapore |
| Store 3 | Thailand |
+------------+--------------+
+-----------+---------------+
| item (PK) | item_category |
+-----------+---------------+
| Gloves | Material |
| Hammer | Tools |
+-----------+---------------+
Surrogate Keys
Surrogate Keyはデータベースが生成するユニークキーで、一つ以上のNatural Keyにマッピングされます。これはビジネス上の意味を持たず、データベースのjoinのためだけに作成されます。
先ほどのの例をアップグレードしてSurrogate Keyを追加します。
Fact table
+------------+----------+---------+-----+--------+
| date | store_id | item_id | qty | amount |
+------------+----------+---------+-----+--------+
| 2021-01-01 | 1 | 1 | 2 | 20 |
| 2021-01-01 | 2 | 3 | 1 | 28 |
+------------+----------+---------+-----+--------+
Dimension tables
+----------+------------+--------------+
| store_id | store_name | country_name |
+----------+------------+--------------+
| 1 | Store 1 | Singapore |
| 2 | Store 2 | Thailand |
+----------+------------+--------------+
+---------+-----------+---------------+
| item_id | item_name | item_category |
+---------+-----------+---------------+
| 1 | Gloves | Material |
| 3 | Hammer | Tools |
+---------+-----------+---------------+
Benefits of Surrogate Keys
Surrogate Keyを作成するのには労力がかかりますが、以下の利点が得られます。
-
運用上の変更に強い
なんらかの理由でNatural Keyが変更された場合、Fact Tableの膨大な行を更新せずに、Dimension Tableを1行更新するだけ済みます。 -
スペース削減とパフォーマンス向上
Natural Keyは多くの場合、文字列型であり、整数よりもスペースを取ります。また文字列型のjoinも効率的ではありません。Fact tableが膨大になるとこの影響が顕著に現れます1。 -
NULL値の扱い
Natural Keyはシステム上のエラーや例外ケースによってNULLになる場合があります。これはユーザーがjoinやfilterを行う際に一貫性がなくなるため、Bad practiceであると言えます。Surrogate Keyを用いれば、これを解決できます。 -
複数の源泉システムを統合する
複数の源泉システムでキーがNatural Keyが重複した場合もそれぞれSurrogate Keyを割り当てることで簡単に処理ができます。
Avoid having null foreign keys
さまざまな理由により、Fact tableの外部キーがNULLになることがあります。初心のアプローチはそのままにしておくことでしょう。
+------------+----------+---------+-----+--------+
| date | store_id | item_id | qty | amount |
+------------+----------+---------+-----+--------+
| 2021-01-01 | 1 | 1 | 2 | 20 |
| 2021-01-01 | 2 | (NULL) | 1 | 28 |
+------------+----------+---------+-----+--------+
これはテーブル間がNULLで結合できず、where
やgroup by
が正常に機能しない可能性があるため、Bad practiceです。このような場合は予約されたエントリを持ったDimension tableを作成することが適切です。
+------------+----------+---------+-----+--------+
| date | store_id | item_id | qty | amount |
+------------+----------+---------+-----+--------+
| 2021-01-01 | 1 | 1 | 2 | 20 |
| 2021-01-01 | 2 | 0 | 1 | 28 |
+------------+----------+---------+-----+--------+
+---------+-----------+---------------+
| item_id | item_name | item_category |
+---------+-----------+---------------+
| 0 | unknown | unknown |
| 1 | Gloves | Materials |
| 2 | Hammer | Tools |
+---------+-----------+---------------+
Normalization
正規化とはデータの冗長性を減らし、整合性を向上させるためにデータベースを再構築することです。Surrogate Keyを使用するようにFact Tableをアップデートしたとき、Fact tableを正規化したことになります。
Normalizing Dimension tables and Snowflaking
店舗ディメンションにさらに列を追加しましょう。
+----------+------------+-----------+--------------+---------------+
| store_id | store_name | size_sqft | country_name | currency_code |
+----------+------------+-----------+--------------+---------------+
| 1 | Store 1 | 10000 | Singapore | SGD |
| 2 | Store 2 | 15000 | Thailand | THB |
| 3 | Store 3 | 15000 | Thailand | THB |
+----------+------------+-----------+--------------+---------------+
このテーブルに国の情報を扱うDimension tableを追加することで正規化することができます。
Store dimension
+----------+------------+-----------+------------+
| store_id | store_name | size_sqft | country_id |
+----------+------------+-----------+------------+
| 1 | Store 1 | 10000 | 1 |
| 2 | Store 2 | 15000 | 2 |
| 3 | Store 3 | 15000 | 2 |
+----------+------------+-----------+------------+
Country dimension
+------------+--------------+---------------+
| country_id | country_name | currency_code |
+------------+--------------+---------------+
| 1 | Singapore | SGD |
| 2 | Thailand | THB |
+------------+--------------+---------------+
このように2段階に分岐したものはSnowflake Schemaとして知られています。しかしこのタイプのスキーマは以下の理由からデータウェアハウスには推奨されません。
- ユーザーにとって複雑さが増す
- 開発者のメンテナンスが困難になる
- 性能低下の可能性
Dimension tableは比較的小さいため、正規化によって削減されるストレージ量はユーザビリティとパフォーマンスの低下に見合わないことが多いです。
3. Other Fact Table Best Practices
Measures
Meatures of measurementsは集計の対象となる数値を表します(例: sum, average, min ,max)。
Null Measures are Okay
Dimension Keyとは異なり、measureにNULLを持つことは標準的なプラクティスです。これらはSQLの集約関数で適切に処理されるため問題ありません。
+------------+----------+---------+-----+--------+
| date | store_id | item_id | qty | amount |
+------------+----------+---------+-----+--------+
| 2021-01-01 | 1 | 1 | 2 | 10 |
| 2021-01-01 | 2 | 3 | 1 | 11 |
| 2021-01-01 | 2 | 4 | 1 | (NULL) |
+------------+----------+---------+-----+--------+
amount
列の平均値は以下になります。
Query:
SELECT AVG(amount) AS amount_avg
FROM f_txn
Result:
+------------+
| amount_avg |
+------------+
| 10.5 | <-- The less meaningful alternative
+------------+ would be (10 + 11)/3 = 7
Long vs Wide Fact Tables
これまで、Fact tableに各列がmeasureを持つ、横持ちの形(wide format)を使用してきました。ここでさらに多くのmeasureを追加しましょう。
+------------+----------+-----+-----+--------+----------+---------+
| date | store_id | ... | qty | amount | discount | revenue |
+------------+----------+-----+-----+--------+----------+---------+
| 2021-01-01 | 1 | ... | 2 | 10 | (NULL) | 10 |
| 2021-01-01 | 2 | ... | 1 | 20 | 2 | 18 |
+------------+----------+-----+-----+--------+----------+---------
設計者はこのテーブルをunpivotしてmeasureをdimensionにした縦持ちの形(long format)にしたくなることがあります。これは私がデータアナリストになりたての頃も同じ傾向がありました。
+------------+----------+-----+----------+-------+
| date | store_id | ... | measure | value |
+------------+----------+-----+----------+-------+
| 2021-01-01 | 1 | ... | qty | 2 |
| 2021-01-01 | 1 | ... | amount | 10 |
| 2021-01-01 | 1 | ... | discount |(NULL) | <--- This row is
| 2021-01-01 | 1 | ... | revenue | 10 | usually removed
| 2021-01-01 | 2 | ... | qty | 1 | to save space.
| 2021-01-01 | 2 | ... | amount | 20 |
| 2021-01-01 | 2 | ... | discount | 2 |
| 2021-01-01 | 2 | ... | revenue | 18 |
+------------+----------+-----+----------+-------+
データウェアハウスでmeasureをdimensionに持つことが好ましくない理由は以下の通りです。
- measureの数に応じて、テーブルの行数が大幅に増加する
- 複数のmeasureを含む計算(例:収益を金額で割る)の難易度が高くなる
ただし、long formatには以下の利点もあります。
- Flexibility: 新しいmeasureが追加されたとき、列を追加するのではなく、measure dimensionに新しい値を追加するだけで新しいmeasureを追加できる
- Improved grouping, filtering of measures: measureに属性を関連づけることでgroup化やフィルタリングができる(例:KPI vs 実績値など)
Long formatのfact tableは以下のような極端なケースで役に立ちます。
- 100以上のmeasureがある
- ほとんどのmeasureに値が入っていない(疎結合)
Different Types of Measures
Measureは次の3つに分類できます。
Additive
Dimension間で足し合わせができるmeasure
e.g. 数量, 金額
+------------+------+--------+
| date | ctry | amount |
+------------+------+--------+
| 2021-10-01 | SG | 100 |
| 2021-10-01 | TH | 200 |
| 2021-10-15 | SG | 300 |
+------------+------+--------+
+------------+-------+--------+
| date | ctry | amount |
+------------+-------+--------+
| 2021-10-01 | <all> | 300 |
| 2021-10-15 | <all> | 300 |
+------------+-------+--------+
+-------+------+--------+
| date | ctry | amount |
+-------+------+--------+
| <all> | SG | 400 |
| <all> | TH | 200 |
+-------+------+--------+
+-------+-------+--------+
| date | ctry | amount |
+-------+-------+--------+
| <all> | <all> | 600 |
+-------+-------+--------+
Semi-Additive
いくつかのDimensionでのみ足し合わせることができるmeasure。よくあるのは時間dimensionで足し合わせられないケース。
e.g. 勘定残高2
+------------+------+---------+
| date | ctry | balance |
+------------+------+---------+
| 2021-10-01 | SG | 100 |
| 2021-10-01 | TH | 200 |
| 2021-10-31 | SG | 300 |
| 2021-10-31 | TH | 400 |
+------------+------+---------+
+------------+-------+---------+
| date | ctry | balance |
+------------+-------+---------+
| 2021-10-01 | <all> | 300 |
| 2021-10-31 | <all> | 700 |
+------------+-------+---------+
以下の例は間違った集計です。時間を跨いで残高を集計しても意味がありません。
+-------+------+---------+
| date | ctry | balance |
+-------+------+---------+
| <all> | SG | 400 |
| <all> | TH | 600 |
+-------+------+---------+
日時で集計する場合は別の方法で集計することができます。
+-------+------+---------------+-------------+-------------+
| date | ctry | balance_start | balance_end | balance_avg |
+-------+------+---------------+-------------+-------------+
| <all> | SG | 100 | 300 | 200 |
| <all> | TH | 200 | 400 | 300 |
+-------+------+---------------+-------------+-------------+
Non-Additive
割合, distinct counts, meanやmedianなどのサマリ値はdimenstion間で足し合わせることができません。
例えば、margin
列は(revenue - cost) / cost
で定義されます。
+---------+------+---------+------+--------+
| country | city | revenue | cost | margin |
+---------+------+---------+------+--------+
| SG | SIN | 200 | 100 | 0.500 |
| TH | BKK | 300 | 100 | 0.667 |
| TH | CNX | 400 | 100 | 0.750 |
+---------+------+---------+------+--------+
margin列の足し合わせは意味のない値になります。
+---------+-------+---------+------+--------+
| country | city | revenue | cost | margin |
+---------+-------+---------+------+--------+
| <all> | <all> | 900 | 300 | 1.917 |
+---------+-------+---------+------+--------+
margin列はrevenue, cost列を用いて再計算する必要があります。
+---------+-------+---------+------+--------+
| country | city | revenue | cost | margin |
+---------+-------+---------+------+--------+
| <all> | <all> | 900 | 300 | 0.667 |
+---------+-------+---------+------+--------+
このようなNon-additive measureはほとんどがBIのエンジンに頼って動的に計算を行なっています。しかし、データの規模によってはBIエンジンが集計を計算することが不可能になることがあります。この解決策の一つはgrainが混ざったテーブルを作成することです。
以下の例ではmarginの値が事前に計算されてテーブルに入っています。より複雑な例ではユーザーの要求に応じて追加のgrain列やmeasureを含めることもできます。
+---------------+---------+-------+--------+
| grain | country | city | margin |
+---------------+---------+-------+--------+
| all_countries | <all> | <all> | 0.667 |
| country | SG | <all> | 0.500 |
| country | TH | <all> | 0.714 |
| city | SG | SIN | 0.500 |
| city | TH | BKK | 0.667 |
| city | TH | CNX | 0.750 |
+---------------+---------+-------+--------+
4. Other Dimension Table Best Practices
データウェアハウスは、多くの場合ディメンションによってのみ機能します。BIの分析力はディメンションの品質と深さに比例します。 - The Data Warehouse Toolkit
Avoid having null dimension attributes
NULLの外部キーと同様、 NULLのDimensionも避ける必要があります。
Bad:
+---------+-----------+--------+
| item_id | item_name | color |
+---------+-----------+--------+
| 1 | Glass | (NULL) |
| 2 | Gloves | Blue |
+---------+-----------+--------+
Good:
+---------+-----------+----------------+
| item_id | item_name | color |
+---------+-----------+----------------+
| 1 | Glass | Not Applicable |
| 2 | Gloves | Blue |
+---------+-----------+----------------+
Include various verbose textual attributes
Dimension Tableはユーザーが操作するBIツールに表示される最終的なDimension名とグループとなります。詳細な属性を作成するのを恐れないでください。
Dimension tableは一般に小さく、必要なストレージ量はわずかです。例えば、日付テーブルでは日付属性のあらゆる種類を含めることが推奨されます。
Too many dimensions
Fact tableが多くのdimensionを持ちすぎると、centipede tableと呼ばれます。dimensionが多すぎると必要なストレージ要衝が増加し、sliceやdiceを行うのが面倒になります。
以下は相関関係にある多くのDimensionを持つcentipede tableの例です。これらは関連するDimensionを一つのDimension tableにグループ化することで容易に修正できます。
+-------------------------+ +-------------------------+
| f_transactions | | f_transactions_improved |
+-------------------------+ +-------------------------+
| date_id | | date_id |
| week_id | | product_id |
| month_id | ---> | store_id |
| quarter_id | | qty |
| year_id | | amount |
| fiscal_month_id | +-------------------------+
| fiscal_year_id |
| product_id |
| product_category_id |
| product_package_type_id |
| store_id |
| store_country_id |
| qty |
| amount |
+-------------------------+
Junk Dimensions
Dimension同士が関連づけられていない場合、それらをグループ化する明確な方法がないときがあります。このとき、Junk Dimensionを実装することができます。例えるなら、ガラクタの引き出しのようなもので、バラバラになったものを一つの場所に捨てておくようなものです。以下に例を示します。
- dim1-dim4は互いに関連がないとする
+------------+------+------+------+------+-----+
| date_id | dim1 | dim2 | dim3 | dim4 | qty |
+------------+------+------+------+------+-----+
| 2021-09-01 | A | 1 | D1 | Z001 | 100 |
| 2021-09-01 | B | 2 | D2 | Z002 | 200 |
| 2021-09-01 | B | 3 | D2 | Z002 | 250 |
+------------+------+------+------+------+-----+
- dim1-dim4までのjunc dimensionを作成する
+---------+------+------+------+------+
| junk_id | dim1 | dim2 | dim3 | dim4 |
+---------+------+------+------+------+
| 1 | A | 1 | D1 | Z001 |
| 2 | A | 1 | D1 | Z002 |
| 3 | A | 1 | D2 | Z001 |
| 4 | A | 1 | D2 | Z002 |
| 5 | A | 2 | D1 | Z001 |
| 6 | A | 2 | D1 | Z002 |
| 7 | A | 2 | D2 | Z001 |
| 8 | A | 2 | D2 | Z002 |
| 9 | A | 3 | D1 | Z001 |
| 10 | A | 3 | D1 | Z002 |
| 11 | A | 3 | D2 | Z001 |
| 12 | A | 3 | D2 | Z002 |
| 13 | B | 1 | D1 | Z001 |
| 14 | B | 1 | D1 | Z002 |
| 15 | B | 1 | D2 | Z001 |
| 16 | B | 1 | D2 | Z002 |
| 17 | B | 2 | D1 | Z001 |
| 18 | B | 2 | D1 | Z002 |
| 19 | B | 2 | D2 | Z001 |
| 20 | B | 2 | D2 | Z002 |
| 21 | B | 3 | D1 | Z001 |
| 22 | B | 3 | D1 | Z002 |
| 23 | B | 3 | D2 | Z001 |
| 24 | B | 3 | D2 | Z002 |
+---------+------+------+------+------+
- dim1-dim4をjunc_idに置き換える
+------------+---------+-----+
| date_id | junk_id | qty |
+------------+---------+-----+
| 2021-09-01 | 1 | 100 |
| 2021-09-01 | 19 | 200 |
| 2021-09-01 | 24 | 250 |
+------------+---------+-----+
Junk Dimensionは関連するDimensionのカーディナリティ(ユニークな値の数)が高いと、Junk Dimensionは非常に大きくなる可能性があります。従ってこれはカーディナリティが低いDimensionにのみ推奨されます。
Too few Dimensions
Fact tableが少数のディメンジョンで構成されており、すべての情報が取得できる場合でも、設計者は複雑さの軽減、ユーザビリティの向上、インサイト発見の支援のために、追加のディメンジョンを提供する必要があります。
この本では以下のような例が挙げられていました。
- Casual/Status dimensions
- Factの状態を示すdimension
- Date-related dimensions
- 日付に関連するDimension
- Degenerate dimensions
- dimension tableを持たないdimension
- Role-playing dimensions
- 1つのFact tableが同じDimension tableを何度も参照する際、参照を独立させるため、個別に作成されるdimension
おおまかなルールでは、The Data Warehouse ToolkitはFact tableに5-20のdimensionを含めることを推奨しています。
Things I Didn’t Cover
ここでは時間の関係で省略した、役に立つトピックを紹介します。
- Slowly Changing Dimensions
- Dimensionの属性は時間と共に変化しますが、古い属性を上書きすると過去のデータの分析ができなくなるため、これを解決する方法
- Fact table variants
- 今回はトランザクションのみを取り上げましたが、他にも定期的なスナップショット、蓄積したスナップショットがあります。
- Enterprise Data Warehouse Bus Architecture and Opportunity/Stakeholder Matrix
- これらはユーザーの要件を整理し、最適なデータウェアハウス設計を支援するための有用なフレームワークです。この本ではこれらをより詳細に取り上げています。
Conclusion
これらのベストプラクティスは絶対的なものとは意図されていないため、あなたのプロジェクトの要件と合っているかを評価してください。迷った時は Simple, Speedy, Flexibleの原則を思い出してください。
Data warehouse toolkitをご自身で読まれることを強くお勧めします。この方には実際のケーススタディも含まれており、私が紹介しきれなかったテクニックも詳細に説明されています。
読んでくれてありがとうございました。良い一日を。
ビジネスの運用では新しいシステムを構築することが不可欠ですが、DWH/BIの利用はそうではありません。ですが、DWH/BIが実用的な情報が得られるシンプルかつ高速なシステムであれば、きっとビジネスユーザーにも受け入れられるでしょう。 - The Data Warehouse Toolkit