{"templateName":"blog-page","cssClassNames":"blog-page page basicpage summit-page","allowedRenditionsWidth":["320","480","640","768","960","1200","1440","1920"],"description":"Learn how Snowflake custom incremental dynamic tables enable custom MERGE logic, stream-static joins, and state reuse for optimized pipeline performance.","language":"en","title":"Snowflake Dynamic Tables: Custom Incrementalization","analyticsPageType":"homepage","analyticsCategory":"general","analyticsSubCategory":"","excludeFromAnalytics":false,":mappedPath":"/en/blog/engineering/custom-incremental-dynamic-tables/",":type":"snowflake-site/components/structure/page",":items":{"root":{"columnCount":12,"columnClassNames":{"experiencefragment-banner":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-sub-header":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-pre-footer":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-header":"aem-GridColumn aem-GridColumn--default--12","markup_editor-table":"aem-GridColumn aem-GridColumn--default--12","responsivegrid":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-footer":"aem-GridColumn aem-GridColumn--default--12","markup_editor":"aem-GridColumn aem-GridColumn--default--12","container_47873732":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12",":items":{"experiencefragment-banner":{"id":"experiencefragment-741529a263","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/pushdown-banner/pushdown-banner-blank/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/pushdown-banner/pushdown-banner-blank.xfmodel.json"},"experiencefragment-header":{"id":"experiencefragment-11d74299a8","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/mega-nav-header/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/mega-nav-header/master.xfmodel.json","languageNavPath":"/content/snowflake-site/global/en/blog/engineering/custom-incremental-dynamic-tables.languagenav.json","appliedCssClassNames":"snowflake-sticky-nav-host"},"experiencefragment-sub-header":{"id":"experiencefragment-2cc3b7a280","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/sub-navigation/engineering-blog-sub-nav/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/sub-navigation/engineering-blog-sub-nav.xfmodel.json"},"responsivegrid":{"columnCount":12,"columnClassNames":{"container_breadcrumb":"aem-GridColumn aem-GridColumn--default--12","container_main_content":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12",":items":{"container_breadcrumb":{"layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"breadcrumb":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"blog-page-breadcrumb-indentation","appliedCssClassNames":"snowflake-container",":type":"snowflake-site/components/container",":items":{"breadcrumb":{"id":"breadcrumb-2e1db260b2","breadcrumbItems":[{"title":"Blog","path":"/en/blog/engineering/","active":false},{"title":"Data Engineering","path":"/en/blog/engineering/data-engineering/","active":false},{"title":"Beyond SELECT Statements: Introducing Dynamic Tables With Custom Incrementalization ","path":"/en/blog/engineering/custom-incremental-dynamic-tables/","active":false}],":type":"snowflake-site/components/blog/breadcrumb"}},":itemsOrder":["breadcrumb"]},"container_main_content":{"layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"flexible_column_container":"aem-GridColumn aem-GridColumn--default--12","related_content":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"main-content","appliedCssClassNames":"snowflake-container",":type":"snowflake-site/components/container",":items":{"flexible_column_container":{"id":"flexible-column-container-fb1a1e7a28","propertiesId":"snowflake-blog-template-main-container","type":"2-column-60-40","alignColumns":"top","containerMaxWidth":"extra-large","topPadding":"none","bottomPadding":"none","spaceBetween":"none","reverseOnMobile":true,"carouselOnMobile":false,"backgroundImageOption":"none","flexible_column_content_container_1":{"layout":"SIMPLE","id":"container-597b2dbd76",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"container_hero":{"layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"blog_hero":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"container-0b3f695def",":type":"snowflake-site/components/container",":items":{"blog_hero":{"id":"blog-hero-4d5a5b5a67","linkedInShareUrl":"https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fwww.snowflake.com%2Fcontent%2Fsnowflake-site%2Fglobal%2Fen%2Fblog%2Fengineering%2Fcustom-incremental-dynamic-tables&title=Beyond+SELECT+Statements%3A+Introducing+Dynamic+Tables+With+Custom+Incrementalization+","twitterShareUrl":"https://x.com/intent/post?url=https%3A%2F%2Fwww.snowflake.com%2Fcontent%2Fsnowflake-site%2Fglobal%2Fen%2Fblog%2Fengineering%2Fcustom-incremental-dynamic-tables&text=Beyond+SELECT+Statements%3A+Introducing+Dynamic+Tables+With+Custom+Incrementalization+","facebookShareUrl":"https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.snowflake.com%2Fcontent%2Fsnowflake-site%2Fglobal%2Fen%2Fblog%2Fengineering%2Fcustom-incremental-dynamic-tables","showClaude":true,"showChatGpt":true,"authors":[{"authorCta":{"id":"button-20c7c4aeda","showOutboundIcon":false,"buttonLink":{"valid":true,"url":"/en/blog/authors/anirudh-santhiar/"},"linkTargetContentType":"DOCUMENT_LEARN",":type":"snowflake-site/components/button","linkType":"SNOWFLAKE_INTERNAL","text":"Anirudh Santhiar"}}],"image":{"id":"image-1f5ad4c456","height":"720","src":"https://www.snowflake.com/adobe/dynamicmedia/deliver/dm-aid--8356a77b-79b0-4938-b4c2-dc928b4ace8d/sf-eng-blog-ml-4-1.png?quality=85&preferwebp=true","lazyEnabled":true,"width":"1680",":type":"snowflake-site/components/image"},"timeToRead":"6","publicationDate":"JUL 27, 2026","tag":{"tagText":"Data Engineering","tagColor":"#29B5E8"},"title":{"lines":["Beyond SELECT Statements: Introducing Dynamic Tables With Custom Incrementalization "],"type":"heading2",":type":"snowflake-site/components/title-v2"},":type":"snowflake-site/components/blog/blog-hero"}},":itemsOrder":["blog_hero"]},"responsivegrid_content":{"columnCount":12,"columnClassNames":{"blog_text_1042472597":"aem-GridColumn aem-GridColumn--default--12","blog_text_1973520463":"aem-GridColumn aem-GridColumn--default--12","code_snippet_215705829":"aem-GridColumn aem-GridColumn--default--12","blog_text_30303678":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1171696641":"aem-GridColumn aem-GridColumn--default--12","blog_text_834708523":"aem-GridColumn aem-GridColumn--default--12","blog_text_428035174":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1685045985":"aem-GridColumn aem-GridColumn--default--12","blog_text_80077891":"aem-GridColumn aem-GridColumn--default--12","blog_text_1748006042":"aem-GridColumn aem-GridColumn--default--12","blog_text_2005467691":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1213013782":"aem-GridColumn aem-GridColumn--default--12","blog_text":"aem-GridColumn aem-GridColumn--default--12","blog_text_1421180790":"aem-GridColumn aem-GridColumn--default--12","blog_text_453880716":"aem-GridColumn aem-GridColumn--default--12","blog_text_1237527855":"aem-GridColumn aem-GridColumn--default--12","blog_text_650743061":"aem-GridColumn aem-GridColumn--default--12","blog_text_1423146289":"aem-GridColumn aem-GridColumn--default--12","blog_text_1669573718":"aem-GridColumn aem-GridColumn--default--12","blog_text_212846980":"aem-GridColumn aem-GridColumn--default--12","blog_text_1484918925":"aem-GridColumn aem-GridColumn--default--12","blog_text_1196225079":"aem-GridColumn aem-GridColumn--default--12","blog_text_1471904003":"aem-GridColumn aem-GridColumn--default--12","blog_text_2027093586":"aem-GridColumn aem-GridColumn--default--12","blog_text_763607309":"aem-GridColumn aem-GridColumn--default--12","blog_text_1463674035":"aem-GridColumn aem-GridColumn--default--12","blog_text_2133377275":"aem-GridColumn aem-GridColumn--default--12","code_snippet_679208649":"aem-GridColumn aem-GridColumn--default--12","blog_text_2058087919":"aem-GridColumn aem-GridColumn--default--12","blog_text_1261347785":"aem-GridColumn aem-GridColumn--default--12","code_snippet":"aem-GridColumn aem-GridColumn--default--12","blog_text_1504885809":"aem-GridColumn aem-GridColumn--default--12","blog_text_701065994":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1777458129":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1866006943":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1237734067":"aem-GridColumn aem-GridColumn--default--12","blog_text_1365803119":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","appliedCssClassNames":"snowflake-layout-container-inner-padding-small",":items":{"blog_text":{"id":"blog-text-49a1269803","text":"\u003Cp\u003ESnowflake's Dynamic Tables are the tool of choice to simplify building data pipelines. Every node in a data pipeline can become a table with a simple SELECT query defining the table's contents. Snowflake automatically refreshes the tables to keep them up-to-date, so that you can focus on driving business value. Dynamic tables guarantee delayed view semantics, where the contents of a dynamic table are equivalent to a corresponding view evaluated at some point in the past — the timestamp at which the refresh is evaluated. While this powerful guarantee simplifies reasoning about application invariants, there are cases where delayed view semantics might not be what your application logic needs.\u003C/p\u003E\r\n\u003Cp\u003EFor example, you might want to:\u003C/p\u003E\r\n\u003Cul\u003E\r\n\u003Cli\u003EPerform stream-static joins where you process one base table (fact) incrementally, but the others (dimensions) at the current version\u003C/li\u003E\r\n\u003Cli\u003EPerform upserts from recent staging data\u003C/li\u003E\r\n\u003Cli\u003EOnly process inserts into the base table, ignoring all updates and deletes. Conversely, you might only want to process deletes\u003C/li\u003E\r\n\u003Cli\u003EAccess current state in the dynamic table to speed up the next refresh\u003C/li\u003E\r\n\u003Cli\u003EPerform data manipulation language (DMLs) into the dynamic table to correct/delete historical data, or process an out-of-band ETL event\u003C/li\u003E\r\n\u003C/ul\u003E\r\n\u003Cp\u003EToday, we introduce \u003Ca href=\"https://docs.snowflake.com/en/user-guide/dynamic-tables/custom-incrementalization\" target=\"_blank\" rel=\"noopener noreferrer\"\u003E\u003Ci\u003Ecustom incremental\u003C/i\u003E dynamic tables\u003C/a\u003E (now generally available), a powerful new abstraction to enhance the expressibility and performance of dynamic tables. With SELECT-based incremental dynamic tables, Snowflake can automatically derive incremental plans to refresh your table based on the SELECT query you provided. With custom incremental dynamic tables, you get to specify exactly how to refresh your dynamic table, choosing a custom merge or insert strategy. You trade simplicity for flexibility — while custom incremental dynamic table definitions are more complex and require you to specify how the refresh should work, they are able to express transformations that SELECT-based dynamic tables can't. We now show you how.\u003C/p\u003E\r\n\u003Cp\u003ECustom incremental dynamic tables define refresh logic with a REFRESH USING statement.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet":{"id":"code-snippet-d03218cd01","language":"sql","codeSnippet":"CREATE DYNAMIC TABLE dt (id int, val string) ... REFRESH USING (...);","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_212846980":{"id":"blog-text-9d578b258f","text":"\u003Cp\u003EThe statement writes to the dynamic table itself, referenced with the keyword SELF, using either MERGE INTO SELF or INSERT INTO SELF. The body of the merge or insert can use Snowflake's \u003Ca href=\"https://docs.snowflake.com/en/sql-reference/constructs/changes\" target=\"_blank\" rel=\"noopener noreferrer\"\u003ECHANGES\u003C/a\u003E clause for fine-grained control of incrementalization. If you are already familiar with CHANGES, feel free to skip ahead. If not, we provide a brief tour.\u003C/p\u003E\r\n\u003Ch1\u003EThe CHANGES primitive\u003C/h1\u003E\r\n\u003Cp\u003EA CHANGES clause returns the rows that changed in a table, view, or dynamic table over a specified interval. For example, suppose the orders table (with \u003Ca href=\"https://docs.snowflake.com/en/sql-reference/constructs/changes#usage-notes\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Echange_tracking\u003C/a\u003E enabled) contains these rows at 2026-06-01 10:00:00:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_2005467691":{"id":"blog-text-a1f6e7adc3","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Eorder_id\u003C/th\u003E\r\n\u003Cth\u003Estatus\u003C/th\u003E\r\n\u003Cth\u003Eamount\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E1001\u003C/td\u003E\r\n\u003Ctd\u003ENEW\u003C/td\u003E\r\n\u003Ctd\u003E25.00\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E1002\u003C/td\u003E\r\n\u003Ctd\u003ENEW\u003C/td\u003E\r\n\u003Ctd\u003E40.00\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1042472597":{"id":"blog-text-fa6d2d5701","text":"\u003Cp\u003EBetween 10:00 and 10:05, one row is inserted and one row is updated:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1866006943":{"id":"code-snippet-ad2d1accb4","language":"sql","codeSnippet":"INSERT INTO orders VALUES (1003, 'NEW', 15.00);\r\nUPDATE orders SET status = 'SHIPPED' WHERE order_id = 1001;","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_428035174":{"id":"blog-text-b1e3b5f17b","text":"\u003Cp\u003EYou can use CHANGES with timestamp markers to query only the rows that changed during that interval:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1213013782":{"id":"code-snippet-ed444d2928","language":"sql","codeSnippet":"SELECT order_id, status, amount, METADATA$ACTION, METADATA$ISUPDATE\r\nFROM orders\r\n  CHANGES()\r\n  AT (TIMESTAMP =\u003E '2026-06-01 10:00:00'::TIMESTAMP)\r\n  END (TIMESTAMP =\u003E '2026-06-01 10:05:00'::TIMESTAMP);","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1748006042":{"id":"blog-text-b035a46944","text":"\u003Cp\u003Ewhich returns\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1237527855":{"id":"blog-text-a0909ba983","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Eorder_id\u003C/th\u003E\r\n\u003Cth\u003Estatus\u003C/th\u003E\r\n\u003Cth\u003Eamount\u003C/th\u003E\r\n\u003Cth\u003EMETADATA$ACTION\u003C/th\u003E\r\n\u003Cth\u003EMETADATA$ISUPDATE\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E1001\u003C/td\u003E\r\n\u003Ctd\u003ESHIPPED\u003C/td\u003E\r\n\u003Ctd\u003E25.00\u003C/td\u003E\r\n\u003Ctd\u003EINSERT\u003C/td\u003E\r\n\u003Ctd\u003ETRUE\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E1001\u003C/td\u003E\r\n\u003Ctd\u003ENEW\u003C/td\u003E\r\n\u003Ctd\u003E25.00\u003C/td\u003E\r\n\u003Ctd\u003EDELETE\u003C/td\u003E\r\n\u003Ctd\u003ETRUE\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E1003\u003C/td\u003E\r\n\u003Ctd\u003ENEW\u003C/td\u003E\r\n\u003Ctd\u003E15.00\u003C/td\u003E\r\n\u003Ctd\u003EINSERT\u003C/td\u003E\r\n\u003Ctd\u003EFALSE\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_2058087919":{"id":"blog-text-d10ea87f1b","text":"\u003Cp\u003EIn a custom incremental dynamic table, Snowflake manages these start and end offsets across refreshes. That means the REFRESH USING query can use CHANGES without specifying AT and END markers and each refresh automatically processes the next interval of base table changes. You can optionally specify CHANGES(INFORMATION =&gt; APPEND_ONLY) to ignore updates and deletes.\u003C/p\u003E\r\n\u003Ch1\u003EPutting custom incremental dynamic tables to work\u003C/h1\u003E\r\n\u003Cp\u003EA custom incremental dynamic table's definition could\u003C/p\u003E\r\n\u003Cul\u003E\r\n\u003Cli\u003ECombine CHANGES with static reads, for example, a stream-static join. Base tables referred to without CHANGES are processed as of the refresh timestamp\u003C/li\u003E\r\n\u003Cli\u003EUse standalone CHANGES logic, for example, to audit all deletes\u003C/li\u003E\r\n\u003Cli\u003ENot use changes at all, for example, to process data from the last n days\u003C/li\u003E\r\n\u003C/ul\u003E\r\n\u003Ch2\u003EStream-static joins\u003C/h2\u003E\r\n\u003Cp\u003ELet us look at a ubiquitous use case that custom incremental dynamic tables help unlock: \u003Ci\u003Estream-static joins\u003C/i\u003E. In the world of data engineering, a stream-static join is a process where a real-time stream of data (which is continuous and fast-moving) is combined with a static data set (which is stable and changes infrequently). In other words, high churn fact data needs to be enriched with slowly changing dimension data. As a data engineer, you would like to avoid dimension table changes causing a rewrite of all historical data to keep refreshes fast, provided your application semantics permit this.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_679208649":{"id":"code-snippet-b5e680e12e","language":"sql","codeSnippet":"CREATE OR REPLACE DYNAMIC TABLE enriched_clicks (\r\n  click_id INT, user_id INT, page_id INT, page_title STRING,\r\n  section STRING, click_ts TIMESTAMP\r\n)\r\n  TARGET_LAG = DOWNSTREAM\r\n  WAREHOUSE = my_wh\r\n  REFRESH USING (\r\n    INSERT INTO SELF\r\n    SELECT c.click_id, c.user_id, p.page_id, p.page_title, p.section, c.click_ts\r\n    FROM clicks CHANGES(INFORMATION =\u003E APPEND_ONLY) AS c\r\n    LEFT OUTER JOIN pages AS p ON c.page_id = p.page_id\r\n  );","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_30303678":{"id":"blog-text-2b5ba84c67","text":"\u003Cp\u003EThe insert-only clicks base table serves as an immutable event stream (see \u003Ca href=\"https://docs.snowflake.com/en/user-guide/dynamic-tables/design-patterns#stream-static-join-with-full-cdc-merge\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Ethis\u003C/a\u003E for an example where a base table is not insert-only). During a refresh cycle, the CHANGES(INFORMATION=&gt;APPEND_ONLY) clause fetches only the newly arrived rows since the previous refresh. To enrich these fresh event records, you perform a LEFT JOIN against the pages dimension table, which is evaluated as a static snapshot at the current refresh timestamp. This mechanism is designed to ensure that while fact data is processed incrementally, it is joined against the most recent state of the dimension metadata before being persisted to the dynamic table. Only the initial refresh processes all the data in the fact table, and even this can be avoided with a \u003Ca href=\"https://docs.snowflake.com/en/user-guide/dynamic-tables/frozen-regions#seed-a-dynamic-table-with-backfill-from\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Ebackfill\u003C/a\u003E.\u003C/p\u003E\r\n\u003Cp\u003EThe key factor driving performance here is that dimension-only changes are not reprocessed. If a page title is renamed but no click row changes, the enriched view keeps the old name until a click change triggers a re-lookup. When these semantics are appropriate for your domain, this can be much more efficient than reprocessing all fact rows for a dimension change (as delayed view semantics would mandate).\u003C/p\u003E\r\n\u003Cp\u003EFor instance, consider the initial state of the \u003Ci\u003Epages\u003C/i\u003E table:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_453880716":{"id":"blog-text-ec9beeb166","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Epage_id\u003C/th\u003E\r\n\u003Cth\u003Epage_title\u003C/th\u003E\r\n\u003Cth\u003Esection\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E10\u003C/td\u003E\r\n\u003Ctd\u003EDynamic Tables\u003C/td\u003E\r\n\u003Ctd\u003EDocs\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E20\u003C/td\u003E\r\n\u003Ctd\u003EPricing\u003C/td\u003E\r\n\u003Ctd\u003EProduct\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1423146289":{"id":"blog-text-9eab631cb8","text":"\u003Cp\u003ESubsequently, the \u003Ci\u003Eclicks\u003C/i\u003E stream receives a pair of new event records:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1669573718":{"id":"blog-text-5bb6736594","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Eclick_id\u003C/th\u003E\r\n\u003Cth\u003Euser_id\u003C/th\u003E\r\n\u003Cth\u003Epage_id\u003C/th\u003E\r\n\u003Cth\u003Eclick_ts\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E1001\u003C/td\u003E\r\n\u003Ctd\u003E7\u003C/td\u003E\r\n\u003Ctd\u003E10\u003C/td\u003E\r\n\u003Ctd\u003E10:00\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E1002\u003C/td\u003E\r\n\u003Ctd\u003E8\u003C/td\u003E\r\n\u003Ctd\u003E20\u003C/td\u003E\r\n\u003Ctd\u003E10:01\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_763607309":{"id":"blog-text-c39d38e376","text":"\u003Cp\u003EDuring the initial refresh, the CHANGES(INFORMATION =&gt; APPEND_ONLY) expression on the fact table captures only these specific records. The logic performs a join with the dimension snapshot and appends the resulting enriched data to \u003Ci\u003ESELF\u003C/i\u003E:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1973520463":{"id":"blog-text-b0b08d9c5e","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Eclick_id\u003C/th\u003E\r\n\u003Cth\u003Euser_id\u003C/th\u003E\r\n\u003Cth\u003Epage_id\u003C/th\u003E\r\n\u003Cth\u003Epage_title\u003C/th\u003E\r\n\u003Cth\u003Esection\u003C/th\u003E\r\n\u003Cth\u003Eclick_ts\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E1001\u003C/td\u003E\r\n\u003Ctd\u003E7\u003C/td\u003E\r\n\u003Ctd\u003E10\u003C/td\u003E\r\n\u003Ctd\u003EDynamic Tables\u003C/td\u003E\r\n\u003Ctd\u003EDocs\u003C/td\u003E\r\n\u003Ctd\u003E10:00\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E1002\u003C/td\u003E\r\n\u003Ctd\u003E8\u003C/td\u003E\r\n\u003Ctd\u003E20\u003C/td\u003E\r\n\u003Ctd\u003EPricing\u003C/td\u003E\r\n\u003Ctd\u003EProduct\u003C/td\u003E\r\n\u003Ctd\u003E10:01\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1504885809":{"id":"blog-text-6b09dc616d","text":"\u003Cp\u003EIf the dimension table is updated independently:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1685045985":{"id":"code-snippet-8790e6a702","language":"sql","codeSnippet":"UPDATE pages\r\nSET page_title = 'Dynamic Tables overview'\r\nWHERE page_id = 10;","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_834708523":{"id":"blog-text-b93991e675","text":"\u003Cp\u003EAssuming no new event data arrives, the CHANGES clause for the clicks table will yield an empty set during the following refresh cycle. Consequently, the row associated with click_id 1001 remains unchanged in the dynamic table and does not reflect the modified page title:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1196225079":{"id":"blog-text-14ff504885","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Eclick_id\u003C/th\u003E\r\n\u003Cth\u003Euser_id\u003C/th\u003E\r\n\u003Cth\u003Epage_id\u003C/th\u003E\r\n\u003Cth\u003Epage_title\u003C/th\u003E\r\n\u003Cth\u003Esection\u003C/th\u003E\r\n\u003Cth\u003Eclick_ts\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E1001\u003C/td\u003E\r\n\u003Ctd\u003E7\u003C/td\u003E\r\n\u003Ctd\u003E10\u003C/td\u003E\r\n\u003Ctd\u003EDynamic Tables\u003C/td\u003E\r\n\u003Ctd\u003EDocs\u003C/td\u003E\r\n\u003Ctd\u003E10:00\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E1002\u003C/td\u003E\r\n\u003Ctd\u003E8\u003C/td\u003E\r\n\u003Ctd\u003E20\u003C/td\u003E\r\n\u003Ctd\u003EPricing\u003C/td\u003E\r\n\u003Ctd\u003EProduct\u003C/td\u003E\r\n\u003Ctd\u003E10:01\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_2027093586":{"id":"blog-text-fb258e7c5d","text":"\u003Cp\u003ESuppose a fresh event record enters the \u003Ci\u003Eclicks\u003C/i\u003E stream after the modification of the page title:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1463674035":{"id":"blog-text-d42636b608","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Eclick_id\u003C/th\u003E\r\n\u003Cth\u003Euser_id\u003C/th\u003E\r\n\u003Cth\u003Epage_id\u003C/th\u003E\r\n\u003Cth\u003Eclick_ts\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E1003\u003C/td\u003E\r\n\u003Ctd\u003E9\u003C/td\u003E\r\n\u003Ctd\u003E10\u003C/td\u003E\r\n\u003Ctd\u003E10:05\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_701065994":{"id":"blog-text-62d1160a4f","text":"\u003Cp\u003EThe subsequent refresh cycle isolates this new click and performs a re-lookup against the now-updated \u003Ci\u003Epages\u003C/i\u003E snapshot:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_2133377275":{"id":"blog-text-c24efe3fc0","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Eclick_id\u003C/th\u003E\r\n\u003Cth\u003Euser_id\u003C/th\u003E\r\n\u003Cth\u003Epage_title\u003C/th\u003E\r\n\u003Cth\u003Esection\u003C/th\u003E\r\n\u003Cth\u003Eclick_ts\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E1003\u003C/td\u003E\r\n\u003Ctd\u003E9\u003C/td\u003E\r\n\u003Ctd\u003EDynamic Tables overview\u003C/td\u003E\r\n\u003Ctd\u003EDocs\u003C/td\u003E\r\n\u003Ctd\u003E10:05\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1471904003":{"id":"blog-text-738fe062aa","text":"\u003Cp\u003EConsequently, the persistent state in the dynamic table reflects a mixture of historical snapshots and current enrichments:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1365803119":{"id":"blog-text-6ee131d555","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003Eclick_id\u003C/th\u003E\r\n\u003Cth\u003Euser_id\u003C/th\u003E\r\n\u003Cth\u003Epage_id\u003C/th\u003E\r\n\u003Cth\u003Epage_title\u003C/th\u003E\r\n\u003Cth\u003Esection\u003C/th\u003E\r\n\u003Cth\u003Eclick_ts\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E1001\u003C/td\u003E\r\n\u003Ctd\u003E7\u003C/td\u003E\r\n\u003Ctd\u003E10\u003C/td\u003E\r\n\u003Ctd\u003EDynamic Tables\u003C/td\u003E\r\n\u003Ctd\u003EDocs\u003C/td\u003E\r\n\u003Ctd\u003E10:00\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E1002\u003C/td\u003E\r\n\u003Ctd\u003E8\u003C/td\u003E\r\n\u003Ctd\u003E20\u003C/td\u003E\r\n\u003Ctd\u003EPricing\u003C/td\u003E\r\n\u003Ctd\u003EProduct\u003C/td\u003E\r\n\u003Ctd\u003E10:01\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E1003\u003C/td\u003E\r\n\u003Ctd\u003E9\u003C/td\u003E\r\n\u003Ctd\u003E10\u003C/td\u003E\r\n\u003Ctd\u003EDynamic Tables overview\u003C/td\u003E\r\n\u003Ctd\u003EDocs\u003C/td\u003E\r\n\u003Ctd\u003E10:05\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1261347785":{"id":"blog-text-b555a9d399","text":"\u003Ch2\u003ECustom merge strategies\u003C/h2\u003E\r\n\u003Cp\u003EAn architectural pattern from the dbt ecosystem that you can now implement is the merge strategy featuring an is_incremental() filter. In the scenario below, the model assumes no late-arriving records, utilizing a global high-watermark to filter any incoming data with an updated_at timestamp preceding the current MAX value stored in SELF. Only records satisfying this temporal constraint are then merged into the target state based on a designated unique key.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_215705829":{"id":"code-snippet-89de281e91","language":"sql","codeSnippet":"CREATE OR REPLACE DYNAMIC TABLE event_facts (\r\n      id NUMBER, event_date DATE, user_id NUMBER, event_type STRING, updated_at TIMESTAMP_NTZ\r\n    )\r\n      TARGET_LAG = '5 minutes'\r\n      WAREHOUSE = transform_wh\r\n      REFRESH USING (\r\n        MERGE INTO SELF t\r\n        USING (\r\n          SELECT id, event_date, user_id, event_type, updated_at\r\n          FROM stg_events\r\n          WHERE updated_at \u003E COALESCE((SELECT MAX(updated_at) FROM SELF),\r\n          '1900-01-01'::TIMESTAMP_NTZ)\r\n        ) s\r\n        ON t.id = s.id\r\n        WHEN MATCHED THEN UPDATE ALL BY NAME\r\n        WHEN NOT MATCHED THEN INSERT ALL BY NAME\r\n      );","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_650743061":{"id":"blog-text-755d313284","text":"\u003Ch2\u003EState reuse with custom incremental dynamic tables\u003C/h2\u003E\r\n\u003Cp\u003EFinally, custom incremental dynamic tables also unlock state reuse and memoization. A SELECT-based dynamic table's contents are a pure function of its inputs. The query can't look at the table's own current contents. However, custom incremental dynamic tables are stateful and their next state (after refresh) can be computed based on the current state before refresh. This allows us to express computations that might depend on the full history of changes rather than just the current batch of changes or the current snapshot. It also allows working with state that isn't in the source tables anymore (for example, because of data aging out), and is instead tracked only in the dynamic table itself.\u003C/p\u003E\r\n\u003Cp\u003EConsider a scenario where you are processing a CDC stream of customer updates to maintain a dimension table. Say you require a \u003Ci\u003Elast_changed_at\u003C/i\u003E column that precisely captures when the attributes of the customer were modified, rather than simply noting when the latest event record was received. This distinction is important in cases where CDC systems can emit snapshot rows, where a record could be re-emitted with a new commit timestamp despite no underlying change in data. A reliable \u003Ci\u003Elast_changed_at\u003C/i\u003E timestamp must filter out these redundant no-op updates. To achieve this, we utilize a \u003Ci\u003Estateful\u003C/i\u003E approach via a custom incremental dynamic table:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1777458129":{"id":"code-snippet-3021cd616f","language":"sql","codeSnippet":"CREATE OR REPLACE DYNAMIC TABLE dim_customer (\r\n  customer_id INT, name STRING, tier STRING, last_changed_at TIMESTAMP_NTZ\r\n)\r\n  TARGET_LAG = '1 hour'\r\n  WAREHOUSE = wh\r\n  REFRESH USING (\r\n    MERGE INTO SELF AS tgt\r\n    USING (\r\n      SELECT customer_id, name, tier, _commit_time\r\n      FROM customers_cdc CHANGES(INFORMATION =\u003E APPEND_ONLY)\r\n      QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY _commit_time DESC) = 1\r\n    ) AS src\r\n    ON tgt.customer_id = src.customer_id\r\n    WHEN MATCHED AND (src.name, src.tier) IS DISTINCT FROM (tgt.name, tgt.tier)\r\n      THEN UPDATE SET tgt.name = src.name, tgt.tier = src.tier,\r\n                      tgt.last_changed_at = src._commit_time\r\n    WHEN NOT MATCHED\r\n      THEN INSERT (customer_id, name, tier, last_changed_at)\r\n           VALUES (src.customer_id, src.name, src.tier, src._commit_time)\r\n  );","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_80077891":{"id":"blog-text-4b000294db","text":"\u003Cp\u003EThe effectiveness of this pattern lies in the WHEN MATCHED AND (...) IS DISTINCT FROM (...) clause. By comparing the arriving change set against the state already persisted in \u003Ci\u003ESELF\u003C/i\u003E, the refresh logic ensures that the change timestamp is only progressed when the actual business values have evolved.\u003C/p\u003E\r\n\u003Cp\u003EState reuse also unlocks opportunities to improve performance. Suppose we're working with a stream of match scores from a league for your favorite sport. We first need a player_scores table that holds each player's total score so far (try implementing it with a custom incremental dynamic table!). We'd like our dynamic table (dt_leaderboard) to maintain a top-three leaderboard. Here's how we'd do it:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1171696641":{"id":"code-snippet-19caa6bbca","language":"sql","codeSnippet":"CREATE OR REPLACE DYNAMIC TABLE player_scores (player_id INT, total_score INT)\r\n  TARGET_LAG = DOWNSTREAM\r\n  WAREHOUSE = transform_wh\r\n  REFRESH USING (\r\n    MERGE INTO SELF AS tgt\r\n    USING (\r\n      SELECT player_id, SUM(score) AS batch_score\r\n      FROM match_results CHANGES(INFORMATION =\u003E APPEND_ONLY) -- match_results is insert only\r\n      GROUP BY player_id\r\n    ) AS src\r\n    ON tgt.player_id = src.player_id\r\n    WHEN MATCHED THEN\r\n      UPDATE SET tgt.total_score = tgt.total_score + src.batch_score\r\n    WHEN NOT MATCHED THEN\r\n      INSERT (player_id, total_score) VALUES (src.player_id, src.batch_score)\r\n  );\r\n\r\nCREATE OR REPLACE DYNAMIC TABLE dt_leaderboard (player_id INT, total_score INT, rank INT)\r\n  TARGET_LAG = '30 minutes'\r\n  WAREHOUSE = transform_wh\r\n  REFRESH USING (\r\n    MERGE INTO SELF AS tgt\r\n    USING (\r\n      SELECT player_id, total_score,\r\n             ROW_NUMBER() OVER (ORDER BY total_score DESC, player_id ASC) AS new_rank\r\n      FROM (\r\n        SELECT COALESCE(ch.player_id, cur.player_id)     AS player_id,\r\n               COALESCE(ch.total_score, cur.total_score) AS total_score\r\n        FROM SELF AS cur\r\n        FULL OUTER JOIN (\r\n              SELECT player_id, total_score\r\n              FROM player_scores CHANGES(INFORMATION =\u003E DEFAULT)\r\n              WHERE METADATA$ACTION = 'INSERT'\r\n            ) AS ch\r\n              ON cur.player_id = ch.player_id\r\n      )\r\n    ) AS src\r\n    ON tgt.player_id = src.player_id\r\n    WHEN MATCHED AND src.new_rank \u003E 3 THEN DELETE\r\n    WHEN MATCHED THEN\r\n      UPDATE SET tgt.total_score = src.total_score, tgt.rank = src.new_rank\r\n    WHEN NOT MATCHED AND src.new_rank \u003C= 3 THEN\r\n      INSERT (player_id, total_score, rank) VALUES (src.player_id, src.total_score, src.new_rank)\r\n  );","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1421180790":{"id":"blog-text-166e4d762a","text":"\u003Cp\u003EThe refresh first builds a candidate set by full-outer-joining the current leaderboard in SELF with the new player_scores changes. The full outer join keeps both kinds of candidates: new or updated players from the change feed, and existing leaders that had no new score but may still belong in the top three. For each joined row, COALESCE(ch.player_id, cur.player_id) and COALESCE(ch.total_score, cur.total_score) pick the changed value when present, otherwise pick the value already stored in SELF. The outer query then ranks those candidates with ROW_NUMBER() OVER (ORDER BY total_score DESC, player_id ASC), producing a deterministic new_rank where higher scores win and player_id breaks ties. The MERGE applies that ranking back to SELF: matched rows with new_rank &gt; 3 are deleted, matched rows still in the top three are updated, and unmatched rows with new_rank &lt;= 3 are inserted. Anything outside the top three and not already stored is ignored.\u003C/p\u003E\r\n\u003Cp\u003EThe trick to ensuring good performance here is to keep the amount of processing that each refresh needs to do proportional to the size of the incoming CHANGES, rather than all the historical score data. We know that for each batch of changes that comes in, the new leaders have to emerge either from the set of old leaders in SELF, or based on the changes. Therefore, all the refresh needs to do is to pull just those new rows via CHANGES, reconcile them against the current three-row leaderboard, and re-rank the resulting rows. Once we have the new top three, we can MERGE them back into the dynamic table, deleting anything that fell out and inserting anything new that broke in. The base table is never re-scanned and the dynamic table never grows beyond three, so the work per refresh depends only on the size of the incoming batch.\u003C/p\u003E\r\n\u003Ch2\u003EDML and schema evolution\u003C/h2\u003E\r\n\u003Cp\u003ESince custom incremental dynamic tables do not adhere to delayed view semantics, direct DML statements can be issued against them, for example, to correct or delete historical records. It is also possible to perform schema evolution on these tables without the need to perform an expensive reinitialization. \u003Ca href=\"https://www.snowflake.com/en/blog/engineering/snowflake-scd1-partial-updates/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003EYou can refer to a companion blog post\u003C/a\u003E that demonstrates how to manage partial values in slowly changing dimensions (SCD) Type 1 scenarios to learn how. To enable periodic deletion of old data from a custom incremental dynamic table, consider defining a storage lifecycle policy on the table.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1237734067":{"id":"code-snippet-eeeada776b","language":"sql","codeSnippet":"CREATE OR REPLACE DYNAMIC TABLE SLP_CIDT (\r\n      id INT, payload STRING, event_ts TIMESTAMP_NTZ\r\n    )\r\n      TARGET_LAG = '1 hour' WAREHOUSE = DT_TRIAGE_4XL\r\n      REFRESH USING (\r\n        MERGE INTO SELF AS tgt\r\n        USING (\r\n          SELECT id, payload, event_ts\r\n          FROM SLP_SRC CHANGES(INFORMATION =\u003E APPEND_ONLY)\r\n          QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY event_ts DESC) = 1\r\n        ) AS src\r\n        ON tgt.id = src.id\r\n        WHEN MATCHED THEN UPDATE SET tgt.payload = src.payload, tgt.event_ts = src.event_ts\r\n        WHEN NOT MATCHED THEN INSERT (id, payload, event_ts) VALUES (src.id, src.payload, src.event_ts)\r\n      );\r\n\r\n    CREATE OR REPLACE STORAGE LIFECYCLE POLICY SLP_EXPIRE_OLD\r\n      AS (ts TIMESTAMP)\r\n      RETURNS BOOLEAN -\u003E\r\n        TO_DATE(ts) \u003C TO_DATE(DATEADD(DAY, -30, CURRENT_TIMESTAMP()));\r\n\r\n    ALTER TABLE SLP_CIDT\r\n      ADD STORAGE LIFECYCLE POLICY SLP_EXPIRE_OLD ON (event_ts);","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1484918925":{"id":"blog-text-98c21ae528","text":"\u003Ch1\u003EConclusion\u003C/h1\u003E\r\n\u003Cp\u003ETo summarize, custom incremental dynamic tables are a powerful new tool you should reach for your ETL toolkit when you have a use case that existing SELECT-based dynamic tables can't express. Many streams-and-task pipelines that only do INSERT/MERGE are also ideal candidates to use custom incremental dynamic tables instead to get the convenience and simplicity of dynamic tables. Custom incremental dynamic tables fit seamlessly into pipelines with SELECT-based dynamic tables. They can be either downstream or upstream of the latter. If you already have a pipeline that \u003Ci\u003Ealmost\u003C/i\u003E does what you want with SELECT-based dynamic tables and you need just one node that requires MERGE logic, you can now use custom incremental tables to implement that node. We are working on further improvements in this space, such as simplifying merge syntax and supporting other DML flavors, so stay tuned for future announcements.\u003C/p\u003E\r\n\u003Cp\u003ETo get started, visit our “Comprehensive Developer Guide” for Dynamic Tables or point your favorite coding agent to the \u003Ca href=\"https://docs.snowflake.com/en/user-guide/dynamic-tables/overview\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Edynamic tables documentation pages\u003C/a\u003E (hint: \u003Ca href=\"https://www.snowflake.com/en/product/snowflake-coco/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003ESnowflake CoCo\u003C/a\u003E is a great resource for dynamic tables).\u003C/p\u003E\r\n\u003Cp\u003E\u003Cb\u003EForward-looking statement\u003C/b\u003E\u003Cbr\u003E\r\nThis blog post contains forward-looking statements, including statements about products, features and capabilities that are under development, not yet generally available or otherwise not yet available. These forward-looking statements are subject to risks and uncertainties that could cause actual results to differ materially. Please refer to Snowflake's SEC filings for a more detailed description of the risks that could cause actual results to differ.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"}},":itemsOrder":["blog_text","code_snippet","blog_text_212846980","blog_text_2005467691","blog_text_1042472597","code_snippet_1866006943","blog_text_428035174","code_snippet_1213013782","blog_text_1748006042","blog_text_1237527855","blog_text_2058087919","code_snippet_679208649","blog_text_30303678","blog_text_453880716","blog_text_1423146289","blog_text_1669573718","blog_text_763607309","blog_text_1973520463","blog_text_1504885809","code_snippet_1685045985","blog_text_834708523","blog_text_1196225079","blog_text_2027093586","blog_text_1463674035","blog_text_701065994","blog_text_2133377275","blog_text_1471904003","blog_text_1365803119","blog_text_1261347785","code_snippet_215705829","blog_text_650743061","code_snippet_1777458129","blog_text_80077891","code_snippet_1171696641","blog_text_1421180790","code_snippet_1237734067","blog_text_1484918925"],":type":"wcm/foundation/components/responsivegrid"},"responsivegrid_premium_content_banner":{"columnCount":12,"columnClassNames":{},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","appliedCssClassNames":"snowflake-responsive-component-top-padding-medium",":items":{},":itemsOrder":[],":type":"wcm/foundation/components/responsivegrid"},"container_author_chip":{"layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"author_chip":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"container-3ef8c194b8","appliedCssClassNames":"snowflake-responsive-component-top-padding-medium",":type":"snowflake-site/components/container",":items":{"author_chip":{"id":"author-chip-b685cd95a2","title":{"id":"title","type":"heading2","lines":["Learn more about the authors"],":type":"snowflake-site/components/title-v2"},"authors":[{"authorCta":{"id":"button-20c7c4aeda","showOutboundIcon":false,"buttonLink":{"valid":true,"url":"/en/blog/authors/anirudh-santhiar/"},"linkTargetContentType":"DOCUMENT_LEARN",":type":"snowflake-site/components/button","linkType":"SNOWFLAKE_INTERNAL","text":"Anirudh Santhiar"},"authorTitle":"Senior Software Engineer"}],":type":"snowflake-site/components/blog/author-chip"}},":itemsOrder":["author_chip"]}},":itemsOrder":["container_hero","responsivegrid_content","responsivegrid_premium_content_banner","container_author_chip"]},"flexible_column_content_container_2":{"layout":"SIMPLE","id":"container-993760ed54",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"blog_table_of_content":{"id":"blog-table-of-content-091948df51",":type":"snowflake-site/components/blog/blog-table-of-content","tableOfContents":[]}},":itemsOrder":["blog_table_of_content"]},":type":"snowflake-site/components/flexible-column-container","isBlogPage":true,"isActiveTOC":false},"related_content":{"id":"related-content-4d23c42d43","relatedContent":[],":type":"snowflake-site/components/blog/related-content","isBlogPage":true}},":itemsOrder":["flexible_column_container","related_content"]}},":itemsOrder":["container_breadcrumb","container_main_content"],":type":"wcm/foundation/components/responsivegrid"},"container_47873732":{"additionalClasses":"section--blog-newsletter","layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"flexible_column_cont":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"container-ca467229ff","appliedCssClassNames":"snowflake-container",":type":"snowflake-site/components/container",":items":{"flexible_column_cont":{"id":"flexible-column-container-46ef9bfc4c","type":"1-column","alignColumns":"top","containerMaxWidth":"extra-large","topPadding":"small","bottomPadding":"none","spaceBetween":"small","reverseOnMobile":false,"carouselOnMobile":false,"propertiesCSSClasses":"section--blog-newsletter","backgroundImageOption":"none","flexible_column_content_container_1":{"layout":"SIMPLE","id":"container-9aeaf67881",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"marketo_v2":{"id":"marketo-v2-f997e610a8","marketoForm":{"hidden":null,"edit":false,"formId":"3320","successUrl":null,"script":null,"values":null},"title":{"id":"title","type":"heading3","lines":["Subscribe to our blog newsletter","Get the best, coolest and latest delivered to your inbox each week"],":type":"snowflake-site/components/title-v2"},"munchkinId":"252-RFO-227","serverInstance":"252-RFO-227.mktoweb.com","marketoConfigured":true,"formConfigured":true,":type":"snowflake-site/components/form/marketo-v2"},"text":{"id":"text-e933edf9df","additionalClasses":"newsletter-disclaimer","text":"\u003Cp\u003EBy submitting this form, I understand Snowflake will process my personal information in accordance with their Privacy Notice.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/text"}},":itemsOrder":["marketo_v2","text"]},":type":"snowflake-site/components/flexible-column-container","isBlogPage":true,"isActiveTOC":false}},":itemsOrder":["flexible_column_cont"]},"experiencefragment-pre-footer":{"id":"experiencefragment-1ebf885750","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/get-started-pre-footer/get-started-pre-footer/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/get-started-pre-footer/get-started-pre-footer.xfmodel.json"},"markup_editor":{"id":"markup-editor-6ff1f8ab4b","title":"Page CSS","cssContent":"@media screen and (min-width:768px){.snowflake-blog-author-chip-wrapper{justify-content:flex-start}.snowflake-blog-related-content-on-blog-page{max-width:1408px;margin-left:auto;margin-right:auto}.snowflake-text{font-family:Lato,sans-serif;font-weight:400;font-size:16px;line-height:24px}}.section--blog-newsletter{max-width:none;width:100%;padding-left:0;padding-right:0;margin-left:0;margin-right:0;margin-bottom:0}.section--blog-newsletter .mktoField{background-color:transparent !important}.section--blog-newsletter\u003E.container{padding-left:0;padding-right:0}@media screen and (min-width:768px){.section--blog-newsletter\u003E.container{padding-left:0;padding-right:0}}.newsletter-disclaimer p{font-size:14px !important}.section--blog-newsletter .snowflake-marketo-form-container{margin-bottom:24px;background-color:#f6f9fa;gap:48px;box-shadow:none}.section--blog-newsletter .snowflake-title p.snowflake-title-line:first-child{font-family:Texta;font-size:24px;line-height:26px;font-weight:700;margin-bottom:4px}.section--blog-newsletter .snowflake-title p.snowflake-title-line{text-transform:none;font-family:\"Lato\",sans-serif;font-size:16px;line-height:24px;font-weight:normal}@media screen and (min-width:1024px){.section--blog-newsletter .snowflake-marketo-form-container{display:flex;justify-content:center}.section--blog-newsletter .snowflake-title .snowflake-title-line{text-align:left}.section--blog-newsletter .snowflake-marketo-form .mktoFormRow:has(\u003E input[type=\"hidden\"]){flex-grow:0}.section--blog-newsletter .snowflake-marketo-form{display:flex;width:50% !important}.section--blog-newsletter .snowflake-marketo-form .mktoButtonRow{flex-grow:0;width:auto !important;margin-left:0;margin-right:0}.section--blog-newsletter .snowflake-marketo-form .mktoFormRow{flex-grow:1}.section--blog-newsletter\u003E.container{padding-left:0;padding-right:0}.section--blog-newsletter .snowflake-marketo-form-title{width:50%;margin-bottom:0 !important}.section--blog-newsletter .center .snowflake-title{align-items:flex-start}}.snowflake-sub-navigation a.snowflake-sub-navigation-primary-link{width:auto !important}.snowflake-blog-hero{align-items:stretch !important}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false},"markup_editor-table":{"id":"markup-editor-1c4b1763d2","title":"Table Styling CSS","cssContent":"#snowflake-blog-template-main-container table{width:100%;background-color:var(--ui-background-01);border-collapse:collapse;border:2px solid var(--ui-background-09);font-family:'Lato',sans-serif;color:var(--ui-background-09)}#snowflake-blog-template-main-container table thead{background-color:var(--ui-01)}#snowflake-blog-template-main-container table th,#snowflake-blog-template-main-container table td{border:2px solid var(--ui-background-09);padding:var(--spacing-01)}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false},"experiencefragment-footer":{"id":"experiencefragment-ac4f5c651a","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/footer/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/footer/master.xfmodel.json"}},":itemsOrder":["experiencefragment-banner","experiencefragment-header","experiencefragment-sub-header","responsivegrid","container_47873732","experiencefragment-pre-footer","markup_editor","markup_editor-table","experiencefragment-footer"],":type":"wcm/foundation/components/responsivegrid"}},":itemsOrder":["root"],":hierarchyType":"page",":path":"/content/snowflake-site/global/en/blog/engineering/custom-incremental-dynamic-tables","isPasswordProtected":false,"coveoConfig":{"apiKey":"xx335921a6-2a0a-40f2-a167-e390b4766c3d","pipeline":"snowflake.com","searchHub":"snowflake.com","organizationId":"snowflakecomputingproduction8neljofn"},"analyticsContentTags":["snowflake-site:taxonomy/blog/engineering-blog/data-engineering"],"analyticsEnabled":true,"analyticsDebugMode":false,"analyticsData":{"excludeFromAnalytics":false,"subCategory":"","pageType":"homepage","templateName":"blog-page","siteName":"snowflake","pageUrl":"/content/snowflake-site/global/en/blog/engineering/custom-incremental-dynamic-tables","language":"en","category":"general","pageName":"Beyond SELECT Statements: Introducing Dynamic Tables With Custom Incrementalization ","contentTags":["snowflake-site:taxonomy/blog/engineering-blog/data-engineering"]},"locale":"en"}
  