
Omniで動的に年次・月次などの集計テーブルを参照してスキャンコストを削減できる「Aggregate Awareness」を試してみた
さがらです。
BIツールを用いた分析を行う際、最も粒度が細かいテーブルを都度参照するとレコード量が多いためスキャンコストがかかってしまうことがあります。
そんな問題に対して、Omniは動的に年次・月次などの集計テーブルを参照してスキャンコストを削減できる「Aggregate Awareness」を備えています。
この機能を試してみたので、試した内容について本記事でまとめてみます。
やること
まず前提として、粒度が細かい注文レベルの情報を保持するordersテーブルがあり、Omniでは以下のようにorders.viewを定義していたとします。
このorders.viewに対して、年次・月次で集計したテーブルを別途用意し、Aggregate Awarenessがどのように動作するかを確かめてみます。
# Reference this view as omni_dbt_mart__orders
schema_label: ""
description: 注文概要データマート 。顧客の最初の注文かどうか、食べ物と飲み物の内訳など、各注文の重要な詳細を提供します。注文ごとに 1 行。
schema: omni_dbt_mart
table_name: ORDERS
dimensions:
order_id:
sql: '"ORDER_ID"'
format: ID
#This description was pulled from dbt.
description: プライマリキー
location_id:
sql: '"LOCATION_ID"'
format: ID
customer_id:
sql: '"CUSTOMER_ID"'
format: ID
#This description was pulled from dbt.
description: 注文顧客と紐づける外部キー
subtotal_cents:
sql: '"SUBTOTAL_CENTS"'
tax_paid_cents:
sql: '"TAX_PAID_CENTS"'
order_total_cents:
sql: '"ORDER_TOTAL_CENTS"'
subtotal:
sql: '"SUBTOTAL"'
tax_paid:
sql: '"TAX_PAID"'
order_total:
sql: '"ORDER_TOTAL"'
#This description was pulled from dbt.
description: 税込みの注文総額 (USD)
order_cost:
sql: '"ORDER_COST"'
#This description was pulled from dbt.
description: 注文を履行するための供給費の合計
order_items_subtotal:
sql: '"ORDER_ITEMS_SUBTOTAL"'
count_food_items:
sql: '"COUNT_FOOD_ITEMS"'
count_drink_items:
sql: '"COUNT_DRINK_ITEMS"'
count_order_items:
sql: '"COUNT_ORDER_ITEMS"'
is_food_order:
sql: '"IS_FOOD_ORDER"'
#This description was pulled from dbt.
description: この注文に食品が含まれているかどうかを示すブール値
is_drink_order:
sql: '"IS_DRINK_ORDER"'
#This description was pulled from dbt.
description: この注文にドリンクアイテムが含まれているかどうかを示すブール値
customer_order_number:
sql: '"CUSTOMER_ORDER_NUMBER"'
purchased_at:
sql: '"PURCHASED_AT"'
#This description was pulled from dbt.
description: 注文が行われたタイムスタンプ
measures:
count:
aggregate_type: count
order_total_sum:
sql: ${omni_dbt_mart__orders.order_total}
aggregate_type: sum
orders_count_new_customer:
sql: |-
COUNT(
CASE
WHEN customer_order_number = 1 THEN 1
ELSE 0
END
)
label: Orders Count New Customer
# --- 以下はaggregate awareness検証用---
total_revenue:
sql: ${omni_dbt_mart__orders.order_total}
aggregate_type: sum
total_cost:
sql: ${omni_dbt_mart__orders.order_cost}
aggregate_type: sum
total_subtotal:
sql: ${omni_dbt_mart__orders.order_items_subtotal}
aggregate_type: sum
total_tax:
sql: ${omni_dbt_mart__orders.tax_paid}
aggregate_type: sum
total_profit:
sql: ${omni_dbt_mart__orders.order_total} - ${omni_dbt_mart__orders.order_cost}
aggregate_type: sum
total_items_sold:
sql: ${omni_dbt_mart__orders.count_order_items}
aggregate_type: sum
total_food_items:
sql: ${omni_dbt_mart__orders.count_food_items}
aggregate_type: sum
total_drink_items:
sql: ${omni_dbt_mart__orders.count_drink_items}
aggregate_type: sum
count_orders_with_food:
aggregate_type: count
filters:
is_food_order:
is: true
count_orders_with_drink:
aggregate_type: count
filters:
is_drink_order:
is: true
#The info below was pulled from your dbt repository and is read-only.
dbt:
name: orders
target_schema: PROD
description: 注文概要データマート 。顧客の最初の注文かどうか、食べ物と飲み物の内訳など、各注文の重要な詳細を提供します。注文ごとに 1 行。
config:
schema: mart
materialized: table
code: |-
with
orders as (
select * from {{ ref('stg_orders') }}
),
order_items as (
select * from {{ ref('order_items') }}
),
order_items_summary as (
select
order_id,
sum(supply_cost) as order_cost,
sum(product_price) as order_items_subtotal,
count(order_item_id) as count_order_items,
sum(
case
when is_food_item then 1
else 0
end
) as count_food_items,
sum(
case
when is_drink_item then 1
else 0
end
) as count_drink_items
from order_items
group by 1
),
compute_booleans as (
select
orders.*,
order_items_summary.order_cost,
order_items_summary.order_items_subtotal,
order_items_summary.count_food_items,
order_items_summary.count_drink_items,
order_items_summary.count_order_items,
order_items_summary.count_food_items > 0 as is_food_order,
order_items_summary.count_drink_items > 0 as is_drink_order
from orders
left join
order_items_summary
on orders.order_id = order_items_summary.order_id
),
customer_order_count as (
select
*,
row_number() over (
partition by customer_id
order by purchased_at asc
-- order by ordered_at asc
) as customer_order_number
from compute_booleans
)
select * from customer_order_count
referenced_by: [ customers, orders_yearly, orders_monthly, product_total_sales ]
dbtで年次・月次の集計テーブルの作成
まず、dbtを使って年次・月次の集計テーブルを作成します。下図のように作成しておきました。(dbtで直にmodelを作成せずとも、OmniのWorkbook上で選択したフィールドに基づいた集計結果をdbtのModelとしてプッシュできる機能を用いる方法もあります。)
- 年次

- 月次

このテーブルを使用するDWHの本番環境にもビルド後、OmniのShared ModelでRefresh schemaを押します。

これで、Omni側にも年次・月次の集計テーブルを参照するviewが作られました。(下図は月次のテーブルを元に作られたview)

年次・月次のviewに対してmaterialized_queryを追加
次に、Omniに作られた年次・月次のviewに対してmaterialized_queryを追加します。materialized_queryはAggregate Awareness用のオブジェクトで、集計前のviewのフィールドと比較して集計テーブルのどのカラムがマッチするのかを定義します。
重要なポイントはfields:の定義順序で、DWH上の集計テーブルのカラムの順序に合わせて、集計前のviewのフィールドのうちどのフィールドがマッチするのか定義する必要があります。また、日付フィールドは[month]や[year]のように書いて、どの粒度の日付フィールドと集計テーブルの日付カラムがマッチするのか定義する必要があるためご注意ください。
今回は下記のように定義しました。(スキーマ名はDynamic Schema検証の影響でvirtual schemaになっています。)
- 年次 ※以下の内容を年次集計テーブルから作られたOmniのviewの最下部に追記
materialized_query:
fields:
[
"omni_dbt_mart__orders.purchased_at[year]",
omni_dbt_mart__orders.total_revenue,
omni_dbt_mart__orders.total_cost,
omni_dbt_mart__orders.total_subtotal,
omni_dbt_mart__orders.total_tax,
omni_dbt_mart__orders.total_profit,
omni_dbt_mart__orders.count,
omni_dbt_mart__orders.total_items_sold,
omni_dbt_mart__orders.total_food_items,
omni_dbt_mart__orders.total_drink_items,
omni_dbt_mart__orders.count_orders_with_food,
omni_dbt_mart__orders.count_orders_with_drink
]
base_view: omni_dbt_mart__orders
- 月次 ※以下の内容を月次集計テーブルから作られたOmniのviewの最下部に追記
materialized_query:
fields:
[
"omni_dbt_mart__orders.purchased_at[month]",
omni_dbt_mart__orders.total_revenue,
omni_dbt_mart__orders.total_cost,
omni_dbt_mart__orders.total_subtotal,
omni_dbt_mart__orders.total_tax,
omni_dbt_mart__orders.total_profit,
omni_dbt_mart__orders.count,
omni_dbt_mart__orders.total_items_sold,
omni_dbt_mart__orders.total_food_items,
omni_dbt_mart__orders.total_drink_items,
omni_dbt_mart__orders.count_orders_with_food,
omni_dbt_mart__orders.count_orders_with_drink
]
base_view: omni_dbt_mart__orders
これらの内容を追記後、下図のようになります。(下図は月次のテーブルを元に作られたview)

Aggregate Awarenessの動作確認
これで設定は完了のため、Aggregate Awarenessがどのように動作するかを確認してみます!(参考までに確認手順としては、今回はブランチでShared Modelを編集しているため、IDEの右上からExploreで動作確認します。)
年次の集計テーブルに対する動作確認
まず、年次の集計テーブルが参照されているか確認するため、Yearのディメンションとメジャーを1つ選択してみます。この状態だとAggregate Awarenessが効いていないように見えます。

実際にDWHに発行されているクエリを確認するため、Inspectorを表示します。

InspectorからDWHへ発行されたクエリを確認すると、FROM句の後が"MART"."ORDERS_YEARLY"と年次の集計テーブルを参照していることがわかります!

月次の集計テーブルに対する動作確認
続いて、Monthのディメンションを用いてクエリを発行してみます。

InspectorからDWHへ発行されたクエリを確認すると、FROM句の後が"MART"."ORDERS_MONTHLY"と月次の集計テーブルを参照していることがわかります!

注意点:キャッシュが効いている場合
注意点として、Omniのキャッシュが効いている場合はAggregate Awarenessは機能せず、キャッシュされた結果からクエリするようになっています。
※今回の検証で私も初めて知ったのですが、キャッシュされた結果から一部フィールドを選択、あるいはWHERE句で絞り込んで結果を返せる場合でも、キャッシュが効きます!これは凄い…(Omniのキャッシュに関する公式Doc)
参考までに、キャッシュが効いている場合のInspectorの画像を以下に貼っておきます。


最後に
Omniで動的に年次・月次などの集計テーブルを参照してスキャンコストを削減できる「Aggregate Awareness」を試してみました。
レコード量が多いデータに対する分析を行う際にとても有用に働く機能だと思いますので、ぜひご活用ください!








