dbt Semantic Layer の custom_granularities で会計年度を追加してみた

dbt Semantic Layer の custom_granularities で会計年度を追加してみた

2026.04.03

はじめに

dbt セマンティックレイヤー(MetricFlow)のタイムスパインにおけるcustom_granularitiesオプションを試した際の内容を記事としました。

custom_granularities の概要

MetricFlow のタイムスパインには、標準の粒度(day/week/month/quarter/year)に加えて、独自の時間粒度を追加できるcustom_granularitiesオプションがあります。
これにより、会計年度など、カレンダー年とは異なる期間軸でメトリクスを集計できるようになります。

本機能については以下に記載があります。

https://docs.getdbt.com/docs/build/metricflow-time-spine?version=1.12#custom-calendar

通常のセマンティックモデル内で会計年度などを categorical ディメンションとして定義することも可能ですが、MetricFlow における時間の粒度としては認識されません。
この場合、--group-by orders__fiscal_yearのように集計軸として使うことはできるものの対応するレコードが存在しない期間は、その部分が歯抜けになります。

simple メトリクスにはjoin_to_timespine: trueというオプションがあり、これを指定するとタイムスパインを左側に置いた JOIN が強制され、タイムスパイン期間全体を含めた結果として返すことができます。さらにfill_nulls_with: 0オプションでこの部分のゼロ埋めも可能です。ただし categorical ディメンションで集計している場合はjoin_to_timespine: trueを付けても効果がありません。

その他、custom_granularitiesを使うことでmetric_time__fiscal_yearのような時間軸として参照できるため、複数のセマンティックモデルをまたいで同一の粒度定義を参照できるというメリットもあります。

join_to_timespinefill_nulls_withについては以下もご参照ください。

https://dev.classmethod.jp/articles/dbt-semantic-layer-main-metrics-try/#join_to_timespine-%252F-fill_nulls_with

前提条件

検証環境

以下の環境を使用しています。

  • dbt
    • dbt Cloud
    • dbt CLI:0.40.15(2026-03-02 ビルド)
  • DWH:Snowflake

この記事では dbt Fusion エンジンおよび dbt platform Latest リリーストラックで利用可能な新しい YAML 記法(モデルの columns 配下にセマンティックモデル定義を記述する形式)を使用しています。

https://docs.getdbt.com/docs/build/latest-metrics-spec?version=1.12

事前準備

検証には公式のサンプルデータ(jaffle_shop)を使用しています。
また、プロジェクトでは MetricFlow 用のタイムスパインとして time_spine_dailyモデルが設定済みの状態です。

time_spine_daily.sql
{{
    config(
        materialized = 'table',
    )
}}

with

base_dates as (
    {{
        dbt.date_spine(
            'day',
            "DATE('2000-01-01')",
            "DATE('2030-01-01')"
        )
    }}
),

final as (
    select
        cast(date_day as date) as date_day
    from base_dates
)

select *
from final
where date_day >= DATE('2017-01-01')
  and date_day < dateadd(day, 30, current_date())
time_spine.yml
version: 2

models:
  - name: time_spine_daily
    description: MetricFlow 用タイムスパイン
    time_spine:
      standard_granularity_column: date_day
    columns:
      - name: date_day
        description: 日次の基準日カラム
        granularity: day

この状態では custom_granularitiesは設定されておらず、標準粒度のみ使える状態です。

また、集計対象のセマンティックモデル(stg_orders)で関連する部分は以下の通りです。

stg_orders.sql
select
    id as order_id,
    user_id as customer_id,
    order_date,
    status

from {{ source('jaffle_shop', 'orders') }}
stg_orders.sql
version: 2

models:
  - name: stg_orders
    semantic_model:
      enabled: true
    agg_time_dimension: order_date  # metric_time として自動公開される

    columns:
      - name: order_id
        entity:
          type: primary
          name: orders  

      - name: customer_id
        entity:
          type: foreign
          name: customer

      - name: order_date
        granularity: day  # 最小粒度。week/month/year への集計も可能
        dimension:
          type: time

      - name: status
        dimension:
          type: categorical

    metrics:
      - name: order_count
        type: simple
        label: 注文数
        agg: count
        expr: 1

      - name: daily_order_count
        type: simple
        label: 日次注文数
        description: 注文がない日も 0 として補完する
        agg: count
        expr: 1
        join_to_timespine: true
        fill_nulls_with: 0

custom_granularities の追加

ここでは日本の一般的な会計年度を追加してみます。
4月始まり・3月終わりとなる会計年度のロジックは以下のとおりです。4〜12月はその年、1〜3月は前年を会計年度として扱います。

期間 会計年度
2017/4/1 〜 2018/3/31 FY2017
2018/4/1 〜 2019/3/31 FY2018

time_spine_daily.sql にカラムを追加

fiscal_year(会計年度)とfiscal_quarter(会計四半期)のカラムをタイムスパインの元となるモデルに追加します。

final as (
    select
        cast(date_day as date) as date_day,
        -- 4月始まりの日本会計年度
        case
            when extract(month from cast(date_day as date)) >= 4
                then extract(year from cast(date_day as date))
            else extract(year from cast(date_day as date)) - 1
        end as fiscal_year,
        -- 4月始まりの日本会計四半期ラベル(文字列型): "FY2017 Q4" 形式
        case
            when extract(month from cast(date_day as date)) between 4  and 6
                then 'FY' || cast(extract(year from cast(date_day as date))      as varchar) || ' Q1'
            when extract(month from cast(date_day as date)) between 7  and 9
                then 'FY' || cast(extract(year from cast(date_day as date))      as varchar) || ' Q2'
            when extract(month from cast(date_day as date)) between 10 and 12
                then 'FY' || cast(extract(year from cast(date_day as date))      as varchar) || ' Q3'
            else
                'FY' || cast(extract(year from cast(date_day as date)) - 1 as varchar) || ' Q4'
        end as fiscal_quarter
    from base_dates
)

fiscal_yearは INT型、fiscal_quarterは文字列型となります。MetricFlow では standard_granularity_column(ここではタイムスパインモデルのdate_dayカラム)を JOIN 時のキー項目として使うため、カスタム粒度カラムの型は INT型や文字列型でも問題ありません。

time_spine.yml に custom_granularities オプションを追加

タイムスパインモデルのプロパティ定義の YAML ファイルでcustom_granularitiesを追加し、先の手順で追加したカラムを登録します。
ここで登録したカラムは、columns:にも追加します。

version: 2

models:
  - name: time_spine_daily
    description: MetricFlow 用タイムスパイン
    time_spine:
      standard_granularity_column: date_day
      custom_granularities:
        - name: fiscal_year                   # クエリで使う粒度名: metric_time__fiscal_year
          column_name: fiscal_year            # time_spine_daily.sql で計算したカラム
        - name: fiscal_quarter                # クエリで使う粒度名: metric_time__fiscal_quarter
          column_name: fiscal_quarter         # time_spine_daily.sql で計算したカラム
    columns:
      - name: date_day
        description: 日次の基準日カラム
        granularity: day
      - name: fiscal_year
        description: "4月始まりの日本会計年度(例: 2018年4月〜2019年3月 → 2018)"
      - name: fiscal_quarter
        description: "4月始まりの日本会計四半期(Q1: 4-6月、Q2: 7-9月、Q3: 10-12月、Q4: 1-3月)"

動作確認

ビルドと MetricFlow の再読み込みを行い、クエリで結果を確認します。

dbt build
dbt parse

会計年度別

追加した会計年度別に集計してみます。custom_granularities で定義した粒度はmetric_time__<カスタム粒度名>の形式で参照します。

$ dbt sl query --metrics order_count --group-by metric_time__fiscal_year
+--------------------------+-------------+
| METRIC_TIME__FISCAL_YEAR | ORDER_COUNT |
+--------------------------+-------------+
|                     2017 |          91 |
|                     2018 |           8 |
+--------------------------+-------------+

会計年度(INT型)でグループ化された結果が返ります。
このサンプルデータは以下のように2018年1〜4月に集中しているため、FY2017(2017/4〜2018/3)に大半が含まれる結果となりました。

$ dbt sl query --metrics order_count --group-by orders__order_date__month --order-by orders__order_date__month

+---------------------------+-------------+
| ORDERS__ORDER_DATE__MONTH | ORDER_COUNT |
+---------------------------+-------------+
| 2018-01-01T00:00:00.000   |          29 |
| 2018-02-01T00:00:00.000   |          27 |
| 2018-03-01T00:00:00.000   |          35 |
| 2018-04-01T00:00:00.000   |           8 |
+---------------------------+-------------+

会計四半期別

会計四半期別に集計してみます。

$ dbt sl query --metrics order_count --group-by metric_time__fiscal_quarter --order-by metric_time__fiscal_quarter
+-----------------------------+-------------+
| METRIC_TIME__FISCAL_QUARTER | ORDER_COUNT |
+-----------------------------+-------------+
| FY2017 Q4                   |          91 |
| FY2018 Q1                   |           8 |
+-----------------------------+-------------+

FY{年度} Q{四半期番号}形式の文字列型で返ります。会計年度と四半期番号が1つのラベルに含まれるため、単独の集計軸として使いやすい形式と思います。

join_to_timespine によるゼロ埋め

先の order_count の結果はデータが存在する2四半期のみでした。simple メトリクスにjoin_to_timespine: truefill_nulls_with: 0を設定すると、タイムスパインを左側に置いた JOIN が強制され、注文がない期間も 0 として返ります。

ここではあらかじめ daily_order_count というメトリクスがこの設定で定義されているので、このメトリクスを参照してみます。

- name: daily_order_count
  type: simple
  label: 日次注文数
  description: 注文がない日も 0 として補完する
  agg: count
  expr: 1
  join_to_timespine: true
  fill_nulls_with: 0

クエリと出力は以下のようになります。
注文がない四半期もタイムスパイン上で期間があれば 0 で埋まった状態で返ります。

$ dbt sl query --metrics daily_order_count --group-by metric_time__fiscal_quarter --order-by metric_time__fiscal_quarter --limit 10
+-----------------------------+-------------------+
| METRIC_TIME__FISCAL_QUARTER | DAILY_ORDER_COUNT |
+-----------------------------+-------------------+
| FY2016 Q4                   |                 0 |
| FY2017 Q1                   |                 0 |
| FY2017 Q2                   |                 0 |
| FY2017 Q3                   |                 0 |
| FY2017 Q4                   |                91 |
| FY2018 Q1                   |                 8 |
| FY2018 Q2                   |                 0 |
| FY2018 Q3                   |                 0 |
| FY2018 Q4                   |                 0 |
| FY2019 Q1                   |                 0 |
+-----------------------------+-------------------+

なお冒頭で述べたように categorical ディメンションとして定義した場合は、join_to_timespine: true を付けても JOIN 自体が発生しないためゼロ埋めは機能しません。
今回は custom_granularities として定義しているため、このような操作が可能です。

また、別のセマンティックモデルのメトリクスであっても custom_granularities はタイムスパイン側の定義なので、time dimension を持つ他のセマンティックモデルから参照できます。

さいごに

MetricFlow の custom_granularitiesオプションを使い、日本の一般的な会計年度・会計四半期をタイムスパインに追加する手順を確認してみました。
こちらの内容がどなたかの参考になれば幸いです。

この記事をシェアする

関連記事