本文へ移動

ホーム / Data / 第2部 データの基礎

第2部 データの基礎・第3章

データモデリング

★★★ 必須11トピック・面接 5問

分析しやすく、間違えにくいデータの形の作り方を押さえる。

別のページで開く →

DBラボを読み込んでいます…

3.1スター型スキーマ

用語★★★ 必須
ひとことで

出来事(ファクト)と、その切り口(ディメンション)に分ける。分析の SQL が単純になる。

スター型スキーマは、分析用のテーブルを2種類に分ける設計です。真ん中にファクト(起きた出来事と、その数値)を置き、周りにディメンション(出来事を切り分ける切り口)を置きます。図にすると星の形になるのでこう呼びます。

ファクトを真ん中に、切り口のディメンションを周りにfct_order_items金額・数量・各キー1行=注文明細1行(粒度)dim_date年月・曜日・祝日dim_customer地域・会員ランクdim_productカテゴリ・ブランドdim_store店舗・チャネル
スター型スキーマ(EC の注文明細の例)
ファクトディメンション
中身注文・クリック・入金などの出来事と、金額・数量などの数値顧客・商品・店舗・日付などの属性
行数多い(増え続ける)少ない〜中くらい
列ディメンションへのキー+数値名前・カテゴリ・地域など文字の属性
例fct_order_itemsdim_customer・dim_product・dim_date

こうしておくと、分析の SQL は「ファクトにディメンションをつないで、属性で GROUP BY して数値を SUM する」という同じ形になり、誰が書いても迷いにくくなります。

SELECT d.year_month, p.category, SUM(f.amount) AS sales
FROM fct_order_items f
JOIN dim_date    d ON f.date_key    = d.date_key
JOIN dim_product p ON f.product_key = p.product_key
GROUP BY d.year_month, p.category;

最初に決める「粒度」

面接ではこう答える

スター型スキーマは、分析用のテーブルをファクトとディメンションに分ける設計です。真ん中のファクトには注文やクリックなどの出来事と金額などの数値、周りのディメンションには顧客・商品・日付などの切り口を置きます。分析の SQL が「ファクトにディメンションをつないで、属性で GROUP BY して SUM する」という同じ形になるので、誰が書いても迷いにくくなります。設計では最初にファクトの粒度を決めます。

3.2粒度の決め方

用語★★★ 必須
ひとことで

ファクトの1行が何を表すか。最初に、業務で起きる一番細かい単位で決める。

ファクトを作るとき、最初に粒度を決めるのが最も大事な手順です。粒度とは「ファクトの1行が何を表すか」で、「注文1件」なのか「注文の明細1行(商品ごと)」なのかで、持てる列も集計の結果も変わります。たとえば注文の粒度のテーブルに商品の列は持てません(1注文に商品が複数あるため)。迷ったら業務で起きる一番細かい単位(明細1行)にしておけば、あとから注文単位・日単位にはまとめられますが、逆はできません。

細かい粒度からは粗い粒度を作れるが、逆はできない明細の粒度(1行=注文の商品1つ)注文商品金額1001りんご3001001みかん2001002りんご300GROUP BY 注文注文の粒度(1行=注文1件)注文合計金額10015001002300「商品別の売上」はもう出せない
同じ注文を、2つの粒度で持った場合
お題粒度の候補決め手
EC の売上注文明細1行(注文×商品)商品・カテゴリ別に見たいなら明細
アプリの利用イベント1件あとで画面別・機能別に分けられる
在庫1日×商品×倉庫の残高残高は日ごとの状態(スナップショット)で持つ
サブスクの契約契約1件(状態の変化を日付で持つ)解約率を出すなら月×契約の表も作る

粒度は文章で書いてテーブルの説明に残します(「1行=注文の商品1つ。キャンセルされた明細も含む」)。それと合わせて、何が一意か(主キー)をテストで守ります。

面接ではこう答える

粒度はファクトの1行が何を表すかで、注文1件なのか、注文の商品ごとの明細1行なのかで、持てる列と集計の結果が変わります。細かい粒度からは粗い粒度にまとめ直せますが、逆はできないので、迷ったら業務で起きる一番細かい単位にします。決めた粒度は文章で残し、主キーの一意をテストで守ります。

ひとことで

出来事を1行にするトランザクション、決まった間隔の状態を1行にする定期スナップショット、手続きの進み具合を1行にする累積スナップショット。

ファクトには、記録の仕方で3つの型があります。在庫や口座の残高のような「状態」を、出来事のファクトから毎回計算するのは重く間違えやすいので、型を使い分けます。

ファクトの3つの種類トランザクション出来事1件が1行例:注文明細・クリック行は追加だけ定期スナップショット決まった間隔の状態が1行例:日別の在庫・残高毎日同じ数の行が増える累積スナップショット手続き1件が1行例:注文→発送→到着進むたびに日付の列を更新
何を記録したいかでファクトの形を選ぶ
型よく出す指標注意
トランザクション件数・金額の合計状態(残高)は出しにくい
定期スナップショット月末の在庫、日別の会員数残高は期間で足せない(月末の値や平均を使う)
累積スナップショット注文から発送までの日数、各段階で止まっている件数行を更新するので、増分処理では MERGE にする

面接ではこう答える

ファクトには3つの型があります。注文やクリックのように出来事1件を1行にするトランザクション型、日別の在庫や残高のように決まった間隔の状態を1行にする定期スナップショット型、注文から発送・到着のように手続き1件を1行にして段階ごとの日付を更新していく累積スナップショット型です。残高や所要日数のような指標は、出来事から毎回計算するより、合う型のファクトを作るほうが速く間違いにくくなります。

ディメンションは「分析で使う切り口」をまとめた表です。いくつか決まった作り方があります。

日付ディメンションに「切り口」を持たせるdate_key日付曜日祝日年月会計年度202611022026-11-02月いいえ2026-11FY2026202611032026-11-03火はい2026-11FY2026202611042026-11-04水いいえ2026-11FY2026「祝日の売上」「会計年度ごと」を、日付の計算なしに GROUP BY できる
日付ディメンションの例
作り方内容ないと困ること
日付ディメンション日付ごとに曜日・祝日・週・会計年度などを持つ表祝日や会計年度を毎回 SQL で計算し、人によって違う
サロゲートキー業務 DB の ID とは別に、分析用のキーを振るSCD Type 2 で同じ顧客の複数の版を区別できない
不明の行キーが NULL や存在しない値のときにつなぐ「不明」の行(キー -1 など)を用意するINNER JOIN で行が消え、合計が合わない
ロールプレイング1つの日付ディメンションを、注文日・発送日など複数の役で使う同じ表を何枚も作って中身がずれる
ひとことで

どの方向に SUM してよいか。率・ユニーク数・残高は、そのまま足すと間違う。

集計テーブルの指標は、どの方向に足してよいかが決まっています。間違えると「日別のコンバージョン率を足して月の率にする」のような、もっともらしいのに間違った数字が出ます。

足せる指標、足せない指標足せる(加法的)売上金額注文件数どの切り口でも SUM してよい一部だけ足せる(半加法的)在庫数・口座残高店舗の間では足せる日付の方向には足せない足せない(非加法的)率(コンバージョン率)ユニーク数・平均・割合分子と分母を別に持つ
指標の種類で、集計の仕方が変わる
指標やってはいけないこと正しい出し方
コンバージョン率日別の率を平均する・足す購入数の合計 ÷ 訪問数の合計
ユニークユーザー数日別の人数を足して月の人数にする月の範囲で数え直す(または HLL のスケッチを合算)
月末の在庫30日分の在庫を足す月末の日の値か、期間の平均を使う
平均単価店舗ごとの平均を平均する売上の合計 ÷ 個数の合計

集計テーブルには、率そのものではなく分子と分母を別々の列で持たせます。そうすれば、どの切り口でまとめても正しい率を計算し直せます。

面接ではこう答える

指標には、売上や件数のようにどの切り口でも足せるもの、在庫や残高のように日付の方向には足せないもの、率やユニーク数や平均のようにそのまま足せないものがあります。日別の率を平均して月の率にすると間違うので、集計テーブルには率ではなく分子と分母を別の列で持たせ、まとめたあとで割り算します。

ファンアウトは、1対多の JOIN で行が増え、上側の表の数値が何重にも数えられることです。エラーにならず、数字がもっともらしく膨らむので気づきにくい失敗です。

1対多を2つ同時につなぐと、行がかけ算で増える注文 1001送料 500円支払い 2件明細 3件JOIN2 × 3 = 6行送料を合計すると 3,000円直し方:それぞれ注文の粒度まで集計してからつなぐ
ファンアウト(行の膨らみ)の例

よくある失敗:

  • 粒度の違うもの(明細の金額と、注文単位の送料)を1つのファクトに混ぜ、送料を明細の数だけ重ねて足してしまう。粒度ごとにファクトを分けます。
  • ファクトどうしを直接 JOIN して行が増え、数字が膨らむ(ファンアウト)。それぞれ同じ粒度まで集計してからつなぎます。
  • ディメンションのキーに業務 DB の ID をそのまま使い、履歴を持てなくなる。必要に応じて分析用のキー(サロゲートキー)を振ります(SCD)。
WITH pay AS (SELECT order_id, SUM(amount) AS paid FROM payments GROUP BY order_id),
     item AS (SELECT order_id, SUM(price * qty) AS item_total FROM order_items GROUP BY order_id)
SELECT o.order_id, o.shipping_fee, pay.paid, item.item_total
FROM orders o
LEFT JOIN pay  USING (order_id)
LEFT JOIN item USING (order_id);

見つけ方は、JOIN の前後で件数と合計を比べることです。JOIN のあとにキーの一意のテストを入れておくと、膨らんだ時点で止められます。

ひとことで

属性の変化を上書きするか(Type 1)、行を足して履歴を残すか(Type 2)。

顧客の住所や会員ランクのように、ディメンションの属性は時間とともに変わります。SCD(Slowly Changing Dimension、ゆっくり変わるディメンション)は、その変化をどう持つかの決まった型です。よく使うのは Type 1 と Type 2 です。

9/15 の注文は「当時の値」で数えたい7/18/19/110/111/112/11/1Type 1上書きゴールド(古い値は残らない)Type 2行を足す一般(7/1 〜 10/1)ゴールド(10/1 〜・is_current)9/15 の注文Type 1:9月の注文も「ゴールド」として数えられるType 2:注文日が有効期間に入る「一般」の行とつながる
SCD Type 1 と Type 2 の時間軸(10/1 に会員ランクが上がった顧客)
Type 1(上書き)Type 2(行を足して履歴を残す)
やり方変わったら古い値を新しい値で上書き古い行の有効期限を閉じ、新しい行を足す
持つ列いつもの属性だけvalid_from・valid_to・is_current・行ごとのキー
集計の結果過去の注文も「今の値」で集計される注文した「当時の値」で集計できる
向くもの誤字の修正、履歴が要らない属性会員ランク・所属部署・担当営業・価格
手間少ないテーブルが大きくなり、JOIN に期間の条件が要る

たとえば顧客が 10月に「一般」から「ゴールド」に上がった場合、Type 1 だと 9月の売上も「ゴールド会員の売上」に数えられます。「ランク別の売上の推移」を見たいなら Type 2 にします。

当時の値で集計する JOIN と落とし穴

面接ではこう答える

SCD はディメンションの属性が時間とともに変わるときの持ち方の型です。Type 1 は上書きなので過去の注文も今の値で集計され、Type 2 は古い行の有効期限を閉じて新しい行を足すので、注文した当時の値で集計できます。会員ランクや担当営業のように、変化の前後で分析したい属性は Type 2、誤字の修正のように履歴が要らないものは Type 1 にします。

別のページで開く →

DBラボを読み込んでいます…

Type 2 のディメンションは、同じ顧客に複数の版(行)があります。ファクトとつなぐときは、キーだけでなく期間の条件も要ります。

注文の日時が入る「版」とつなぐ版1:一般(〜9/30)版2:ゴールド(10/1〜)9/15 の注文10/20 の注文10/1 0:00境目の 10/1 0:00 の注文は版2だけに当たる(以上・未満でそろえる)
有効期間(valid_from 以上・valid_to 未満)でつなぐ

当時の値で集計するには、ファクトの日時がディメンションの有効期間に入る行とつなぎます。

SELECT c.rank, SUM(o.amount) AS sales
FROM fct_orders o
JOIN dim_customer_scd2 c
  ON  o.customer_id = c.customer_id
  AND o.ordered_at >= c.valid_from
  AND o.ordered_at <  COALESCE(c.valid_to, TIMESTAMP '9999-12-31')
GROUP BY c.rank;

読み込みの時点で、ファクトに当時のディメンション行のキー(customer_sk)を入れておく方法もあり、こちらは集計の JOIN が単純になります。

落とし穴:期間を「以上・未満」で統一しないと、境目の時刻で2行に当たったり、どれにも当たらなかったりします。今の値だけほしい分析では is_current で絞り忘れると行が増えます。dbt の snapshot 機能を使うと、元テーブルの変化を Type 2 の形で自動で記録できます。ただし snapshot を取る間隔より短い変化は残りません(1日1回なら、1日の中の変化は最後の値だけ)。

3.9正規化と非正規化

用語★★☆ 重要
ひとことで

業務 DB は正規化して更新の矛盾を防ぎ、分析用は非正規化して読みやすくする。

正規化は、同じ情報を1か所にだけ持つようにテーブルを分けることです。業務 DB(OLTP)では、更新が速く、矛盾(ある行だけ住所が古い)が起きないことが大事なので正規化します。非正規化は、読みやすさと速さのために、あえて情報を複製してテーブルをまとめることで、分析(OLAP)ではこちらが基本です。

分けて持つか、つないで1枚にするか正規化(業務 DB)ordersorder_idcustomer_idproduct_idcustomers名前・地域products商品名・カテゴリつないで保存ワイドテーブル(分析)order_id顧客名地域商品名カテゴリ金額1佐藤東京ペン文具3002佐藤東京ノート文具500同じ「佐藤・東京」が何行にも複製される→ 読むのは楽、属性が変わると作り直し
正規化(左)とワイドテーブル(右)
正規化(業務 DB)非正規化(分析)
目的更新の矛盾を防ぐJOIN を減らして速く・分かりやすく
書き込み1か所を直せば済む複製をすべて作り直す(バッチで一括)
読み込みJOIN が多い1テーブルで済むことが多い

ワイドテーブル(OBT)の使いどころ

正規形の詳しい説明は SRE の「正規化」へ

面接ではこう答える

正規化は同じ情報を1か所にだけ持つようにテーブルを分けることで、更新が多い業務 DB では矛盾を防ぐために正規化します。分析では、読みやすさと速さのためにあえて情報を複製する非正規化が基本で、スター型スキーマやワイドテーブルにします。分析側の複製はバッチで一括して作り直すので、更新の矛盾が起きにくいのが前提です。

別のページで開く →

DBラボを読み込んでいます…

ひとことで

ファクトに必要なディメンションの列をすべてつないでおいた、列の多い1枚のテーブル。

ワイドテーブル(One Big Table、OBT)は、ファクトに必要なディメンションの列をすべてつないでおいた、列の多い1枚のテーブルです。列指向の DWH では使わない列は読まれないので、列が多くても大きな損はなく、BI ツールや SQL に慣れていない人にも使いやすくなります。一方で、属性が変わると全体の作り直しが要る、同じ列が何枚ものテーブルに散って定義がずれる、といった弱点があります。スター型スキーマを正として持ち、よく使う組み合わせだけワイドテーブルを作るのが扱いやすい形です。

スター型スキーマとワイドテーブルスター型スキーマファクト+ディメンション属性の変更はディメンションだけJOIN が要るワイドテーブル(OBT)必要な列をすべてつないだ1枚JOIN なしで BI から使える属性が変わると全体を作り直す
スター型を正として持ち、よく使う組み合わせだけワイドテーブルにする
向くとき向かないとき
BI ツールで、SQL に慣れていない人が使う属性がよく変わり、全体の作り直しが重い
同じ JOIN を多くのダッシュボードが繰り返している粒度の違うものを1枚に混ぜたくなる
ML の学習データとして1行に特徴量を並べたい同じ列が何枚にも散って、定義がずれていく

面接ではこう答える

ワイドテーブルは、ファクトに必要なディメンションの列をすべてつないでおいた1枚の表です。列指向の DWH では使わない列は読まれないので列が多くても損は小さく、BI や SQL に慣れていない人にも使いやすくなります。一方で属性の変更で全体の作り直しが要り、同じ列が何枚もの表に散ると定義がずれるので、スター型を正として持ち、よく使う組み合わせだけワイドテーブルにします。

分析用のデータの形の作り方には、よく知られた考え方がいくつかあります。今の実務では、dbt でスター型(Kimball)の mart を作る形が最もよく見られます。

手法考え方向くところ
Kimball(ディメンショナル)業務の出来事ごとにスター型スキーマを作り、ディメンションを共有する分析・BI。早く使い始めたいとき
Inmonまず会社全体の正規化した DWH を作り、そこから部門ごとの mart を作る会社全体で一貫したデータが要る大きな組織
Data Vaultハブ(キー)・リンク(関係)・サテライト(属性と履歴)に分けて、変更に強くする元のシステムが多く、よく変わる。履歴と監査が大事

面接で聞かれたら、手法の名前より「利用者がどう使うか、変更がどれくらいあるかで選ぶ」と答えられれば十分です。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • 業務 DB(正規化)と分析(スター型)の違い
  • 粒度と、足せる指標・足せない指標
  • 履歴の持ち方(SCD)とファンアウトの防ぎ方

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1スター型スキーマの粒度をどう決めますか?

模範回答

まず「どんな問いに答えるテーブルか」を利用者と確認し、その問いに答えられる中で、業務で起きる一番細かい単位を粒度にします。EC の売上なら、商品別の分析もしたいので「注文1件」ではなく「注文の明細1行」にします。細かい粒度からは注文単位や日単位にまとめられますが、逆はできないからです。

そのうえで、粒度を「1行=注文明細1行」と言葉で書いてテーブルの説明に残し、その粒度に合わない数値(注文単位の送料など)は別のファクトに分けます。行数が多すぎて重い場合は、細かいファクトを正として持ちつつ、日次の集計テーブルを別に作ります。

深掘りされたら

  • 粒度の違う数値を1つのレポートに出したいときはどうしますか?

見られているポイント分析の要件と、最も細かい単位

Q2ファクトとディメンションの違いを説明してください。

模範回答

ファクトは「起きた出来事」とその数値で、注文・クリック・入金などです。行は出来事が起きるたびに増え続け、列はディメンションへのキーと、金額・数量など足し算できる数値が中心です。ディメンションは出来事を切り分ける「切り口」で、顧客・商品・日付・店舗などの属性を持ちます。行数は比較的少なく、名前やカテゴリなど文字の列が多いです。

分析の SQL は「ファクトにディメンションをつなぎ、属性で GROUP BY して数値を SUM する」という同じ形になるので、誰が書いても迷いにくくなります。例えば「カテゴリ別・月別の売上」は、注文明細のファクトに商品と日付のディメンションをつなぐだけで出せます。

深掘りされたら

  • 日付のディメンションをわざわざ作るのはなぜですか?
  • ファクトにない属性でも分析したいときは?

見られているポイント用途・行数・列の性質での区別

Q3顧客の会員ランクの変化を分析に反映するには、どう設計しますか?

模範回答

「過去の注文を当時のランクで集計したいか」で決めます。必要なら SCD Type 2 にして、ランクが変わるたびに新しい行を足し、各行に valid_from・valid_to・is_current を持たせます。集計では、注文の日時がその行の有効期間に入るものとつなぎます。過去を今の値で見てよいなら、Type 1 の上書きで十分で、作りも簡単です。

注意点は、期間を「以上・未満」で統一して境目の重なりや抜けをなくすこと、今の値だけほしいときに is_current で絞り忘れないことです。dbt なら snapshot で Type 2 を自動で記録できますが、取る間隔より短い変化は残らないので、変化の頻度も確かめます。

深掘りされたら

  • Type 2 のテーブルが大きくなりすぎたら?
  • ファクトに当時のキーを入れておく方法との違いは?

見られているポイントSCD の使い分けと、当時の値での JOIN

Q4分析用のテーブルで、正規化と非正規化はどう使い分けますか?

模範回答

業務 DB は更新の矛盾を防ぐために正規化し、分析用は読みやすさと速さのために非正規化するのが基本です。分析では書き込みはバッチでまとめて行い、読む回数のほうがずっと多いので、JOIN を減らしたほうが得です。列指向の DWH では使わない列は読まれないので、列の多いワイドテーブルでも大きな損はありません。

ただ、すべてをワイドテーブルにすると、属性が変わるたびに作り直しが要り、同じ列がいろいろなテーブルに散って定義がずれます。そこで、スター型スキーマを正として持ち、BI でよく使う組み合わせだけワイドテーブルにする、という二段構えにしています。

深掘りされたら

  • ワイドテーブルの列の定義がずれないようにするには?

見られているポイント読み書きの比率と、運用の手間まで考えた判断

Q5ファクトどうしを JOIN したら売上が実際より大きくなりました。原因と直し方は?

模範回答

粒度の違うテーブルをそのままつないで、行が増えた(ファンアウト)のが原因のことが多いです。例えば注文のファクトと、1注文に複数ある支払いのファクトを注文 ID でつなぐと、支払いが2回ある注文は2行になり、注文の金額を足すと2倍に数えてしまいます。

直し方は、つなぐ前にそれぞれを同じ粒度(注文単位)まで集計してから JOIN することです。確かめ方としては、JOIN の前後で行数と主キーの重複を比べ、主キーに unique のテストを入れておくと、同じ間違いを早く見つけられます。

深掘りされたら

  • 粒度の違う数値を1つのダッシュボードに出すには?

見られているポイント粒度の理解と、検証の手順