Semantic View の NON ADDITIVE BY 句と Range Join を試してみた #SnowflakeDB

Semantic View の NON ADDITIVE BY 句と Range Join を試してみた #SnowflakeDB

2026.03.08

はじめに

2026年2、3月のアップデートでセマンティックビューに関する以下の機能が追加されました。

  • NON ADDITIVE BY 句(半加算メトリクスの定義)
  • 値の範囲を含む論理テーブルの結合(範囲結合)

https://docs.snowflake.com/en/release-notes/2026/other/2026-03-05-semantic-views-semi-additive-metrics

https://docs.snowflake.com/en/release-notes/2026/other/2026-02-25-semantic-views-range-joins

それぞれ試してみた内容を本記事でまとめてみます。

NON ADDITIVE BY 句

本オプションについては、以下に記載があります。

https://docs.snowflake.com/en/user-guide/views-semantic/sql#label-semantic-views-metrics-semi-additive

セマンティックビューでは、ディメンション・メトリクスを定義することで、指定の分析軸での集計処理が可能となります。

この際、データの内容によっては、あるディメンションで GROUP BY して合計すると二重計上になったり、過大計上になることで、意味が意図するものと異なる可能性があります。そこで、そういったディメンションは非加算(non-additive)として扱うように設定できるオプションとなります。

公式ドキュメントの例がわかりやすく、残高や在庫数の日次でのスナップショットなどを対象に使用できます。 SQL 構文上は NON ADDITIVE BY(非加算)と指定しますが、ドキュメントではこの機能で作られるメトリクスを半加算(semi-additive)メトリクスと呼んでいます。

具体的には、「時間などの特定のディメンション方向には合計せず最新値を取得し、それ以外のディメンション(顧客や組織など)方向には取得した最新値同士を合計する」という処理が可能で、非加算と加算の両方の性質を併せ持っているメトリクスとなります。

サンプルデータ

テーブルスキーマとしては、こちらに記載がある REMAINING_BALANCE_DAILY ビューを参考に、サンプルデータとして以下のテーブルを用意します。組織Aと組織Bが日々残高を消費していく例です。

CREATE OR REPLACE TABLE demo_remaining_balance_daily (
  organization_name  VARCHAR,
  contract_number    VARCHAR,
  date               DATE,
  currency           VARCHAR,
  rollover_balance   NUMBER(38,2),
  capacity_balance   NUMBER(38,2),
  free_usage_balance NUMBER(38,2)
);

-- 前提:
-- - 日々の消費はまず rollover_balance から引かれる
-- - rollover_balance が 0 になったら capacity_balance から引かれる
-- - いずれも期末残高(end-of-day snapshot)として記録されるイメージ

INSERT INTO demo_remaining_balance_daily VALUES
-- === orgA のデータ ===
-- Day 0(開始時点)
('orgA','contract-001','2026-03-01','USD', 120.00, 500.00, 0.00),
-- 3/02 消費 70 → rollover 120 -> 50(capacityは維持)
('orgA','contract-001','2026-03-02','USD',  50.00, 500.00, 0.00),
-- 3/03 消費 60 → rollover 50 を使い切り、残り10を capacityから消費
('orgA','contract-001','2026-03-03','USD',   0.00, 490.00, 0.00),
-- 3/04 消費 40 → rolloverは0なので capacityから消費:490 -> 450
('orgA','contract-001','2026-03-04','USD',   0.00, 450.00, 0.00),
-- 3/05 消費 100 → capacity:450 -> 350
('orgA','contract-001','2026-03-05','USD',   0.00, 350.00, 0.00),

-- === orgB のデータ(追加分) ===
-- Day 0(開始時点)
('orgB','contract-002','2026-03-01','USD', 200.00, 1000.00, 0.00),
-- 3/02 消費 100 → rollover 200 -> 100
('orgB','contract-002','2026-03-02','USD', 100.00, 1000.00, 0.00),
-- 3/03 消費 50 → rollover 100 -> 50
('orgB','contract-002','2026-03-03','USD',  50.00, 1000.00, 0.00),
-- 3/04 消費 150 → rollover 50 を使い切り、残り100を capacityから消費
('orgB','contract-002','2026-03-04','USD',   0.00,  900.00, 0.00),
-- 3/05 消費 200 → capacity:900 -> 700
('orgB','contract-002','2026-03-05','USD',   0.00,  700.00, 0.00);

>SELECT * FROM demo_remaining_balance_daily ORDER BY organization_name, date;
+-------------------+-----------------+------------+----------+------------------+------------------+--------------------+
| ORGANIZATION_NAME | CONTRACT_NUMBER | DATE       | CURRENCY | ROLLOVER_BALANCE | CAPACITY_BALANCE | FREE_USAGE_BALANCE |
|-------------------+-----------------+------------+----------+------------------+------------------+--------------------|
| orgA              | contract-001    | 2026-03-01 | USD      |           120.00 |           500.00 |               0.00 |
| orgA              | contract-001    | 2026-03-02 | USD      |            50.00 |           500.00 |               0.00 |
| orgA              | contract-001    | 2026-03-03 | USD      |             0.00 |           490.00 |               0.00 |
| orgA              | contract-001    | 2026-03-04 | USD      |             0.00 |           450.00 |               0.00 |
| orgA              | contract-001    | 2026-03-05 | USD      |             0.00 |           350.00 |               0.00 |
| orgB              | contract-002    | 2026-03-01 | USD      |           200.00 |          1000.00 |               0.00 |
| orgB              | contract-002    | 2026-03-02 | USD      |           100.00 |          1000.00 |               0.00 |
| orgB              | contract-002    | 2026-03-03 | USD      |            50.00 |          1000.00 |               0.00 |
| orgB              | contract-002    | 2026-03-04 | USD      |             0.00 |           900.00 |               0.00 |
| orgB              | contract-002    | 2026-03-05 | USD      |             0.00 |           700.00 |               0.00 |
+-------------------+-----------------+------------+----------+------------------+------------------+--------------------+

セマンティックビューを定義(NON ADDITIVE BY なし)

はじめに、このオプションを使わずに単純に合計するセマンティックビューを定義してみます。

CREATE OR REPLACE SEMANTIC VIEW demo_remaining_balance_daily_sv_1

  TABLES (
    remaining_balance AS TEST_DB.PUBLIC.demo_remaining_balance_daily
      PRIMARY KEY (organization_name, contract_number, date)
      COMMENT = 'Daily remaining balance data by organization and contract'
  )

  DIMENSIONS (
    remaining_balance.organization_dim AS organization_name
      COMMENT = 'Organization name',
    remaining_balance.contract_dim AS contract_number
      COMMENT = 'Contract number',
    remaining_balance.balance_date_dim AS date
      COMMENT = 'Balance date'
  )

  METRICS (
    remaining_balance.m_rollover_balance AS SUM(rollover_balance)
      COMMENT = 'Total rollover balance',
    remaining_balance.m_capacity_balance AS SUM(capacity_balance)
      COMMENT = 'Total capacity balance',
    remaining_balance.m_free_usage_balance AS SUM(free_usage_balance)
      COMMENT = 'Total free usage balance',
    remaining_balance.m_total_remaining_balance AS SUM(rollover_balance + capacity_balance + free_usage_balance)
      COMMENT = 'Total remaining balance (rollover + capacity + free usage)'
  )

  COMMENT = 'Semantic view for remaining balance analysis';

この状態で、Cortex Analystに問い合わせを行ってみます。

orgAの3/4時点のトータル残高を教えて

この質問では、以下のクエリが生成され、意図した結果になります。

SELECT
  SUM(
    rollover_balance + capacity_balance + free_usage_balance
  ) AS total_remaining_balance
FROM
  remaining_balance
WHERE
  organization_name = 'orgA'
  AND balance_date_dim = CAST('2026-03-04' AS DATE)
  /* Generated by Cortex Analyst (request_id: 535779b3-9a05-4cd3-b0cc-8588c732d491) */

image

次に、月単位として聞いてみます。

orgAの3月のトータル残高を教えて

image 1

SELECT
  *
FROM
  SEMANTIC_VIEW(
    TEST_DB.PUBLIC.DEMO_REMAINING_BALANCE_DAILY_SV_1 METRICS m_total_remaining_balance
    WHERE
      organization_dim = 'orgA'
      AND balance_date_dim >= '2026-03-01'
      AND balance_date_dim < '2026-04-01'
  ) -- Generated by Cortex Analyst (request_id: cd161678-5a28-4b8c-9da4-e44549b3d0e8)
;

結果として「2460.00」が返ってきました。指定された期間(3月)の日々の残高がすべて足し上げられてしまっています。

さらに組織を指定せず、全体のトータル残高を聞いてみます。

3月のトータル残高を教えて

image 2

SELECT
  *
FROM
  SEMANTIC_VIEW(
    TEST_DB.PUBLIC.DEMO_REMAINING_BALANCE_DAILY_SV_1 METRICS m_total_remaining_balance
    WHERE
      balance_date_dim >= DATE_TRUNC('MONTH', CURRENT_DATE)
      AND balance_date_dim < DATE_TRUNC('MONTH', CURRENT_DATE) + INTERVAL '1 month'
  ) -- Generated by Cortex Analyst (request_id: 709590ac-acfc-4433-acdf-1b4510abbdcc)
;

結果は「7410.00」となりました。単純な合計(非加算を指定しない場合)をしてしまうと、orgA の5日分の合計と、orgB の5日分の合計がすべて足されてしまい、過大計上された意味のない数字になってしまいます。

セマンティックビューを修正(NON ADDITIVE BY あり)

ここで、日付ディメンションを非加算として扱うようにセマンティックビューを修正します。メトリクスの定義にNON ADDITIVE BY (balance_date_dim ASC NULLS LAST)を追加します。

CREATE OR REPLACE SEMANTIC VIEW demo_remaining_balance_daily_sv

  TABLES (
    remaining_balance AS TEST_DB.PUBLIC.demo_remaining_balance_daily
      PRIMARY KEY (organization_name, contract_number, date)
      COMMENT = 'Daily remaining balance data by organization and contract'
  )

  DIMENSIONS (
    remaining_balance.organization_name_dim AS organization_name
      COMMENT = 'Organization name',

    remaining_balance.contract_number_dim AS contract_number
      COMMENT = 'Contract number',

    remaining_balance.balance_date_dim AS date 
      COMMENT = 'Balance date (snapshot date)'
  )

  METRICS (
    remaining_balance.m_rollover_balance
        NON ADDITIVE BY (balance_date_dim ASC NULLS LAST)
        AS SUM(rollover_balance)
      COMMENT = 'Rollover balance (semi-additive by balance_date)',

    remaining_balance.m_capacity_balance
        NON ADDITIVE BY (balance_date_dim ASC NULLS LAST)
        AS SUM(capacity_balance)
      COMMENT = 'Capacity balance (semi-additive by balance_date)',

    remaining_balance.m_free_usage_balance 
        NON ADDITIVE BY (balance_date_dim ASC NULLS LAST)
        AS SUM(free_usage_balance)
          COMMENT = 'Free usage balance (semi-additive by balance_date)',

    remaining_balance.m_total_remaining_balance
      NON ADDITIVE BY (balance_date_dim ASC NULLS LAST)
      AS  SUM(rollover_balance + capacity_balance + free_usage_balance)
      COMMENT = 'Total remaining balance (rollover + capacity + free usage)'
  )

  COMMENT = 'Semantic view for remaining balance analysis';

この状態で再度問いあわせます。

orgAの3月のトータル残高を教えて

image 3

結果として「350.00」が返ってきました。3月の中で最新時点(3/5)のスナップショットを返してくれています。

生成された SQL:

SELECT
  *
FROM
  SEMANTIC_VIEW(
    TEST_DB.PUBLIC.DEMO_REMAINING_BALANCE_DAILY_SV METRICS m_total_remaining_balance
    WHERE
      organization_name_dim = 'orgA'
      AND balance_date_dim >= DATE_TRUNC('month', CURRENT_DATE)
      AND balance_date_dim < DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month'
  ) -- Generated by Cortex Analyst (request_id: d258afed-19e2-435a-b7e6-0ebc542dbd25)
;

組織を指定せずに、全体のトータル残高を聞いてみます。

3月のトータル残高を教えて

image 4

結果は「1050.00」となりました。 半加算(NON ADDITIVE BY)が機能することで、非加算として指定された日付ディメンションでデータが並べ替えられ、期間内の各組織の一番最後(最新)の行が特定されます。 今回のデータでは、3月の最新スナップショットは「3月5日」のデータになります。

  • orgA の 3/05 時点のトータル残高:350.00
  • orgB の 3/05 時点のトータル残高:700.00

これらの最新値(スナップショット)同士が合計され、最終的に全体のトータル残高として 1050.00 が正しく返ってくることが確認できました。

生成された SQL:

SELECT
  *
FROM
  SEMANTIC_VIEW(
    TEST_DB.PUBLIC.DEMO_REMAINING_BALANCE_DAILY_SV METRICS m_total_remaining_balance
    WHERE
      DATE_TRUNC('MONTH', balance_date_dim) = DATE_TRUNC('MONTH', CURRENT_DATE)
  ) -- Generated by Cortex Analyst (request_id: ad77990d-38fd-43ba-ba09-64f8581e9e67)
;

なお、ここではSQLで定義しましたが、GUIからでも設定可能です。下図は SQL で定義した設定を GUI で確認した際の表示です。

image 5

Joining logical tables that contain ranges of values

本オプションについては、以下に記載があります。

https://docs.snowflake.com/en/user-guide/views-semantic/sql#label-semantic-views-custom-range-joins

セマンティックビューでは、論理テーブル間のリレーションシップ(結合条件)を定義することで、複数のテーブルを紐づけた分析が可能となります。

この際、データの内容によっては、イベントの発生時刻などを特定の期間や範囲に当てはめたい場合があります。通常の完全一致による結合では対応できないため、あるテーブルのタイムスタンプが、別のテーブルで定義された開始時刻と終了時刻の範囲内に収まるかどうかを条件として範囲結合できる機能となります。注意点として、結合先となる範囲同士は重複しないように定義する必要があります。

こちらも公式ドキュメントの例がわかりやすく、特定のタイムスタンプを持つ受注データを、四半期などの特定の期間のマスターデータに紐づけて分析するケースなどを対象に使用できます。

なお、セマンティックビューでは ASOF Join の定義も可能で、この場合はマスターデータ側に開始日などの単一の列しかなく、その時点での有効な最新の履歴データを紐づける点が異なります。

明確な開始と終了の範囲を持つ場合は、今回検証する範囲結合を使用します。

サンプルデータ

サンプルとして、四半期ごとの期間を定義するテーブル(demo_fiscal_quarters)と日々の売上データ(demo_sales_orders)を用意します。

範囲の終了日は、その日自体は含まない扱いとなるため、Q1 の終了日と Q2 の開始日を境界値(2024-04-01)として同じに設定しています。

-- 範囲を定義するテーブル
CREATE TABLE demo_fiscal_quarters (
  quarter_name VARCHAR,
  start_date DATE,
  end_date DATE
);

-- 範囲が重複しないように定義する ※終了日は「その日自体は含まない」扱いとなる
INSERT INTO demo_fiscal_quarters VALUES
  ('2024-Q1', '2024-01-01', '2024-04-01'),
  ('2024-Q2', '2024-04-01', '2024-07-01'),
  ('2024-Q3', '2024-07-01', '2024-10-01'),
  ('2024-Q4', '2024-10-01', '2025-01-01');

>SELECT * FROM demo_fiscal_quarters;
+--------------+------------+------------+
| QUARTER_NAME | START_DATE | END_DATE   |
|--------------+------------+------------|
| 2024-Q1      | 2024-01-01 | 2024-04-01 |
| 2024-Q2      | 2024-04-01 | 2024-07-01 |
| 2024-Q3      | 2024-07-01 | 2024-10-01 |
| 2024-Q4      | 2024-10-01 | 2025-01-01 |
+--------------+------------+------------+

-- タイムスタンプを持つテーブル
CREATE TABLE demo_sales_orders (
  order_id VARCHAR,
  order_date DATE,
  amount NUMBER
);

INSERT INTO test_db.public.demo_sales_orders VALUES
  ('ORD-001', '2024-01-15', 100), -- q1に分類される
  ('ORD-002', '2024-03-31', 150), -- q1に分類される
  ('ORD-003', '2024-04-01', 200), -- 境界値:q2に分類されるはず
  ('ORD-004', '2024-06-20', 300), -- q2に分類される
  ('ORD-005', '2024-10-15', 250); -- q4に分類される

>SELECT * FROM demo_sales_orders;
+----------+------------+--------+
| ORDER_ID | ORDER_DATE | AMOUNT |
|----------+------------+--------|
| ORD-001  | 2024-01-15 |    100 |
| ORD-002  | 2024-03-31 |    150 |
| ORD-003  | 2024-04-01 |    200 |
| ORD-004  | 2024-06-20 |    300 |
| ORD-005  | 2024-10-15 |    250 |
+----------+------------+--------+

セマンティックビューを定義

範囲を定義するディメンションテーブルにはDISTINCT RANGE BETWEEN ... EXCLUSIVEを使用した制約を追加し、イベントを範囲に紐づけるリレーションシップにはREFERENCESBETWEEN ... EXCLUSIVEを使用して結合条件を定義します。

CREATE OR REPLACE SEMANTIC VIEW demo_sales_quarter_sv

  TABLES (
    -- 範囲を持つディメンションテーブル
    fiscal_quarters AS test_db.public.demo_fiscal_quarters
      -- 範囲が重複しないことを保証する制約を追加
      CONSTRAINT no_overlap_ranges DISTINCT RANGE BETWEEN start_date AND end_date EXCLUSIVE
      COMMENT = 'Fiscal quarters defining date ranges',

    -- イベント(ファクト)が記録されたテーブル
    sales_orders AS test_db.public.demo_sales_orders
      PRIMARY KEY (order_id)
      COMMENT = 'Sales orders with specific dates'
  )

  RELATIONSHIPS (
    -- REFERENCES と BETWEEN ... EXCLUSIVE を使用して結合条件を定義
    sales_to_quarters AS sales_orders (order_date) REFERENCES fiscal_quarters (BETWEEN start_date AND end_date EXCLUSIVE)
  )

  DIMENSIONS (
    fiscal_quarters.quarter_name_dim AS quarter_name
      COMMENT = 'Fiscal Quarter Name (e.g., 2024-Q1)',

    sales_orders.order_id_dim AS order_id
      COMMENT = 'Order ID'
  )

  METRICS (
    sales_orders.m_total_sales_amount AS SUM(amount)
      COMMENT = 'Total sales amount'
  )

  COMMENT = 'Semantic view for testing range joins mapping events to time period'
  ;

GUIで確認すると、下図のように「Range」として設定されていました。

image 6

問い合わせ

この状態で以下の問い合わせを行いました。

2024-Q1のトータル売上を教えて

image 7

結果は「250」となりました。生成されたクエリを見ると、開始日以上(>=)、終了日未満(<)の条件で結合されていることがわかります。

SELECT
  SUM(so.amount) AS total_sales
FROM
  sales_orders AS so
  LEFT OUTER JOIN fiscal_quarters AS fq ON (
    fq.start_date IS NULL
    OR so.order_date >= fq.start_date
  )
  AND (
    fq.end_date IS NULL
    OR so.order_date < fq.end_date
  )
WHERE
  fq.quarter_name_dim = '2024-Q1'
  /* Generated by Cortex Analyst (request_id: 8d4ff580-a3c6-4229-8c76-6d55209eb066) */

別の問い合わせも行ってみます。

2024-Q2のトータル売上を教えて

image 8

結果は「500」となりました。 セマンティックビューの定義でEXCLUSIVEと指定したことで、終了時刻側の境界はその時刻まで(ただしその時刻自体は含まない)として扱われます。

そのため、境界線上の日付(4/1)とイベントのタイムスタンプが完全に一致した場合は、前のレンジの終了日には含まれず、その日付を開始日としている新しいレンジ(Q2)にデータが紐づくことが確認できました。

生成された SQL:

SELECT
  SUM(so.amount) AS total_sales
FROM
  sales_orders AS so
  LEFT OUTER JOIN fiscal_quarters AS fq ON (
    fq.start_date IS NULL
    OR so.order_date >= fq.start_date
  )
  AND (
    fq.end_date IS NULL
    OR so.order_date < fq.end_date
  )
WHERE
  fq.quarter_name_dim = '2024-Q2'
  /* Generated by Cortex Analyst (request_id: 31f01f5a-be9a-4bba-8b58-b2388f14a0bd) */

さいごに

セマンティックビューの NON ADDITIVE BY 句と Range Join を試してみました。時系列データの複雑な集計を意図する形で実現できる機能と思います。
こちらの内容がどなたかの参考になれば幸いです。

この記事をシェアする

FacebookHatena blogX

関連記事