Custom incrementalization¶
When standard refresh modes (INCREMENTAL, FULL) can’t express your transformation, custom incrementalization lets you write MERGE or INSERT logic that Snowflake executes on each refresh. You define exactly how changes reach the dynamic table: Snowflake handles scheduling, retries, and transactional guarantees.
When to use custom incrementalization¶
Consider custom incrementalization when SELECT-based dynamic tables can’t express the transformation you need.
| Aspect | SELECT-based dynamic tables | Custom incremental dynamic tables |
|---|---|---|
| Logic type | Declarative SELECT | Imperative MERGE or INSERT |
| Incremental strategy | Auto-inferred by Snowflake | User-defined |
| Semantics | Delayed-view equivalence | User-defined (no system guarantee) |
| Best for | Transformations expressible as SELECT | CDC with deletes, stream-static joins, audit trails |
| Migration from streams and tasks | Requires rewrite to SELECT | Accepts single-statement MERGE or INSERT patterns |
Scenarios suited to custom incrementalization:
-
Query not expressible as SELECT: Stream-static joins, time range deduplication logic, or conditional branching that a single SELECT can’t capture.
-
Explicit control over update and delete semantics: Soft-deletes, conditional updates, or merge logic where you need to decide per-row whether to update, delete, or skip.
-
User-defined output semantics: Audit trails, accumulators, or point-in-time snapshots.
-
State reuse and memoization: Accumulate intermediate results across refreshes to avoid re-scanning historical data. For example, maintain a running top-K leaderboard by combining previous results with new changes. See Top-K leaderboard.
-
Migrating from streams and tasks: Single-statement MERGE or INSERT patterns port directly into
REFRESH USING. You gain managed scheduling and monitoring without rewriting to SELECT. Pipelines that use stored procedures, UDTFs, or multi-statement transactions require restructuring into a single supported DML statement.
Syntax¶
Key requirements:
- An explicit column list is required. Snowflake can’t infer the schema from DML.
REFRESH_MODE = AUTOresolves toCUSTOM_INCREMENTALwhenREFRESH USINGis present.- Only one DML statement is allowed per
REFRESH USINGblock (no multi-statement transactions).
MERGE INTO SELF¶
Use MERGE INTO SELF when you need to update or delete existing rows in addition to inserting
new ones. SELF refers to the dynamic table being created, and you can alias it.
You can read SELF in the USING subquery to join incoming changes with current contents of
the dynamic table. You can’t reference the dynamic table by its object name inside
REFRESH USING.
INSERT INTO SELF¶
Use INSERT INTO SELF for append-only patterns where you don’t need to modify existing rows.
INSERT INTO SELF only appends rows. Use MERGE when you need to update or delete existing rows.
The CHANGES clause¶
The CHANGES clause replaces stream semantics. Snowflake automatically binds the change interval
to refresh boundaries, so you don’t specify time bounds (AT, BEFORE, END).
Constraints:
- You can’t use
CHANGESonSELF(Snowflake returns an error). - Base tables must have change tracking enabled. Creating a custom incremental dynamic table attempts to enable change tracking on the base tables.
- You can’t specify time bounds: the interval is managed automatically by Snowflake.
INFORMATION modes control which change types are visible:
If INFORMATION isn’t specified, the DEFAULT mode is used (for example, CHANGES()).
DEFAULT: Inserts, updates, and deletes.APPEND_ONLY: Inserts only.
Metadata columns available in the change set:
| Column | Description |
|---|---|
| METADATA$ACTION | 'INSERT' or 'DELETE' (updates appear as a DELETE + INSERT pair) |
| METADATA$ISUPDATE | TRUE if the row is part of an UPDATE operation |
Use SELECT * EXCLUDE (METADATA$ACTION, METADATA$ISUPDATE) to strip metadata columns before
insertion.
The SELF keyword¶
SELF has two roles inside REFRESH USING:
- Write target:
MERGE INTO SELForINSERT INTO SELF. - Read source:
FROM SELF AS curin theUSINGsubquery reads the current contents of the dynamic table.
You can’t reference the dynamic table by its object name inside REFRESH USING. Use SELF
exclusively. CHANGES() on SELF is not allowed.
BACKFILL FROM and START AT¶
These clauses control how the dynamic table is initially populated and where subsequent refreshes begin reading changes.
Without BACKFILL FROM, the initial refresh runs your REFRESH USING statement against the full
change history of the source. Because CHANGES() treats all existing rows as INSERTs on the initial
run, the initial refresh effectively replays every row in the source through your MERGE or INSERT
logic. For large base tables, this can be expensive and slow.
BACKFILL FROM provides a faster alternative: Snowflake populates the dynamic table by cloning from
an existing table without executing the REFRESH USING
logic. Subsequent refreshes then process only new changes that arrive after the backfill.
| Configuration | Initial population | Subsequent refresh start |
|---|---|---|
| Neither (default) | Initial refresh processes all existing source rows as INSERTs through REFRESH USING | From creation time |
BACKFILL FROM <table> | Cloned from the backfill table (bypasses REFRESH USING) | From creation time |
BACKFILL FROM + START AT | Cloned from the backfill table (bypasses REFRESH USING) | From the START AT point |
START AT accepts four options:
TIMESTAMP => <timestamp>: A specific point in time.STATEMENT => <query_id>: The point after a specific query completed.STREAM => <stream_name>: The current offset of an existing stream.OFFSET => -<seconds>: A negative offset in seconds from the current time.
BACKFILL FROM and START AT can only be set at creation time. You can’t add or change them via
ALTER DYNAMIC TABLE or CREATE OR ALTER DYNAMIC TABLE.
Examples¶
Append-only enrichment with stream-static join (INSERT INTO)¶
This example enriches new click events with page metadata. The clicks table is append-only,
and pages is a static dimension table.
Only new clicks since the last refresh are processed. The LEFT OUTER JOIN enriches each click with its page title and section without re-scanning historical data. For a full CDC variant that handles updates and deletes using MERGE, see Stream-static join with full CDC.
Audit deletes log (INSERT INTO)¶
This example captures standalone deletes from a users table into an audit log, filtering out
the DELETE half of updates.
The DEFAULT information mode exposes all change types (inserts, updates, deletes). The WHERE
clause keeps only standalone deletes by excluding rows where METADATA$ISUPDATE is TRUE.
EXCLUDE strips the metadata columns before insertion.
Incremental aggregation on append-only changes¶
This example maintains a running total of player scores. Each refresh sums only the new rows and adds them to existing totals.
Existing players get their score updated by adding the batch total. New players are inserted
with their first batch score. This avoids re-scanning the entire match_results history on
every refresh.
Because this example uses APPEND_ONLY mode, only inserts are processed.
Deletes and updates in the source are ignored.
If you need to handle deletes, use DEFAULT mode instead.
Usage notes¶
- Each refresh executes as a single autocommit transaction. If the REFRESH USING statement fails, the entire refresh rolls back.
- If data retention expires on a base table queried through
CHANGES()before the next refresh, the refresh fails. Set retention periods on those tables to exceed your longest expected refresh gap. - Custom incremental dynamic tables don’t automatically derive primary keys. To enable downstream dynamic tables or streams to consume changes efficiently, add a RELY primary key constraint manually.
- MERGE operations are subject to standard nondeterminism rules. If multiple source rows match the same target row, the result is nondeterministic. Add deduplication (such as
ROW_NUMBER()withQUALIFY) to guarantee deterministic results. - Objects outside a
CHANGES()clause (such as dimension tables in JOINs) are read at their state at the refresh snapshot time, not incrementally. - The
CHANGES(INFORMATION => APPEND_ONLY)clause gives custom incremental dynamic tables native append-only semantics. Dynamic tables that use a SELECT-based definition don’t support this directly.
RELY primary key and CHANGES() behavior¶
How Snowflake identifies rows in CHANGES() output depends on whether the base table has a RELY primary key:
| Base table configuration | Row identity | Effect on CHANGES() |
|---|---|---|
| No RELY primary key | Internal row tracking | A DELETE then INSERT of the same values appear as two separate change rows |
| With RELY primary key | Primary key value | A DELETE then INSERT of the same key value cancel out (net zero change) |
If delete-then-insert of the same row should be invisible to your merge logic (for example, arising from INSERT OVERWRITE), add RELY to the primary key constraint so that Snowflake can compact these pairs before they reach CHANGES.
Limitations¶
- No cloning or replicating custom incremental dynamic tables.
- No dbt or DCM integration.
- Only
CREATE OR ALTERcan modify theREFRESH USINGdefinition and properties (such asTARGET_LAG). - Upstream schema changes cause the next refresh to fail with a compile error. If the upstream object was altered (not replaced or dropped), use
CREATE OR ALTERwith an updatedREFRESH USINGdefinition to recover. The next refresh continues from the last successful refresh. If the upstream usedCREATE OR REPLACE, change tracking is broken and you must recreate the downstream withCREATE OR REPLACE. - Immutability constraints (
IMMUTABLE WHERE) andINSERT ONLY INPUTScan’t be combined withREFRESH USING. CHANGESfrom Iceberg tables with an external catalog isn’t supported withoutRELYon the source primary key.- With managed Iceberg base tables in the presence of external writes:
- Custom incremental dynamic tables using
CHANGESfail to refresh if there is an external write to the base table between refreshes. This is a permanent failure. - Changing a column’s
DEFAULTvalue on a v2 managed Iceberg table with external write causes permanent failure for custom incremental dynamic tables usingDEFAULTchanges.
- Custom incremental dynamic tables using
What’s next¶
- Design patterns: More examples including full CDC MERGE and SELF-read patterns.
- Migrate from streams and tasks: Move existing stream-task pipelines to dynamic tables.
- Streams and change tracking: How change tracking works in Snowflake.
- CREATE DYNAMIC TABLE: Full SQL reference.