Snowflake ウェアハウス最適化
クイックサマリー
適切なウェアハウスを正確に選択するには、さまざまなウェアハウスのベンチマークが必要です:
- ユーザーは、クエリ内でスキャンされるパーティション数に基づいて初期の推定ウェアハウスサイズを設定する必要があります
- ウェアハウスサイズを増やして「コスト」対「クエリ時間の短縮」を比較することでベンチマークします
- dbt DAG が消費するクレジットの合計の観点から、最小のモデルに対してモデルごとにウェアハウスを適切にサイジングしてもコスト削減にはあまりつながりません。
前置き
現在の dbt セットアップでは、モデルタグに基づいて本番 dbt ジョブを実行するために Large と X-Large ウェアハウスの組み合わせを使用しています。
ただし、dbt はモデルレベルでウェアハウスサイズを指定する機能を提供しています。
当初は、すべてのモデルに対してウェアハウスを適切にサイジングすることを検討し、潜在的なコスト削減を確認するためにこの Issue でベンチマークを実施しました。小さいモデルのウェアハウスを適切にサイジングしても、全体的なクレジット消費量を見たときに消費されるクレジットに大きな違いがないことがわかりました。
ただし、これはウェアハウスをモデルに適切にサイジングすることが不要なタスクであることを意味しません。適切にサイジングを試みるモデルを選択する必要があることを示しています。各モデルを適切にサイジングするには時間がかかるため、現在最も多くのクレジットを消費しているモデルに時間を割り当てる必要があります。
このページの残りの部分では以下を文書化します:
- ウェアハウス効率に関する主要な理論的概念:
- ウェアハウスのサイジングがコスト対パフォーマンスに与える影響
- 私たちのニーズにとって最も「効率的な」ウェアハウスとは何か
- 仮想的な例
- ウェアハウスを適切にサイジングする方法
- 必要な手順
- 実際の例
- モデルの適切なサイジングに時間を費やす場合
- 今後の手順
セクション 1: ウェアハウスのサイジング - 概念
ウェアハウスのサイジング - 概念
概念 1: コスト対パフォーマンス
ウェアハウスのサイズを1レベル増やすたびに、コンピューティングインスタンスの CPU と RAM を2倍にしています。対応して、消費するクレジットを通じて価格も2倍にしています。
価格が2倍になる一方で、理想的な世界では適切なワークロード/クエリに対して、コンピューティングインスタンスのリソースが2倍になるためクエリ時間は半減します。したがって、この状況では、どちらのウェアハウスでも同じ価格を支払いますが、大きいウェアハウスはクエリの実行時間を半分にするため、より効率的な選択肢になります。
最終的に、ウェアハウスをアップサイジングし続けると、クエリの実行時間がわずかに短縮されるだけでコストが不釣り合いに増加する収穫逓減のポイントに達します。 こちらに select.dev のコスト対パフォーマンスダイアグラムがあり、この点を示しています [1]。
概念 2: ウェアハウス効率の定義
ウェアハウス効率にはコストとパフォーマンスの2つの次元があります。「ウェアハウス効率」はモデル効率とは異なる概念です。
2つの次元があるため、「ウェアハウス効率」には明確な定義がありません。「コスト」と「パフォーマンス」をどれだけ重視するかに基づいて決定する必要があります。
私たちの場合、異なるウェアハウスを使用する際に次の2つの結果のいずれかを望んでいます:
- 既存の実行時間を維持しながらコストを削減する
- コストを大幅に増やさずに実行時間を短縮する
上記の要件に基づき、ウェアハウス効率を次のように明確に定義します:
ウェアハウスは、次のサイズが小さいウェアハウスと比較したとき、クエリの実行時間が 40% 以上短縮される場合に効率的です。
説明: 前のセクションで説明したように、次のサイズが大きいウェアハウスが損益分岐点に達するためには、2倍の速さで実行する(クエリの実行時間が**50%**短縮される)必要があります。これにより 2 倍の価格増加が相殺されます。
私たちの定義では、実行時間が少なくとも 40% 短縮されることを要求しており、これはコストの最大 20% 増加に相当します。実行時間の 50% 削減(コストの損益分岐点)を要求しない理由は次のとおりです:
- 既存の dbt 実行時間に近いパフォーマンスを維持する必要があります
- イテレーション的に作業したいため、より多くの余裕を持つ現実的な目標を設定し、さらに学ぶにつれてゴールポストを調整します。
補足: 実行時間のパーセンテージ改善の計算
技術的な詳細として、クエリ実行時間のパーセンテージ改善は、パフォーマンスが実行時間が減少するときに改善されるため(逆の関係)、少し混乱する場合があります。
実行時間のパーセンテージ改善は、従来の式 [2] を使用して計算します:
$$ \frac{new - old}{old} \times 100% $$
例えば、new = 5 と old = 10 という値がある場合、計算は次のようになります:
$$ \begin{align*} \frac{new - old}{old} \times 100% &= \frac{5 - 10}{10} \times 100% \ &= \frac{-5}{10} \times 100% \ &= -50% \end{align*} $$
つまり、新しい値は古い値より 50% 小さい(応答時間を話しているので、より速い)ということです。
少しあいまいに聞こえるかもしれないので、いくつかの仮想的な例を示します:
仮想的な例
例 1
実行時間が 50% 短縮され、1時間 -> 30分になりました。この場合、ウェアハウスサイズを増やすことは当然の選択です:
| ウェアハウスサイズ | クレジット/時 | 実行時間(時間) | コスト |
|---|---|---|---|
| X-small | 1 | 1 | $2 |
| Small | 2 | 0.5 | $2.00 |
| 増加率 | -50% | 0% |
例 2
実行時間が 40% 短縮され、1時間 -> 36分になりました。一方、コストは $2 から $2.40 に 20% 増加しました:
| ウェアハウスサイズ | クレジット/時 | 実行時間(時間) | コスト |
|---|---|---|---|
| X-small | 1 | 1 | $2.00 |
| Small | 2 | 0.6 | $2.40 |
| 増加率 | -40% | 20% |
この場合、ウェアハウスのアップサイジングは当然の選択ではなくなります。コストとクエリの実行時間をどれだけ重視するかによって異なります。
コストのみを気にする場合は X-small を選び、速度のみを気にする場合は Small を選びます。
私たちの場合、両方のバランスを見つけたいため、以前に確立したガイドラインを使用します:
ウェアハウスは、次のサイズが小さいウェアハウスと比較したとき、クエリの実行時間が 40% 未満の場合は効率的と見なします。
この場合、クエリの実行時間が 40% 短縮されており、「効率的」と見なされる閾値にちょうど達しているため、「S」ウェアハウスを選ぶべきです。
例 3
例 3 では、実行時間が 20% 短縮され、1時間 -> 48分になりました。一方、コストは $2 から $3.20 に 60% 増加しました:
| ウェアハウスサイズ | クレジット/時 | 実行時間(時間) | コスト |
|---|---|---|---|
| X-small | 1 | 1 | $2.00 |
| Small | 2 | 0.8 | $3.20 |
| 増加率 | -20% | 60% |
この場合、実行時間が 20% しか短縮されていないため、クエリは「非効率」と見なされます。「効率的」と見なされるためには、実行時間が 40% 以上短縮される必要があります。したがって、「XS」ウェアハウスを選ぶべきです。
セクション 2: ウェアハウスの適切なサイジング手順
ウェアハウスの適切なサイジング手順
理論的に「最適な」ウェアハウスとは何かを説明しましたが、実際には各モデルに対して適切なウェアハウスをどのように選択するのでしょうか?
手順は以下のとおりです:
- テーブルのパーティション数を把握するために explain plan を実行する
- パーティション数に基づいて、select.dev パーティション数ダイアグラムを使用して開始ウェアハウスを推定する
- 前の手順の推定ウェアハウスを使用して
dbtでクエリを実行する - 下の「ベンチマークのヒューリスティック」セクションを確認して、すぐにウェアハウスを割り当てられるかどうか確認する。できない場合は続ける
- この Google スプレッドシートを使用して
クエリ時間を記録する - ウェアハウスをアップサイジングする
- 次に高いウェアハウスでクエリを再実行する
- Google スプレッドシートに結果を再度記録する
- コストがパフォーマンスより速く増加したら停止する
以下のセクションでは次の内容を確認します:
- ベンチマークを dbt で行う必要がある理由
- これらの手順に従ってウェアハウスを適切にサイジングする2つの例:
- シンプルなクエリ:
gitlab_dotcom_deployment_todo_dedupe_source - より多くのパーティションを持つより複雑なクエリ:
prep_ci_stage
- シンプルなクエリ:
ベンチマークを dbt 環境で行う必要がある理由
ベンチマークは dbt で行う必要があります
上記の手順のリストでは、クエリを dbt で実行する必要があると述べています。これは、ほぼすべてのモデルで少なくとも2セットの SQL 文が実行されるためです。まず、SELECT 文が実行されますが、さらにその下で次の2つの文のいずれかも実行されます:
- 増分モデルの場合、
MERGE文が実行されます - 新しいモデルと full_refresh モデルの場合、
CREATE TABLE文が実行されます
これらの追加の SQL 文は計算コストが高く、ベンチマークの一部である必要があります。
dbt run を使用すると、これらの文が自動的に実行されます。したがって、すべてのウェアハウスベンチマーククエリをローカルまたは CI ジョブを通じて dbt 経由で実行するのが最善です。
増分実行のベンチマーク
特に incremental モデルのベンチマークを行う場合、「最も効率的な」ウェアハウスは増分実行か full_refresh 実行かによって異なります。
少なくとも、増分実行は次のようにベンチマークする必要があります:
- 対応するテーブルを完全にロードする
- 前日のデータを削除し、パーティションサイズに基づく推定ウェアハウスを使用してモデルを増分的に実行する
オプション: モデルを full_refresh でベンチマークすることもできます。こうすることで、増分実行か full_refresh 実行かに基づいて異なるウェアハウスでモデルを設定できます:
{% if is_incremental() %}
{{ config(
warehouse='smaller_warehouse' # Use this warehouse for incremental runs
) }}
{% else %}
{{ config(
warehouse='larger_warehouse' # Use this warehouse for full refresh runs
) }}
{% endif %}
SELECT *
FROM my_table
実際のベンチマーク例
以下のセクションでは、ベンチマーク手順に従う方法の2つの例を見ていきます。
例 1: gitlab_dotcom_deployment_todo_dedupe_source
最初のステップは explain plan を実行することです。クエリに 630 パーティションがあることが示されます:
EXPLAIN
SELECT
*
FROM
"RAW".tap_postgres.GITLAB_DB_TODOS
QUALIFY ROW_NUMBER() OVER ( PARTITION BY id ORDER BY _uploaded_at DESC) = 1;
2番目のステップは、上記のダイアグラムに基づいてパーティション数に基づくウェアハウスを選択することです。この場合、630 パーティションで XS サイズのウェアハウスから開始できます。
ウェアハウス XS で、クエリ 01b5ff9d-080a-e214-0000-289d77d4f1e2 は 7分14秒かかったため、Google スプレッドシートテンプレートにその統計を追加します
次に M ウェアハウスを使用してみます。まず、キャッシュを削除するために次を実行します:
ALTER SESSION SET USE_CACHED_RESULT = FALSE;
ウェアハウス M でクエリ 01b5ffc3-080a-e214-0000-289d77d57b3e は 1分45秒かかったため、Google スプレッドシートテンプレートにその統計を追加します:
| ウェアハウスサイズ | クレジット/時 | 実行時間(分)部分 | 実行時間(秒)部分 | 実行時間(時間) | コスト |
|---|---|---|---|---|---|
| X-small | 1 | 7 | 14 | 0.12 | $0.24 |
| Medium | 4 | 1 | 45 | 0.03 | $0.23 |
| 増加率 | -76% | -3% |
上記から、M ウェアハウスでは実行時間が 73% 短縮され、コストも 3% 削減されており、この状況では M ウェアハウスが XS ウェアハウスより優れていることがわかります。
パフォーマンスがコストより速く増加しているため、L ウェアハウスにアップサイジングして、それが継続するかどうか確認できます。
L ウェアハウスを使用すると、クエリ 01b5ffdc-080a-e214-0000-289d77d5c756 は 1分23秒で完了しました。スプレッドシートに貼り付けると:
| ウェアハウスサイズ | クレジット/時 | 実行時間(分)部分 | 実行時間(秒)部分 | 実行時間(時間) | コスト |
|---|---|---|---|---|---|
| X-small | 1 | 7 | 14 | 0.12 | $0.24 |
| Medium | 4 | 1 | 45 | 0.03 | $0.23 |
| Large | 8 | 1 | 23 | 0.02 | $0.37 |
| 増加率 | -21% | 58% |
上記のスプレッドシートは、クエリの実行時間が 21% 短縮(良い)されたが、コストは 58% 増加(悪い)していることを示しています。したがって、価格はパフォーマンスの向上よりずっと速く増加しています。
結論: M ウェアハウスが最良の実行時間対コスト比を持っており、このモデルに使用すべきです。
例 2: prep_ci_stage
explain plan
EXPLAIN
WITH
dim_project AS (
SELECT
*
FROM
"PROD".common.dim_project
),
dim_ci_pipeline AS (
SELECT
*
FROM
"PROD".common.dim_ci_pipeline
),
dim_namespace_plan_hist AS (
SELECT
*
FROM
"PROD".common.dim_namespace_plan_hist
),
dim_date AS (
SELECT
*
FROM
"PROD".common.dim_date
),
ci_stages AS (
SELECT
*
FROM
"PREP".gitlab_dotcom.gitlab_dotcom_ci_stages_dedupe_source
WHERE
created_at IS NOT NULL
),
joined AS (
SELECT
ci_stages.id AS dim_ci_stage_id,
IFNULL(dim_project.dim_project_id, -1) AS dim_project_id,
IFNULL(dim_ci_pipeline.dim_ci_pipeline_id, -1) AS dim_ci_pipeline_id,
IFNULL(dim_namespace_plan_hist.dim_plan_id, 34) AS dim_plan_id,
IFNULL(dim_namespace_plan_hist.dim_namespace_id, -1) AS ultimate_parent_namespace_id,
dim_date.date_id AS created_date_id,
ci_stages.created_at::TIMESTAMP AS created_at,
ci_stages.updated_at::TIMESTAMP AS updated_at,
ci_stages.name AS ci_stage_name,
ci_stages.status AS ci_stage_status,
ci_stages.lock_version AS lock_version,
ci_stages.position AS POSITION
FROM
ci_stages
LEFT JOIN dim_project ON ci_stages.project_id = dim_project.dim_project_id
LEFT JOIN dim_namespace_plan_hist ON dim_project.ultimate_parent_namespace_id = dim_namespace_plan_hist.dim_namespace_id
AND ci_stages.created_at >= dim_namespace_plan_hist.valid_from
AND ci_stages.created_at < COALESCE(dim_namespace_plan_hist.valid_to, '2099-01-01')
LEFT JOIN dim_ci_pipeline ON ci_stages.pipeline_id = dim_ci_pipeline.dim_ci_pipeline_id
INNER JOIN dim_date ON TO_DATE(ci_stages.created_at) = dim_date.date_day
)
SELECT
*,
'@mpeychet_'::VARCHAR AS created_by,
'@chrissharp'::VARCHAR AS updated_by,
'2021-06-29'::DATE AS model_created_date,
'2022-06-01'::DATE AS model_updated_date,
CURRENT_TIMESTAMP() AS dbt_updated_at,
CURRENT_TIMESTAMP() AS dbt_created_at
FROM
joined;
EXPLAIN プランは、スキャンする必要があるパーティションが合計 9211 あることを示しています。推定ウェアハウスダイアグラムには 8,000 と 16,000 のマルチパーティションに関するガイドラインがあります。
L ウェアハウスから開始します。
まず、ベンチマークのためにキャッシュを無効にするために以下の文を実行します:
ALTER SESSION SET USE_CACHED_RESULT = FALSE;
クエリの実行時間は以下のとおりでした:
L: 32分17秒XL: 11分32秒
スプレッドシートに実行時間を貼り付けます:
| ウェアハウスサイズ | クレジット/時 | 実行時間(分)部分 | 実行時間(秒)部分 | 実行時間(時間) | コスト |
|---|---|---|---|---|---|
| Large | 8 | 32 | 17 | 0.54 | $17.22 |
| XL | 16 | 11 | 32 | 0.19 | $12.30 |
| 増加率 | -64% | -29% |
実行時間が 64% 短縮され、コストが 29% 削減されているため、XL がここでより「効率的」であることは明らかです。
セクション 3: モデルを適切にサイジングする時期
モデルを適切にサイジングする時期
前置きセクションで述べたように、non-product dbt タスク内のすべてのモデルを適切にサイジングするベンチマークがこのベンチマーク Issue に従って実施されました。最小のモデルを適切にサイジングしても、これらのタイプのモデルが実行内のモデルの 90% 以上を占めているにもかかわらず、大幅なコスト削減にはつながらない(まったくつながらない場合もある)ことが示されています。
したがって、dbt.py DAG のコンテキストでは、最も高コストのモデルに対してベンチマークが最も価値があります。Snowflake の query_attribution テーブルを使用してこれらのモデルを見つけることができます。
上記の注意点は、本番の dbt.py DAG 以外の環境でも dbt モデルを実行することです:
- ローカル
- CI ジョブ
後者の環境では、モデルが小さい場合でも、すべてのモデルに対してウェアハウスを適切にサイジングすることが意味をなす場合があります。これらの場合、適切にサイジングする方法はそれほど精密である必要はありません。つまり、複数のウェアハウスサイズと比較する必要はなく、モデルによってスキャンされるパーティション数に基づいて推定ウェアハウスをすぐに割り当てることができます。
まとめると、ウェアハウスの適切なサイジングの優先順位は、最も長く実行されて最も多くのクレジットを消費する最大のモデルであるべきです。これらのモデルは消費される総クレジットの不均衡な割合を占めているためです。
今後の手順
- 最も高コストのモデルのクエリ最適化に取り組む際は、最も「効率的な」ウェアハウスが使用されていることを確認するためにウェアハウスのベンチマークも検討する
- これらのベンチマーク手順をプログラム的に実装することを検討する。例えば、CI ジョブは、使用すべきウェアハウスサイズをユーザーに伝えるこのエンドプロダクトの一つになり得ます。
ソース
- [1] select.dev: ウェアハウスのサイジング
- [2] 実行時間のパーセンテージ改善の計算、StackOverflow
