dbt Semantic LayerとSteepの連携ガイド
dbt Advent Calendar 2025 の 5 日目の記事です。
dbt Semantic Layer と Steep の組み合わせは、ビジネスメトリクスの一元管理とビジネスユーザーへの直感的なデータアクセスを両立できる強力な DW/BI 環境です。利用し始めてから 1 年ちょっと経過したので、その間に学んだ実践的なノウハウをまとめてみました。
この記事がきっかけで、dbt Semantic Layer と Steep の組み合わせを試してみたい方が増えると嬉しいです。
はじめに
dbt Semantic Layer とは
dbt Semantic Layer は、セマンティックレイヤーの実装で、MetricFlow を中核とする複数のコンポーネント(Semantic Models、Metrics)で構成されています。
多くの BI ツールは、それぞれが独自のセマンティックレイヤーを持っており、メトリクス定義がツールごとに分散していました。dbt Semantic Layer は BI ツールより上流に位置するため、複数のアプリケーション(Tableau、Power BI、Steep、Google Sheets など)から同じ定義のメトリクスを参照できるようになります。
また、Multi-hop joins の制限(※後述しています)により、ファントラップやキャズムトラップを構造的に防止し、安全なメトリクスの集計を保証しています。
Steep とは
Steep は、メトリクスファーストというアプローチで設計された BI ツールです。
従来の BI ツールは、ビジュアライゼーションごとにクエリを書く必要があり、メトリクス定義が分散・重複していました。Steep は、この課題を「一度メトリクスを定義すれば、複数のビジュアライゼーションに適用できる」というアプローチで解決します。
Steep の最大の特徴は、無駄な機能が削ぎ落とされ、圧倒的に洗練されている UI/UX にあります。チャートの種類も、棒グラフ、折れ線グラフ、ランクチャート、ピボットテーブルなど、基本的なものに絞られています。
アナリストからするとデータの表現方法が乏しく感じるかもしれませんが、定点観測用のレポートでは基本的なチャートで十分です。Steep は、高度な分析を行うアナリスト向けというより、データリテラシーがそこまで高くないビジネスユーザーが、日常的にメトリクスを探索・可視化するために設計された BI ツールだと言えます。
dbt Semantic Layer と Steep の組み合わせることの何が嬉しいのか
まだ、道半ばですが、dbt Semantic Layer と Steep の組み合わせを 1 年ほど運用してきて、以下のようなメリットを実感しています。
- 全社員のデータ利活用のレベルがベースアップされた
- 特定のパワーユーザーに対する属人化が解消され、持続可能なデータ利活用環境が構築されつつある
- データスキルの習得や使用時にかかる時間が大幅に削減された(300 時間 → 3 時間程度)
この組み合わせにより、組織のデータ利活用における高度な汎用性を実現し、誰もが『簡単に』『素早く』『安全に』データを利用でき、新しい価値創造を支援することができると考えています。
導入前
- データ構造: 3NF
- BI ツール: Looker Studio
- メトリクス定義: レポートを作成するパワーユーザー
導入後
- データ構造: スタースキーマ
- BI ツール: Steep
- メトリクス定義: データガバナンスチーム(データチームとビジネスチームが共同で行う)
本記事で使用するデータセット
dbt の利用者がイメージしやすいように、dbt Labs 社が提供している架空の E コマース注文データ「Jaffle Shop」を使用します。
Jaffle Shop は、ジャッフル(オーストラリア発祥のホットサンドの一種)とドリンクを展開する架空のチェーン店のデータセットです。顧客の注文情報、商品マスタ、店舗情報、原価情報などが含まれており、典型的な注文プロセスのユースケースを学ぶのに適しています。
事前準備
dbt Semantic Layer と Steep を連携させるには、データモデルの設計とセマンティックレイヤー(セマンティックモデル、メトリクス)の定義の 2 つのステップが必要です。このセクションでは、Jaffle Shop データを例に、具体的な実装方法を説明します。
データモデルの設計(スタースキーマへの変換)
dbt Semantic Layer を使用するためには、分析対象のデータ構造をスタースキーマまたは OBT に変換する必要があります。
これは、dbt Semantic Layer の仕様上、メトリクスが定義されているセマンティックモデルから見て 3 つ以上離れているテーブルを結合することができないためです。
MetricFlow can join up to three tables, supporting multi-hop joins with a limit of two hops.
セマンティックモデルの定義
セマンティックモデルは、dbt Semantic Layer においてメトリクスを定義する基盤となるオブジェクトです。ここでは、スタースキーマに変換した 5 つのテーブル(fct_order、dim_customer、dim_product、dim_store、dim_time)に対応するセマンティックモデルを定義します。
セマンティックモデルは、以下の要素で構成されます。
-
entities: テーブル間の結合キー(
primaryまたはforeign) -
dimensions: 分析軸となるディメンショナル属性(
categoricalまたはtime) - measures: 集計対象となるファクト(ファクトテーブルのみ)
dim_customer(2 つのディメンショナル属性を持つ):
semantic_models:
- name: customer_dimension
model: ref('dim_customer')
description: "顧客ディメンションテーブル"
entities:
- name: customer
type: primary
expr: customer_key
dimensions:
- name: customer_id
type: categorical
expr: customer_id
label: "顧客ID"
description: "ここには「顧客ID」の説明文が入ります。"
- name: customer_name
type: categorical
expr: customer_name
label: "顧客名"
description: "ここには「顧客名」の説明文が入ります。"
dim_product(8 つのディメンショナル属性を持つ):
semantic_models:
- name: product_dimension
model: ref('dim_product')
description: "商品ディメンションテーブル"
entities:
- name: product
type: primary
expr: product_key
dimensions:
- name: sku
type: categorical
expr: sku
label: "SKU"
description: "ここには「SKU」の説明文が入ります。"
- name: product_name
type: categorical
expr: product_name
label: "商品名"
description: "ここには「商品名」の説明文が入ります。"
- name: product_type
type: categorical
expr: product_type
label: "商品種別(jaffle|beverage)"
description: "ここには「商品種別」の説明文が入ります。"
- name: product_price
type: categorical
expr: product_price
label: "商品単価"
description: "ここには「商品単価」の説明文が入ります。"
- name: product_cost
type: categorical
expr: product_cost
label: "商品原価"
description: "ここには「商品原価」の説明文が入ります。"
- name: cost_rate
type: categorical
expr: cost_rate
label: "原価率"
description: "ここには「原価率」の説明文が入ります。"
- name: profitability_level
type: categorical
expr: profitability_level
label: "収益レベル(高|中|低)"
description: "ここには「収益レベル」の説明文が入ります。"
- name: product_description
type: categorical
expr: product_description
label: "商品詳細"
description: "ここには「商品詳細」の説明文が入ります。"
dim_store(4 つのディメンショナル属性を持つ):
semantic_models:
- name: store_dimension
model: ref('dim_store')
description: "店舗ディメンションテーブル"
entities:
- name: store
type: primary
expr: store_key
dimensions:
- name: store_id
type: categorical
expr: store_id
label: "店舗ID"
description: "ここには「店舗ID」の説明文が入ります。"
- name: store_name
type: categorical
expr: store_name
label: "店舗名"
description: "ここには「店舗名」の説明文が入ります。"
- name: opened_date
type: time
type_params:
time_granularity: day
expr: opened_date
label: "オープン日"
description: "ここには「オープン日」の説明文が入ります。"
- name: tax_rate
type: categorical
expr: tax_rate
label: "税率"
description: "ここには「税率」の説明文が入ります。"
dim_time(4 つのディメンショナル属性を持つ):
semantic_models:
- name: order_time_dimension
model: ref('dim_time')
description: "注文時間ディメンションテーブル"
entities:
- name: order_time
type: primary
expr: time_key
dimensions:
- name: order_hour
type: categorical
expr: hour
label: "注文時間(h)"
description: "ここには「注文時間(h)」の説明文が入ります。"
- name: order_minute
type: categorical
expr: minute
label: "注文分(m)"
description: "ここには「注文分(m)」の説明文が入ります。"
- name: ampm
type: categorical
expr: ampm
label: "午前午後"
description: "ここには「午前午後」の説明文が入ります。"
fct_order(2 つのディメンショナル属性、8 個の加法性ファクト属性を持つ):
semantic_models:
- name: order_fact
model: ref('fct_order')
description: "注文トランザクション・ファクトテーブル"
defaults:
agg_time_dimension: order_date
entities:
- name: order
type: primary
expr: order_key
- name: product
type: foreign
expr: product_key
- name: customer
type: foreign
expr: customer_key
- name: store
type: foreign
expr: store_key
- name: order_time
type: foreign
expr: order_time_key
dimensions:
- name: order_id
type: categorical
expr: order_id
label: "注文ID"
description: "ここには「注文ID」の説明文が入ります。"
- name: order_item_no
type: categorical
expr: order_item_no
label: "注文明細番号"
description: "ここには「注文明細番号」の説明文が入ります。"
- name: order_date
type: time
expr: order_date
type_params:
time_granularity: day
label: "注文日時"
description: "ここには「注文日時」の説明文が入ります。"
measures:
- name: order_fact_measure_record_count
agg: count_distinct
expr: 1
- name: order_fact_measure_order_count
agg: count_distinct
expr: order_id
- name: order_fact_measure_item_quantity
agg: sum
expr: item_quantity
- name: order_fact_measure_item_price
agg: sum
expr: item_price
- name: order_fact_measure_item_tax
agg: sum
expr: item_tax
- name: order_fact_measure_sales_amount
agg: sum
expr: sales_amount
- name: order_fact_measure_item_cost
agg: sum
expr: item_cost
- name: order_fact_measure_gross_profit
agg: sum
expr: gross_profit
メトリクスの定義
メトリクスは、セマンティックモデルで定義した measures を基に作成します。Steep 上でビジネスユーザーが実際に選択・集計するのはこのメトリクスです。ここでは fct_order に対して 8 つのメトリクスを定義します。
fct_order(8 個のメトリクスを持つ):
metrics:
- name: order_fact_metrics_record_count
label: "注文レコードカウント"
description: "ここには「注文レコードカウント」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_record_count
- name: order_fact_metrics_order_count
label: "注文件数"
description: "ここには「注文件数」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_order_count
- name: order_fact_metrics_item_quantity
label: "注文数量合計"
description: "ここには「注文数量合計」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_item_quantity
- name: order_fact_metrics_item_price
label: "注文明細合計"
description: "ここには「注文明細合計」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_item_price
- name: order_fact_metrics_item_tax
label: "注文消費税合計"
description: "ここには「注文消費税合計」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_item_tax
- name: order_fact_metrics_sales_amount
label: "注文売上合計"
description: "ここには「注文売上合計」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_sales_amount
- name: order_fact_metrics_item_cost
label: "注文原価合計"
description: "ここには「注文原価合計」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_item_cost
- name: order_fact_metrics_gross_profit
label: "注文粗利合計"
description: "ここには「注文粗利合計」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_gross_profit
Steep との接続設定
セマンティックモデルとメトリクスの定義が完了したら、Steep と dbt Semantic Layer を接続します。このセクションでは、初回接続の手順、定義変更時の同期方法、そしてクエリ結果のキャッシュ設定について説明します。
初回接続の手順
dbt Semantic Layer と Steep を接続する方法は、Steep の公式ドキュメントにスクショ付きで詳しく紹介されているため、そちらを参照してください。
接続に成功すると、dbt Semantic Layer で定義されているセマンティックモデルとメトリクスが Steep に同期されます。

セマンティックレイヤーの同期方法
初回接続以降で、dbt 側でセマンティックモデルやメトリクスの定義を変更した場合、必ず Steep 側で再同期が必要です。同期する方法は 2 種類あります。
- API Sync - API による自動同期、本番環境での自動同期に最適
- Manual Sync - UI からの手動同期、開発環境での動作確認に最適
API Sync - API による自動同期
API Sync では、API エンドポイントを使用してセマンティックレイヤーを更新することができます。
curl 'https://api.steep.app/semantic-layer-syncs' \
-H 'Content-Type: application/json' \
-H 'Authorization: ApiKey <API_KEY>' \
-X POST -d '{ "datasource_id": "<DATASOURCE_ID>" }'
API Sync は、dbt の Webhooks 機能と連携させることで自動同期することができます。
具体的には、dbt Cloud からの Webhook を受け取るサーバーやサービスを用意し、そこからこの API エンドポイントへ HTTP リクエストを実行します。これにより、dbt Platform の本番ジョブの成功と同時に Steep 側の定義も最新化されます。
Manual Sync - UI からの手動同期
Manual Sync では、Steep の UI 上から手動で同期ボタン(Sync now)をクリックしてセマンティックレイヤーを更新することができます。

開発環境での動作確認や、小規模な変更時に便利ですが、本番環境では API Sync + dbt Webhooks による自動化を推奨します。
データ(クエリ結果)の更新
セマンティックレイヤーの同期は「セマンティックモデルとメトリクスの定義」の更新ですが、実際のデータ(クエリ結果)は別途キャッシュ機構で管理されます。
Steep で実行したクエリ結果は Steep 側でキャッシュされます。キャッシュの有効期限はワークスペース設定で指定でき、以下から選択可能です。
- 1 時間
- 2 時間
- 4 時間(デフォルト)
- 8 時間
設定した期限を過ぎると、次回クエリ実行時に自動的に dbt Semantic Layer から最新データが取得されます。
連携ガイド
このセクションでは、dbt Semantic Layer と Steep の連携における以下の 3 つのポイントを説明します。
- dbt Semantic Layer で定義したプロパティが Steep の UI 上でどのように表示されるか
- Steep 独自の拡張プロパティによるメトリクスの分類・管理方法
- 非加法性メトリクスを適切に表示するための実装パターン
プロパティと UI の対応関係
セマンティックモデルの表示
| dbt Semantic Layer | Steep |
|---|---|
entities.name |
ディメンショナル属性のグループ名 |
dimensions.label |
ディメンショナル属性の表示名 |
dimensions.description |
ディメンショナル属性の説明文(ツールチップ) |
entities:
- name: store
type: primary
expr: store_key
dimensions:
- name: store_id
type: categorical
expr: store_id
label: "店舗ID"
description: "ここには「店舗ID」の説明文が入ります。"

メトリクスの表示
| dbt Semantic Layer | Steep |
|---|---|
label |
メトリクスの表示名 |
description |
メトリクスの説明文 |
metrics:
- name: order_fact_metrics_order_count
label: "注文件数"
description: "ここには「注文件数」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_order_count

Steep の拡張プロパティ
config.meta 配下に Steep の拡張プロパティを設定することで、メトリクスの分類や公開範囲を制御できます。
metrics:
- name: order_fact_metrics_order_count
label: "注文件数"
description: "ここには「注文件数」の説明文が入ります。"
type: simple
type_params:
measure: order_fact_measure_order_count
config:
meta:
category_name: "注文"
owner_emails: ["tanu@example.com"]
is_private: false
unlisted: false
| プロパティ名 | 機能 | 補足説明 |
|---|---|---|
category_name |
メトリクスが所属するカテゴリ名 | メトリクスを用途ごとにグルーピングできる機能です |
owners_emails |
メトリクスオーナーのメールアドレス | Steep に登録しているユーザーのメールアドレスを配列で指定します |
is_private |
メトリクスを非公開にするかどうか、デフォルトは false
|
機密性の高いメトリクスなど、アクセスを制限したいメトリクスに使用します |
unlisted |
メトリクスを非表示にするかどうか、デフォルトは false
|
派生メトリクスの計算に使用する中間メトリクスなど、エンドユーザーには見せたくないが削除はできないメトリクスを非表示にする機能です |

非加法性メトリクスの実装パターン
利益率や構成比、客単価などの非加法性メトリクスを実装する際、メトリクスタイプの選択によって Steep の UI 上での表示形式が異なります。パーセント表記が必要な場合は Ratio metrics、通貨や数値表記の場合は Derived metrics を使い分けます。
Derived metrics の場合
metrics:
- name: order_fact_metrics_gross_profit_rate
label: "注文粗利率"
description: "ここには「注文粗利率」の説明文が入ります。"
type: derived
type_params:
expr: coalesce(safe_divide(order_fact_metrics_gross_profit, order_fact_metrics_sales_amount), 0)
metrics:
- name: order_fact_metrics_gross_profit
- name: order_fact_metrics_sales_amount

Ratio metrics の場合
metrics:
- name: order_fact_metrics_gross_profit_rate
label: "注文粗利率"
description: "ここには「注文粗利率」の説明文が入ります。"
type: ratio
type_params:
numerator:
name: order_fact_metrics_gross_profit
denominator:
name: order_fact_metrics_sales_amount

制限事項
dbt Semantic Layer と Steep を連携して使用する場合、Steep が独自に提供している一部の機能が制限されます。これは、サードパーティ製のセマンティックレイヤー(dbt Semantic Layer や Cube など)を使用する場合の共通の制約です。
Steep のネイティブセマンティックレイヤーでのみ利用可能な機能
以下の機能は、Steep のネイティブセマンティックレイヤーを使用している場合にのみ利用できます。
- Cohorts analysis: コホート分析(共通の特性を持つユーザーグループの行動を時系列で追跡・分析できる機能)
- Maps: 地理データの可視化(H3 セルインデックス形式の地理データを持つメトリクスを地図上で可視化できる機能)
- Entities: エンティティのドリルダウン(メトリクスの集計値から行レベルのデータにドリルダウンできる機能)
さいごに
本記事では、dbt Semantic Layer と Steep を連携させる際の実践的なノウハウを、Jaffle Shop のデータセットを例に紹介しました。
dbt Semantic Layer でメトリクスを一元管理し、Steep でビジネスユーザーが直感的にデータを探索できる環境を構築することで、真の意味で組織全体にデータドリブンな意思決定を促進できると確信しています。
熱量がもれてしまいましたが、dbt Semantic Layer ✕ Steep の組み合わせは、multi-hop joins の制限やコホート分析などの機能制約もあるため、自組織の要件と照らし合わせながら導入を検討することをおすすめします。
冒頭での繰り返しになりますが、この記事がきっかけで、dbt Semantic Layer と Steep の組み合わせを試してみたい方が増えると幸甚の極みです。
最後までお読みいただき、ありがとうございました。
Discussion