3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

セマンティックレイヤってなんなのをBigQueryで確認する

Posted at

はじめに

「セマンティックレイヤ(Semantic Layer)」という言葉を最近よく聞く。

「指標の一元管理ができる」「ビジネスユーザーが迷わない」といったメリットはよく語られるが、「で、結局裏側では何が起きてるの?」 という部分がいまいちよくわからない。
今回は、dbt Cloudを使って実際にセマンティックレイヤを構築し、Google スプレッドシートからデータを引き出した時に、裏側のBigQueryで一体どのようなクエリが実行され、どのように処理されているのかを検証してみた。

結論から言うと、セマンティックレイヤは「複雑な論理をBigQueryの圧倒的な計算リソースに丸投げするアーキテクチャ」 だった。

なお、dbt Cloudの設定やGoogle スプレッドシートのアドオンの使い方については本記事では言及しない。公式ドキュメントを参照されたい。

検証構成

なるべく無料で、かつ誰でも再現できるように以下の構成で行った。

  • Data: BigQuery Public Data (thelook_ecommerce)
  • Logic: dbt Cloud (Trial)
  • Interface: Google Sheets
  • Compute: Google BigQuery

準備: dbtで「売上」を定義する

まずはdbt側でSemantic Layerの定義を行う。 使用するのは bigquery-public-data.thelook_ecommerce のデータ。
ここでのポイントは、あえて 「前月比(MoM)」のような、SQLで書くと面倒な指標 を定義に含めている点。

semantic_models:
  # --------------------------------------------------
  # 1. 物理データ(Table)を論理概念(Model)にマッピングする
  # --------------------------------------------------
  - name: orders_semantic
    description: "注文データのセマンティックモデル"
    model: ref('orders') # ここで実体となるdbtモデル(物理テーブル)を参照
    defaults:
      agg_time_dimension: ordered_at # デフォルトの時間軸を指定
    
    # Entity: データの「結合キー」を定義する場所
    # これが定義されていることで、dbtが自動的にJOINパスを解決できる(Semantic Graph)
    entities:
      - name: order_id
        type: primary
      - name: user_id
        type: foreign # usersテーブルのprimary keyと結合可能になる

    # Dimension: 分析の「切り口」を定義する場所
    dimensions:
      - name: ordered_at
        type: time
        type_params:
          time_granularity: day
        # BigQueryの型厳密性への対策(Time Spineとの型合わせ)
        expr: DATETIME(ordered_at)
        
      - name: status
        type: categorical

    # Measure: 指標の「素材」となる集計計算
    # ユーザーが直接見るものではなく、Metricsを作るための部品
    measures:
      - name: revenue_sum
        description: "売上金額の合計"
        expr: revenue  # 物理カラム名
        agg: sum       # 集計方法

  - name: users_semantic
    description: "ユーザー属性データ"
    model: ref('users')
    entities:
      - name: user_id
        type: primary # ここがorders側のforeign keyと繋がる
        expr: user_id
    dimensions:
      - name: country
        type: categorical
        description: "国"
      - name: age
        type: categorical
        description: "年齢"
      - name: gender
        type: categorical
        description: "性別"

  # --------------------------------------------------
  # 2. 時間計算のための「背骨(Spine)」定義
  # --------------------------------------------------
  # 前月比や累積などを計算するために必須となるカレンダーテーブル
  - name: metricflow_time_spine
    model: ref('metricflow_time_spine')
    defaults:
      agg_time_dimension: date_day
    entities:
      - name: date_day_key
        type: primary
        expr: date_day
    dimensions:
      - name: date_day
        type: time
        type_params:
          time_granularity: day

# --------------------------------------------------
# 3. ビジネスユーザーに見せる「指標(Metrics)」の定義
# --------------------------------------------------
metrics:
  # シンプルな指標:Measureをそのまま見せるだけ
  - name: total_revenue
    label: "売上合計"
    description: "全期間の売上合計"
    type: simple
    type_params:
      measure: revenue_sum
  
  # 派生指標(Derived Metric)
  # SQLで書くと面倒な「前月比」などを、数式(expr)として定義するだけでよい
  - name: revenue_mom_growth
    label: "売上_前月比(MoM)"
    description: "売上の前月比成長率"
    type: derived
    type_params:
      expr: (current_rev - prev_rev) / prev_rev
      metrics:
        - name: total_revenue
          alias: current_rev
        - name: total_revenue
          offset_window: 1 month # 1ヶ月ずらしたデータを自動取得
          alias: prev_rev

これで「売上(Revenue)」という指標が、コードとして定義される。

スプレッドシートからセマンティックレイヤを体験する

「dbt Semantic Layer for Google Sheets」アドオンを使うと、エンジニアがSQLを書かなくても、スプレッドシートのサイドバーから「Revenue(売上)」と「Created At(日付)」を選ぶだけでデータが参照可能。

semantic_layer.gif

YAMLで定義した 売上合計売上_前月比(MoM) が、スプレッドシート上で簡単に取得できる。これがセマンティックレイヤの便利なところ。

BigQueryの「実行グラフ」でセマンティックレイヤの裏側を覗く

スプレッドシートで「データ取得」ボタンを押したその瞬間、裏側のBigQueryでは何が起きていたのか?
Google Cloudコンソールの「ジョブ履歴」から、実際に発行されたクエリを覗いてみる。

WITH sma_10001_cte AS (
  SELECT
    DATETIME_TRUNC(DATETIME(ordered_at), month) AS order_id__ordered_at__month
    , user_id
    , revenue AS __total_revenue
  FROM `PROJECT`.`semantic_layer_demo`.`orders` orders_semantic_src_10000
)

, rss_10003_cte AS (
  SELECT
    country
    , user_id
  FROM `PROJECT`.`semantic_layer_demo`.`users` users_semantic_src_10000
)

SELECT
  order_id__ordered_at__month AS order_id__ordered_at__month
  , user_id__country AS user_id__country
  , (current_rev - prev_rev) / prev_rev AS revenue_mom_growth
FROM (
  SELECT
    COALESCE(subq_8.order_id__ordered_at__month, subq_21.order_id__ordered_at__month) AS order_id__ordered_at__month
    , COALESCE(subq_8.user_id__country, subq_21.user_id__country) AS user_id__country
    , MAX(subq_8.current_rev) AS current_rev
    , MAX(subq_21.prev_rev) AS prev_rev
  FROM (
    SELECT
      sma_10001_cte.order_id__ordered_at__month AS order_id__ordered_at__month
      , rss_10003_cte.country AS user_id__country
      , SUM(sma_10001_cte.__total_revenue) AS current_rev
    FROM sma_10001_cte
    LEFT OUTER JOIN
      rss_10003_cte
    ON
      sma_10001_cte.user_id = rss_10003_cte.user_id
    GROUP BY
      order_id__ordered_at__month
      , user_id__country
  ) subq_8
  FULL OUTER JOIN (
    SELECT
      subq_19.order_id__ordered_at__month AS order_id__ordered_at__month
      , subq_15.user_id__country AS user_id__country
      , subq_15.__total_revenue AS prev_rev
    FROM (
      SELECT
        DATETIME_TRUNC(date_day, month) AS order_id__ordered_at__month
      FROM `PROJECT`.`semantic_layer_demo`.`metricflow_time_spine` time_spine_src_10000
      GROUP BY
        order_id__ordered_at__month
    ) subq_19
    INNER JOIN (
      SELECT
        sma_10001_cte.order_id__ordered_at__month AS order_id__ordered_at__month
        , rss_10003_cte.country AS user_id__country
        , SUM(sma_10001_cte.__total_revenue) AS __total_revenue
      FROM sma_10001_cte
      LEFT OUTER JOIN
        rss_10003_cte
      ON
        sma_10001_cte.user_id = rss_10003_cte.user_id
      GROUP BY
        order_id__ordered_at__month
        , user_id__country
    ) subq_15
    ON
      DATE_SUB(CAST(subq_19.order_id__ordered_at__month AS DATETIME), INTERVAL 1 month) = subq_15.order_id__ordered_at__month
  ) subq_21
  ON
    (
      subq_8.user_id__country = subq_21.user_id__country
    ) AND (
      subq_8.order_id__ordered_at__month = subq_21.order_id__ordered_at__month
    )
  GROUP BY
    order_id__ordered_at__month
    , user_id__country
) subq_22

かなり冗長かつ複雑なSQLに変換されている。
人間が書くなら LAG() 関数などでスマートに済ませるところだが、dbtは「今月の集計」と「先月の集計」をそれぞれ独立して計算し、最後に FULL OUTER JOIN で結合している。
これは、「データ欠損時でも計算が破綻しないようにする」 という、ツールならではの安全性重視のアプローチゆえの冗長性だ。

実際、Geminiに「リファクタして」というとかなりシンプルなSQLに書き換えられる
WITH monthly_revenue AS (
  -- 1. 注文とユーザーを結合し、月×国ごとの売上を集計
  SELECT
    DATETIME_TRUNC(DATETIME(o.ordered_at), MONTH) AS order_month,
    u.country,
    SUM(o.revenue) AS current_rev
  FROM `PROJECT.semantic_layer_demo.orders` AS o
  LEFT JOIN `PROJECT.semantic_layer_demo.users` AS u
    ON o.user_id = u.user_id
  GROUP BY
    1, 2
),

rev_with_prev AS (
  -- 2. LAG関数を使って「同じ国の1ヶ月前」の売上を取得
  SELECT
    order_month,
    country,
    current_rev,
    LAG(current_rev) OVER (PARTITION BY country ORDER BY order_month) AS prev_rev
  FROM monthly_revenue
)

-- 3. 前月比成長率を計算
SELECT
  order_month,
  country,
  -- ゼロ除算エラーを防ぐため SAFE_DIVIDE を推奨
  SAFE_DIVIDE(current_rev - prev_rev, prev_rev) AS revenue_mom_growth
FROM rev_with_prev
ORDER BY
  order_month DESC, country

「こんな複雑なクエリ、パフォーマンスは大丈夫なのか?」と思うところ。BigQueryの 「実行グラフ(Execution Graph)」 を確認してみる。

BigQueryの実行グラフ

本来なら複数のステージに分かれて実行される(はず)の複雑なクエリだが、実行グラフは『S00』の1つだけ。
これはBigQueryがクエリを解析し、データの移動コストが最小になるよう、全ての演算を単一のステージに押し込めた(最適化した)結果と考えられる。
人間が書いた冗長なクエリを、BigQueryは瞬時に『最短経路』に書き換えて実行している。

また、約50万行のデータを読み込み、別テーブルと結合し、月次集計して1000行にまとめるという処理が、0.1秒で完了している。
セマンティックレイヤがどんなに複雑なSQLを生成しようとも、このレスポンス速度なら『Web画面(Looker Studioやスプレッドシート)』での体感速度は爆速のまま。

まとめ

セマンティックレイヤの実態は 「複雑性(Complexity)を人手によるSQL管理から、BigQueryのコンピュートリソースへとオフロードする技術」 だと言える。

  • セマンティックレイヤは、SQLを自動生成する「コンパイラ」である
  • 生成されるクエリは複雑になるが、BigQueryのオプティマイザと分散処理がそれを解決する

「論理を定義するツール」と「それを処理する強力なプラットフォーム(Google Cloud)」が組み合わさって初めて価値が発揮されると感じた。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?