🍔

経営管理でディメンショナルモデリングに入門する

に公開

はじめに

5月に株式会社ログラスにソフトウェアエンジニアとして入社した安福です。
本記事では、ログラスでも取り入れているディメンショナルモデリングの基礎について、例を交えて解説します。

業務システムと分析システムの違い

ディメンショナルモデリングを理解する前に、業務システムと分析システムの違いを整理しておきます。

業務システムは、ECサイト・受発注システム・顧客管理システムなどに代表される、トランザクションデータを主として扱うシステムです。
データの登録・更新・削除機能を持ち、リクエストに対してデータの不整合を起こさないように更新することが求められるため、多数の細かいテーブルに分割し正規化するモデリングが行われます。

分析システムは、業務システムによって蓄積されたデータを集計・分析するシステムです。
BIツールに代表されるように、膨大な量のデータを効率的に集計し、意思決定を支援することが目的です。
クエリの単純性や集計の高速性を重視するため、非正規化されたテーブルで構成されます。
ディメンショナルモデリングは分析システムの基本となるデータモデリングです。

ディメンショナルモデリングの基本概念

ファクトとディメンション

ディメンショナルモデリングは、ビジネスプロセスを分析するためのデータモデリングです。
ビジネスプロセスの数値データを記録するファクトと、そのイベントに関する文脈情報を提供するディメンションで構成されます。
例えば、「2025年1月の新宿店でのチーズバーガーの売上1,000,000円」という情報は、以下のようにファクトとディメンションに分けられます。

  • ファクト: 売上金額 1,000,000円
  • ディメンション:
    • 日付: 2025年1月
    • 店舗: 新宿店
    • 商品: チーズバーガー

ディメンションはファクトに文脈を与えて意味を持たせる役割を果たすため、「いつ」「どこで」「誰が」「何を」といった情報で構成されます。
売上金額1,000,000円というファクトだけでは、何の売上か、いつの売上かがわからないため意味のない情報ですが、ディメンションが加わることで具体的な意味を持つようになります。

スタースキーマ

ディメンショナルモデリングの最も基本的な実装パターンがスタースキーマです。
中心にファクトを配置し、その周囲にディメンションを配置する形からそう呼ばれています。

業務システムにおいてはデータの整合性を重視し冗長性を減らすために正規化を行いますが、スタースキーマはクエリの高速化や簡略化のために意図的に非正規化された構造を持ちます。
例えば、店舗を表す店舗ディメンションに、店舗の種別(駅前、モール、路面など)を表す属性を持たせたいケースについて考えてみます。
業務システムでは、店舗種別を表すテーブルを別に作成し、店舗テーブルとリレーションを張ることで正規化しますが、スタースキーマでは店舗ディメンションに店舗種別を直接持たせます。

サロゲートキー

ディメンショナルモデリングでは、ナチュラルキー(店舗コード、商品コードなど)ではなくサロゲートキーを主キーとして使用することが一般的です。

CREATE TABLE dim_store (
    store_key SERIAL PRIMARY KEY,         -- サロゲートキー
    store_id VARCHAR(10) NOT NULL UNIQUE, -- ナチュラルキー
    -- 省略
);

サロゲートキーを用いることが多い理由はいくつかありますが、履歴管理に対応しやすいことが大きな理由の一つです。
本記事では詳細までは触れませんが、履歴管理はSCD(Slowly Changing Dimension)と呼ばれる手法を用います。
例えば、店舗の名称が変更された場合でも変更前のデータを保持しつつ新しいデータを追加することができるようにするために、以下のようなテーブル設計を行います。(SCD Type2の例)

CREATE TABLE dim_store (
    store_key SERIAL PRIMARY KEY,
    store_id VARCHAR(10) NOT NULL UNIQUE,
    store_name VARCHAR(50) NOT NULL,
    area_name VARCHAR(20) NOT NULL,
    store_type VARCHAR(20) NOT NULL,
    effective_date DATE NOT NULL, -- 有効開始日
    expiry_date DATE,              -- 有効終了日(NULLは現在有効)
    is_current BOOLEAN NOT NULL DEFAULT TRUE -- 現在有効かどうか
);

ディメンショナルモデリングの実践

基本概念を理解したところで、実際にハンバーガーショップの経営管理を題材にモデリングと分析を行なってみます。

要件の整理

ディメンションとファクトを考える前に、まずは分析の目的を整理します。
ユーザーにヒアリングをした結果、以下のような分析がしたいという要件が出てきたとします。

  1. エリアマネージャー
    • 担当エリア(例:関東)の店舗種別ごとの月次売上と予算達成率を把握したい
  2. 店舗店長
    • 自店舗の平日・週末ごとの売上傾向を知りたい
  3. 商品企画担当
    • 商品ごとの月次売上を把握し、新商品の販売戦略を立てたい
  4. 経営層
    • 全社の売上・原価・販管費の月次推移と利益率を月次で把握したい

分析の要件を表で整理すると、必要な分析軸(ディメンション)を洗い出せます。

ユーザー 店舗 日付 商品 費用分類 予実
エリアマネージャー エリア・店舗種別 月次 - 売上 予算・実績
店舗店長 自店舗 平日・週末 - 売上 実績
商品企画担当 - - 新商品・既存商品 売上 実績
経営層 全店舗 月次 - 売上・原価・販管費 実績

粒度の検討

細かい粒度の情報は荒くできますが、荒い粒度の情報を細かくすることはできません。
例えば、日次データがあれば月次集計は可能ですが、月次データからは日次は復元できません。
ただし、粒度が細かいほどデータ量が増え、クエリのパフォーマンスに影響が出る可能性があります。
また、一定以上の粒度でないと扱いづらい情報もあり、例えば賃料や人件費などの費用は月単位で集計することが一般的ですし、商品の送料などは商品単位ではなく注文単位で決まることが多いです。

分析の要件が将来的に変わる可能性も考慮すると、原則としては粒度はできるだけ細かく設定するべきですが、その他の要件も考慮して実際の粒度を決定する必要があります。
今回は、経営管理という観点から、以下の粒度で設計することにします。

  • 日付: 日次
  • 店舗: 店舗単位
  • 商品: 商品単位

ディメンションの設計

要件と粒度の検討内容をもとに、ディメンションを設計していきます。

属性の選定

ディメンションには、分析に必要な属性を含めます。
また、多角的な分析をすばやく行えるようにするために、あえて冗長な属性を持たせることもあります。

日付ディメンションについて考えてみると、月次・日次での分析が必要であり、日付が平日か週末かを区別する必要があります。
業務システムの場合、月は日付から導けますし、平日か週末かも計算できるため、日付を単一のカラムとして持つことが多いですが、ディメンショナルモデリングでは利用頻度の高い属性は計算可能な値もカラムとして持たせることが一般的です。

CREATE TABLE dim_date (
    date_key SERIAL PRIMARY KEY,                    -- サロゲートキー(主キー)
    full_date DATE NOT NULL UNIQUE,                 -- 実際の日付(2024-01-15など)
    year INTEGER NOT NULL,                          -- 年(2024など)
    month INTEGER NOT NULL,                         -- 月(1-12)
    day INTEGER NOT NULL,                           -- 日(1-31)
    year_month VARCHAR(7) NOT NULL,                 -- 年月('2024-01'など)
    day_name VARCHAR(10) NOT NULL,                  -- 曜日名('月曜日'など)
    is_weekend BOOLEAN NOT NULL                     -- 週末フラグ(土日=TRUE)
);

階層構造

ディメンションは階層を持つことが多いです。
店舗を例に考えてみると、今回の要件では、エリアと店舗の2階層が必要です。

  • エリア:関東
    • 店舗:新宿店
    • 店舗:渋谷店
  • エリア:関西
    • 店舗:梅田店
    • 店舗:心斎橋店

階層をモデリングする方法としては、カラムとして表現する方法と、別テーブルとして表現する方法があります。
業務システムの場合は正規化の観点から別テーブルにすることが多いですが、ディメンショナルモデリングではカラムとして表現して非正規化することが多いです。

今回は、店舗ディメンションにエリア名をカラムとして持たせる形で設計してみます。

CREATE TABLE dim_store (
    store_key SERIAL PRIMARY KEY,                   -- サロゲートキー(主キー)
    store_id VARCHAR(10) NOT NULL UNIQUE,          -- 店舗コード('S001'など)
    store_name VARCHAR(50) NOT NULL,               -- 店舗名('新宿店'など)
    area_name VARCHAR(20) NOT NULL,                -- エリア名('東京'、'大阪'、'名古屋')
    store_type VARCHAR(20) NOT NULL                -- 店舗種別('駅前'、'モール'、'路面'など)
);

ファクトの設計

ファクトの設計では、数値の加算性に注意が必要です。

売上金額や数量は加算可能です。しかし単価や利益率のような比率は単純に加算できません。
例えば、売上金額は加算できますが、平均単価は単純に加算してしまうと意味を保つことができません。

  • 売上金額:35,000円 + 22,500円 = 57,500円(加算可能)
  • 平均単価:(35,000円 + 22,500円) ÷ 150個 = 383.33円(単純加算は不可)

できる限り単純な加算が可能な数値をファクトとして持つようにするために、加算ができない数値は加算が可能な数値に変換できないか検討します。
例えば、平均単価をファクトとして持つのではなく、売上金額と販売数量を持つようにし、平均単価はクエリで計算するようにします。

今回は、売上と費用の金額や販売個数がファクトとなるため、単純な加算が可能です。

実装

ここまでの内容をもとに、ハンバーガーショップの経営管理を実装してみます。
データベースはPostgreSQLを使用します。

DDL実装

日付ディメンション

CREATE TABLE dim_date (
    date_key SERIAL PRIMARY KEY,                    -- サロゲートキー(主キー)
    full_date DATE NOT NULL UNIQUE,                 -- 実際の日付(2024-01-15など)
    year INTEGER NOT NULL,                          -- 年(2024など)
    month INTEGER NOT NULL,                         -- 月(1-12)
    day INTEGER NOT NULL,                           -- 日(1-31)
    year_month VARCHAR(7) NOT NULL,                 -- 年月('2024-01'など)
    day_name VARCHAR(10) NOT NULL,                  -- 曜日名('月曜日'など)
    is_weekend BOOLEAN NOT NULL                     -- 週末フラグ(土日=TRUE)
);

year_monthは月次集計で頻繁に使用されます。毎回yearmonthから計算するよりもクエリがシンプルになりパフォーマンスが向上します。

店舗ディメンション

CREATE TABLE dim_store (
    store_key SERIAL PRIMARY KEY,                   -- サロゲートキー(主キー)
    store_id VARCHAR(10) NOT NULL UNIQUE,          -- 店舗コード('S001'など)
    store_name VARCHAR(50) NOT NULL,               -- 店舗名('新宿店'など)
    area_name VARCHAR(20) NOT NULL,                -- エリア名('東京'、'大阪'、'名古屋')
    store_type VARCHAR(20) NOT NULL                -- 店舗種別('駅前'、'モール'、'路面'など)
);

store_typeは店舗の種別を表します。正規化のために別テーブルにすることもできますが、クエリの効率化のために非正規化しています。

商品ディメンション

CREATE TABLE dim_product (
    product_key SERIAL PRIMARY KEY,                 -- サロゲートキー(主キー)
    product_id VARCHAR(10) NOT NULL UNIQUE,        -- 商品コード('P001'など)
    product_name VARCHAR(50) NOT NULL,             -- 商品名('チーズバーガー'など)
    category_name VARCHAR(20) NOT NULL,            -- カテゴリ名('バーガー'、'サイド'など)
    unit_price DECIMAL(8,2) NOT NULL               -- 単価(350.00など)
);

費用分類ディメンション

CREATE TABLE dim_cost_category (
    cost_category_key SERIAL PRIMARY KEY,           -- サロゲートキー(主キー)
    cost_code VARCHAR(20) NOT NULL UNIQUE,         -- 費用コード('SALES'、'COGS_MATERIAL'など)
    cost_name VARCHAR(30) NOT NULL,                -- 費用名('売上'、'材料費'など)
    cost_type VARCHAR(20) NOT NULL                 -- 費用タイプ('売上'、'売上原価'、'販管費')
);

コスト構造の内訳を管理するためのディメンションです。
cost_typeが大分類を表し、cost_codeが小分類を表す形で、階層構造を持たせています。

予実ディメンション

CREATE TABLE dim_budget_actual (
    budget_actual_key SERIAL PRIMARY KEY,           -- サロゲートキー(主キー)
    type_code VARCHAR(10) NOT NULL UNIQUE,         -- 区分コード('BUDGET'、'ACTUAL')
    type_name VARCHAR(10) NOT NULL                 -- 区分名('予算'、'実績')
);

予算と実績の比較を行うために、ファクトの値が予算か実績かを区別するためのディメンションです。

売上ファクト

CREATE TABLE fact_sales (
    -- ディメンションの外部キー
    date_key INTEGER NOT NULL,                      -- 日付
    store_key INTEGER NOT NULL,                     -- 店舗
    product_key INTEGER NOT NULL,                   -- 商品
    cost_category_key INTEGER NOT NULL,             -- 費用分類
    budget_actual_key INTEGER NOT NULL,             -- 予実

    -- 数値データ
    amount DECIMAL(12,2) NOT NULL DEFAULT 0,       -- 金額(売上金額または費用金額)
    quantity INTEGER NOT NULL DEFAULT 0,           -- 数量(売上数量、費用の場合は0)

    -- 複合主キー
    PRIMARY KEY (date_key, store_key, product_key, cost_category_key, budget_actual_key)
);

各ディメンションを外部キーで参照するファクトテーブルです。
売上金額と数量は同時に使用することが多く、更新も同時に行われるため、同じテーブルに格納しています。

分析クエリの実践

要件を満たす分析クエリを実装していきます。
ファクトテーブルを基底テーブルとして分析したい軸のディメンションテーブルを結合し、条件で絞り込んだ上で分析したい単位に応じて集約して集計を行います。
目的によっては集計部分が若干長くなることもありますが、基本的にはシンプルなSQLで実装できます。

実際には必ずしも集計をすべてSQLで行う必要はありませんが、今回はSQLで完結する形で実装します。
また、今回は実践的な内容ではなく基礎の説明を目的としているため、素朴なクエリにするために共通テーブル式やウィンドウ関数を使用しない例を示します。

エリアマネージャー向け店舗種別ごとの予実比較分析

エリアマネージャーが担当エリア内の店舗種別ごとの月次売上と予算達成率を把握するためのクエリです。

SELECT
    s.store_type as 店舗種別,
    d.year_month as 年月,
    TO_CHAR(SUM(CASE WHEN ba.type_code = 'BUDGET' THEN f.amount ELSE 0 END), 'FM999,999,999') as 予算,
    TO_CHAR(SUM(CASE WHEN ba.type_code = 'ACTUAL' THEN f.amount ELSE 0 END), 'FM999,999,999') as 実績,
    ROUND(SUM(CASE WHEN ba.type_code = 'ACTUAL' THEN f.amount ELSE 0 END) * 100.0 /
          NULLIF(SUM(CASE WHEN ba.type_code = 'BUDGET' THEN f.amount ELSE 0 END), 0), 1) as 達成率_パーセント
FROM fact_sales f
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_store s ON f.store_key = s.store_key
JOIN dim_cost_category c ON f.cost_category_key = c.cost_category_key
JOIN dim_budget_actual ba ON f.budget_actual_key = ba.budget_actual_key
WHERE c.cost_code = 'SALES'
AND s.area_name = '東京'  -- 特定エリアに絞り込み
GROUP BY s.store_type, d.year_month
ORDER BY s.store_type, d.year_month;

クエリ結果の例

店舗種別 年月 予算 実績 達成率_パーセント
モール 2024-01 3,235,510 3,140,963 97.1
モール 2024-02 3,650,176 3,813,119 104.5
モール 2024-03 4,728,628 4,751,664 100.5
モール 2024-04 3,934,562 3,747,530 95.2
モール 2024-05 5,183,786 5,120,222 98.8
モール 2024-06 3,258,221 2,980,758 91.5
モール 2024-07 5,401,323 5,298,595 98.1
モール 2024-08 4,179,667 3,796,146 90.8
モール 2024-09 4,258,264 4,046,827 95.0
モール 2024-10 5,760,422 5,520,940 95.8
モール 2024-11 4,113,616 4,309,076 104.8
モール 2024-12 3,203,675 3,093,549 96.6
駅前 2024-01 3,890,870 3,969,705 102.0
駅前 2024-02 4,182,597 4,165,635 99.6
駅前 2024-03 4,314,524 3,765,205 87.3
駅前 2024-04 5,133,523 5,058,642 98.5
駅前 2024-05 6,341,317 5,695,210 89.8
駅前 2024-06 5,074,517 4,873,510 96.0
駅前 2024-07 3,610,553 3,601,729 99.8
駅前 2024-08 3,476,472 3,069,160 88.3
駅前 2024-09 5,891,121 5,387,221 91.4
駅前 2024-10 4,315,724 4,041,317 93.6
駅前 2024-11 4,697,599 4,554,009 96.9
駅前 2024-12 5,084,254 5,125,325 100.8

店舗店長向け平日・週末ごとの売上傾向分析

店舗店長が自店舗の平日・週末ごとの売上傾向を把握するためのクエリです。

SELECT
    s.store_name as 店舗名,
    CASE WHEN d.is_weekend THEN '週末' ELSE '平日' END as 区分,
    COUNT(DISTINCT d.date_key) as 日数,
    TO_CHAR(SUM(f.amount), 'FM999,999,999') as 売上合計,
    TO_CHAR(SUM(f.amount) / COUNT(DISTINCT d.date_key), 'FM999,999,999') as 日別平均売上
FROM fact_sales f
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_store s ON f.store_key = s.store_key
JOIN dim_cost_category c ON f.cost_category_key = c.cost_category_key
JOIN dim_budget_actual ba ON f.budget_actual_key = ba.budget_actual_key
WHERE c.cost_code = 'SALES'
AND ba.type_code = 'ACTUAL'
AND s.store_name = '新宿店'  -- 特定店舗に絞り込み
GROUP BY s.store_name, d.is_weekend
ORDER BY d.is_weekend;

クエリ結果の例

店舗名 区分 日数 売上合計 日別平均売上
新宿店 平日 9 37,920,611 4,213,401
新宿店 週末 3 15,386,057 5,128,686

商品企画担当向け商品別月次売上分析

商品企画担当者が商品ごとの売上を把握するためのクエリです。
OVER句とPARTITION BYを使用して、各月の売上合計に対する構成比を計算しています。

SELECT
    d.year_month as 年月,
    p.product_name as 商品名,
    p.category_name as カテゴリ,
    SUM(f.amount) as 売上金額,
    ROUND(SUM(f.amount) * 100.0 / SUM(SUM(f.amount)) OVER (PARTITION BY d.year_month), 2) as 構成比
FROM fact_sales f
JOIN dim_product p ON f.product_key = p.product_key
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_cost_category c ON f.cost_category_key = c.cost_category_key
JOIN dim_budget_actual ba ON f.budget_actual_key = ba.budget_actual_key
WHERE c.cost_code = 'SALES' AND ba.type_code = 'ACTUAL'
GROUP BY d.year_month, p.product_name, p.category_name
ORDER BY d.year_month DESC, SUM(f.amount) DESC;

クエリ結果の例

年月 商品名 カテゴリ 売上金額 構成比
2024-12 ビッグバーガー バーガー 4,863,615 21.49
2024-12 チーズバーガー バーガー 4,408,394 19.48
2024-12 フライドポテトM サイド 2,882,156 12.74
2024-12 チキンナゲット5個 サイド 2,873,398 12.70
2024-12 コーラM ドリンク 2,288,073 10.11
2024-12 アイスコーヒーM ドリンク 2,185,951 9.66
2024-12 バニラアイス デザート 1,643,017 7.26
2024-12 アップルパイ デザート 1,482,736 6.55
2024-11 ビッグバーガー バーガー 5,038,113 20.18
2024-11 チーズバーガー バーガー 4,876,211 19.53
2024-11 チキンナゲット5個 サイド 3,063,168 12.27
2024-11 フライドポテトM サイド 2,921,274 11.70
2024-11 コーラM ドリンク 2,668,054 10.69
2024-11 アイスコーヒーM ドリンク 2,595,027 10.39
2024-11 アップルパイ デザート 1,913,223 7.66
2024-11 バニラアイス デザート 1,894,664 7.59

経営層向け全社売上・原価・販管費・利益率の月次推移分析

経営層が全社の売上・原価・販管費の月次推移と利益率を把握するためのクエリです。
コスト分類による分岐やフォーマットがあるため少し長くなっていますが、内容は複雑ではありません。

SELECT
    d.year_month as 年月,
    TO_CHAR(SUM(CASE WHEN c.cost_code = 'SALES' THEN f.amount ELSE 0 END), 'FM999,999,999') as 売上高,
    TO_CHAR(SUM(CASE WHEN c.cost_type = '売上原価' THEN f.amount ELSE 0 END), 'FM999,999,999') as 売上原価,
    TO_CHAR(SUM(CASE WHEN c.cost_type = '販管費' THEN f.amount ELSE 0 END), 'FM999,999,999') as 販管費,
    TO_CHAR(
        SUM(CASE WHEN c.cost_code = 'SALES' THEN f.amount ELSE 0 END) -
        SUM(CASE WHEN c.cost_type = '売上原価' THEN f.amount ELSE 0 END) -
        SUM(CASE WHEN c.cost_type = '販管費' THEN f.amount ELSE 0 END), 'FM999,999,999'
    ) as 営業利益,
    ROUND((SUM(CASE WHEN c.cost_code = 'SALES' THEN f.amount ELSE 0 END) -
           SUM(CASE WHEN c.cost_type = '売上原価' THEN f.amount ELSE 0 END) -
           SUM(CASE WHEN c.cost_type = '販管費' THEN f.amount ELSE 0 END)) * 100.0 /
          NULLIF(SUM(CASE WHEN c.cost_code = 'SALES' THEN f.amount ELSE 0 END), 0), 1) as 営業利益率
FROM fact_sales f
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_cost_category c ON f.cost_category_key = c.cost_category_key
JOIN dim_budget_actual ba ON f.budget_actual_key = ba.budget_actual_key
WHERE ba.type_code = 'ACTUAL'
GROUP BY d.year_month
ORDER BY d.year_month;

クエリ結果の例

年月 売上高 売上原価 販管費 営業利益 営業利益率
2024-01 19,085,112 6,572,240 10,397,097 2,115,774 11.1
2024-02 23,353,873 7,559,427 12,568,265 3,226,181 13.8
2024-03 19,972,217 7,044,604 10,846,505 2,081,108 10.4
2024-04 24,238,983 7,860,850 13,008,563 3,369,570 13.9
2024-05 23,782,058 8,171,964 12,449,118 3,160,976 13.3
2024-06 23,581,932 7,750,277 12,616,225 3,215,431 13.6
2024-07 22,785,248 7,771,629 12,014,975 2,998,644 13.2
2024-08 18,611,425 6,360,020 10,595,090 1,656,315 8.9
2024-09 24,148,416 8,034,694 12,458,184 3,655,538 15.1
2024-10 25,345,131 8,050,512 13,361,264 3,933,355 15.5
2024-11 24,969,735 8,359,229 13,233,450 3,377,056 13.5
2024-12 22,627,340 7,583,712 11,835,790 3,207,838 14.2

まとめと次のステップ

本記事では、ハンバーガーチェーンの経営管理を題材に、ディメンショナルモデリングの基礎とその簡単な実例を紹介し、要件に応じたさまざまな分析ができることを示しました。

今回の内容は基礎に留めましたが、実際の業務では複雑な要件やデータ量に対応するための工夫が必要になるため、SCDによる履歴管理、集約テーブルを用いたパフォーマンス向上、コンフォームドディメンションの活用など、より高度な技術が求められます。

本記事がこれからデータ分析を始める方々の参考になれば幸いです。

参考文献

株式会社ログラス テックブログ

Discussion