AQ Tech Blog

WebエンジニアがはじめてのML案件で学んだ「ディメンショナルモデリング」

作成者: takumi.yamaguchi|2026年07月13日

はじめに

最近、はじめて機械学習(ML)の案件にアサインされました。 MLというと「Pythonで高度なアルゴリズムを実装して予測モデルを作る」というイメージで臨んだのですが、実際に開発に入ってまず印象的だったのは、その手前にあるデータ分析のためのデータベース設計(ディメンショナルモデリング)が、Webエンジニアとして慣れ親しんできたWebアプリの設計と大きく異なっていたことでした。

幸い、案件の冒頭でディメンショナルモデリングの考え方を丁寧に説明してもらえたので、大半は「なるほど、分析の世界はこういうルールなのか」と素直に飲み込めました。とはいえ、最初からすべてがすっと入ってきたわけではなく、考え込んだ部分もありました。

今回は、Webエンジニアの視点から「なるほど」と腑に落ちた2つのポイントを紹介したあと、逆に特にこんがらがったポイント(ディメンションのサロゲートキー管理)にも一節割いて、当時の混乱を正直に書いてみます。

1. 「3NF(正規化)」の常識が通用しない?2つの設計手法の違い

Webアプリケーションの開発(いわゆるOLTP)では、データの整合性を保ち、重複をなくすために「正規化(第3正規化など)」を行うのが教科書的なアプローチです。テーブル同士を細かく分解し、リレーションを繋いでいきます。

しかし、データ分析やMLの世界(OLAP)で使われる「ディメンショナルモデリング」は、これとは異なるアプローチをとります。

  • Webアプリの設計: データの書き込み(更新・追加)を不整合なく高速に行うための設計
  • データ分析の設計: データの読み込み(集計・特徴量抽出)を分かりやすく高速に行うための設計

日頃から「重複の排除」を意識して設計している身としては、分析のためにあえてデータを冗長に持たせたり、テーブルをシンプルに結合しやすい形(スタースキーマ)に整えたりするアプローチは、非常に新鮮に感じられました。


スタースキーマとは

スタースキーマは、中心に「ファクトテーブル(売上や注文などの数値データ)」を置き、その周りに「ディメンションテーブル(顧客・商品・日付などの属性)」を放射状に配置する構造です。中心から外側にテーブルが伸びる形が星のように見えることから、この名前が付いています。

例えばECサイトの売上分析であれば、以下のような構造になります。

  • ファクトテーブル: 注文明細(1行=1商品の注文。注文金額、数量など)
  • ディメンションテーブル: 顧客、商品、店舗、日付

このとき重要なのが、ファクトテーブルのグレイン(1行が何を表すか)を最初に決めることです。今回は「注文明細1行」をグレインとし、本記事では以降もこの粒度で統一して説明します。グレインが定まると、どのディメンションを紐付けられるか、どの数値を集計できるかが自然と決まります。

なお、グレインは「ファクトテーブルが保持する元データの粒度」のことです。MLで使う「翌月の売上」のような目的変数や、顧客ごとの集計といった特徴量は、このファクトテーブルを集計・加工した結果として作られます。グレインを細かく持っておくほど、後段の集計の自由度が高くなります。

JOINがシンプルになるため、分析クエリが書きやすく、BIツールでの集計にも適しています。


図で見る:RDB(正規化)とスタースキーマの違い

同じECサイトの「注文」を表現する場合でも、両者の構造は大きく異なります。

RDB(正規化)の例

テーブルが細かく分解され、1対多の関連が鎖状に広がっているのが特徴です。金額を持つ明細テーブル(order_items)にたどり着くまでに複数のJOINが必要になります。

スタースキーマの例

中心にファクト(注文明細)、周りにディメンション(顧客・商品・店舗・日付)が並び、JOINが「ファクト ↔ ディメンション」の1段で済むことが分かります。RDB例の users / stores / products / categories がそれぞれ dim_customer / dim_store / dim_product(カテゴリは商品ディメンションに統合)に対応します。

補足:サロゲートキーとSCDについて

ディメンションテーブルの主キーには、業務上のID(user_idなど)ではなく サロゲートキー(代理キー、以下SK) を採用するのが定石です。これは、業務側のIDが変わってもデータウェアハウス側を安定して保てるようにするためです。特に「過去のある時点での顧客のステータス」を保持したまま分析したいケース(SCD: Slowly Changing Dimension)において、この設計が真価を発揮します。本記事の主旨である「ファクトとディメンションに分ける」考え方からはやや踏み込んだ話題ですが、実は自分が当時一番こんがらがったのもこのSK管理周辺でした。詳しくは記事の終盤で触れます。

2. 思考プロセスの反転:「測定したい事実」から周りを埋めていくアプローチ

もう1つの大きな気づきは、設計するときの「思考のスタート地点」の違いです。

Webアプリの設計では、「ユーザーがいて、商品があって、注文というアクションが発生して……」と、システムのエンティティ(実体)の関係性をボトムアップで組み立てていくことが多いと思います。

一方で、ディメンショナルモデリング(特にMLの文脈)では、「まず分析・予測したいビジネス上の事実(=ファクト)を中心に据え、その周りを埋めていく」というトップダウンの組み立て方になります。


「その事実を分析するには、何が必要か?」

  • 中心(ファクト): 分析対象となる「ビジネス上のイベント(注文・購入など)と、その測定値」(例:注文という出来事と、その注文金額・販売数量)。MLでは、この測定値が予測の対象(目的変数)になることが多いです。
  • 周り(ディメンション): そのファクトを紐解くための「軸や属性(誰の、いつの、どの状態か)」。MLの特徴量の元になります。

具体例:ECサイトの売上を分析・予測する場合

実は、セクション1で見た売上のスタースキーマこそ、この考え方の産物です。「売上を分析・予測したい」という事実をまず中心に置き、「それを左右するのは誰の・どの商品の・いつの注文か?」と軸を足していくと、自然と先ほどのスタースキーマと同じ構造にたどり着きます。

  • 中心(ファクト)= fact_order_items 注文金額・数量。MLで「翌月の売上」を予測するなら、この集計値が目的変数になります。
  • 周り(ディメンション)= dim_customer / dim_product / dim_store / dim_date 顧客の会員ランク、商品カテゴリ、曜日・祝日など。これらがMLの特徴量の切り口になります。

この形になっていると、「祝日の顧客ごとの購入金額」「曜日別の平均注文件数」といった特徴量を作るSQLが格段に書きやすくなります

補足:日付はファクトとディメンションのどちらに持つ?

「日付こそファクトに持つのが自然では?」と感じるかもしれません。実際には役割分担します。「いつ起きたか」という日付キー(や正確なタイムスタンプ)はファクトに曜日・季節・祝日といった日付の「意味づけ」はディメンションに 持たせるのが定石です。生のタイムスタンプを毎回加工せず、「祝日か」「何曜日か」を属性としてそのまま絞り込み・集計できるのが利点です。


SQLで比較してみる

祝日に各顧客がいくら使ったか」という特徴量を取り出す例で比べてみます(祝日マスタが既に存在する前提の擬似SQLです)。

-- RDB(正規化): 金額を得るための明細JOIN + 祝日判定のJOINが必要
SELECT users.id, SUM(order_items.amount) AS holiday_spend
FROM users
JOIN orders      ON orders.user_id = users.id
JOIN order_items ON order_items.order_id = orders.id           -- 金額は明細にあるため、ここまでJOINが必要
JOIN holidays    ON DATE(orders.created_at) = holidays.date    -- 祝日判定に別マスタとのJOINが要る
GROUP BY users.id;

-- スタースキーマ: 金額はファクト、祝日判定は dim_date のフラグで完結
SELECT fact_order_items.customer_key, SUM(fact_order_items.amount) AS holiday_spend
FROM fact_order_items
JOIN dim_date ON fact_order_items.date_key = dim_date.date_key
WHERE dim_date.is_holiday = true                               -- 「祝日か」を属性として持っておく
GROUP BY fact_order_items.customer_key;

RDB側は、金額を得るための明細JOINに加えて「祝日か」を判定するholidaysテーブルとのJOINまで必要になります。一方スタースキーマでは、金額がファクトにあるため明細をまたぐJOINが要らず、「祝日か」という日付の意味づけもdim_dateの属性として事前に焼き込まれているため、JOINはdim_date1つで済みます。

もう1点、スタースキーマ側ではGROUP BY fact_order_items.customer_keyがそのまま書けます。ファクトに全ディメンションのキーが直接並んでいるため、RDBで必要な「顧客・注文・明細」のJOIN連鎖が丸ごと消える、というのが非正規化の核心です。

「何を集計したいか」という分析意図がそのままSQLに表れるので、MLの特徴量を量産するフェーズでは、この差が開発速度に効いてきます。

「この事実を分析するためには、周りにどんな切り口(軸)の情報があれば良いか」という視点で、中心から外側に向かって構成を考えていく感覚は、Webアプリの設計とは異なる新鮮な視点でした。Webアプリでは「システムをどう正しく動かすか」からデータ構造を考えますが、分析では「何を知りたいか」からデータ構造を決めます。

補足:特にこんがらがったポイント — ディメンションのSK管理

ここまで紹介した2つのポイントは、案件冒頭に丁寧な説明を受けたこともあり、「なるほど、そういうものか」と比較的素直に受け入れられました。

ただ、1つだけ最初は頭の中で整理するのに時間がかかったのが、SCDが絡むディメンションのSK管理です。具体的には、「1つのディメンションを、複数のソーステーブルから組み立てる」ケースで詰まりました。


詰まった構成

実際の案件を抽象化すると、店舗(store)の情報がソース側で2つのテーブルに分かれていました。

  • メイン店舗テーブル: 店舗のコア情報(住所など)
  • 店舗追加情報テーブル: 店舗の付加情報(店舗種類など)

両者ともそれぞれ独立に履歴を保持しており、別々のタイミングで更新されます。これらをDWH層のdim_store1テーブルに統合する必要がありました。

ここで悩ましかったのは、「片方だけ更新があったときにdim_storeのレコードをどう扱うか」といった運用ルール自体を、自分たちで考えて決める必要があったことです。更新タイミングが揃わない複数ソースの統合ならではの判断で、SK発行ロジック(hash(店舗ID + valid_from))と合わせて、チーム内で議論しながら決めていきました。


何がこんがらがったか

頭の中で整理しきれなかったのは、主に次のような点でした。

  • どちらのvalid_fromを基準にSKを採番するか: 両ソースがそれぞれ独立にvalid_from/valid_toを持ち、しかも非同期に更新されます。SKをhash(店舗ID + valid_from)で発行しようとすると、どちらのソースのvalid_fromを使えば一意かつ正しい有効期間を表せるのかが悩ましく、当時はこの一意性の担保に一番頭を悩ませました。
  • 「履歴を持つソースが複数ある」ことの整理: ディメンションの過去の状態を新しい行として残す仕組み自体は素直に理解できたのですが、ソース側がそれぞれ独立に履歴管理されていて、その組み合わせの履歴がdim_storeに集約される、という二段構えになると、頭の中で時系列を描き直すのに少し時間がかかりました。

どう解決したか

最終的にチームで落ち着いたのは、SKの式(hash(店舗ID + valid_from))を変えるのではなく、valid_from の作り方を変えるという方針でした。片方のソースの valid_from をそのまま使うのではなく、両ソースの有効期間(valid_from/valid_to)の境界をすべて集めて1本の時系列に引き直し、その統合後の区間ごとにSKを採番する、という考え方です。

上の図では、メイン店舗テーブルは04-01に、店舗追加情報テーブルは02-15に、それぞれ独立したタイミングで更新されています。両者の境界(01-01 / 02-15 / 04-01)をすべて集めて1本の時系列に引き直すと、dim_storeは3つの区間に分割され、それぞれの開始日を使って一意なSKが採番されます。

こうすると、どちらか一方のソースだけが更新されても、新しい境界が増えて時系列が別の区間に分割されます。その結果、区間の開始日(valid_from)が必ず変わるため、hash(店舗ID + valid_from) も自然と一意になり、重複の心配がなくなります。

さらに、dim_storeにはSKだけでなく店舗IDvalid_from/valid_toを列として残しておくので、各レコードから元ソースの該当行も引き当てられます(SKのハッシュ値を逆算するのではなく、店舗IDとその区間で両ソースを引く形)。統合区間は両ソースの境界をすべて集めて作っているため、1つの区間の中ではどちらのソースも値が変化せず、「この区間 ↔ メイン店舗テーブルのこの行 ↔ 追加情報テーブルのこの行」が一意に対応づく点も安心材料でした。

正直なところ、自分はこの方針を考え出した側ではなく、チームが詰めた解法を見て「なるほど、SKの構成要素を増やすのではなく、有効期間そのものを両ソースから再構築すればよかったのか」と腑に落ちた、というのが実際でした。


学んだこと

  • ディメンショナルモデリングの「ファクトとディメンションに分ける」というコア概念は、説明を聞けば素直に飲み込めるシンプルなものでした。
  • 一方で、SCD+複数ソースが絡んだ瞬間、時系列を含めた頭の使い方が一段増えることを実感しました。実装の細部はチームで議論しながら詰めていきましたが、概念としては「SKの一意性は、構成要素を増やして担保するというより、複数ソースの有効期間を統合して『どの区間か』を確定させることで担保される」と腑に落ち、当初の混乱は整理できました。
  • 今となっては整理がついて違和感なく扱えるようになりましたが、Webアプリ設計の感覚で踏み込むと最初に戸惑いやすい部分だと感じています。

まとめ:MLの生産性は「モデル」の前に「データ構造」で決まる

今回の案件を通して、MLの開発生産性は、アルゴリズムの選定以前に「データがどうモデリングされているか」で大きく左右されると学びました。

ディメンショナルモデリングの手法に沿ってファクト(予測したい対象)を中心にデータが整理されていると、MLの特徴量(モデルに投入するデータ)を作るSQLがシンプルになり、特徴量定義の修正や再集計といった手戻りも減ります。

MLのモデル学習では最終的に、ファクトとディメンションをすべて結合した1枚の平たい特徴量テーブル(ABT: Analytical Base Table) を作るのが一般的です。スタースキーマは、このABTを効率的に組み立てるための「最強の中間形態」と位置付けると、Webエンジニアにとってもイメージが掴みやすくなると思います。

WebエンジニアとしてのDB設計の視野が、OLTPの枠を越えて一段広がる良い経験となりました。もし「データ分析基盤やMLのデータ構造」に関心がある方がいれば、ぜひディメンショナルモデリングの手法に触れてみてください。