
Dynamic Table の SCHEDULER 属性が dbt-snowflake の scheduler config でネイティブ制御できるようになったので試してみた
かわばたです。
これまでdbtでDynamic TableのSCHEDULER属性を制御しようとすると、pre_hook/post_hookによるワークアラウンドが必要だったのですが、dbt-snowflakeアダプタがscheduler configキーにネイティブ対応したので、実際に検証してみました。
結果を共有します。
背景・課題
以前、Dynamic TableのSCHEDULER属性を試してみたという記事を書きました。SCHEDULER = DISABLEにすると、Snowflakeの自動スケジューラによるTARGET_LAGベースの自動リフレッシュが止まり、dbtやAirflowのような外部オーケストレータが明示的にALTER DYNAMIC TABLE ... REFRESHを叩くまでリフレッシュされなくなります。
-- SCHEDULER = DISABLE の生SQL例
CREATE OR REPLACE DYNAMIC TABLE KAWABATA_MART_DB.PUBLIC.DT_ORDERS_SCHEDULER_DISABLED
WAREHOUSE = COMPUTE_WH
REFRESH_MODE = FULL
INITIALIZE = ON_CREATE
SCHEDULER = DISABLE
AS
SELECT o.ID AS ORDER_ID
FROM KAWABATA_MART_DB.KAWABATA_RAW.RAW_ORDERS o;
ただし2026年3月31日の記事執筆時点では、dbt-snowflakeアダプタ(v1.9.2)がSCHEDULER属性にネイティブ対応しておらず、target_lagは必須のままdbt runをそのまま叩くとALTER文が競合してしまう問題がありました。当時は以下のようにpre_hookで一旦SCHEDULER = ENABLEに戻し、post_hookで手動リフレッシュした後にSCHEDULER = DISABLEへ戻す、というワークアラウンドで凌いでいました。
-- models/intermediate/int_order_items_dynamic_table.sql(旧記事のワークアラウンド)
{{
config(
materialized='dynamic_table',
snowflake_warehouse='COMPUTE_WH',
target_lag='DOWNSTREAM',
pre_hook=[
"ALTER DYNAMIC TABLE IF EXISTS {{ this }} SET SCHEDULER = ENABLE TARGET_LAG = DOWNSTREAM"
],
post_hook=[
"ALTER DYNAMIC TABLE {{ this }} REFRESH",
"ALTER DYNAMIC TABLE {{ this }} SET SCHEDULER = DISABLE"
]
)
}}
SELECT
i.ITEM_ID,
i.ORDER_ID
FROM {{ ref('stg_items') }} i
JOIN {{ ref('stg_orders') }} o
ON i.ORDER_ID = o.ORDER_ID;
hookで無理やりSCHEDULER属性を書き換える構成だったため、dbt runのたびにALTER文が2〜3本余分に走る、hookの実行順序を間違えると意図しない状態でリフレッシュされてしまう、といった扱いにくさがありました。当時の記事でも「公式のdbtアダプタ対応を待つのが本筋」とまとめていたので、実際に対応されたか気になっていました。
技術的アプローチ
Snowflake公式ドキュメントを確認したところ、materialized='dynamic_table'のconfigに scheduler キーが正式追加されており、ENABLE/DISABLEをhookなしで直接指定できるようになっていました。
-- models/marts/dt_orders_scheduler_demo.sql(新しい書き方)
{{ config(
materialized='dynamic_table',
snowflake_warehouse='COMPUTE_WH',
refresh_mode='FULL',
scheduler='DISABLE'
) }}
SELECT
o.ID AS ORDER_ID,
o.CUSTOMER AS CUSTOMER_ID
FROM {{ source('ecom', 'raw_orders') }} o
dbtのDynamic Tableモデルでは、target_lagを省略するかscheduler: DISABLEを指定すると、Snowflakeの自動スケジューリングを使わない、dbt/外部オーケストレータ管理のリフレッシュ方式になります。SCHEDULER = DISABLEの場合、TARGET_LAGは定義できないため、configからはtarget_lagを削除します。
dbtがDynamic Tableモデルを実行すると、Dynamic Tableの作成・変更に加えてALTER DYNAMIC TABLE ... REFRESHによる明示的なリフレッシュが行われます。なお、公式ドキュメントにはSQL未変更時に以後のdbt runがモデルをスキップするという記載もありますが、今回の検証環境ではSQL変更なしのdbt runでもリフレッシュが実行されました(後半の削除反映の検証で確認します)。pre_hook/post_hookで自前管理していた属性の切り替えが、config 1行に置き換わる形です。
制限事項
制限事項
- dbt-snowflakeアダプタは v1.11.5以上 が必須です
- Dynamic Tableへ直接・間接的にデータを供給するすべてのベーステーブルに、
CHANGE_TRACKING = TRUEを明示的に設定します。特に、上流テーブルをCREATE OR REPLACEする運用では、change trackingメタデータが失われ、下流Dynamic Tableの増分リフレッシュが失敗する可能性があります - dbtのModel Contractsは非対応です
copy_grantsconfigは、SnowflakeのDynamic Table向けdbtドキュメントで非対応と明記されています(CREATE OR REPLACEのたびにグラントがリセットされる)。Dynamic Tableへの権限はcopy_grantsに依存させず、デプロイ後に必要なGRANTを再適用する運用を前提にしてください- Dynamic TableのモデルSQL(
SELECT文)を変更すると、dbtはCREATE OR REPLACE DYNAMIC TABLEを実行してDynamic Tableを再初期化します。ユーザーが--full-refreshを明示的に指定する必要はありません。逆にdbt run --full-refreshを実行すると、選択対象(--select/--excludeの指定結果)に含まれる変更のないモデルもCREATE OR REPLACEされ再初期化されるため、対象を絞らずに実行しないよう注意してください - dbtテストはDynamic Tableの「現在の状態」に対して実行され、特定のリフレッシュ結果に対するテストにはなりません。リフレッシュ中にテストが実行された場合は、その時点で利用可能な状態が読み取られます
- Dynamic Tableの上流ソースとして、Materialized View・External Table・Directory Table・Streamは参照できません
前提条件
- dbt-snowflakeアダプタ: v1.11.5以上(
schedulerconfig利用の最低条件) - 検証環境: dbt Core 1.11.11、2026年7月24日時点。実行コマンドは
dbt run --select dt_orders_scheduler_demo - Snowflake: 別途構築したjaffle-shop環境(
JAFFLE_SHOP_DBのDEV/PROD/RAWスキーマ、JAFFLE_SHOP_WH、DBT_CICD_ROLE)をそのまま継続利用します
事前準備
ベーステーブルにCHANGE_TRACKINGを設定する
-- RAW_ORDERSはdbt(seed)が作成しているため、OWNERSHIPを持つロールで実行する
ALTER TABLE JAFFLE_SHOP_DB.RAW.RAW_ORDERS SET CHANGE_TRACKING = TRUE;
注意:
ALTER TABLE ... SET CHANGE_TRACKING = TRUEには対象テーブルのOWNERSHIPが必要です。jaffle-shop環境ではRAW.RAW_ORDERSはdbtのseedがDBT_CICD_ROLEで作成したテーブルのため、同ロール(またはロール階層上位)で実行します。

dynamic_tableモデルファイルを追加する
models/marts/dt_orders_scheduler_demo.sqlを新規作成し、モデルを配置します。

試してみた
実行して確認
configをscheduler='DISABLE'でdbt runを実行します。
dbt run --select dt_orders_scheduler_demo


dbt runではDynamic Tableの設定変更のSQLに加えてALTER DYNAMIC TABLE ... REFRESHが発行されます。

あわせてSHOW DYNAMIC TABLESで、scheduler列がDISABLEになり、target_lagがNULLになっていることを確認します。
SHOW DYNAMIC TABLES LIKE 'DT_ORDERS_SCHEDULER_DEMO' IN DATABASE JAFFLE_SHOP_DB;

ENABLEとDISABLEの挙動を比較すると、以下のようになります。
| 設定 | リフレッシュのトリガー | リフレッシュが実行されない間の挙動 |
|---|---|---|
scheduler: ENABLE(既定) |
target_lagに基づきSnowflakeが自動実行 |
引き続き自動でリフレッシュされ続ける |
scheduler: DISABLE |
dbtによるモデルの作成・変更時、または外部オーケストレータからの明示的なALTER DYNAMIC TABLE ... REFRESH |
Snowflakeによる自動リフレッシュは行われず、データは最後にリフレッシュした時点のまま |
target_lagによるSnowflake管理の自動運用と、dbt/外部オーケストレータ管理の運用を、config 1行の書き換えだけで切り替えられることが確認できました。
ソース側の削除が反映されるか確認する
scheduler: DISABLEの運用で気になるのが、ベーステーブル側のデータ変更、特に行の削除がDynamic Tableへどう反映されるかです。dbtの増分モデル(incremental)では削除の伝播にdelete+insert等の工夫が必要ですが、Dynamic TableはクエリSQLの結果を宣言的に維持するオブジェクトなので、リフレッシュが正常に完了すれば、ソース側の削除もクエリ結果へ反映されるはずです。
あわせて確認したいのが、SQL変更なしのdbt runでリフレッシュが実行されるかどうかです。公式ドキュメントのdbt-managed refreshの説明には、各dbt runがDynamic Tableを作成・変更したうえでALTER DYNAMIC TABLE ... REFRESHを発行するという趣旨の記述がある一方、モデル例のセクションには "Subsequent runs detect if the SQL is unchanged and skip the model entirely."(SQLが未変更なら以後の実行はモデルをスキップする)という記述もあり、ページ内で読み取れる挙動が分かれています。なお、ここでの「SQL is unchanged」はdbtモデル側のSELECT文・configが未変更という意味で、ソーステーブルのデータ変更は関係しません。どちらの挙動になるか、実際に確認します。
まず、削除前の件数と対象行を確認しておきます。
-- 削除前の状態確認
SELECT COUNT(*) AS CNT FROM JAFFLE_SHOP_DB.RAW.RAW_ORDERS;
SELECT COUNT(*) AS CNT FROM JAFFLE_SHOP_DB.DEV.DT_ORDERS_SCHEDULER_DEMO;


ベーステーブルから行をDELETEします。
-- ソース側で行を削除
DELETE FROM JAFFLE_SHOP_DB.RAW.RAW_ORDERS
WHERE ID IN (
SELECT ID FROM JAFFLE_SHOP_DB.RAW.RAW_ORDERS LIMIT 10
);

注意: ここで削除しているのはseedでロードしたソースデータです。検証後は
seedコマンドの再実行(--vars '{load_source_data: true}'付き)で元に戻せます。
この時点でDynamic Table側を確認すると、scheduler: DISABLEのためSnowflakeによる自動リフレッシュは走らず、削除前の件数のままです。
-- DELETE直後: Dynamic Tableにはまだ反映されていない
SELECT COUNT(*) AS CNT FROM JAFFLE_SHOP_DB.DEV.DT_ORDERS_SCHEDULER_DEMO;

続いて、モデルSQLを変更しないままdbt runを実行してみます。
dbt run --select dt_orders_scheduler_demo
-- SQL変更なしのdbt run後: 削除が反映されている
SELECT COUNT(*) AS CNT FROM JAFFLE_SHOP_DB.DEV.DT_ORDERS_SCHEDULER_DEMO;

検証の結果、SQLに変更がないdbt runでもリフレッシュが実行され、ソース側の削除がDynamic Tableに反映されました(dbt Core 1.11.11、2026年7月24日時点の観測結果です)。モデル例セクションの「SQLが未変更ならモデルをスキップする」という記載とは異なり、dbt-managed refreshの説明(各dbt runでREFRESHを発行する)に沿った挙動です。
Query Historyで発行されたSQLを確認すると、このdbt runでは既存のDynamic Tableに対するalter dynamic table ... refreshが発行されており、CREATE OR REPLACEによる再作成は行われていませんでした。SQL未変更のモデルに対しては、dbtがリフレッシュのみを実行していることが確認できます。

なお、dbtを介さずに反映させたい場合は、AirflowやSnowflake Tasksなどのオーケストレータから明示的にリフレッシュを実行できます。
-- 明示的なリフレッシュ(オーケストレータから直接実行する場合)
ALTER DYNAMIC TABLE JAFFLE_SHOP_DB.DEV.DT_ORDERS_SCHEDULER_DEMO REFRESH;
権限に関する注意:
ALTER DYNAMIC TABLE ... REFRESHの実行には、対象Dynamic Tableに対するOPERATE(またはOWNERSHIP)権限が必要です。日常的なリフレッシュ専用のロールには必要以上にOWNERSHIPを付与せず、OPERATEを利用する最小権限設計を検討してください。
削除前後の挙動をまとめると次のとおりです。
| タイミング | ベーステーブル | Dynamic Table(scheduler: DISABLE) |
|---|---|---|
| DELETE実行直後 | 削除済み | 未反映(最後のリフレッシュ時点のまま) |
SQL変更なしのdbt run実行後 |
削除済み | 削除が反映され、ベーステーブルと一致 |
Dynamic Tableは、リフレッシュが実行されればソース側の削除も自然に結果へ反映します。増分モデルのように削除伝播のロジックを自前で書く必要はありません。scheduler: DISABLEではSnowflakeによる自動リフレッシュが行われないため、dbt runまたは明示的なALTER DYNAMIC TABLE ... REFRESHを実行したタイミングだけがリフレッシュのトリガーになる、という制御のしやすさを確認できました。
注意: 公式ドキュメントにはSQL未変更時にモデルがスキップされるという記載も残っているため、dbtやアダプタのバージョンによって挙動が変わる可能性があります。運用に組み込む際は、自環境の実行ログでREFRESHの発行を確認することをおすすめします。
注意: 本検証は
refresh_mode='FULL'での結果です。INCREMENTALでの削除反映は、Dynamic Tableのクエリが増分リフレッシュ可能であり、かつ上流のCHANGE_TRACKINGによる変更追跡情報を利用できることが前提になります。
旧ワークアラウンドとの比較
旧記事のhookベースの実装と、今回のscheduler config指定を並べると、シンプルさの違いがわかりやすいです。
Before(pre_hook/post_hookワークアラウンド)
pre_hook=[
"ALTER DYNAMIC TABLE IF EXISTS {{ this }} SET SCHEDULER = ENABLE TARGET_LAG = DOWNSTREAM"
],
post_hook=[
"ALTER DYNAMIC TABLE {{ this }} REFRESH",
"ALTER DYNAMIC TABLE {{ this }} SET SCHEDULER = DISABLE"
]
After(scheduler config)
scheduler='DISABLE'
hookでSCHEDULER属性を行ったり来たりさせていた実装が、scheduler='DISABLE'というconfig 1行に置き換わりました。hookに起因する不要な状態遷移や実行順序への依存がなくなるため、定義の保守性が向上します。
補足: 現在のSnowflakeでは
SCHEDULER = DISABLEとTARGET_LAGは併用できません。旧記事のhookベース実装(TARGET_LAG = DOWNSTREAMを設定したままSCHEDULER = DISABLEへ切り替える構成)は当時の検証結果として掲載しており、現在この構成を新規に採用することは推奨しません。scheduler: DISABLEを使う場合はtarget_lagをconfigから削除してください。
最後に
Dynamic TableのSCHEDULER属性まわりを、dbtのscheduler configで検証してみました。旧記事の時点での課題と、現状を整理するとこうなります。
| 時点 | dbtでのSCHEDULER制御 |
|---|---|
| 2026年3月31日(旧記事の検証時点、dbt-snowflake v1.9.2) | 非対応。pre_hook/post_hookによるワークアラウンドが必要 |
| 2026年7月24日(今回の検証時点、dbt Core 1.11.11) | scheduler: ENABLE / DISABLEをconfigに1行書くだけで制御可能(dbt-snowflake v1.11.5以上が必要) |
旧記事で「公式対応を待つのが本筋」と書いていた通り、待った甲斐がありました。hookで無理やり属性を書き換える必要がなくなったので、Dynamic Tableをdbt管理下に置きたい場合は、素直にscheduler configを使うのがおすすめです。
なお、SCHEDULER = DISABLEのDynamic Tableを単体で手動リフレッシュした場合、リフレッシュ対象はそのDynamic Tableだけであり、上流・下流のDynamic Tableにはカスケードしません(SCHEDULER = ENABLEの場合は手動リフレッシュが上流のDynamic Tableへカスケードしますが、上流にSCHEDULER = DISABLEのテーブルがあればそこで停止します)。そのため、scheduler: DISABLEで依存関係を持つDynamic Table群を運用する場合は、実行順序、失敗時の再実行、監視をdbtやAirflowなどのオーケストレータ側で設計する必要があります。要件に応じて使い分けてみてください。
この記事が何かの参考になれば幸いです!





