![[新機能]Dynamic TableのSCHEDULER attributeが一般提供となったのでdbtと合わせて試してみた](https://images.ctfassets.net/ct0aopd36mqt/wp-refcat-img-3610e3c1ff5961bdb7b464e17f8bf06d/90b168b240005ead852ec1d474bb74fb/snowflake-logo-1200x630-1.png?w=3840&fm=webp)
[新機能]Dynamic TableのSCHEDULER attributeが一般提供となったのでdbtと合わせて試してみた
かわばたです。
2026年3月26日にDynamic TableのSCHEDULER attributeが一般提供(GA)となりました。
従来、Dynamic Table は作成すると必ず自動リフレッシュが有効になっていましたが、SCHEDULER = DISABLE を指定することで 自動リフレッシュを無効化 し、手動(ALTER DYNAMIC TABLE ... REFRESH)でのみ更新する運用が可能になりました。
本記事では、この新機能を Snowflakeネイティブ および dbt連携 の両面から検証します。
概要
背景
従来の Dynamic Table は、作成時に TARGET_LAG を指定すると Snowflake のスケジューラが自動的にリフレッシュを管理していました。この仕組みは便利ですが、外部オーケストレーター(dbt、Airflow など)でリフレッシュタイミングを制御したい 場合には以下の課題がありました。
- 自動リフレッシュが走るため、上流データが未準備のままリフレッシュが実行される可能性がある
- パイプライン全体の実行順序を外部から制御できない
- 不要なリフレッシュによるウェアハウスコストが発生する
新機能の内容
2026年3月26日にGAとなった SCHEDULER 属性により、Dynamic Table の自動リフレッシュを ENABLE / DISABLE で制御 できるようになりました。
| 設定 | 動作 |
|---|---|
SCHEDULER = ENABLE |
Snowflake のスケジューラが TARGET_LAG に基づき自動リフレッシュを実行(従来動作) |
SCHEDULER = DISABLE |
自動リフレッシュを無効化。ALTER DYNAMIC TABLE ... REFRESH による手動リフレッシュのみ |
| 未指定 | ENABLE と同じ(既存のDynamic Tableとの後方互換性を維持) |
構文
-- 作成時に指定
CREATE DYNAMIC TABLE my_dt
WAREHOUSE = my_wh
SCHEDULER = DISABLE -- TARGET_LAG は指定しない
AS SELECT ...;
-- 既存DTの変更
ALTER DYNAMIC TABLE my_dt SET SCHEDULER = DISABLE;
-- ENABLE に戻す場合は TARGET_LAG も再設定
ALTER DYNAMIC TABLE my_dt SET SCHEDULER = ENABLE TARGET_LAG = '10 minutes';
-- 手動リフレッシュ
ALTER DYNAMIC TABLE my_dt REFRESH;
検証環境
| 項目 | 値 |
|---|---|
| ロール | ACCOUNTADMIN |
| ウェアハウス | COMPUTE_WH |
| データベース | KAWABATA_MART_DB |
| dbt Core | 1.9.4 |
| dbt-snowflake | 1.9.2 |
| データ | jaffle-shop サンプルデータ(RAW_ORDERS: 686件, RAW_ITEMS: 997件) |
SCHEDULER の基本動作検証(SQL)
SCHEDULER = ENABLE(従来動作)
CREATE OR REPLACE DYNAMIC TABLE KAWABATA_MART_DB.PUBLIC.DT_ORDERS_SCHEDULER_ENABLED
TARGET_LAG = '10 minutes'
WAREHOUSE = COMPUTE_WH
REFRESH_MODE = FULL
INITIALIZE = ON_CREATE
SCHEDULER = ENABLE
AS
SELECT
o.ID AS ORDER_ID,
o.CUSTOMER AS CUSTOMER_ID,
o.ORDERED_AT,
o.STORE_ID,
o.ORDER_TOTAL,
c.NAME AS CUSTOMER_NAME
FROM KAWABATA_MART_DB.KAWABATA_RAW.RAW_ORDERS o
JOIN KAWABATA_MART_DB.KAWABATA_RAW.RAW_CUSTOMERS c
ON o.CUSTOMER = c.ID;

結果: scheduler = ENABLE、scheduling_state = ACTIVE、target_lag = 10 minutes となり、自動リフレッシュが有効であることを確認した。
SCHEDULER = DISABLE
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,
o.CUSTOMER AS CUSTOMER_ID,
o.ORDERED_AT,
o.STORE_ID,
o.ORDER_TOTAL,
c.NAME AS CUSTOMER_NAME
FROM KAWABATA_MART_DB.KAWABATA_RAW.RAW_ORDERS o
JOIN KAWABATA_MART_DB.KAWABATA_RAW.RAW_CUSTOMERS c
ON o.CUSTOMER = c.ID;

結果: scheduler = DISABLE、target_lag = 未設定。自動リフレッシュは行われない。
注意:
SCHEDULER = DISABLEとTARGET_LAGを同時に指定するとエラーになります。
手動リフレッシュ
テストデータを挿入して、更新がされているかを確認します。
-- =============================================
-- 検証3: SCHEDULER=DISABLE のDTを手動リフレッシュ
-- → テストデータ挿入後に手動リフレッシュし、更新結果の反映を確認
-- =============================================
-- 3a: リフレッシュ前の件数を確認
SELECT COUNT(*) AS ROW_COUNT_BEFORE_INSERT FROM KAWABATA_MART_DB.PUBLIC.DT_ORDERS_SCHEDULER_DISABLED;
-- 3b: ソーステーブルにテストデータを挿入
INSERT INTO KAWABATA_MART_DB.KAWABATA_RAW.RAW_CUSTOMERS (ID, NAME)
VALUES ('test-customer-001', 'Test User For DT Verification');
INSERT INTO KAWABATA_MART_DB.KAWABATA_RAW.RAW_ORDERS (ID, CUSTOMER, ORDERED_AT, STORE_ID, SUBTOTAL, TAX_PAID, ORDER_TOTAL)
VALUES
('test-order-001', 'test-customer-001', '2026-03-31', '1', 1000, 100, 1100),
('test-order-002', 'test-customer-001', '2026-03-31', '2', 2000, 200, 2200);
-- 3c: 手動リフレッシュ実行
ALTER DYNAMIC TABLE KAWABATA_MART_DB.PUBLIC.DT_ORDERS_SCHEDULER_DISABLED REFRESH;
-- 3d: リフレッシュ後の件数を確認(+2 されていれば手動リフレッシュにより更新結果が反映されたと判断できる)
SELECT COUNT(*) AS ROW_COUNT_AFTER_REFRESH FROM KAWABATA_MART_DB.PUBLIC.DT_ORDERS_SCHEDULER_DISABLED;
-- 3e: 挿入したテストデータがDTに反映されているか確認
SELECT * FROM KAWABATA_MART_DB.PUBLIC.DT_ORDERS_SCHEDULER_DISABLED
WHERE CUSTOMER_ID = 'test-customer-001';
-
件数チェック

-
テストデータを挿入後、手動実行

-
テストデータが更新されていることを確認

結果: REFRESH_TRIGGER = MANUAL で正常にリフレッシュが実行された。
タスクでの設定
タスクで設定を行い、任意のスケジュールで更新を行います。
-- =============================================
-- 検証4: タスクによる SCHEDULER=DISABLE DTの定期リフレッシュ
-- → CRON スケジュールで手動リフレッシュを自動化
-- =============================================
-- 4a: タスクを作成(1時間ごとに手動リフレッシュを実行)
CREATE OR REPLACE TASK KAWABATA_MART_DB.PUBLIC.TASK_REFRESH_DT_ORDERS
WAREHOUSE = COMPUTE_WH
SCHEDULE = 'USING CRON 0 * * * * UTC'
COMMENT = 'Hourly manual refresh of DT_ORDERS_SCHEDULER_DISABLED'
AS
ALTER DYNAMIC TABLE KAWABATA_MART_DB.PUBLIC.DT_ORDERS_SCHEDULER_DISABLED REFRESH;
-- 4b: タスクを有効化
ALTER TASK KAWABATA_MART_DB.PUBLIC.TASK_REFRESH_DT_ORDERS RESUME;
-- 4c: タスクの状態確認
SHOW TASKS LIKE 'TASK_REFRESH_DT_ORDERS' IN SCHEMA KAWABATA_MART_DB.PUBLIC;
-- 4d: タスクを即時実行(スケジュールを待たずにすぐ実行)
EXECUTE TASK KAWABATA_MART_DB.PUBLIC.TASK_REFRESH_DT_ORDERS;
-- 4e: タスク実行履歴の確認(実行後しばらくしてから確認)
SELECT NAME, STATE, SCHEDULED_TIME, COMPLETED_TIME, ERROR_CODE, ERROR_MESSAGE
FROM TABLE(KAWABATA_MART_DB.INFORMATION_SCHEMA.TASK_HISTORY(
TASK_NAME => 'TASK_REFRESH_DT_ORDERS',
SCHEDULED_TIME_RANGE_START => DATEADD('hour', -2, CURRENT_TIMESTAMP())
))
ORDER BY SCHEDULED_TIME DESC
LIMIT 5;
-- 4f: タスクを停止(検証完了後)
ALTER TASK KAWABATA_MART_DB.PUBLIC.TASK_REFRESH_DT_ORDERS SUSPEND;
タスクが稼働して、Dynamic Tableが更新されていることを確認できました。

ENABLE/DISABLE 切替
-- ENABLE → DISABLE
ALTER DYNAMIC TABLE ... SET SCHEDULER = DISABLE;
-- DISABLE → ENABLE(TARGET_LAG の再設定が必要)
ALTER DYNAMIC TABLE ... SET SCHEDULER = ENABLE TARGET_LAG = '10 minutes';
結果: 双方向に動的変更が可能です。
基本動作まとめ
| 検証項目 | 結果 |
|---|---|
| SCHEDULER = ENABLE で作成 | 自動リフレッシュ有効(従来通り) |
| SCHEDULER = DISABLE で作成 | 自動リフレッシュ無効、TARGET_LAG 指定不可 |
| DISABLE の手動リフレッシュ | ALTER ... REFRESH で正常動作 |
| ALTER で ENABLE↔DISABLE 切替 | 双方向に変更可能 |
dbt プロジェクト構成
jaffle-shop のサンプルデータを使い、 Dynamic Table を定義して検証しました。
プロジェクト構造
jaffle_shop_incr_dt/
├── dbt_project.yml
├── profiles.yml
├── packages.yml
├── macros/
│ └── refresh_dynamic_table.sql
└── models/
├── sources.yml
├── schema.yml
├── staging/
│ ├── stg_orders.sql (view)
│ ├── stg_customers.sql (view)
│ ├── stg_items.sql (view)
│ └── stg_products.sql (view)
├── intermediate/
│ └── int_order_items_dynamic_table.sql ← dynamic_table(実行後に SCHEDULER = DISABLE へ変更)
└── marts/
└── fct_orders_from_dynamic_table.sql (view)
ソース定義(sources.yml)
version: 2
sources:
- name: jaffle_raw
database: KAWABATA_MART_DB
schema: KAWABATA_RAW
tables:
- name: RAW_CUSTOMERS
- name: RAW_ORDERS
- name: RAW_ITEMS
- name: RAW_PRODUCTS
- name: RAW_STORES
- name: RAW_SUPPLIES
dynamic_table モデル(実行後に SCHEDULER = DISABLE へ変更する運用)
dbt-snowflake v1.9.2 は SCHEDULER 属性をネイティブにサポートしていないため、作成時点では SCHEDULER 指定なし(= ENABLE 相当)で Dynamic Table が作られます。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,
i.SKU,
o.CUSTOMER_ID,
o.ORDERED_AT,
o.STORE_ID,
o.ORDER_TOTAL,
p.PRODUCT_NAME,
p.PRODUCT_TYPE,
p.PRICE AS UNIT_PRICE
FROM {{ ref('stg_items') }} i
JOIN {{ ref('stg_orders') }} o ON i.ORDER_ID = o.ORDER_ID
JOIN {{ ref('stg_products') }} p ON i.SKU = p.SKU
重要:
IF EXISTSについて
dbt-snowflake のdynamic_tableマテリアライゼーションでは、pre_hookは Dynamic Table の作成(CREATE)より前に実行されます。初回作成時(--full-refresh)にはまだ対象テーブルが存在しないため、IF EXISTSを付けないとALTERが失敗します。IF EXISTSを付けることで、初回は pre_hook がスキップされ、2回目以降は正常に動作します。
dbt × SCHEDULER = DISABLE の課題と解決策
dbt-snowflake v1.9.2 の制約
dbt-snowflake アダプタ(v1.9.2)は SCHEDULER 属性をまだ認識していません。そのため以下の制約があります。
| 制約 | 詳細 |
|---|---|
target_lag が必須 |
dbt の dynamic_table マテリアライゼーションで省略するとコンパイルエラー |
dbt run(通常)で ALTER エラー |
dbt が ALTER ... SET TARGET_LAG=... を発行 → SCHEDULER = DISABLE と矛盾 |
scheduler config 未サポート |
config(scheduler='DISABLE') と書いても無視される |
実行手順
# 初回作成(pre_hook は IF EXISTS により安全にスキップされる)
dbt run --select int_order_items_dynamic_table --full-refresh
# 2回目以降の差分更新(dbt run だけでOK)
dbt run --select int_order_items_dynamic_table
実行フロー(2回目以降)
1. pre_hook: ALTER ... IF EXISTS ... SET SCHEDULER = ENABLE TARGET_LAG = DOWNSTREAM
→ dbt の ALTER 比較でエラーにならないようにする
2. dbt 本体: 設定比較 → 変更なし → skip(DDL 発行なし)
3. post_hook①: ALTER DYNAMIC TABLE ... REFRESH
→ 手動リフレッシュ実行(差分データ更新)
4. post_hook②: ALTER ... SET SCHEDULER = DISABLE
→ 自動リフレッシュを再度無効化
検証結果
-
ソースに3件追加後

-
更新前件数チェック

-
dbt run(--full-refreshなし)を実行

- 件数チェック

| 確認項目 | Before | After |
|---|---|---|
| DT行数 | 997 | 1000(+3件反映) |
| REFRESH_TRIGGER | - | MANUAL |
| 自動リフレッシュの発火 | - | なし(今回の検証では、pre_hookで一時的にENABLEにしてもscheduled refreshの発火は確認されなかった) |
| scheduler | DISABLE | DISABLE(維持) |
メリット・デメリット
| メリット | デメリット |
|---|---|
dbt run だけで差分更新が完結 |
hook の仕組みに依存(dbt アダプタ更新で挙動変化の可能性あり) |
| マクロ不要でシンプル | pre_hook で一瞬 ENABLE 状態になる(今回の検証では実害なし。ただし Snowflake はスケジューラの発火タイミングを厳密に保証していないため、観測結果として扱うのが安全) |
| 既存のパイプラインに組み込みやすい | hook が3つあり、モデル定義がやや複雑 |
incrementalモデルとの使い分けの指針
| ユースケース | 推奨 |
|---|---|
| 差分条件を細かく制御したい | incremental |
| 差分ロジックを Snowflake に任せたい | dynamic_table |
dbt run だけで完結させたい |
どちらも可能(本記事で検証済み) |
| Snowflake の自動リフレッシュに任せたい | dynamic_table(SCHEDULER = ENABLE) |
| 外部オーケストレーターから制御したい | どちらも可能 |
dbtのincrementalモデルはデフォルトでは新規・更新レコードのみ追加/更新する仕組みなので、ソース側で削除されたレコードはincremental先のテーブルに残り続けます。
一方、Dynamic Tableは宣言的に更新を行うためソースデータが削除されていた場合はDynamic Tableのリフレッシュ時に反映されます。
まとめ
Dynamic Table の SCHEDULER 属性により、「DT の定義(クエリ)は保持しつつ、リフレッシュのタイミングは自分で制御する」という柔軟な運用が可能になりました。
なお、ALTER DYNAMIC TABLE ... REFRESH による手動リフレッシュは upstream / downstream の Dynamic Table に cascade しないため、外部オーケストレーターから個別テーブルの更新タイミングを制御しやすい点も大きな利点です。
dbt-snowflakeアダプタ(v1.9.2)はまだ SCHEDULER 属性をネイティブサポートしていませんが、pre_hook / post_hook を活用することで dbt run だけでの更新を実現できました。
しかし、推奨の方法ではないため、dbt-snowflake アダプタのアップデートをお待ちいただくのが無難です。
※dbt-snowflake 1.11.0-rc3 も同様にサポートはされていないことを確認
この記事が何かの参考になれば幸いです!








