SnowflakeのDynamic Table（プレビュー機能）でドツボにハマった話

# はじめに
お世話になっております。primeNumberの庵原です。
三寒四温の候、皆様いかがお過ごしでしょうか。

今回はSnowflakeの利用でDynamic Table（動的テーブル）の仕様をちゃんと理解せず安易に利用した結果、クレジットを浪費してしまった話を共有できればと思います！この話を持ってDynamic Tableと正しく付き合っていただきたいと思い、啓蒙の意味も含んでおります🙇‍♂️

**※注意点として、SnowflakeのDynamic Tableは2024年3月現在プレビュー機能であるため、今後のアップデートによっては仕様が変更される可能性がある点はご留意ください。**

（プレビュー機能を色々試してみたかったんじゃぁぁ）

# そもそもDynamic Tableって何？
（少々長いので、すでに知ってるよ〜という方は読み飛ばしていただいて問題ないです！）

データ基盤を構築している際に下記のようなデータを用意する必要がある、みたいなことがあります。

- 集計元のテーブルのサイズが数十GB〜TBのような中〜大規模なテーブル
- 複数のテーブル間で結合をする必要がある
- 集計のロジックが複雑すぎる
- BIなどの利用側からのアクセスが多い
- けど元テーブルの更新頻度が低い（大事）

このような場合ですと、Snowflakeには**Materialized View**と呼ばれる実体を持つビューの利用を検討できますが、この利用には制約が多く、上記の例にマッチしないことがままあります。
例えば、Materialized Viewでは「複数のテーブル間で結合をする必要がある」の観点から、自己結合を含む結合を行うことができません。また、「集計のロジックが複雑すぎる」の観点からは、SELECTリスト内にないカラムによる`GROUP BY`などの集計や、`MIN()`　`MAX()`など以外の複雑な関数の利用に制限があります。詳しくは、公式ドキュメントをご参照ください。

https://docs.snowflake.com/ja/user-guide/views-materialized#limitations-on-creating-materialized-views

上記の状況を打開すべく、Snowflakeで用意されている機能として、**Stream**と**Task**という機能があります。
下記のような実装になります。（公式ドキュメントから抜粋）

```sql
-- JSON型のストアデータの格納するテーブル
CREATE OR REPLACE TABLE raw (var VARIANT);

-- rawテーブルの変更を検知するSTREAM
CREATE OR REPLACE STREAM rawstream1 ON TABLE raw;

-- rawからオフィス訪問者の名前を格納するテーブル
CREATE OR REPLACE TABLE names (id INT, first_name STRING, last_name STRING);

-- rawstream1ストリームからnamesテーブルに新しい名前レコードを挿入するタスク
-- ストリームにレコードが含まれる場合、タスクを毎分実行する。
CREATE OR REPLACE TASK raw_to_names
    WAREHOUSE = mywh
    SCHEDULE = '1 minute'
    WHEN
        SYSTEM$STREAM_HAS_DATA('rawstream1')
    AS
        MERGE INTO names n
        USING (
            SELECT var:id id, var:fname fname, var:lname lname
            FROM rawstream1
        ) r1 ON n.id = TO_NUMBER(r1.id)
        WHEN MATCHED AND metadata$action = 'DELETE' THEN　DELETE
        WHEN MATCHED AND metadata$action = 'UPDATE' THEN
        UPDATE SET
            n.first_name = r1.fname, n.last_name = r1.lname
        WHEN NOT MATCHED AND metadata$action = 'INSERT' THEN
            INSERT (id, first_name, last_name) VALUES (r1.id, r1.fname, r1.lname);
```

やや小難しいことをしていますが、流れとしては
1. rawにデータが挿入・更新・削除される
1. 変更された内容をrawstream1が検知する
1. raw_to_namesのタスクが1分に1回rawstream1に変更があったかを確認して、変更があった場合は、MERGE文の内容に従って、namesに変更を格納する。この際、処理自体は`WAREHOUSE = mywh`で指定しているように`mywh`が処理を実行する

のような流れです。
これを読んでいる皆さんがどう思われるか、人それぞれかなと思われますが、私は「ちょっとごちゃごちゃしてるなぁ」
と率直に感じてしまいました。

ですが、件のDynamic Tableで上記のSQLを実装すると以下のように綺麗にできます。（公式ドキュメントから抜粋）

```sql
-- JSON型のストアデータの格納するテーブル
CREATE OR REPLACE TABLE raw (var VARIANT);

-- rawから、オフィス訪問者の名前を含む動的テーブルを作成する。
-- データはリアルタイムで1分以内に更新されるようにする。
CREATE OR REPLACE DYNAMIC TABLE names
TARGET_LAG = '1 minute'
WAREHOUSE = mywh
AS
SELECT var:id::int id, var:fname::string first_name,
var:lname::string last_name FROM raw;
```

**美しい...**

ト◯コのサ◯ーのような感想が出てしまいそうになるほど"つくしい"です。
中身自体は先ほどのStream & Taskと行っていることは変わらないですが、かなり簡略化できていますね。

このように複雑なパイプラインの簡略化や、実装コストの削減に貢献できる便利な機能だとご理解いただけたかと思います。

Dynamic Tableの詳しい説明は公式ドキュメントをご覧ください。

https://docs.snowflake.com/ja/user-guide/dynamic-tables-about


# それで今回何が起きた？
今回Snowflakeでデータマートを作成しようとなり、Dynamic Tableを採用しました。
理由として
- データマートのテーブル数が多くなる（数百テーブル）
- ウェアハウス層のベーステーブルが、1日2回しか更新されない
- 簡単ではあるが、結合を行なっている

という観点からtroccoのようなワークフローを用いた管理ではなく、Dynamic Tableで変更検知を行った上で、更新処理をSnowflake上だけで完結することができるようにする実装にしていました。

# 落とし穴
この際、仕様をちゃんと確認できていなかった完全な私の落ち度ですが、
`TARGET_LAG = '1 minute'`
の仕様を勘違いしていました...

## （参考に）Materialized Viewの場合
Materialized Viewでは、ベーステーブルから変更があった場合、「**自動的かつ透過的に維持される**」とあるように、ベーステーブルの変更があったら自動的に、ユーザーの仮想ウェアハウスを利用せず、**Snowflakeが用意しているコンピュートリソース**を用いて更新が行われます。
もちろんこの更新処理には別途コンピュートコストとして請求対象になりますが、あくまで、変更があった場合のみのコストなので、必要最低限に抑えられる点がこのMaterialized Viewの魅力の1つでもあります。

## Dynamic Tableの場合
Dynamic Tableではベーステーブル群の変更検知は、`CREATE DYNAMIC TABLE`時に指定する、`TARGET_LAG = '1 minute'`の頻度で変更があるかを確認しています。
問題はこのチェック処理を`Snowflakeが用意しているコンピュートリソース`が行なっているわけではなく、`CREATE DYNAMIC TABLE`時に指定した仮想ウェアハウスがチェック処理を行なっている点です。

## ということは...?
察しの良い方はここで戦慄している思いますが、要はどういうことかというと、
`TARGET_LAG = '1 minute'`に設定していると、`WAREHOUSE`で設定した仮想ウェアハウスはチェック作業を1分に1回処理を行うので**基本止まらないのです。**

Snowflakeの仮想ウェアハウスによるコンピュートコストは、「**仮想ウェアハウスが稼働した時間分**」によって従量課金になっているため、仮想ウェアハウスが動き続けることによって、永久にコストが流れっぱなしになるのです。

私はこれを、「Dynamic Tableでも透過的に変更チェックしてくれて、処理の時だけ仮想ウェアハウス使うんやな〜便利やな〜」と勘違いして、コストが流れっぱなしにしてしまったのです。

今回私が経験した場合ですと、数百テーブルを1つの仮想ウェアハウスにチェック＆更新処理を寄せており、1分に1回のスケジュールを設定していました...その上で下記の画像が実際のコンピュートの比率です。

![スクリーンショット 2024-03-10 16.38.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333022/0e06db40-71e7-ce58-9aa6-737b273c1d3e.png)

（消費クレジット数は伏せてあります...）

青が実際にデータを処理した`COMPUTE`の消費量で、黄色がDynamic Tableでのチェックで利用していた`CLOUD`の消費量です。
この仮想ウェアハウスでは、他の処理する用途でも利用していたのですが、それを差し引いても、チェックで消費した量が9割以上を超えていました。

幸か不幸か、事前にリソースモニターで1日にクレジット消費上限を入れていた点から、「なんかクレジット消費早くね？」と確認でき、今回の事象の発覚に繋がったのですが、それすらもなかったと思うと本当に恐ろしい話でした。

# 教訓
## 仕様はちゃんと理解しよう
「そりゃそうだろ！」とお叱りを受けそうですが、特に便利な機能ほど仕様の理解は本当に必要だと感じました。

## リソースモニター大事
先ほどのチラッと言及しましたが、Snowflakeでは1日の仮想ウェアハウスによるクレジットの消費数に制限をつけることが可能です。これにより、明らかに処理が遅くなっていたり、無駄な設定が追加されてしまっていないかなどの検知に役立ちます。
コストマネジメント大事。開発段階でもしっかり設定しておきましょう。

## ACCOUNT_USAGE有能
Dynamic Tableの変更チェックのクエリは`SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY`ビューで確認することが可能です。実際にどれくらい変更チェックをしてしまっていたのか、設定変更後にちゃんとチェックがされないようになっているか、などのチェックで活用しました。
このほかにも誰が重い処理をしているか、やどのクエリが頻繁に叩かれているかなどを確認することも可能です。
詳しくはこちらも公式ドキュメントをご覧ください。

https://docs.snowflake.com/ja/sql-reference/account-usage/query_history

# まとめ
いかがでしたでしょうか？
データ基盤構築にはありがちなコストのハンドリングで起きた問題の１例をご紹介しました...本当にこういうことが起きると肝が冷えます。
今回得られた教訓をもとに、これを読んだ皆様が同じミスを犯さないようにしていただければ何よりでございます。
今後もデータ関連の記事を発信していければと思いますので、他の記事もぜひご覧ください！