Overview
Dynamic Tables (DTs) are declarative, auto-refreshing materialized queries. You write a SELECT, set a TARGET_LAG, and Snowflake keeps results fresh on the schedule you pick. This skill helps you decide when DTs are the right tool, design multi-layer pipelines, and avoid the failure modes that catch real teams in production.
Use this skill when picking between DTs, materialized views, streams+tasks, or dbt — or when an existing DT pipeline is misbehaving (full-refresh fallback, lag drift, runaway cost).
Quick Decision Flowchart
Need to transform data in Snowflake?
├─ Single table, accelerate queries? → Materialized View
├─ Multi-step SQL pipeline, fresh data? → Dynamic Tables
├─ Stream-static joins / append-only? → Custom Incremental DTs (Public Preview)
├─ Cross-warehouse portability or dbt tests? → dbt models
├─ Procedural logic, IF/ELSE, API calls? → Streams + Tasks
└─ Sub-1-minute latency? → Streams + Tasks
Comparison Matrix
| Dimension | Dynamic Tables | Materialized Views | Streams + Tasks | dbt |
|---|---|---|---|---|
| Refresh | Target lag (60s minimum) | Auto, near-real-time | Manual schedule/trigger | Batch (dbt run) |
| SQL support | Full SELECT, JOINs, windows | Single table only | Full + procedural | Full + Jinja |
| Chaining | TARGET_LAG = DOWNSTREAM |
No | Manual DAG | Ref graph |
| Incremental | Built-in for supported ops | Auto | You write it | Manual is_incremental |
| Side effects | None | None | Email, API, externals | None |
| Cost | Your warehouse | Serverless | Your warehouse | Your warehouse |
Rule of thumb: Start with DTs. Reach for streams+tasks only when you need procedural logic, side effects, or sub-1-minute latency. Use dbt when you need its testing framework or cross-warehouse portability.
Pipeline Pattern: Bronze → Silver → Gold
-- Bronze: parse raw VARIANT
CREATE DYNAMIC TABLE bronze_events
TARGET_LAG = DOWNSTREAM
WAREHOUSE = pipeline_wh
AS SELECT
record_content:event_id::STRING AS event_id,
record_content:event_type::STRING AS event_type,
record_content:timestamp::TIMESTAMP_NTZ AS event_ts,
record_content:payload AS payload
FROM raw_events_topic;
-- Silver: business logic + joins
CREATE DYNAMIC TABLE silver_purchases
TARGET_LAG = DOWNSTREAM
WAREHOUSE = pipeline_wh
AS SELECT e.event_id, e.event_ts, p.product_name, p.category
FROM bronze_events e
JOIN products p ON e.payload:product_id::STRING = p.product_id
WHERE e.event_type = 'purchase';
-- Gold: only the leaf has a time-based lag
CREATE DYNAMIC TABLE gold_hourly_sales
TARGET_LAG = '5 minutes'
WAREHOUSE = pipeline_wh
AS SELECT DATE_TRUNC('hour', event_ts) AS sales_hour, category,
COUNT(*) AS order_count
FROM silver_purchases
GROUP BY 1, 2;
Key rule: Only the leaf DT has a time-based TARGET_LAG. Intermediates use DOWNSTREAM so Snowflake derives their lag from the leaf.
Monitoring Essentials
-- Health check (refresh_mode / refresh_mode_reason come from SHOW DYNAMIC TABLES)
SELECT name, scheduling_state, last_completed_refresh_state,
time_within_target_lag_ratio
FROM TABLE(INFORMATION_SCHEMA.DYNAMIC_TABLES())
ORDER BY time_within_target_lag_ratio ASC;
-- Recent refresh history
SELECT name, state, refresh_action,
DATEDIFF('second', refresh_start_time, refresh_end_time) AS duration_sec
FROM TABLE(INFORMATION_SCHEMA.DYNAMIC_TABLE_REFRESH_HISTORY(
NAME_PREFIX => '<db>.<schema>'))
ORDER BY refresh_start_time DESC LIMIT 20;
-- Errors only
SELECT name, state, state_message
FROM TABLE(INFORMATION_SCHEMA.DYNAMIC_TABLE_REFRESH_HISTORY(
NAME_PREFIX => '<db>.<schema>', ERROR_ONLY => TRUE));
For account-wide DT cost, query SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY filtered to refresh queries.
Common Mistakes
- Unplanned
SELECT *in DT definition —SELECT *auto-adapts to most base-table schema changes, but some changes (dropping and re-adding a column, adding a column with a DEFAULT) trigger reinitialization, and it propagates every column, including sensitive ones. Prefer explicit column lists when you need control over the output schema. - Time-based lag on every layer — only the leaf should have a time-based lag. Use
TARGET_LAG = DOWNSTREAMon intermediates. - Change tracking off on base tables — DTs require
CHANGE_TRACKING = TRUEfor incremental refresh. Check withSHOW TABLES. - Falling back to FULL refresh silently — check
refresh_mode_reasoninSHOW DYNAMIC TABLESifrefresh_modeshowsFULLwhen you expectedINCREMENTAL. - Missing PRIMARY KEY RELY — without it,
INSERT OVERWRITEreprocesses everything downstream and you lose incremental chains. - DISTINCT/UNION on large, poorly clustered data — both are supported for incremental refresh, but they are locality-sensitive and can be slow. Prefer
QUALIFY ROW_NUMBER() ... = 1orUNION ALLwhere possible.EXCEPT/INTERSECT/MINUSdo force full refresh. - Sharing one warehouse with interactive queries — DT refreshes will compete with user queries. Use a dedicated warehouse.
- No
INITIALIZATION_WAREHOUSEfor big initial loads — first refresh on large DTs can OOM a small warehouse. Set a larger init WH, then unset.
Production Readiness Checklist
- Explicit column lists, or a deliberate
SELECT */SELECT * EXCLUDEfor schema evolution -
CHANGE_TRACKING = TRUEon all base tables - Intermediates use
TARGET_LAG = DOWNSTREAM - Leaf target lag ≥ all upstream lags
-
refresh_modeisINCREMENTAL(verifyrefresh_mode_reason) - Dedicated warehouse for DT refreshes
- Monitoring on
time_within_target_lag_ratio > 0.95 - Alerting on refresh failures
-
INITIALIZATION_WAREHOUSEset for large initial loads
Hybrid Pattern: DT + Task for Side Effects
CREATE STREAM gold_metrics_stream ON DYNAMIC TABLE gold_metrics;
CREATE TASK notify_on_refresh
WAREHOUSE = ops_wh
WHEN SYSTEM$STREAM_HAS_DATA('gold_metrics_stream')
AS
BEGIN
LET change_count INT := (SELECT COUNT(*) FROM gold_metrics_stream);
CALL SYSTEM$SEND_EMAIL('my_email_integration', 'team@example.com', 'Metrics Updated',
:change_count || ' rows changed');
CREATE OR REPLACE TEMP TABLE _consume AS SELECT * FROM gold_metrics_stream;
END;
Workflow
- Assess fit — run the decision flowchart. If DTs aren't the right tool, stop here.
- Pick refresh mode —
AUTO(default),INCREMENTAL(force, fail if ineligible),FULL, orCUSTOM_INCREMENTAL(Public Preview). - Design layers — Bronze→Silver→Gold with
DOWNSTREAMon intermediates. - Harden — apply the production checklist.
⚠️ STOPPING POINT: Before running CREATE OR REPLACE DYNAMIC TABLE against existing pipelines, show the user the planned DDL and confirm. Replacing a DT triggers a full reload and may invalidate downstream incremental chains.
⚠️ STOPPING POINT: Before applying ALTER DYNAMIC TABLE ... SUSPEND or DROP DYNAMIC TABLE, confirm with the user — downstream DTs depending on the target will stop refreshing.
⚠️ STOPPING POINT: Before altering base tables (ALTER TABLE ... SET CHANGE_TRACKING, ADD PRIMARY KEY ... RELY) or running snow dcm deploy, show the statement or plan and confirm. RELY is not enforced; a non-unique key can produce wrong refresh results.
Stopping Points
- Workflow Step 4 — confirm DDL before
CREATE OR REPLACE DYNAMIC TABLEon existing pipelines (including examples fromreferences/) - Workflow Step 4 — confirm before
ALTER ... SUSPENDorDROP DYNAMIC TABLE - Workflow Step 4 — confirm before base-table
ALTER TABLEchanges orsnow dcm deploy
References
references/pitfalls-and-pks.md— full pitfall deep-dive plusPRIMARY KEY RELY,FROZEN WHERE(formerlyIMMUTABLE WHERE),BACKFILL FROMreferences/custom-incremental.md— Custom Incremental DTs (Public Preview) syntax and patternsreferences/dcm-for-dts.md— Database Change Management for git-native DT deployment- Built-in Cortex Code skill:
dynamic-tables