複数スタースキーマ
複数スタースキーマ(Multiple star schema)
1つのファクトで、全ての分析対象がカバー出来ることは稀である。ほとんどのケースで複数のファクトテーブルが必要になるだろう。本当に価値ある分析は複数のプロセスを横断した分析である。これを誤った方法で実現するとどうなるか?どうすれば良いのかを見ていく。
スタースキーマの作り方に関しては、別の記事にまとめている 。
発生タイミングが異なるファクト
2つ以上のファクトがあったとする。それらは同時に発生しないファクトである場合、異なるファクトテーブルに配置するべきである。誤って単一ファクトテーブルにまとめられると、個々の分析が困難になる。もし分けていれば個々に分析が可能になる。
ある営業部門で以下のような分析要件があったとする。
- 日付、顧客、製品別注文数量の分析
- 日付、顧客、製品別出荷量の分析
ディメンションは日付と顧客。ファクトは製品別注文数量と製品別出荷量である。
ディメンションは同じものを共有している。ファクトは同タイミングでは発生しない。これは例えば、顧客が製品を注文した日に、すぐに出荷がない場合があることを指している。
誤った単一ファクトテーブルによる弊害
単一のファクトテーブルで、上記の要件を対応するとどうなるか見てみよう。
図1
図1のファクトテーブルは、粒度が注文数量(quantity_ordered
)と出荷量(quantity_shipped
)、またはその両方の日別(day_key
)、製品別(product_key
)、および顧客別(customer_key
)に記録する。このような「または」、「および」が出てくるテーブルは地雷である。
day_key | customer_key | product_key | quantity_ordered | quantity_shipped |
---|---|---|---|---|
123 | 777 | 111 | 100 | 0 |
123 | 777 | 222 | 200 | 0 |
123 | 777 | 333 | 50 | 0 |
456 | 777 | 111 | 0 | 100 |
456 | 777 | 222 | 0 | 75 |
789 | 777 | 222 | 0 | 125 |
上記はファクトテーブルの内容である。day_key
とcustomer_key
とproduct_key
は、ディメンションテーブルを参照するための外部キーで、適当な値を入れている。そして、quantity_ordered
とquantity_shipped
はファクトである。
さて、このレコードが表している事実を紐解いてみよう。day_key
が123の時にcustomer_key
777は3つの製品を購入した様である。しかし、ここで問題が起きる。注文はしたが出荷はまだ完了していないため、出荷量を示すquantity_shipped
には何を入れるべきかが悩ましい。恐らくNULLか0を入れるだろう。今回は0を入れることを想定してみよう。
次にday_key
が456の時に、product_key
の111と222の出荷が発生した。この時、注文はないが出荷は存在することになり、quantity_ordered
にNULLを入れるか0を入れるか考える必要がある。今回は0を入れる想定でやろう。
さらに最後にday_key
789にproduct_key
の出荷が発生している。これも注文に何を入れるべきか悩ましいが、今回はquantity_ordered
に0を入れておこう。
例えば、customer_key
が777の出荷に関する調査をしたいとする。このファクトテーブルからレポートを作成してみよう。1つのファクトテーブルが、2つ以上のプロセスを記録している場合、誰かが1つのプロセスのみを調査する場合に問題が起きる。そして、今回の調査対象は2つのプロセス(quantity_ordered
とquantity_shipped
)の内の1つだけに興味があり、この問題が浮き彫りになる。
product | quantity_shipped |
---|---|
Product111 | 100 |
Product222 | 200 |
Product333 | 0 |
※一般的にはレポートにproduct_key
は出さず、product_name
を出すため、レポート結果にはproduct_name
を出力している。
上記のレポートを見ればすぐに分かると思うが、最終行にproduct333の出荷量に0になった意味のないレコードがある。これはレポートを取得した時点で、product_key
333が出荷はされてないが、quantity_shipped
に0が入ってることによる弊害である。これが実業務となるともっと意味のないレコードが大量に並ぶだろう。こうなっていると、恐らく担当者からは、「この0は何ですか?」と聞かれるだろう。データ構造的に仕方ないが、エンドユーザーからすると混乱の種でしかない。
基本的に2つ以上のファクトがあった時に、それらが同時に発生しないのであれば、それぞれファクトテーブルを用意することで問題は起きない。例えば、HAVING SUM(quantity_shipped) > 0
のようなことをして、設計の欠点をクエリで解決したとしよう。これはクエリが複雑になっているため、本質的な解決にはなっていない。当たり前だが、これを前提とした設計をしてしまうと、他のレポートクエリも同様にHAVING SUM(quantity_shipped) > 0
で必ず回避する必要が出てくる。例えば、集計や平均値を取るとか、副問合せなどいろんな全ての場面でこの制約が生まれる。これが辛いのは火を見るより明らかである。
ファクトテーブルを分けたモデリング
最も好ましい解決策は素直に、プロセスごとにファクトテーブルを作ることである。
図2
複数のファクトテーブルの分析
個々のプロセスの分析は有用だが、業務においてはそれらを比較することも重要だろう。例えば、生産と受注の比較、受注と出荷の比較などである。適切に設計すると複数プロセス分析が可能になる。
ここで注意したいのが、異なるファクトテーブルを比較するには、同じSQLのSELECT句で収集してはいけない。これは重複したレコードが発生する危険性がある。これについては、ファントラップ の記事で解説している。では、どうすれば良いか?それはドリルアクロスと呼ばれる手法で収集する必要がある。これを誤った手法で行った場合どうなるか?正しく行うとどうなるかをこれから解説していく。
扱うテーブルは、図2で示したものを使って、次のようなレポートを作るとする。
product | quantity_ordered | quantity_shipped |
---|---|---|
Product111 | 100 | 100 |
Product222 | 200 | 200 |
Product333 | 50 |
ファクトテーブルの結合の危険性
ファクトテーブル間で同じディメンションを共有した場合、各ファクトテーブルは兄弟関係にあると考えられる。
以下にテーブルに入っているレコードの例を示した。まずは、誤った結合の例を示す。
fact_orders
day_key | customer_key | product_key | quantity_ordered |
---|---|---|---|
123 | 777 | 111 | 100 |
123 | 777 | 222 | 200 |
123 | 777 | 333 | 50 |
fact_shipments
day_key | customer_key | product_key | quantity_shipped |
---|---|---|---|
456 | 777 | 111 | 100 |
456 | 777 | 222 | 75 |
789 | 777 | 222 | 125 |
例えば、以下のようなクエリを投げると誤った結果が返ってくる。
SELECT
dimmension_products.product_name
,SUM(fact_orders.quantity_ordered)
,SUM(fact_shipments.quantity_shipped)
FROM
fact_orders
,fact_shipments
,dimmension_products
WHERE
fact_orders.product_key = dimension_products.prodcut_key
AND fact_shipments.product_key = dimension_products.prodcut_key
-- 必要に応じて日付でフィルター
GROUP BY
dimension_products.product_key
クエリ結果。
product_name | SUM(quantity_ordered) | SUM(quantity_shipped) |
---|---|---|
Product111 | 100 | 100 |
Product222 | 400 | 200 |
Product222
のSUM(quantity_ordered)
の結果が400になっている。これは誤りで、本来なら200が返ってくるべきである。これは集計範囲内に1つの注文に対して2つの出荷があるため、素直に結合をしてしまうと重複が発生するからである。また、Product333
はfact_orders
にあるがレポートには表示されていないことも注目してほしい。これはfact_shipments
に対応するレコードがないからである。
※ディメンションテーブルとの結合には、外部結合を代用するのがおすすめするが、根本的な問題は解決されるわけではない。
ドリルアクロス
では、早速2つのプロセスを比較してみよう。ここに対して有効な手法はドリルアクロスである。これはドリルアップやドリルダウンといったドリルスルー機能とは無関係である。これは複数のプロセスを横断して分析することを意味した用語である。
ドリルアクロスの具体的なやり方としては、各スタースキーマのファクトをまず共通の粒度でまとめる。そして、それらを結合する。これだけである。
実際にドリルアクロスをしたSQLを以下に示した。ここで大事なポイントは、それぞれのファクトが同じ粒度になっていることである。これによって別々の結果であっても全く同じ粒度になり、重複なしで結合することが出来る。結合には完全な外部結合(FULL OUTER JOIN)を使うことで、片方の結果しかない場合でも結果を返すことが出来る。
WITH orders AS (
SELECT
dimension_products.product_key
,dimension_products.product_name
,SUM(fact_orders.quantity_ordered) as quantity_ordered
FROM
fact_orders
LEFT JOIN dimension_products ON dimension_products.product_key = fact_orders.product_key
GROUP BY
dimesion_products.product_key
) , shipments AS (
SELECT
dimension_products.product_key
,dimension_products.product_name
,SUM(fact_orders.quantity_shipped) as quantity_shipped
FROM
fact_shipments
LEFT JOIN dimension_products ON dimension_products.product_key = fact_shipments.product_key
GROUP BY
dimesion_products.product_key
)
SELECT
orders.product_name
,orders.quantity_ordered
,quantity_shipped
FROM
orders
FULL OUTER JOIN shipments ON shipments.product_key = orders.product_key
product | quantity_ordered | quantity_shipped |
---|---|---|
Product111 | 100 | 100 |
Product222 | 200 | 200 |
Product333 | 50 |
ドリルアクロスの手順
全てのドリルアクロス操作は、以下の手順になる。手順に従えばファクトテーブルはいくつでも実現可能である。
-
フェーズ1 各ファクトテーブルに対してクエリする。
- 各クエリで同じディメンションを使う。
- 必要に応じて値を加工する。
-
フェーズ2 結果を組み合わせる。
- 共通のディメンションで完全な外側結合(FULL OUTER JOIN)で結果をマージする。
- 必要に応じてファクトの比較や計算する。
まとめ
単一のスタースキーマ の世界から、実業務で要求される複数のスタースキーマをどうやって扱うかを見てきた。ドリルアクロスを使うことで、ファクトを横断した分析を可能になる。
- 複数のプロセスを扱う場合、別々のファクトテーブルを作ることで、個々のプロセスの分析が簡単になる。
- 2つのファクトが異なるプロセスなのか判断が難しい場合は、それらが異なるタイミングに発生しているか、または異なる粒度なのかを確認する。もし、異なればそれは別ファクトテーブルにするべきである。
- 複数のファクトテーブルを結合する場合、ファントラップ が発生する危険があるため、ドリルアクロスを用いる。
複数スタースキーマにおけるディメンション設計
Conformed Dimensions にまとめました。
参考文献
以下の本を参考に、私なりの理解を書いたものになります。ちゃんと理解を深めたい方は、自分で読むことをおすすめします。
Star Schema: The Complete Reference
Discussion