Eコマース企業がデータウェアハウスを構築する際、過去 30 日間のユニークビジター (UV)、購入者数、リピート購入者数などのローリングウィンドウメトリックを計算する必要があることがよくあります。これらのメトリックは長期間にわたって蓄積されたデータに対して計算され、ナイーブなローリングウィンドウクエリは、大規模になると処理が非常に遅くなるか、完全に失敗します。
根本原因は常に同じです。実行ごとに大量のデータがゼロから再計算されるためです。このトピックで紹介する 2 つの最適化アプローチは、データ事前集計または増分蓄積によって完全な再計算を回避するという共通の原則を共有しており、その原則をどの程度まで適用するかという点で異なります。
すべてのコードサンプルは、DataWorks のスケジューリング変数 (例: ${bdp.system.bizdate}) を使用しています。これらのサンプルは、DataWorks のスケジューリングノードにのみ適用されます。ローリングウィンドウクエリの問題点
一般的な 30 日間の UV クエリは次のようになります。
SELECT item_id,
COUNT(DISTINCT visitor_id) AS ipv_uv_1d_001
FROM vistor_item_detail_log
WHERE ds <= ${bdp.system.bizdate}
AND ds >= TO_CHAR(DATEADD(TO_DATE(${bdp.system.bizdate},'yyyymmdd'),-29,'dd'),'yyyymmdd')
GROUP BY item_id;このクエリは、実行ごとに生ログデータの 30 個のパーティションをスキャンします。ログボリュームが大きい場合、MaxCompute は 99,999 を超えるマップタスクを生成する必要がある場合があります。その時点でジョブは失敗します。その制限を下回る場合でも、毎日 30 日間の生ログをスキャンするのは無駄です。ほとんどのデータは前回の実行から変更されていません。
アプローチの選択
この問題に対処する 2 つの最適化パターンがあります。データとクエリの構造に基づいて選択してください。
| アプローチ | 仕組み | 使用する状況 |
|---|---|---|
| 中間テーブル | 日次で生ログを重複排除し、サマリーテーブルに集計します。生ログの代わりにサマリーテーブルをクエリします。 | 日次ログボリュームが大きい場合;パーティションごとのデータが実行間で大幅に変化する場合。 |
| 増分蓄積 | すべての既存パーティションを単一の蓄積パーティションにマージし、毎日新しいデータのみを追加します。 | 同じローリングウィンドウを繰り返しクエリする場合;日次重複排除よりも複数パーティションスキャンの削減が重要である場合。 |
ほとんどの Eコマースワークロードでは、中間テーブルアプローチから開始します。中間テーブルが導入された後も複数パーティションスキャンがボトルネックのままである場合は、増分蓄積に移行してください。
アプローチ 1: 中間テーブル
仕組み
30 個の生ログパーティションをクエリする代わりに、毎日ログを事前集計してサマリーテーブルに格納します。サマリーテーブルの各行は、その日のユニークな (item_id, visitor_id) ペアを表します。事前集計されたデータの 30 個のパーティションをクエリする方が、生ログの 30 個のパーティションをクエリするよりもはるかに安価です。これは、事前集計ステップによって、ローリングウィンドウクエリが実行される前にカーディナリティの高い重複行が削除されるためです。
ステップ 1: 日次サマリーの作成
これを、メトリックノードの前に DataWorks で日次スケジューリングノードとして実行します。
INSERT OVERWRITE TABLE mds_itm_vsr_xx (ds='${bdp.system.bizdate} ')
SELECT item_id,
visitor_id,
COUNT(1) AS pv
FROM (
SELECT item_id,
visitor_id
FROM vistor_item_detail_log
WHERE ds = ${bdp.system.bizdate}
GROUP BY item_id, visitor_id
) a;これにより、今日のデータパーティションの mds_itm_vsr_xx に、(item_id, visitor_id) ペアごとに重複排除された 1 行が書き込まれます。
ステップ 2: サマリーテーブルからのクエリ
サマリーテーブルが用意されている場合、ローリングウィンドウクエリがスキャンするデータははるかに少なくなります。
SELECT item_id,
COUNT(DISTINCT visitor_id) AS uv,
SUM(pv) AS pv
FROM mds_itm_vsr_xx
WHERE ds <= '${bdp.system.bizdate} '
AND ds >= TO_CHAR(DATEADD(TO_DATE('${bdp.system.bizdate} ','yyyymmdd'),-29,'dd'),'yyyymmdd')
GROUP BY item_id;このアプローチでは、mds_itm_vsr_xx を入力するための専用の日次ノードが必要です。DataWorks で、このノードをメトリックノードの上流依存関係として構成し、ダウンストリームクエリが実行される前にサマリーテーブルが常に最新の状態になるようにします。
アプローチ 2: 増分蓄積
仕組み
中間テーブルアプローチは、実行ごとに 30 個のパーティションをスキャンします。増分蓄積はさらに進んで、すべての既存データを単一の蓄積パーティションにマージし、毎日新しいデータのみを追加します。これにより、ローリングウィンドウクエリは 1 つのパーティションからのみ読み取ります。
これは、日次で変更されるデータの割合が、既存データセット全体に対して小さい場合に最も効果的です。つまり、蓄積パーティション内のほとんどのデータが実行間で安定している場合です。
実装: リピート購入者ディメンションテーブル
過去 30 日以内に購入した購入者であるリピート購入者のユースケースは良い例です。ナイーブなアプローチでは、実行ごとに課金ログの 30 個のパーティションをスキャンします。
SELECT item_id,
buyer_id AS old_buyer_id
FROM buyer_item_detail_log
WHERE ds < ${bdp.system.bizdate}
AND ds >= TO_CHAR(DATEADD(TO_DATE(${bdp.system.bizdate},'yyyymmdd'),-29,'dd'),'yyyymmdd')
GROUP BY item_id,
buyer_id;代わりに、購入者とアイテムの関係を表すディメンションテーブルを維持し、初回購入時間、最終購入時間、購入アイテムの合計、合計支出などのフィールドを記録します。これを前日の課金ログで毎日更新します。
購入者がリピート購入者であるかどうかを判断するには、その last_purchase_time が過去 30 日以内であるかどうかを確認します。これにより、課金ログの 30 パーティションスキャンが、ディメンションテーブルに対する単一パーティションルックアップに置き換えられ、実行ごとのフルスキャン重複排除が不要になります。
パフォーマンスに関する考慮事項
| 要因 | 中間テーブル | 増分蓄積 |
|---|---|---|
| 実行ごとにスキャンされるパーティション数 | 30(ただし、行の高さが小さい場合) | 1 |
| 実装の複雑さ | 低 — 日次ノードが 1 つ追加されるだけ | 高 — マージロジックと依存関係管理 |
| データの新鮮さ | 日次 | 日次 |
| 最適な適合 | カーディナリティの高いキーを持つ大規模な日次ログボリューム | 安定した既存データに対する繰り返しのローリングウィンドウクエリ |
どちらのアプローチも、高コストな計算をクエリ時間からインジェスト時間にシフトします。中間テーブルはこれを日次で行い、増分蓄積はすべての既存日に対して行います。