🫧

Snowflake Semantic Viewのコード管理

に公開

この記事は、Nowcast Advent Calendar 2025 の16日目の記事です。

はじめに

こんにちは。ナウキャストでデータエンジニアをしている加山です。

生成AIでの活用の文脈でも注目を集めているSnowflakeのSemantic Viewについて、そのコード管理に関して現状の方法を整理し、それぞれの評価と実装方法をまとめました。

本記事では、Semantic Viewそのものの概要(「そもそも何か」「ユースケース」など)は扱いません。コード管理の評価(何が管理できて何ができないか)と実装方法に関して記載しています。
またSemantic Viewの具体的な設定、Tipsに関しては、
https://zenn.dev/finatext/articles/tips-for-maintaining-semantic-view
を参照してください。

なお、本記事で言及するdbtはdbt coreを前提としており、Terraform、dbt coreの実行基盤がすでに整っていることを前提としています。

利用するデータ

本記事では、以下のER図で示す売上データを例として使用します。

サンプルデータ作成SQL
-- =========================
-- 0) 作業スキーマ(任意)
-- =========================
-- USE DATABASE YOUR_DB;
-- USE SCHEMA YOUR_SCHEMA;

-- =========================
-- 1) DIM: 商品マスタ
-- =========================
CREATE OR REPLACE TABLE DIM_PRODUCT (
  PRODUCT_ID     NUMBER       NOT NULL,
  PRODUCT_CODE   VARCHAR      NOT NULL,
  PRODUCT_NAME   VARCHAR      NOT NULL,
  CATEGORY_L1    VARCHAR      NOT NULL,
  UNIT_PRICE_YEN NUMBER(10,0) NOT NULL,
  IS_ACTIVE      BOOLEAN      NOT NULL,
  CREATED_AT     TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP()
);

-- =========================
-- 2) DIM: 店舗マスタ
-- =========================
CREATE OR REPLACE TABLE DIM_STORE (
  STORE_ID    NUMBER        NOT NULL,
  STORE_CODE  VARCHAR       NOT NULL,
  STORE_NAME  VARCHAR       NOT NULL,
  PREFECTURE  VARCHAR       NOT NULL,
  REGION      VARCHAR       NOT NULL,
  OPEN_DATE   DATE          NOT NULL,
  CREATED_AT  TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP()
);

-- =========================
-- 3) FACT: 売上(明細)
-- =========================
CREATE OR REPLACE TABLE FACT_POS_SALES (
  SALES_ID        NUMBER        NOT NULL,
  SALES_TS        TIMESTAMP_NTZ NOT NULL,
  SALES_DATE      DATE          NOT NULL,
  STORE_ID        NUMBER        NOT NULL,
  PRODUCT_ID      NUMBER        NOT NULL,
  QUANTITY        NUMBER(10,0)  NOT NULL,
  UNIT_PRICE_YEN  NUMBER(10,0)  NOT NULL,
  SALES_AMOUNT_YEN NUMBER(12,0) NOT NULL,
  PAYMENT_METHOD  VARCHAR       NOT NULL,
  CREATED_AT      TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP()
);

-- ==========================================================
-- 4) ダミーデータ投入(DIM)
-- ==========================================================

-- 商品 50件
INSERT INTO DIM_PRODUCT (PRODUCT_ID, PRODUCT_CODE, PRODUCT_NAME, CATEGORY_L1, UNIT_PRICE_YEN, IS_ACTIVE)
WITH src AS (
  SELECT
    SEQ4() + 1 AS PRODUCT_ID,
    'P' || LPAD(TO_VARCHAR(SEQ4() + 1), 6, '0') AS PRODUCT_CODE,
    '商品_' || LPAD(TO_VARCHAR(SEQ4() + 1), 3, '0') AS PRODUCT_NAME,
    CASE MOD(SEQ4(), 5)
      WHEN 0 THEN '飲料'
      WHEN 1 THEN '菓子'
      WHEN 2 THEN '日配'
      WHEN 3 THEN '冷凍'
      ELSE '雑貨'
    END AS CATEGORY_L1,
    /* 100円〜1000円 */
    100 + 10 * UNIFORM(0, 91, RANDOM()) AS UNIT_PRICE_YEN,
    TRUE AS IS_ACTIVE
  FROM TABLE(GENERATOR(ROWCOUNT => 50))
)
SELECT * FROM src;

-- 店舗 20件
INSERT INTO DIM_STORE (STORE_ID, STORE_CODE, STORE_NAME, PREFECTURE, REGION, OPEN_DATE)
WITH src AS (
  SELECT
    SEQ4() + 1 AS STORE_ID,
    'S' || LPAD(TO_VARCHAR(SEQ4() + 1), 5, '0') AS STORE_CODE,
    '店舗_' || LPAD(TO_VARCHAR(SEQ4() + 1), 3, '0') AS STORE_NAME,
    CASE MOD(SEQ4(), 6)
      WHEN 0 THEN '東京都'
      WHEN 1 THEN '神奈川県'
      WHEN 2 THEN '千葉県'
      WHEN 3 THEN '埼玉県'
      WHEN 4 THEN '大阪府'
      ELSE '愛知県'
    END AS PREFECTURE,
    CASE
      WHEN MOD(SEQ4(), 6) IN (0,1,2,3) THEN '関東'
      WHEN MOD(SEQ4(), 6) = 4 THEN '関西'
      ELSE '中部'
    END AS REGION,
    DATEADD('day', -UNIFORM(365, 3650, RANDOM()), CURRENT_DATE()) AS OPEN_DATE
  FROM TABLE(GENERATOR(ROWCOUNT => 20))
)
SELECT * FROM src;

-- ==========================================================
-- 5) ダミーデータ投入(FACT)
--    例:直近30日・10,000明細
-- ==========================================================
INSERT INTO FACT_POS_SALES (
  SALES_ID, SALES_TS, SALES_DATE,
  STORE_ID, PRODUCT_ID,
  QUANTITY, UNIT_PRICE_YEN, SALES_AMOUNT_YEN,
  PAYMENT_METHOD
)
WITH base AS (
  SELECT
    SEQ4() + 1 AS SALES_ID,
    /* 直近30日からランダム */
    DATEADD('day', -UNIFORM(0, 30, RANDOM()), CURRENT_DATE()) AS SALES_DATE,
    /* 時刻もランダム(0〜23時) */
    DATEADD(
      'hour',
      UNIFORM(0, 24, RANDOM()),
      TO_TIMESTAMP_NTZ(DATEADD('day', -UNIFORM(0, 30, RANDOM()), CURRENT_DATE()))
    ) AS SALES_TS,
    UNIFORM(1, 21, RANDOM()) AS STORE_ID,    -- 1..20
    UNIFORM(1, 51, RANDOM()) AS PRODUCT_ID,  -- 1..50
    UNIFORM(1, 6, RANDOM())  AS QUANTITY,    -- 1..5
    CASE MOD(UNIFORM(0, 100, RANDOM()), 4)
      WHEN 0 THEN 'CASH'
      WHEN 1 THEN 'CREDIT'
      WHEN 2 THEN 'QR'
      ELSE 'IC'
    END AS PAYMENT_METHOD
  FROM TABLE(GENERATOR(ROWCOUNT => 10000))
),
priced AS (
  SELECT
    b.SALES_ID,
    b.SALES_TS,
    b.SALES_DATE,
    b.STORE_ID,
    b.PRODUCT_ID,
    b.QUANTITY,
    p.UNIT_PRICE_YEN,
    (b.QUANTITY * p.UNIT_PRICE_YEN) AS SALES_AMOUNT_YEN,
    b.PAYMENT_METHOD
  FROM base b
  JOIN DIM_PRODUCT p
    ON p.PRODUCT_ID = b.PRODUCT_ID
)
SELECT * FROM priced;

コード管理の方法

Semantic Viewのコード管理には主に3つの方法があります。

方法 概要
YAML + Snowflake CLI YAMLファイルで定義し、Snowflake CLIでデプロイ
dbt dbt_semantic_viewパッケージを使用してdbt経由で作成
Terraform Terraform Provider(v2.11.0以降)を使用

1. YAML + Snowflake CLIを使う方法

概要

YAMLファイルでSemantic Viewを定義し、Snowflake CLIでデプロイする方法です。

実装

YAMLファイル例
name: SEMANTIC_POS
description: POS売上分析用セマンティックビュー

tables:
  - name: SALES
    synonyms:
      - 売上
      - POS売上
    description: POS売上明細テーブル
    base_table:
      database: YOUR_DATABASE
      schema: '"YOUR_SCHEMA"'
      table: FACT_POS_SALES
    primary_key:
      columns:
        - SALES_ID
    dimensions:
      - name: SALES_DATE
        synonyms:
          - 売上日
        description: 売上日
        expr: SALES_DATE
        data_type: DATE
      - name: SALES_TS
        synonyms:
          - 売上日時
        description: 売上日時(タイムスタンプ)
        expr: SALES_TS
        data_type: TIMESTAMP_NTZ
      - name: PAYMENT_METHOD
        synonyms:
          - 支払方法
          - 決済方法
        description: 支払方法(CASH, CREDIT, QR, IC)
        expr: PAYMENT_METHOD
        data_type: VARCHAR
        sample_values:
          - CASH
          - CREDIT
          - QR
          - IC
        is_enum: true
    facts:
      - name: QUANTITY
        synonyms:
          - 数量
          - 販売数量
        description: 販売数量
        expr: QUANTITY
        data_type: NUMBER
      - name: SALES_AMOUNT_YEN
        synonyms:
          - 売上金額
          - 売上
        description: 売上金額(円)= 数量 × 単価
        expr: SALES_AMOUNT_YEN
        data_type: NUMBER
    metrics:
      - name: TOTAL_SALES
        synonyms:
          - 売上合計
          - 総売上
        description: 売上合計(円)
        expr: SUM(SALES_AMOUNT_YEN)
      - name: TOTAL_QUANTITY
        synonyms:
          - 販売数量合計
        description: 販売数量合計
        expr: SUM(QUANTITY)
      - name: ORDER_COUNT
        synonyms:
          - 注文件数
          - 取引件数
        description: 注文件数
        expr: COUNT(*)

  - name: PRODUCT
    synonyms:
      - 商品
      - 商品マスタ
    description: 商品マスタテーブル
    base_table:
      database: YOUR_DATABASE
      schema: '"YOUR_SCHEMA"'
      table: DIM_PRODUCT
    primary_key:
      columns:
        - PRODUCT_ID
    dimensions:
      - name: PRODUCT_CODE
        synonyms:
          - 商品コード
        description: 商品コード
        expr: PRODUCT_CODE
        data_type: VARCHAR
      - name: PRODUCT_NAME
        synonyms:
          - 商品名
        description: 商品名
        expr: PRODUCT_NAME
        data_type: VARCHAR
      - name: CATEGORY_L1
        synonyms:
          - カテゴリ
          - 大分類
        description: カテゴリ(大分類)- 飲料, 菓子, 日配, 冷凍, 雑貨
        expr: CATEGORY_L1
        data_type: VARCHAR
        sample_values:
          - 飲料
          - 菓子
          - 日配
          - 冷凍
          - 雑貨
        is_enum: true
      - name: IS_ACTIVE
        synonyms:
          - 有効フラグ
        description: 有効フラグ
        expr: IS_ACTIVE
        data_type: BOOLEAN
        sample_values:
          - "true"
          - "false"
        is_enum: true
    filters:
      - name: active_product
        synonyms:
          - available_product
          - current_product
          - enabled_product
          - functioning_product
          - in_use_product
          - live_product
          - operational_product
          - running_product
          - valid_product
          - working_product
        description: 販売中の有効な商品を絞るためのflag
        expr: IS_ACTIVE = TRUE

  - name: STORE
    synonyms:
      - 店舗
      - 店舗マスタ
    description: 店舗マスタテーブル
    base_table:
      database: YOUR_DATABASE
      schema: '"YOUR_SCHEMA"'
      table: DIM_STORE
    primary_key:
      columns:
        - STORE_ID
    dimensions:
      - name: STORE_CODE
        synonyms:
          - 店舗コード
        description: 店舗コード
        expr: STORE_CODE
        data_type: VARCHAR
      - name: STORE_NAME
        synonyms:
          - 店舗名
        description: 店舗名
        expr: STORE_NAME
        data_type: VARCHAR
      - name: PREFECTURE
        synonyms:
          - 都道府県
        description: 都道府県
        expr: PREFECTURE
        data_type: VARCHAR
        sample_values:
          - 東京都
          - 神奈川県
          - 千葉県
          - 埼玉県
          - 大阪府
          - 愛知県
        is_enum: false
      - name: REGION
        synonyms:
          - 地域
          - エリア
        description: 地域(関東, 関西, 中部)
        expr: REGION
        data_type: VARCHAR
        sample_values:
          - 関東
          - 関西
          - 中部
        is_enum: false
      - name: OPEN_DATE
        synonyms:
          - 開店日
        description: 開店日
        expr: OPEN_DATE
        data_type: DATE

relationships:
  - name: SALES_TO_PRODUCT
    left_table: SALES
    right_table: PRODUCT
    relationship_columns:
      - left_column: PRODUCT_ID
        right_column: PRODUCT_ID
    relationship_type: many_to_one
  - name: SALES_TO_STORE
    left_table: SALES
    right_table: STORE
    relationship_columns:
      - left_column: STORE_ID
        right_column: STORE_ID
    relationship_type: many_to_one

verified_queries:
  - name: storeコード S00004 の飲料の商品ごとの月次の売り上げの推移
    question: storeコード S00004 の飲料の商品ごとの月次の売り上げの推移
    sql: |-
      SELECT
        DATE_TRUNC('MONTH', s.sales_date) AS month,
        p.product_name,
        p.product_code,
        MIN(s.sales_date) AS start_date,
        MAX(s.sales_date) AS end_date,
        SUM(s.sales_amount_yen) AS monthly_sales
      FROM
        sales AS s
        LEFT OUTER JOIN product AS p ON s.product_id = p.product_id
        LEFT OUTER JOIN store AS st ON s.store_id = st.store_id
      WHERE
        st.store_code = 'S00004'
        AND p.category_l1 = '飲料'
      GROUP BY
        DATE_TRUNC('MONTH', s.sales_date),
        p.product_name,
        p.product_code
      ORDER BY
        p.product_name,
        month DESC NULLS LAST
    use_as_onboarding_question: false
    verified_by: System
    verified_at: 1765676734
module_custom_instructions:
  question_categorization: お店の売上データに関するデータがあります。
  sql_generation: 条件(where句)を日本語で指定された場合は、英語に翻訳せずsingle quotesで囲って指定してください

実行方法

SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAMLを使用してSemantic Viewを作成・更新します。PythonスクリプトやGitHub Actionsなど、任意の方法で実行可能です。

CALL SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML(
  'YOUR_DATABASE.YOUR_SCHEMA.SEMANTIC_POS',
  '<yaml_content>'
);

CIでの構文チェック

SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAMLの第3引数にverify_onlyTRUEに設定することで、CIパイプライン上でYAMLの構文チェックが可能です。GitHub ActionsなどのCI/CDパイプラインに組み込むことで、デプロイ前に構文エラーを検出できます。

CALL SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML(
  'YOUR_DATABASE.YOUR_SCHEMA.SEMANTIC_POS',
  '<yaml_content>',
  TRUE  -- verify_only = TRUE で構文チェックのみ実行
);

評価

管理可能な項目

管理項目 対応状況
Fact ✅ 対応
Dim ✅ 対応
Description ✅ 対応
Primary Key ✅ 対応
Unique Key ✅ 対応
Custom Instructions ✅ 対応
Verified Queries ✅ 対応
Synonyms ✅ 対応
Sample values (enumの登録) ✅ 対応
Named filter ✅ 対応
Connect Cortex Search ✅ 対応

基本的に全ての項目に対応しています。(Semantic ViewのGUI上でもYAMLに変換できるので、当たり前ですが)
Custom InstructionsやVerified Queriesなど、生成AI連携に必要な機能をすべて定義できるため、Cortex AnalystやCortexSearchなど、Snowflakeのマネージドなサービスをフル活用したい場合に有効です。ただしCI/CDのパイプラインを自前で実装、メンテナンスをする必要があったり、実装面、運用面でコストがかかります。

2. dbtを使う方法

概要

Snowflake Labsが公開しているdbt_semantic_viewを使う方法です。このパッケージを使うと、dbtのmaterialization semantic_view としてSemantic Viewを定義できます。

実装

インストール

packages.ymlに追加してdbt depsを実行します:

packages:
  - package: Snowflake-Labs/dbt_semantic_view

モデル定義例

{{ config(materialized='semantic_view') }}

TABLES(
  sales   AS {{ ref('fact_pos_sales') }} PRIMARY KEY (SALES_ID),
  product AS {{ ref('dim_product') }}    PRIMARY KEY (PRODUCT_ID),
  store   AS {{ ref('dim_store') }}      PRIMARY KEY (STORE_ID)
)

RELATIONSHIPS(
  sales_to_product AS
    sales (PRODUCT_ID) REFERENCES product (PRODUCT_ID),
  sales_to_store AS
    sales (STORE_ID) REFERENCES store (STORE_ID)
)

FACTS(
  sales.QUANTITY AS QUANTITY,
  sales.SALES_AMOUNT_YEN AS SALES_AMOUNT_YEN
)

DIMENSIONS(
  sales.SALES_DATE AS SALES_DATE
    COMMENT = '売上日',
  sales.SALES_TS AS SALES_TS
    COMMENT = '売上日時',
  sales.PAYMENT_METHOD AS PAYMENT_METHOD
    COMMENT = '支払方法(CASH, CREDIT, QR, IC)',
  product.PRODUCT_CODE AS PRODUCT_CODE
    COMMENT = '商品コード',
  product.PRODUCT_NAME AS PRODUCT_NAME
    COMMENT = '商品名',
  product.CATEGORY_L1 AS CATEGORY_L1
    COMMENT = 'カテゴリ(飲料, 菓子, 日配, 冷凍, 雑貨)',
  store.STORE_CODE AS STORE_CODE
    COMMENT = '店舗コード',
  store.STORE_NAME AS STORE_NAME
    COMMENT = '店舗名',
  store.PREFECTURE AS PREFECTURE
    COMMENT = '都道府県',
  store.REGION AS REGION
    COMMENT = '地域(関東, 関西, 中部)'
)

METRICS(
  sales.total_sales AS SUM(sales.SALES_AMOUNT_YEN)
    COMMENT = '売上合計(円)',
  sales.total_quantity AS SUM(sales.QUANTITY)
    COMMENT = '販売数量合計',
  sales.order_count AS COUNT(*)
    COMMENT = '注文件数'
)

COMMENT = 'POS売上分析用セマンティックビュー'

実行方法

dbt buildで全てのモデルをビルドするか、特定のSemantic Viewだけを作成したい場合はdbt build --select <model_name>を使用します。

dbt build --select semantic_pos

評価

管理可能な項目

管理項目 対応状況
Fact ✅ 対応
Dim ✅ 対応
Description ✅ 対応
Primary Key ✅ 対応
Unique Key ❌ 非対応
Custom Instructions ❌ 非対応
Verified Queries ❌ 非対応
Synonyms ❌ 非対応
Sample values (enumの登録) ❌ 非対応
Named filter ❌ 非対応
Connect Cortex Search ❌ 非対応

BIツールなどでのSemantic Layerとして使う場面では有効です。dbtのエコシステムと完全に統合されており、他のテーブル、ビューと共に管理できたり、リネージの可視化やモデル間の依存関係管理が容易にできます。また、以下のように、Semantic Viewとその依存するモデル間のリネージの可視化も可能です。

リネージの例

ただし、現状はCortex Analyst連携時のCustom InstructionsやVerified Queriesなど、Snowflakeのマネージドサービス特有の機能は利用できません。Updateに期待です。

3. Terraformを用いる方法

概要

Terraform Provider for Snowflakeのv2.11.0以降で、Semantic Viewをリソースとして管理できるようになりました。既存のTerraformインフラと統合して管理できます。

実装

Terraform Provider設定

Semantic Viewリソースを使用するには、preview_features_enabledsnowflake_semantic_view_resourceを追加する必要があります。

terraform {
  required_providers {
    snowflake = {
      source  = "snowflakedb/snowflake"
      version = ">= 2.11.0"
    }
  }
}

provider "snowflake" {
  # ... 認証情報などの設定 ...

  preview_features_enabled = [
    ...
    "snowflake_semantic_view_resource",
  ]
}

Semantic Viewリソース定義例

resource "snowflake_semantic_view" "semantic_pos" {
  database = "YOUR_DATABASE"
  schema   = "YOUR_SCHEMA"
  name     = "SEMANTIC_POS"
  comment  = "POS売上分析用セマンティックビュー"

  #-----------------------------------------------------------------------------
  # テーブル定義
  #-----------------------------------------------------------------------------

  # SALES テーブル(POS売上明細テーブル)
  tables {
    table_alias = "SALES"
    table_name  = "\"YOUR_DATABASE\".\"YOUR_SCHEMA\".\"FACT_POS_SALES\""
    comment     = "POS売上明細テーブル"
    synonym     = ["売上", "POS売上"]
    primary_key = ["SALES_ID"]
  }

  # PRODUCT テーブル(商品マスタテーブル)
  tables {
    table_alias = "PRODUCT"
    table_name  = "\"YOUR_DATABASE\".\"YOUR_SCHEMA\".\"DIM_PRODUCT\""
    comment     = "商品マスタテーブル"
    synonym     = ["商品", "商品マスタ"]
    primary_key = ["PRODUCT_ID"]
  }

  # STORE テーブル(店舗マスタテーブル)
  tables {
    table_alias = "STORE"
    table_name  = "\"YOUR_DATABASE\".\"YOUR_SCHEMA\".\"DIM_STORE\""
    comment     = "店舗マスタテーブル"
    synonym     = ["店舗", "店舗マスタ"]
    primary_key = ["STORE_ID"]
  }

  #-----------------------------------------------------------------------------
  # Dimensions(ディメンション)
  #-----------------------------------------------------------------------------

  # SALES テーブルのディメンション
  dimensions {
    qualified_expression_name = "\"SALES\".\"SALES_DATE\""
    sql_expression            = "\"SALES\".\"SALES_DATE\""
    comment                   = "売上日"
    synonym                   = ["売上日"]
  }

  dimensions {
    qualified_expression_name = "\"SALES\".\"SALES_TS\""
    sql_expression            = "\"SALES\".\"SALES_TS\""
    comment                   = "売上日時(タイムスタンプ)"
    synonym                   = ["売上日時"]
  }

  dimensions {
    qualified_expression_name = "\"SALES\".\"PAYMENT_METHOD\""
    sql_expression            = "\"SALES\".\"PAYMENT_METHOD\""
    comment                   = "支払方法(CASH, CREDIT, QR, IC)"
    synonym                   = ["支払方法", "決済方法"]
  }

  # PRODUCT テーブルのディメンション
  dimensions {
    qualified_expression_name = "\"PRODUCT\".\"PRODUCT_CODE\""
    sql_expression            = "\"PRODUCT\".\"PRODUCT_CODE\""
    comment                   = "商品コード"
    synonym                   = ["商品コード"]
  }

  dimensions {
    qualified_expression_name = "\"PRODUCT\".\"PRODUCT_NAME\""
    sql_expression            = "\"PRODUCT\".\"PRODUCT_NAME\""
    comment                   = "商品名"
    synonym                   = ["商品名"]
  }

  dimensions {
    qualified_expression_name = "\"PRODUCT\".\"CATEGORY_L1\""
    sql_expression            = "\"PRODUCT\".\"CATEGORY_L1\""
    comment                   = "カテゴリ(大分類)- 飲料, 菓子, 日配, 冷凍, 雑貨"
    synonym                   = ["カテゴリ", "大分類"]
  }

  dimensions {
    qualified_expression_name = "\"PRODUCT\".\"IS_ACTIVE\""
    sql_expression            = "\"PRODUCT\".\"IS_ACTIVE\""
    comment                   = "有効フラグ"
    synonym                   = ["有効フラグ"]
  }

  # STORE テーブルのディメンション
  dimensions {
    qualified_expression_name = "\"STORE\".\"STORE_CODE\""
    sql_expression            = "\"STORE\".\"STORE_CODE\""
    comment                   = "店舗コード"
    synonym                   = ["店舗コード"]
  }

  dimensions {
    qualified_expression_name = "\"STORE\".\"STORE_NAME\""
    sql_expression            = "\"STORE\".\"STORE_NAME\""
    comment                   = "店舗名"
    synonym                   = ["店舗名"]
  }

  dimensions {
    qualified_expression_name = "\"STORE\".\"PREFECTURE\""
    sql_expression            = "\"STORE\".\"PREFECTURE\""
    comment                   = "都道府県"
    synonym                   = ["都道府県"]
  }

  dimensions {
    qualified_expression_name = "\"STORE\".\"REGION\""
    sql_expression            = "\"STORE\".\"REGION\""
    comment                   = "地域(関東, 関西, 中部)"
    synonym                   = ["地域", "エリア"]
  }

  dimensions {
    qualified_expression_name = "\"STORE\".\"OPEN_DATE\""
    sql_expression            = "\"STORE\".\"OPEN_DATE\""
    comment                   = "開店日"
    synonym                   = ["開店日"]
  }

  #-----------------------------------------------------------------------------
  # Facts(ファクト)
  #-----------------------------------------------------------------------------

  facts {
    qualified_expression_name = "\"SALES\".\"QUANTITY\""
    sql_expression            = "\"SALES\".\"QUANTITY\""
    comment                   = "販売数量"
    synonym                   = ["数量", "販売数量"]
  }

  facts {
    qualified_expression_name = "\"SALES\".\"SALES_AMOUNT_YEN\""
    sql_expression            = "\"SALES\".\"SALES_AMOUNT_YEN\""
    comment                   = "売上金額(円)= 数量 × 単価"
    synonym                   = ["売上金額", "売上"]
  }

  #-----------------------------------------------------------------------------
  # Metrics(メトリクス)
  #-----------------------------------------------------------------------------

  metrics {
    semantic_expression {
      qualified_expression_name = "\"SALES\".\"TOTAL_SALES\""
      sql_expression            = "SUM(\"SALES\".\"SALES_AMOUNT_YEN\")"
      comment                   = "売上合計(円)"
      synonym                   = ["売上合計", "総売上"]
    }
  }

  metrics {
    semantic_expression {
      qualified_expression_name = "\"SALES\".\"TOTAL_QUANTITY\""
      sql_expression            = "SUM(\"SALES\".\"QUANTITY\")"
      comment                   = "販売数量合計"
      synonym                   = ["販売数量合計"]
    }
  }

  metrics {
    semantic_expression {
      qualified_expression_name = "\"SALES\".\"ORDER_COUNT\""
      sql_expression            = "COUNT(*)"
      comment                   = "注文件数"
      synonym                   = ["注文件数", "取引件数"]
    }
  }

  #-----------------------------------------------------------------------------
  # Relationships(リレーションシップ)
  #-----------------------------------------------------------------------------

  # SALES → PRODUCT (many_to_one)
  relationships {
    relationship_identifier = "SALES_TO_PRODUCT"
    relationship_columns    = ["PRODUCT_ID"]

    table_name_or_alias {
      table_alias = "SALES"
    }

    referenced_table_name_or_alias {
      table_alias = "PRODUCT"
    }

    referenced_relationship_columns = ["PRODUCT_ID"]
  }

  # SALES → STORE (many_to_one)
  relationships {
    relationship_identifier = "SALES_TO_STORE"
    relationship_columns    = ["STORE_ID"]

    table_name_or_alias {
      table_alias = "SALES"
    }

    referenced_table_name_or_alias {
      table_alias = "STORE"
    }

    referenced_relationship_columns = ["STORE_ID"]
  }
}

デプロイ

terraform init
terraform plan
terraform apply

評価

管理可能な項目

管理項目 対応状況
Fact ✅ 対応
Dim ✅ 対応
Description ✅ 対応
Primary Key ✅ 対応
Unique Key ❌ 非対応
Custom Instructions ❌ 非対応
Verified Queries ❌ 非対応
Synonyms ✅ 対応
Sample values (enumの登録) ❌ 非対応
Named filter ❌ 非対応
Connect Cortex Search ❌ 非対応

既存のTerraformインフラと統合したい場合には選択肢となりますが、Semantic Viewはビジネス要件の変更に応じて頻繁に更新される一方で、Terraformは通常インフラリソースの安定性を重視するため、ライフサイクルが大きく異なります。また、Semantic Viewの管理はデータアナリストやビジネスサイドの担当者が行うことが多いのに対し、Terraformの管理はインフラエンジニアが担当することが多く、管理者や作業者が異なる場合が多いです。そのため大きなメリットがない現状、Terraformでの管理は現実的ではないかもしれません。

比較

観点 YAML + Snowflake CLI dbt Terraform
基本的なSemantic Viewの定義
CortexAnalystとの統合 ⚠️ ⚠️
CortexSearchとの統合
Modern Data Stackとの統合 ⚠️
初期実装コスト ※1 ⚠️
運用コスト ⚠️ ⚠️

※1:dbt、Terraformの実行環境が整っている前提での評価です。

まとめ

Semantic Viewのコード管理には、YAML + Snowflake CLI、dbt、Terraformの3つの方法がありますが、それぞれ一長一短があります。

各方法の選択指針

個人的には各方法の選択は、以下のような観点で判断すると良いと思います。

  • CortexAnalystなど生成AIを活用、Snowflakeのマネージドな機能をフル活用したい場合YAML + Snowflake CLI
  • BI用のみに活用する場合dbt

運用の課題

Cortex Analystの最適化機能では、Verified Queriesを活用してSemantic Viewをエンハンスする方法が提供されています。

一方で、現状Custom InstructionsやVerified Queriesも上書きするような仕様になっているため、コード管理が複雑になっています。また、Verified Queriesをユーザー側で追加して成長させたり、Custom Instructionsのチューニングはコードで管理しづらい側面があります。

理想的な運用としては、変更が多い部分(Custom Instructions、Verified Queriesなど)と、メトリックスの定義など管理したい部分を分けて運用できるようになると良いでしょう。例えば、メトリックスやディメンションの定義はコードで管理し、Verified QueriesやCustom InstructionsはSnowflakeのマネージド機能で管理する、といった使い分けが考えられます。

参考リンク

https://github.com/Snowflake-Labs/dbt_semantic_view
https://github.com/snowflakedb/terraform-provider-snowflake/releases/tag/v2.11.0
https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst/analyst-optimization

Finatext Tech Blog

Discussion