Snowflake Semantic View を dbt で定義し、MetricFlow と同じメトリクスをクエリしてみる

Snowflake Semantic View を dbt で定義し、MetricFlow と同じメトリクスをクエリしてみる

2026.04.16

はじめに

Snowflake のセマンティックビュー(Semantic View)と dbt Semantic Layer の両方から同じメトリクスをクエリし、結果が一致するかを確認した際の内容を記事としました。

Snowflake Semantic View の概要

Snowflake Semantic View は、Snowflake ネイティブのセマンティックレイヤー機能です。

物理テーブルに対してビジネス指標(メトリクス)・ディメンション・テーブル間のリレーションを SQL で定義できます。

定義したセマンティックビューは Cortex Analyst からの使用の他、通常の SQL でもクエリ可能です。これにより、BI ツールや分析クエリからビジネス指標を直接参照できます。

https://docs.snowflake.com/en/sql-reference/sql/create-semantic-view

https://docs.snowflake.com/en/user-guide/views-semantic/querying

https://www.snowflake.com/en/engineering-blog/dbt-semantic-view-package/

SQL でのセマンティックビューのクエリについては、以下の記事が参考になります。

https://zenn.dev/snowflakejp/articles/25dce291abb65c

また、dbt プロジェクトからセマンティックビューを管理する場合は、以下のパッケージを使用できます。

https://hub.getdbt.com/Snowflake-Labs/dbt_semantic_view/latest/

dbt Semantic Layer の概要

dbt Semantic Layer は dbt が提供するセマンティックレイヤーの実装です。

こちらはセマンティックモデルとして、YAML でメトリクスやディメンションを定義します。dbt Semantic Layer を経由する場合、直接 DWH に接続せず、対象となる dbt プロジェクト専用のエンドポイントにアクセスします。

集計したい軸やメトリクスなどを指定すると、内部エンジンの MetricFlow が SQL を組み立て、DWH 側にクエリを発行し結果を取得します。

https://docs.getdbt.com/docs/use-dbt-semantic-layer/sl-architecture?version=1.12

前提条件

検証環境

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

  • DWH:Snowflake
  • dbt platform
    • パッケージ:Snowflake-Labs/dbt_semantic_view v1.0.3
  • BI ツール
    • Tableau Desktop:2026.1

事前準備

検証には jaffle_shop の公式サンプルデータを使用しています。以下のテーブルを事前に用意しておきました。

ソース テーブル 説明
raw.jaffle_shop customers 顧客データ(1行=1顧客)
raw.jaffle_shop orders 注文データ(1行=1注文)

これらをステージングモデルとして stg_ordersstg_customers に整形しています。

dbt_semantic_view パッケージのインストールは packages.yml に以下を追記し dbt deps を実行します。

packages:
  - package: Snowflake-Labs/dbt_semantic_view
    version: 1.0.3

セマンティックモデルの定義

定義方法

今回は同じ物理テーブル(stg_ordersstg_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_ordersstg_customers それぞれのモデルと、対応する YAML ファイルにセマンティックモデルを定義します。

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

from {{ source('jaffle_shop', 'orders') }}
models/stg_orders.yml
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 カラムを追加しました。

models/stg_customers.sql
select
    id as customer_id,
    first_name,
    last_name,
    first_name || ' ' || last_name as full_name

from {{ source('jaffle_shop', 'customers') }}
models/stg_customers.yml
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_ordersstg_customers の2モデルを sv_orders.sql としてセマンティックビューを定義してみます。

models/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)
)

TABLESPRIMARY 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') でセマンティックビューを参照するモデルを作成します。

models/order_count_by_status.sql
{{ 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 が実行されます。

データの流れは以下のとおりです。

2026-04-16_18h37_47

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

2026-04-16_18h48_48

dbt Semantic Layer

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

2026-04-16_18h53_21

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 に接続できれば利用できる
    • 表示されるのは事前に定義したカラムのみで、BI 側での動的な粒度変更には対応しない
    • Cortex Analyst など Snowflake ネイティブの AI 機能とも連携できる

さいごに

Snowflake Semantic View と dbt Semantic Layer の両方から同じメトリクスをクエリし、単一テーブルの集計から複数テーブルにまたがる JOIN を含むクエリまで、結果が一致することを確認してみました。
こちらの内容がどなたかの参考になれば幸いです。

この記事をシェアする

関連記事