dbt Semantic Layer の custom_granularities で会計年度を追加してみた
はじめに
dbt セマンティックレイヤー(MetricFlow)のタイムスパインにおけるcustom_granularitiesオプションを試した際の内容を記事としました。
custom_granularities の概要
MetricFlow のタイムスパインには、標準の粒度(day/week/month/quarter/year)に加えて、独自の時間粒度を追加できるcustom_granularitiesオプションがあります。
これにより、会計年度など、カレンダー年とは異なる期間軸でメトリクスを集計できるようになります。
本機能については以下に記載があります。
通常のセマンティックモデル内で会計年度などを 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_timespineやfill_nulls_withについては以下もご参照ください。
前提条件
検証環境
以下の環境を使用しています。
- dbt
- dbt Cloud
- dbt CLI:0.40.15(2026-03-02 ビルド)
- DWH:Snowflake
この記事では dbt Fusion エンジンおよび dbt platform Latest リリーストラックで利用可能な新しい YAML 記法(モデルの columns 配下にセマンティックモデル定義を記述する形式)を使用しています。
事前準備
検証には公式のサンプルデータ(jaffle_shop)を使用しています。
また、プロジェクトでは MetricFlow 用のタイムスパインとして time_spine_dailyモデルが設定済みの状態です。
{{
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())
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)で関連する部分は以下の通りです。
select
id as order_id,
user_id as customer_id,
order_date,
status
from {{ source('jaffle_shop', 'orders') }}
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: trueとfill_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オプションを使い、日本の一般的な会計年度・会計四半期をタイムスパインに追加する手順を確認してみました。
こちらの内容がどなたかの参考になれば幸いです。






