Snowflake Semantic View を dbt で定義し、MetricFlow と同じメトリクスをクエリしてみる
はじめに
Snowflake のセマンティックビュー(Semantic View)と dbt Semantic Layer の両方から同じメトリクスをクエリし、結果が一致するかを確認した際の内容を記事としました。
Snowflake Semantic View の概要
Snowflake Semantic View は、Snowflake ネイティブのセマンティックレイヤー機能です。
物理テーブルに対してビジネス指標(メトリクス)・ディメンション・テーブル間のリレーションを SQL で定義できます。
定義したセマンティックビューは Cortex Analyst からの使用の他、通常の SQL でもクエリ可能です。これにより、BI ツールや分析クエリからビジネス指標を直接参照できます。
SQL でのセマンティックビューのクエリについては、以下の記事が参考になります。
また、dbt プロジェクトからセマンティックビューを管理する場合は、以下のパッケージを使用できます。
dbt Semantic Layer の概要
dbt Semantic Layer は dbt が提供するセマンティックレイヤーの実装です。
こちらはセマンティックモデルとして、YAML でメトリクスやディメンションを定義します。dbt Semantic Layer を経由する場合、直接 DWH に接続せず、対象となる dbt プロジェクト専用のエンドポイントにアクセスします。
集計したい軸やメトリクスなどを指定すると、内部エンジンの MetricFlow が SQL を組み立て、DWH 側にクエリを発行し結果を取得します。
前提条件
検証環境
以下の環境を使用しています。
- DWH:Snowflake
- dbt platform
- パッケージ:
Snowflake-Labs/dbt_semantic_viewv1.0.3
- パッケージ:
- BI ツール
- Tableau Desktop:2026.1
事前準備
検証には jaffle_shop の公式サンプルデータを使用しています。以下のテーブルを事前に用意しておきました。
| ソース | テーブル | 説明 |
|---|---|---|
raw.jaffle_shop |
customers |
顧客データ(1行=1顧客) |
raw.jaffle_shop |
orders |
注文データ(1行=1注文) |
これらをステージングモデルとして stg_orders・stg_customers に整形しています。
dbt_semantic_view パッケージのインストールは packages.yml に以下を追記し dbt deps を実行します。
packages:
- package: Snowflake-Labs/dbt_semantic_view
version: 1.0.3
セマンティックモデルの定義
定義方法
今回は同じ物理テーブル(stg_orders・stg_customers)に対して、2種類のセマンティックレイヤーを並行して定義してみます。
| 項目 | dbt Semantic Layer(MetricFlow) | Snowflake Semantic View |
|---|---|---|
| 定義場所 | models/stg_orders.yml(YAML インライン) |
models/sv_orders.sql(SQL DDL) |
| SQL 生成 | MetricFlow(dbt platform 側) | Snowflake |
dbt Semantic Layer の定義
stg_orders・stg_customers それぞれのモデルと、対応する YAML ファイルにセマンティックモデルを定義します。
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 # stg_customers との JOIN キー
- name: order_date
granularity: day
dimension:
type: time
- name: status
dimension:
type: categorical
metrics:
- name: order_count
type: simple
agg: count
expr: 1
stg_customers 側では、後の集計用に full_name カラムを追加しました。
select
id as customer_id,
first_name,
last_name,
first_name || ' ' || last_name as full_name
from {{ source('jaffle_shop', 'customers') }}
version: 2
models:
- name: stg_customers
semantic_model:
enabled: true
columns:
- name: customer_id # プライマリエンティティ
entity:
type: primary
name: customer
- name: full_name
dimension:
type: categorical
Snowflake Semantic View の定義
stg_orders と stg_customers の2モデルを sv_orders.sql としてセマンティックビューを定義してみます。
{{ config(materialized='semantic_view') }}
TABLES(
o AS {{ ref('stg_orders') }},
c AS {{ ref('stg_customers') }}
PRIMARY KEY (customer_id)
)
RELATIONSHIPS(
orders_customers AS o (customer_id)
REFERENCES c (customer_id)
)
FACTS(
o.order_id AS o.order_id,
c.customer_id AS c.customer_id
)
DIMENSIONS(
o.order_date AS o.order_date,
o.status AS o.status,
c.full_name AS c.full_name
)
METRICS(
o.order_count AS COUNT(o.order_id)
)
TABLES に PRIMARY KEY (...) を記述することで、物理テーブルに制約がなくても RELATIONSHIPS を定義できます。Snowflake の RELATIONSHIPS は参照先テーブルに PRIMARY KEY または UNIQUE 制約が必要なため、この記法で制約を補完しています。
パッケージの追加からビルドまでは以下で実行できます。
# パッケージのインストール(初回のみ)
dbt deps
# semantic view のみビルド
dbt run -s sv_orders
双方にクエリしてみる
クエリ 1:ステータス別注文数
dbt Semantic Layer
$ dbt sl query --metrics order_count --group-by orders__status
+----------------+-------------+
| ORDERS__STATUS | ORDER_COUNT |
+----------------+-------------+
| completed | 67 |
| shipped | 13 |
| returned | 4 |
| return_pending | 2 |
| placed | 13 |
+----------------+-------------+
Snowflake Semantic View
Snowflake SQL では以下のように記載できます。メトリクス列を AGG() でラップします。具体的な集計関数は、セマンティックビュー側の定義に依存します。
SELECT
status,
AGG(order_count) AS order_count
FROM dbt_sl_db.dbt_tyasuhara.sv_orders
GROUP BY status
ORDER BY status;
+----------------+-------------+
| STATUS | ORDER_COUNT |
|----------------+-------------|
| completed | 67 |
| placed | 13 |
| return_pending | 2 |
| returned | 4 |
| shipped | 13 |
+----------------+-------------+
双方で同じ結果を得られました。
クエリ 2:月別注文数
dbt Semantic Layer
$ dbt sl query --metrics order_count --group-by metric_time__month
+-------------------------+-------------+
| METRIC_TIME__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 |
+-------------------------+-------------+
Snowflake Semantic View
order_date は Date 型のため、月別に集計する際は Snowflake Semantic View のクエリ側で DATE_TRUNC 関数を使用します。
SELECT
DATE_TRUNC('MONTH', order_date) AS month,
AGG(order_count) AS order_count
FROM dbt_sl_db.dbt_tyasuhara.sv_orders
GROUP BY DATE_TRUNC('MONTH', order_date)
ORDER BY month;
+------------+-------------+
| MONTH | ORDER_COUNT |
|------------+-------------|
| 2018-01-01 | 29 |
| 2018-02-01 | 27 |
| 2018-03-01 | 35 |
| 2018-04-01 | 8 |
+------------+-------------+
同じ結果を得られました。
クエリ 3:顧客名別注文数
stg_orders のメトリクスを stg_customers のディメンションでグループ化するクエリを試してみます。このクエリでは、両モデルをまたぐ JOIN が発生します。
dbt Semantic Layer
dbt sl query --metrics order_count --group-by customer__full_name --order-by customer__full_name
+---------------------+-------------+
| CUSTOMER__FULL_NAME | ORDER_COUNT |
+---------------------+-------------+
| Aaron R. | 2 |
| Adam A. | 1 |
| Adam T. | 1 |
| Adam W. | 3 |
| Amanda H. | 1 |
| ...(62行) | |
+---------------------+-------------+
foreign → primary の向きのため、LEFT OUTER JOIN が使用されます。
SELECT
stg_customers_src_10000.full_name AS customer__full_name,
SUM(subq_1.__order_count) AS order_count
FROM (
SELECT
customer_id AS customer,
1 AS __order_count
FROM dbt_sl_db.dbt_tyasuhara.stg_orders stg_orders_src_10000
) subq_1
LEFT OUTER JOIN
dbt_sl_db.dbt_tyasuhara.stg_customers stg_customers_src_10000
ON
subq_1.customer = stg_customers_src_10000.customer_id
GROUP BY
stg_customers_src_10000.full_name
LIMIT 100
Snowflake Semantic View
SELECT
full_name,
AGG(order_count) AS order_count
FROM dbt_sl_db.dbt_tyasuhara.sv_orders
GROUP BY full_name
ORDER BY full_name;
+--------------+-------------+
| FULL_NAME | ORDER_COUNT |
|--------------+-------------|
| Aaron R. | 2 |
| Adam A. | 1 |
| Adam T. | 1 |
| Adam W. | 3 |
| Amanda H. | 1 |
| ...(62行) | |
+--------------+-------------+
62 Row(s) produced. Time Elapsed: 0.502s
同様の結果となります。
Semantic View 側の JOIN の種類の確認
dbt 側は仕様に基づき LEFT OUTER JOIN が使用されます。Snowflake 側も同様と考えられますが、確認します。
データの全体像は以下の通りです。
-- 全顧客数
SELECT COUNT(*) AS total_customers
FROM dbt_sl_db.dbt_tyasuhara.stg_customers;
+-----------------+
| TOTAL_CUSTOMERS |
|-----------------|
| 100 |
+-----------------+
-- 注文あり顧客数・注文なし顧客数・full_name のユニーク数
SELECT
COUNT(DISTINCT c.customer_id) AS customers_with_orders,
(SELECT COUNT(*) FROM dbt_sl_db.dbt_tyasuhara.stg_customers)
- COUNT(DISTINCT c.customer_id) AS customers_without_orders,
COUNT(DISTINCT c.full_name) AS unique_full_names
FROM dbt_sl_db.dbt_tyasuhara.stg_customers c
INNER JOIN dbt_sl_db.dbt_tyasuhara.stg_orders o ON c.customer_id = o.customer_id;
+-----------------------+--------------------------+-------------------+
| CUSTOMERS_WITH_ORDERS | CUSTOMERS_WITHOUT_ORDERS | UNIQUE_FULL_NAMES |
|-----------------------+--------------------------+-------------------|
| 62 | 38 | 62 |
+-----------------------+--------------------------+-------------------+
stg_customers には 100 名が存在しますが、注文を持つ顧客は 62 名(注文なし = 38 名)です。62行という結果は LEFT JOIN(orders 駆動)と INNER JOIN の両方で説明できるため、顧客 ID が存在しない注文(order_id=99999, customer_id=9999)を一時挿入して確認します。
-- テストデータ挿入(customer_id=9999 は stg_customers に存在しない)
INSERT INTO dbt_sl_db.dbt_tyasuhara.stg_orders (order_id, customer_id, order_date, status)
VALUES (99999, 9999, '2018-03-15', 'completed');
この状態で再度セマンティックビューをクエリしてみます。
SELECT
full_name,
AGG(order_count) AS order_count
FROM dbt_sl_db.dbt_tyasuhara.sv_orders
WHERE order_date = '2018-03-15'
GROUP BY full_name
ORDER BY full_name;
+-----------+-------------+
| FULL_NAME | ORDER_COUNT |
|-----------+-------------|
| NULL | 1 |
+-----------+-------------+
顧客 ID が存在しない注文も full_name = NULL として結果に含まれます。INNER JOIN であれば除外されるため RELATIONSHIP の定義に基づき、こちらも orders を基準とする LEFT JOIN が実行されたと考えられます。
BI から確認
Snowflake Semantic View
Snowflake Semantic View を BI ツールから参照する場合、セマンティックビューをクエリして集計済みテーブルとして物理化しておく方法があります。BI ツールはこのテーブルを通常のテーブルとして参照します。
以下のように ref('sv_orders') でセマンティックビューを参照するモデルを作成します。
{{ config(materialized='table') }}
SELECT
status,
AGG(order_count) AS order_count
FROM {{ ref('sv_orders') }}
GROUP BY status
ORDER BY status
ref('sv_orders') で参照することで、dbt の DAG に sv_orders → order_count_by_status の依存関係が自動登録されます。dbt run 時は sv_orders が先にビルドされ、その後 order_count_by_status が実行されます。
データの流れは以下のとおりです。

Tableau からこの order_count_by_status テーブルを参照すると、以下のように集計済みテーブルで定義している最低限の項目が表示されます。

dbt Semantic Layer
dbt Semantic Layer の場合は、BI ツール側に専用コネクタを設定することで、定義済みのメトリクスやディメンションの中から利用するものを直接指定できます。

BI からの利用感
2つの機能を経由して各指標をクエリをしてみました。本記事で試した内容では、以下の印象です。
-
dbt Semantic Layer
- プロジェクト全体で定義したメトリクス・ディメンションが BI ツール上に一覧で表示され、組み合わせて自由に集計できる
- ステータス別・月別・顧客名別など、粒度の切り替えを BI 側で動的に行える
- メトリクスの定義は YAML に一元集約されているため、複数の BI ツールから参照しても同じ計算ロジックが保証される
- dbt Semantic Layer 専用のコネクタに対応した BI ツールで利用可能
-
Snowflake Semantic View
- セマンティックビュー自体を BI ツールから直接参照することはできないため、集計済みテーブル(例:
order_count_by_status)を dbt モデルとして事前に用意しておく必要がある - 一方で、用意したテーブルは普通の Snowflake テーブルとして参照できるため、dbt Semantic Layer に対応していない BI ツールでも Snowflake に接続できれば利用できる
- これに近い dbt 側の機能もあります
- 表示されるのは事前に定義したカラムのみで、BI 側での動的な粒度変更には対応しない
- Cortex Analyst など Snowflake ネイティブの AI 機能とも連携できる
- セマンティックビュー自体を BI ツールから直接参照することはできないため、集計済みテーブル(例:
さいごに
Snowflake Semantic View と dbt Semantic Layer の両方から同じメトリクスをクエリし、単一テーブルの集計から複数テーブルにまたがる JOIN を含むクエリまで、結果が一致することを確認してみました。
こちらの内容がどなたかの参考になれば幸いです。







