<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>article Merge - Deep Dive in Technical Blog</title>
    <link>https://community.databricks.com/t5/technical-blog/merge-deep-dive/ba-p/111190</link>
    <description>&lt;P&gt;&lt;SPAN&gt;In this blog we are going beyond the basics to explore the internals of Databricks &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/sql/language-manual/delta-merge-into.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Merge into&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;By the end of this article, you will learn:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How the merge command works.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How running MERGE using Photon + Liquid clustering the target table can supercharge your merge statements with ~5x improvement.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How to access merge performance metrics and use them to tune the merge statement.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How to using broadcasting with merge to achieve ~7x improvement.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How to rewrite certain merge statements to achieve ~2x improvement.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;This blog assumes some familiarity with the below concepts:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;A href="https://docs.databricks.com/en/delta/clustering.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Liquid Clustering&lt;/SPAN&gt;&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;A href="https://docs.databricks.com/en/delta/deletion-vectors.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Deletion Vectors&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;A href="https://www.databricks.com/blog/2020/04/30/faster-sql-queries-on-delta-lake-with-dynamic-file-pruning.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Dynamic File pruning&lt;/SPAN&gt;&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Types of joins (&lt;/SPAN&gt;&lt;A href="https://www.databricks.com/discover/pages/optimize-data-workloads-guide#data-shuffling" target="_blank" rel="noopener"&gt;&lt;I&gt;&lt;SPAN&gt;Broadcast-Hash and Shuffle-Hash&lt;/SPAN&gt;&lt;/I&gt;&lt;/A&gt;&lt;SPAN&gt;)&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H1&gt;&lt;SPAN&gt;How merge works&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;Lets start with the merge query below, lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;merge into
  target_data as tgt
using
  source_data1 as src
on tgt.user_id = src.user_id
when matched then update set *
when not matched then insert *&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The terminology used here is &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data)&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; for the table we are merging into, and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source_data1) &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;for the table that has the change feed/data that needs to be merged into the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. In a nut shell we can break down the execution of merge into 2 major phases: Scan and Rewrite.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Scan phase&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;In the scan phase of MERGE, both source and target are scanned and joined together to find matches. The join can be &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; or &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;shuffle-hash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, depending on the sizes of the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, as well as the type of join (inner, left-anti, and others). Lets refer to the number of files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; with matching data from both the tables as K. So the outcome of this phase is to get the list of K files, in preparation for the next Rewrite phase.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Rewrite phase&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;In this phase only those K files are scanned from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and everything from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, joined to apply updates for the matching(M) rows and inserts for the new(N) or not matched rows. While M + N rows get written as new Delta files, there will be K &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;DeletionVectors&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; or Delta file rewrites(or both) in the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. During this phase, since the scanning of K files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; tables is happening for the second time &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/optimizations/disk-cache.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Disk Caching&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; comes into effect. During this phase the tables are scanned from the SSDs of the compute nodes instead of cloud storage, so table scans are much faster.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Rewriting a Delta file vs Deletion vectors&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;If the portion of data modified in a Delta file from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table reaches a level where read amplification(using Deletion vectors) exceeds the rewrite amplification, the Delta file will be rewritten. For example, if more than 50% of a Delta file’s rows have a deletion vector associated with them, then it probably makes sense to rewrite the Delta file rather than creating a new deletion vector entry. On the flip side, if less than 1% of a Delta file needs to be rewritten, then it probably makes more sense to create a deletion vector rather than rewriting the Delta file.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;How LC + DFP + Photon improve Merge Performance&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;Before we dive into this topic let's do a quick summary on input tables, the code required for creating input tables is at the bottom of this blog.&amp;nbsp;&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 5B rows stored in 500 files, &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source_data1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 8k rows in 5 files, &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is the unique key in both the tables. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;doesn’t have liquid clustering enabled.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Lets use the same merge query above(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) for the 3 scenarios below. All the merge queries covered in this blog are run on either Classic/Serverless warehouses, this helps to skim through pruning metrics easily.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;No Optimizations&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;This scenario covers running &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; on Classic warehouses or non-Photon enabled Compute clusters. Although classic warehouses support certain Photon operations, they don’t fully Photonize Merge. Also for this scenario the tables don’t have liquid clustering enabled.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Scan phase&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Spark reads/scans all the 500 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, and determines that all 500 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; have matching rows from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. This list of files names/paths will be input to the next phase, the rewrite.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Rewrite phase&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;During this phase, 500 files from target, 5 files from source will be scanned, 4001 rows that are matching from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; get updated, rest of the rows from source are inserted. Since all the 500 files are impacted due to merge, 500 &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;DeletionVectors&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; are created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Metrics&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Query profiler metrics show that it took (wall clock time) 92 sec to run this query and 1010 files read.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;1010 =&amp;nbsp; 2 * 500 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) + 2 * 5 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;).&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Delta history metrics show # of rows modified (inserted+updated), # of deletion vectors created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_0-1740542713302.png" style="width: 381px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15061i47566B2930B51B4B/image-dimensions/381x479?v=v2" width="381" height="479" role="button" title="MuraliTalluri_0-1740542713302.png" alt="MuraliTalluri_0-1740542713302.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_1-1740542713333.png" style="width: 322px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15063i969E38C2C7B1F671/image-dimensions/322x499?v=v2" width="322" height="499" role="button" title="MuraliTalluri_1-1740542713333.png" alt="MuraliTalluri_1-1740542713333.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Liquid Clustering&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;This scenario covers running &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; on Classic warehouses or non-Photon enabled Compute clusters. But the difference here is that the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is liquid clustered by the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; column.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Below is the command for liquid clustering:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;%sql
alter table target_data cluster by (user_id);
optimize target_data;&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The above command changes the underlying file layout. Key metrics: &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numFilesAdded&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;: 430, &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numFilesRemoved&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;: 500.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Scan phase&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Spark reads/scans all the 430 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, and determines that only 8 out of 430 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; have matching rows from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. In simple terms, the same 4001 rows matching from both the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; were found in just 8 files! This list of files names/paths will be input to the next phase.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Rewrite phase&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;During this phase, 8 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; are scanned, 4001 rows that are matching from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; get updated, rest of the rows from source are inserted. Since only 8 files are impacted due to merge, 8 Deletion vectors are created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Metrics&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Query profiler metrics show that it took(wall clock time) 74 sec to run this query and 448 files read, 422 files pruned.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;448 =&amp;nbsp; 430 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; during scan phase) + 8(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; during rewrite phase) + 2 * 5 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;).&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Delta history metrics show # of rows modified(inserted+updated), # of deletion vectors created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_2-1740542713324.png" style="width: 392px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15062iB5700119C593318E/image-dimensions/392x533?v=v2" width="392" height="533" role="button" title="MuraliTalluri_2-1740542713324.png" alt="MuraliTalluri_2-1740542713324.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_3-1740542713336.png" style="width: 345px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15064i3722987F4788B917/image-dimensions/345x521?v=v2" width="345" height="521" role="button" title="MuraliTalluri_3-1740542713336.png" alt="MuraliTalluri_3-1740542713336.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Liquid Clustering + Photon&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;This scenario covers running &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; on Serverless warehouses or Photon enabled Compute clusters or Serverless. DFP kicks in automatically for &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;Merge&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; when using the Photon engine.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;The &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is liquid clustered by the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; column, and has 430 files.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Scan phase&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Unlike Spark, Photon doesn’t scan the entire &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table and perform the join. Photon first checks if it can &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. In our case, it did broadcast, which means that our query likely attempted to dynamically prune files.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Dynamic File Pruning (DFP)&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;As part of DFP, all the distinct keys(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table are collected and broadcasted, in this case there are 8k unique &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_ids&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. The &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table (which is already liquid clustered by &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) would then be filtered/queried for only those keys; the operator is called &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;SubqueryBroadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, seen below, which leads to much better pruning. On the flip side, if the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table had not been liquid clustered, DFP would still execute &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;SubqueryBroadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, but the pruning may not be as efficient.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;In this scenario, 104M rows are read from source data, 4.89B rows were skipped.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_4-1740542713297.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15066i5C5CF9311DC0F406/image-size/medium?v=v2&amp;amp;px=400" role="button" title="MuraliTalluri_4-1740542713297.png" alt="MuraliTalluri_4-1740542713297.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;So during this phase, DFP helps to read only 8 files from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, and gets the list of files names/paths with matching rows, this will be input to the next phase.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Rewrite phase&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;During this phase, 8 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; are scanned, 4001 rows that are matching from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; get updated, rest of the rows from source are inserted. Since only 8 files are impacted due to merge, 8 Deletion vectors are created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Metrics&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Query profiler metrics show that it took(wall clock time) 17 sec to run this query and 26 files read, 844 files pruned.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;26 =&amp;nbsp; 8 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; during scan phase) + 8(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; during rewrite phase) + 2 * 5 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;).&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;We can see how the merge time got reduced from 74 sec to 17 sec. This is why we always recommend using Liquid clustering + Photon for merge.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Delta history metrics show # of rows modified(inserted+updated), # of deletion vectors created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_5-1740542713330.png" style="width: 398px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15065iBF020F840007B1AF/image-dimensions/398x548?v=v2" width="398" height="548" role="button" title="MuraliTalluri_5-1740542713330.png" alt="MuraliTalluri_5-1740542713330.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_6-1740542713336.png" style="width: 339px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15068i81FF37596789D7F9/image-dimensions/339x529?v=v2" width="339" height="529" role="button" title="MuraliTalluri_6-1740542713336.png" alt="MuraliTalluri_6-1740542713336.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;We could achieve 92 sec to 17 sec, 5x improvement using Photon + Liquid clustering target table.&amp;nbsp;&lt;/STRONG&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Gotchas with DFP&lt;/SPAN&gt;&lt;/H3&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;DFP kicks in only when the join performed during the scan phase can be a &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; join. Otherwise DFP won’t kick in, a full &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table scan is performed. While AQE does its best to determine if the table needs to be &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcasted&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, you can force the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; using &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-qry-select-hints.html#join-hint-types" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;hints&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;. We will cover in depth about this topic in the next section.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;If the number of unique keys in the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is greater than 10k, min &amp;amp; max of this list is used for &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;SubqueryBroadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, rather than forming a sub query with every element in the list. This can sometimes make the pruning less efficient, meaning more files might be scanned from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, but it should still be more efficient than scanning everything from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H1&gt;&lt;SPAN&gt;Merge metrics from Delta table history&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;In this section we will see how the Spark UI side of things look like, and how to get the same metrics from Delta history.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Querying Delta history: just showing the metrics related to scan and rewrite phase, recommend you to go through all the metrics.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;scanTimeMs: Total duration of the scan phase during merge.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;rewriteTimeMs: Total duration of the rewrite phase during merge.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;executionTimeMs: Total duration of the merge&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;Querying Delta table history:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;%sql
with history_tab as (
 describe history target_data
)
select
operationmetrics.scanTimeMs,
operationmetrics.rewriteTimeMs,
operationmetrics.executionTimeMs
from history_tab where history_tab.operation = 'MERGE'
order by version&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The entries from Spark UI that have &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;scanning files for matches&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, show the tasks for scan phase. The entries that have&lt;/SPAN&gt; &lt;I&gt;&lt;SPAN&gt;rewriting N files&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, show the tasks for the rewrite phase.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;No Optimizations&lt;/SPAN&gt;&lt;/H2&gt;
&lt;H3&gt;&lt;SPAN&gt;SparkUI&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;We can see that all 500 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; were rewritten, but they are not fully rewritten/overridden, it means that 500 deletion vectors are created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_7-1740542713361.png" style="width: 747px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15069i636E068D5221C141/image-dimensions/747x279?v=v2" width="747" height="279" role="button" title="MuraliTalluri_7-1740542713361.png" alt="MuraliTalluri_7-1740542713361.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;&amp;nbsp;Delta history - merge metrics&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Scan phase took ~57s vs total merge time ~82s.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_8-1740542713258.png" style="width: 600px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15067iEA7EB05A7927F1B2/image-dimensions/600x75?v=v2" width="600" height="75" role="button" title="MuraliTalluri_8-1740542713258.png" alt="MuraliTalluri_8-1740542713258.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Liquid Clustering&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;H3&gt;&lt;SPAN&gt;SparkUI&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Notice that only 8 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; were rewritten.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_9-1740542713358.png" style="width: 732px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15072i172315A3F3BB15B0/image-dimensions/732x236?v=v2" width="732" height="236" role="button" title="MuraliTalluri_9-1740542713358.png" alt="MuraliTalluri_9-1740542713358.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Delta history - merge metrics&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Scan phase took ~44s vs total merge time ~63s. Note that scanTimeMs is slightly lower than previous run (57s), because its 430 vs 500 files are read during the scan phase.&amp;nbsp;&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;rewriteTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is 5s vs 9s, this is due to the fact that only 8 files were scanned/rewritten from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table vs 500 from the previous scenario.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_10-1740542713259.png" style="width: 593px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15070i5E7DC6BBFF92622A/image-dimensions/593x76?v=v2" width="593" height="76" role="button" title="MuraliTalluri_10-1740542713259.png" alt="MuraliTalluri_10-1740542713259.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Liquid Clustering + Photon&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;H3&gt;&lt;SPAN&gt;SparkUI&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;I&gt;&lt;SPAN&gt;On Photon runtimes this view is compacted. &lt;/SPAN&gt;&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_11-1740542713353.png" style="width: 735px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15071iAAB703CDA4B6C4B5/image-dimensions/735x136?v=v2" width="735" height="136" role="button" title="MuraliTalluri_11-1740542713353.png" alt="MuraliTalluri_11-1740542713353.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Delta history - merge metrics&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;The scan phase duration was drastically cut, taking 7 seconds versus our previous 57 seconds and 44 seconds. This is due to how complimentary DFP and clustering are.. The rewrite phase took 4s, which is slightly faster than the previous scenario because it is operating on the same amount of data, and is using Photon for the speed boost.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_12-1740542713181.png" style="width: 638px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15073iB725FA8EB7A1AF8E/image-dimensions/638x85?v=v2" width="638" height="85" role="button" title="MuraliTalluri_12-1740542713181.png" alt="MuraliTalluri_12-1740542713181.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Gotchas with rewriteTimeMs&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;In all the scenarios above, we observed that &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;rewriteTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is much lower than &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;scanTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. While this may not always be the case, in this instance, the rewrite phase doesn’t involve rewriting the entire Delta file; instead, only deletion vectors are generated. If the portion of data modified in a Delta file reaches a level where read amplification exceeds rewrite amplification, the Delta file will be rewritten. In such scenarios you will see rewriteTimeMs higher.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Why is Merge metrics from Delta table history important?&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Having to tune the Merge for performance is very common. When you observe that your merge command is taking longer than expected, you can simply query the merge metrics from Delta history to identity one of these:&lt;/SPAN&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How has each part of the merge been performing over time? Has its performance degraded over time? Is its performance cyclical or predictable?&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;What fraction of the merge time is going for the scan vs rewrite phase?&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;If &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;scanTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is taking longer, check to see if pruning has happened&lt;/SPAN&gt;&lt;SPAN&gt; and if we can make pruning better by clustering on &lt;/SPAN&gt;&lt;A href="https://youtu.be/yZmrpXJg-G8?si=49yetG6Frr4M1lGW&amp;amp;t=2151" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;meaningful merge keys&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;If the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;rewriteTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is taking longer, we can check what's going with Deletion vectors. We can compare &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numTargetDeletionVectorsAdded&lt;/SPAN&gt;&lt;/I&gt; &lt;SPAN&gt;vs &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numTargetRowsUpdated&lt;/SPAN&gt;&lt;/I&gt; &lt;SPAN&gt;vs &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numTargetFilesAdded&lt;/SPAN&gt;&lt;/I&gt; &lt;SPAN&gt;to see if the slowness is because of Delta files being overwritten.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;&lt;SPAN&gt;For example if you run the above query for Delta history, you can see below.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_13-1740542713153.png" style="width: 651px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15074i09F56CE1F95AE49D/image-dimensions/651x146?v=v2" width="651" height="146" role="button" title="MuraliTalluri_13-1740542713153.png" alt="MuraliTalluri_13-1740542713153.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;It's easy to identify the merge trend, which part of the merge is taking longer etc..&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Note: If your source table used in the merge is a view (either created from a Dataframe or a query), it can add additional time as the source dataset needs to be materialized first. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;materializeSourceTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; represents the duration of materialization of the source dataset in merge. If your source dataset is a Delta table, this duration can be negligible(milli sec).&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&amp;nbsp;&lt;/H1&gt;
&lt;H1&gt;&lt;SPAN&gt;How to Broadcast in Merge query&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;For this section, both the merge queries are run on a medium serverless warehouse, this means you expect LC + DFP + Photon, and we don’t emphasize on the rewrite phase of merge, as we are trying to optimize the scan phase.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Quick summary on input tables: &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 5B rows in 430 files (81GB), liquid clustered by &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source_data2&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 5M rows in 5 files (76MB), &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is the unique key in both the tables.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;No broadcast hint&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Lets start with the merge query below, lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query2&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;merge into
  target_data as tgt
using
  source_data2 as src
on tgt.user_id = src.user_id
when matched then update set *
when not matched then insert *&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Even though we are using Photon here, DFP doesn’t kick in during the scan phase. The size of the source table is 76MB, which is too large to automatically be broadcasted, so &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;ShuffleHash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; join is performed instead of &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;BroadcastHash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. DFP kicks in only when the join can be &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcasted&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. Since there is no DFP, file pruning is also not effective.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_14-1740542713310.png" style="width: 378px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15075i3A0F4DFA88FEFD73/image-dimensions/378x539?v=v2" width="378" height="539" role="button" title="MuraliTalluri_14-1740542713310.png" alt="MuraliTalluri_14-1740542713310.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_15-1740542713251.png" style="width: 331px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15078i93553E20A52F43BC/image-dimensions/331x507?v=v2" width="331" height="507" role="button" title="MuraliTalluri_15-1740542713251.png" alt="MuraliTalluri_15-1740542713251.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The query took 2 min 39s to finish, but most of the time went in the scan phase (2 min 29 s).&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_16-1740542713041.png" style="width: 718px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15076iD979466616278EA9/image-dimensions/718x66?v=v2" width="718" height="66" role="button" title="MuraliTalluri_16-1740542713041.png" alt="MuraliTalluri_16-1740542713041.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Broadcast hint&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Even though we are using Photon, and the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is liquid clustered, merge is still slow.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Now lets try to speed up the merge query by using broadcast hint on the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. We are trying to explicitly broadcast a 76MB (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) table, to avoid shuffling of the 81GB (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) table.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query3&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;merge into 
  target_data as tgt 
using (
  select /*+ BROADCAST */ * from source_data2
) as src 
on tgt.user_id = src.user_id
when matched then update set *
when not matched then insert *&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Now &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;BroadcastHash &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;join&lt;/SPAN&gt; &lt;SPAN&gt;is performed instead of &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;ShuffleHash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; join, DFP kicks in; file pruning is also very effective.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_17-1740542713168.png" style="width: 351px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15077iDA6D546D25109E0A/image-dimensions/351x486?v=v2" width="351" height="486" role="button" title="MuraliTalluri_17-1740542713168.png" alt="MuraliTalluri_17-1740542713168.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_18-1740542713331.png" style="width: 352px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15080iD88540B4EEE6CB2B/image-dimensions/352x476?v=v2" width="352" height="476" role="button" title="MuraliTalluri_18-1740542713331.png" alt="MuraliTalluri_18-1740542713331.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The query took 21s to finish (previously 2 min 39s), scan phase is much faster.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_19-1740542713260.png" style="width: 718px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15081iC3719D3FAF0C8432/image-dimensions/718x60?v=v2" width="718" height="60" role="button" title="MuraliTalluri_19-1740542713260.png" alt="MuraliTalluri_19-1740542713260.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;If the same query was run using &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; hint on a classic warehouse, we should still see good improvement vs without hint. The improvement can be attributed to the fact that we are still avoiding &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;shufflehash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; join, but DFP won’t kick in classic warehouses.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;We can see that merge execution time is reduced from 159 sec to 21 sec by &lt;/STRONG&gt;&lt;STRONG&gt;&lt;I&gt;broadcasting&lt;/I&gt;&lt;/STRONG&gt;&lt;STRONG&gt; the table source table, 7x improvement.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The goal is not to suggest broadcasting always while doing merge, it worked out in this case since by explicitly broadcasting a 76MB (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) table we avoided shuffling of the 81GB (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) table. Another successful example can be broadcasting a 2GB &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table vs a 2TB target table. While broadcasting 2GB might sound unrealistic but still worth it considering the target table size. An unsuccessful example can be broadcasting a 2GB source table vs a 10GB target table.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Merge query - Left Anti&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;In this section we are going to cover a MERGE that has only &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;when not matched insert &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;clause, and how to rewrite it to become faster. This kind of MERGE is more of an INSERT, but it checks to see if a row’s key already exist in the target.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Both the queries are run on a medium serverless warehouse.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Quick summary on input tables. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 5B rows(81GB), liquid clustered by &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source_data1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 8k rows(155 KB), &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is the unique key in both the tables. There are 3,999 user_ids that exist in &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; but not in &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Merge query - only when not matched&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Lets start with the merge query below, lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query4&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;merge into
  target_data as tgt
using
  source_data1 as src
on tgt.user_id = src.user_id
when not matched then insert *&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;In this scenario the query optimizer recognizes that the user is just inserting data, and chooses to perform a left anti join between source and target.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_20-1740542713189.png" style="width: 352px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15079i7CA352160B028950/image-dimensions/352x517?v=v2" width="352" height="517" role="button" title="MuraliTalluri_20-1740542713189.png" alt="MuraliTalluri_20-1740542713189.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_21-1740542713303.png" style="width: 447px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15083i0434C69230BF09EE/image-dimensions/447x458?v=v2" width="447" height="458" role="button" title="MuraliTalluri_21-1740542713303.png" alt="MuraliTalluri_21-1740542713303.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;It took 48 sec to finish the query, and there is no scan phase, only rewrite.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_22-1740542713258.png" style="width: 574px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15082i32AABA9D07D6E328/image-dimensions/574x76?v=v2" width="574" height="76" role="button" title="MuraliTalluri_22-1740542713258.png" alt="MuraliTalluri_22-1740542713258.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Query profile shows that its &lt;/SPAN&gt;&lt;SPAN&gt;source left anti join target&lt;/SPAN&gt;&lt;SPAN&gt; and then &lt;/SPAN&gt;&lt;SPAN&gt;insert into target&lt;/SPAN&gt;&lt;SPAN&gt;.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Broadcast hints likely will not work. The left side may be small enough to be broadcasted, but you can only broadcast the right side of a left anti join. The right side is often too large to be broadcasted.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_23-1740542713363.png" style="width: 657px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15084iF1B6453D27E681F7/image-dimensions/657x424?v=v2" width="657" height="424" role="button" title="MuraliTalluri_23-1740542713363.png" alt="MuraliTalluri_23-1740542713363.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Refactoring merge query with joins&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;As discussed above, the &lt;/SPAN&gt;&lt;SPAN&gt;source left anti join target&lt;/SPAN&gt;&lt;SPAN&gt; can be very expensive, as it involves shuffling the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. Lets speed up the merge process by changing it to below.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query5&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;insert into target_data 
with matching as (
    select source_data1.user_id
    from target_data inner join source_data1 
    on target_data.user_id = source_data1.user_id
)
select
  source_data1.*
from source_data1 left anti join matching 
on source_data1.user_id = matching.user_id&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;With query5, you may have noticed that we aren’t performing a merge at all, and you’d be right. We rely on the nature of the type of this merge, it is just an insert, we do not rewrite any rows in the target. Instead of performing a left anti join, we first perform a broadcastable inner join between source and target to find any matching rows. Next, we anti join the source table with these matches. Finally, we safely insert rows resulting from both joins into the table.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_24-1740542713330.png" style="width: 381px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15085i7008605005CFD11C/image-dimensions/381x444?v=v2" width="381" height="444" role="button" title="MuraliTalluri_24-1740542713330.png" alt="MuraliTalluri_24-1740542713330.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_25-1740542713346.png" style="width: 424px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15086iF4A672A112B121B6/image-dimensions/424x421?v=v2" width="424" height="421" role="button" title="MuraliTalluri_25-1740542713346.png" alt="MuraliTalluri_25-1740542713346.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;In this case, we were able to broadcast the first join, leading to a runtime improvement of ~2x (43s vs&amp;nbsp; 23s).&lt;/STRONG&gt;&lt;/P&gt;
&lt;H1&gt;&amp;nbsp;&lt;/H1&gt;
&lt;H1&gt;&lt;SPAN&gt;Summary&lt;/SPAN&gt;&lt;/H1&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Your merge will be most performant if you are using Photon engine + your &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is liquid clustered by a merge key. This is due to DFP, and we always recommend using Photon for merge.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;While you can get merge metrics from Spark UI, we can always programmatically get them from Delta table history. This comes in very handy for identifying slowness/bottlenecks in merge.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;If your &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is not broadcasted during the scan phase of the merge, and depending on the size of your &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; tables, we can use broadcast hints to speed up the merge.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Merge statement with only &lt;/SPAN&gt;&lt;SPAN&gt;when not matched then insert&lt;/SPAN&gt;&lt;SPAN&gt; can be rewritten using joins and can be faster than merge most of the times.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H1&gt;&lt;SPAN&gt;Code for creating input tables&lt;/SPAN&gt;&lt;/H1&gt;
&lt;DIV&gt;&lt;LI-CODE lang="python"&gt;%pip install dbldatagen&lt;/LI-CODE&gt;&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;%sql
set spark.sql.shuffle.partitions = auto;
create catalog if not exists mt_blog;
create schema if not exists merge_demo;&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;import dbldatagen as dg
from pyspark.sql import functions as F
from pyspark.sql.types import IntegerType, StringType, DateType, LongType, TimestampType
from datetime import timedelta, datetime, date

merge_source_generator = (dg.DataGenerator(spark, name="merge_source", rows=5000000000, partitions=400)
                      .withColumn("user_id", LongType(), minValue=1, maxValue=5000000000, step=1, random=False, uniqueValues = 5000000000)
                      .withColumn("segment_id", IntegerType(), minValue=1, maxValue=10000000, step=1, random=False)
                      .withColumn("dt", DateType(), random=False)
                      .withColumn("event_ts", TimestampType(), begin="2010-01-01 01:00:00", end="2024-12-31 23:59:00", interval="1 second", random=False )
                      )

merge_source_df = merge_source_generator.build()  # build our dataset
merge_source_df.repartition(500, 'user_id').write.format('delta').mode('overwrite').saveAsTable('mt_blog.merge_demo.target_data')&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;%sql
alter table mt_blog.merge_demo.target_data cluster by (user_id);
optimize mt_blog.merge_demo.target_data;&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;merge_change_generator1 = (dg.DataGenerator(spark, name="merge_source", rows=8000, partitions=5)
                      .withColumn("user_id", LongType(), minValue=4900000000, maxValue=5100000000, step=25000, random=False, uniqueValues = 8000)
                      .withColumn("segment_id", IntegerType(), minValue=10000001, maxValue=11000000, step=1, random=True)
                      .withColumn("dt", DateType(), random=True)
                      .withColumn("event_ts", TimestampType(), begin="2025-01-01 01:00:00", end="2025-12-31 23:59:00", interval="1 second", random=False )
                      )

merge_change_df1 = merge_change_generator1.build()  # build our dataset
merge_change_df1.write.format('delta').mode('overwrite').saveAsTable('mt_blog.merge_demo.source_data1')&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;merge_change_generator2 = (dg.DataGenerator(spark, name="merge_source", rows=5000000, partitions=5)
                      .withColumn("user_id", LongType(), minValue=4800000000, maxValue=5300000000, step=100, random=False, uniqueValues = 5000000)
                      .withColumn("segment_id", IntegerType(), minValue=10000001, maxValue=11000000, step=1, random=True)
                      .withColumn("dt", DateType(), random=True)
                      .withColumn("event_ts", TimestampType(), begin="2025-01-01 01:00:00", end="2025-12-31 23:59:00", interval="1 second", random=False )
                      )

merge_change_df2 = merge_change_generator2.build()  # build our dataset
merge_change_df2.write.format('delta').mode('overwrite').saveAsTable('mt_blog.merge_demo.source_data2')&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
    <pubDate>Wed, 26 Feb 2025 12:42:24 GMT</pubDate>
    <dc:creator>MuraliTalluri</dc:creator>
    <dc:date>2025-02-26T12:42:24Z</dc:date>
    <item>
      <title>Merge - Deep Dive</title>
      <link>https://community.databricks.com/t5/technical-blog/merge-deep-dive/ba-p/111190</link>
      <description>&lt;P&gt;&lt;SPAN&gt;In this blog we are going beyond the basics to explore the internals of Databricks &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/sql/language-manual/delta-merge-into.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Merge into&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;By the end of this article, you will learn:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How the merge command works.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How running MERGE using Photon + Liquid clustering the target table can supercharge your merge statements with ~5x improvement.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How to access merge performance metrics and use them to tune the merge statement.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How to using broadcasting with merge to achieve ~7x improvement.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How to rewrite certain merge statements to achieve ~2x improvement.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;This blog assumes some familiarity with the below concepts:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;A href="https://docs.databricks.com/en/delta/clustering.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Liquid Clustering&lt;/SPAN&gt;&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;A href="https://docs.databricks.com/en/delta/deletion-vectors.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Deletion Vectors&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;A href="https://www.databricks.com/blog/2020/04/30/faster-sql-queries-on-delta-lake-with-dynamic-file-pruning.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Dynamic File pruning&lt;/SPAN&gt;&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Types of joins (&lt;/SPAN&gt;&lt;A href="https://www.databricks.com/discover/pages/optimize-data-workloads-guide#data-shuffling" target="_blank" rel="noopener"&gt;&lt;I&gt;&lt;SPAN&gt;Broadcast-Hash and Shuffle-Hash&lt;/SPAN&gt;&lt;/I&gt;&lt;/A&gt;&lt;SPAN&gt;)&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H1&gt;&lt;SPAN&gt;How merge works&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;Lets start with the merge query below, lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;merge into
  target_data as tgt
using
  source_data1 as src
on tgt.user_id = src.user_id
when matched then update set *
when not matched then insert *&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The terminology used here is &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data)&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; for the table we are merging into, and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source_data1) &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;for the table that has the change feed/data that needs to be merged into the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. In a nut shell we can break down the execution of merge into 2 major phases: Scan and Rewrite.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Scan phase&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;In the scan phase of MERGE, both source and target are scanned and joined together to find matches. The join can be &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; or &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;shuffle-hash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, depending on the sizes of the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, as well as the type of join (inner, left-anti, and others). Lets refer to the number of files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; with matching data from both the tables as K. So the outcome of this phase is to get the list of K files, in preparation for the next Rewrite phase.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Rewrite phase&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;In this phase only those K files are scanned from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and everything from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, joined to apply updates for the matching(M) rows and inserts for the new(N) or not matched rows. While M + N rows get written as new Delta files, there will be K &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;DeletionVectors&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; or Delta file rewrites(or both) in the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. During this phase, since the scanning of K files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; tables is happening for the second time &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/optimizations/disk-cache.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Disk Caching&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; comes into effect. During this phase the tables are scanned from the SSDs of the compute nodes instead of cloud storage, so table scans are much faster.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Rewriting a Delta file vs Deletion vectors&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;If the portion of data modified in a Delta file from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table reaches a level where read amplification(using Deletion vectors) exceeds the rewrite amplification, the Delta file will be rewritten. For example, if more than 50% of a Delta file’s rows have a deletion vector associated with them, then it probably makes sense to rewrite the Delta file rather than creating a new deletion vector entry. On the flip side, if less than 1% of a Delta file needs to be rewritten, then it probably makes more sense to create a deletion vector rather than rewriting the Delta file.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;How LC + DFP + Photon improve Merge Performance&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;Before we dive into this topic let's do a quick summary on input tables, the code required for creating input tables is at the bottom of this blog.&amp;nbsp;&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 5B rows stored in 500 files, &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source_data1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 8k rows in 5 files, &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is the unique key in both the tables. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;doesn’t have liquid clustering enabled.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Lets use the same merge query above(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) for the 3 scenarios below. All the merge queries covered in this blog are run on either Classic/Serverless warehouses, this helps to skim through pruning metrics easily.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;No Optimizations&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;This scenario covers running &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; on Classic warehouses or non-Photon enabled Compute clusters. Although classic warehouses support certain Photon operations, they don’t fully Photonize Merge. Also for this scenario the tables don’t have liquid clustering enabled.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Scan phase&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Spark reads/scans all the 500 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, and determines that all 500 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; have matching rows from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. This list of files names/paths will be input to the next phase, the rewrite.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Rewrite phase&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;During this phase, 500 files from target, 5 files from source will be scanned, 4001 rows that are matching from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; get updated, rest of the rows from source are inserted. Since all the 500 files are impacted due to merge, 500 &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;DeletionVectors&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; are created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Metrics&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Query profiler metrics show that it took (wall clock time) 92 sec to run this query and 1010 files read.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;1010 =&amp;nbsp; 2 * 500 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) + 2 * 5 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;).&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Delta history metrics show # of rows modified (inserted+updated), # of deletion vectors created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_0-1740542713302.png" style="width: 381px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15061i47566B2930B51B4B/image-dimensions/381x479?v=v2" width="381" height="479" role="button" title="MuraliTalluri_0-1740542713302.png" alt="MuraliTalluri_0-1740542713302.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_1-1740542713333.png" style="width: 322px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15063i969E38C2C7B1F671/image-dimensions/322x499?v=v2" width="322" height="499" role="button" title="MuraliTalluri_1-1740542713333.png" alt="MuraliTalluri_1-1740542713333.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Liquid Clustering&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;This scenario covers running &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; on Classic warehouses or non-Photon enabled Compute clusters. But the difference here is that the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is liquid clustered by the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; column.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Below is the command for liquid clustering:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;%sql
alter table target_data cluster by (user_id);
optimize target_data;&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The above command changes the underlying file layout. Key metrics: &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numFilesAdded&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;: 430, &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numFilesRemoved&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;: 500.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Scan phase&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Spark reads/scans all the 430 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, and determines that only 8 out of 430 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; have matching rows from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. In simple terms, the same 4001 rows matching from both the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; were found in just 8 files! This list of files names/paths will be input to the next phase.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Rewrite phase&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;During this phase, 8 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; are scanned, 4001 rows that are matching from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; get updated, rest of the rows from source are inserted. Since only 8 files are impacted due to merge, 8 Deletion vectors are created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Metrics&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Query profiler metrics show that it took(wall clock time) 74 sec to run this query and 448 files read, 422 files pruned.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;448 =&amp;nbsp; 430 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; during scan phase) + 8(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; during rewrite phase) + 2 * 5 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;).&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Delta history metrics show # of rows modified(inserted+updated), # of deletion vectors created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_2-1740542713324.png" style="width: 392px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15062iB5700119C593318E/image-dimensions/392x533?v=v2" width="392" height="533" role="button" title="MuraliTalluri_2-1740542713324.png" alt="MuraliTalluri_2-1740542713324.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_3-1740542713336.png" style="width: 345px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15064i3722987F4788B917/image-dimensions/345x521?v=v2" width="345" height="521" role="button" title="MuraliTalluri_3-1740542713336.png" alt="MuraliTalluri_3-1740542713336.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Liquid Clustering + Photon&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;This scenario covers running &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; on Serverless warehouses or Photon enabled Compute clusters or Serverless. DFP kicks in automatically for &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;Merge&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; when using the Photon engine.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;The &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is liquid clustered by the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; column, and has 430 files.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Scan phase&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Unlike Spark, Photon doesn’t scan the entire &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table and perform the join. Photon first checks if it can &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. In our case, it did broadcast, which means that our query likely attempted to dynamically prune files.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Dynamic File Pruning (DFP)&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;As part of DFP, all the distinct keys(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table are collected and broadcasted, in this case there are 8k unique &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_ids&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. The &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table (which is already liquid clustered by &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) would then be filtered/queried for only those keys; the operator is called &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;SubqueryBroadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, seen below, which leads to much better pruning. On the flip side, if the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table had not been liquid clustered, DFP would still execute &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;SubqueryBroadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, but the pruning may not be as efficient.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;In this scenario, 104M rows are read from source data, 4.89B rows were skipped.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_4-1740542713297.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15066i5C5CF9311DC0F406/image-size/medium?v=v2&amp;amp;px=400" role="button" title="MuraliTalluri_4-1740542713297.png" alt="MuraliTalluri_4-1740542713297.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;So during this phase, DFP helps to read only 8 files from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, and gets the list of files names/paths with matching rows, this will be input to the next phase.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Rewrite phase&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;During this phase, 8 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, 5 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; are scanned, 4001 rows that are matching from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; get updated, rest of the rows from source are inserted. Since only 8 files are impacted due to merge, 8 Deletion vectors are created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Metrics&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Query profiler metrics show that it took(wall clock time) 17 sec to run this query and 26 files read, 844 files pruned.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;26 =&amp;nbsp; 8 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; during scan phase) + 8(&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; during rewrite phase) + 2 * 5 (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;).&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;We can see how the merge time got reduced from 74 sec to 17 sec. This is why we always recommend using Liquid clustering + Photon for merge.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Delta history metrics show # of rows modified(inserted+updated), # of deletion vectors created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_5-1740542713330.png" style="width: 398px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15065iBF020F840007B1AF/image-dimensions/398x548?v=v2" width="398" height="548" role="button" title="MuraliTalluri_5-1740542713330.png" alt="MuraliTalluri_5-1740542713330.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_6-1740542713336.png" style="width: 339px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15068i81FF37596789D7F9/image-dimensions/339x529?v=v2" width="339" height="529" role="button" title="MuraliTalluri_6-1740542713336.png" alt="MuraliTalluri_6-1740542713336.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;We could achieve 92 sec to 17 sec, 5x improvement using Photon + Liquid clustering target table.&amp;nbsp;&lt;/STRONG&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Gotchas with DFP&lt;/SPAN&gt;&lt;/H3&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;DFP kicks in only when the join performed during the scan phase can be a &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; join. Otherwise DFP won’t kick in, a full &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table scan is performed. While AQE does its best to determine if the table needs to be &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcasted&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, you can force the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; using &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-qry-select-hints.html#join-hint-types" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;hints&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;. We will cover in depth about this topic in the next section.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;If the number of unique keys in the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is greater than 10k, min &amp;amp; max of this list is used for &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;SubqueryBroadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, rather than forming a sub query with every element in the list. This can sometimes make the pruning less efficient, meaning more files might be scanned from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, but it should still be more efficient than scanning everything from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H1&gt;&lt;SPAN&gt;Merge metrics from Delta table history&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;In this section we will see how the Spark UI side of things look like, and how to get the same metrics from Delta history.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Querying Delta history: just showing the metrics related to scan and rewrite phase, recommend you to go through all the metrics.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;scanTimeMs: Total duration of the scan phase during merge.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;rewriteTimeMs: Total duration of the rewrite phase during merge.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;executionTimeMs: Total duration of the merge&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;Querying Delta table history:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;%sql
with history_tab as (
 describe history target_data
)
select
operationmetrics.scanTimeMs,
operationmetrics.rewriteTimeMs,
operationmetrics.executionTimeMs
from history_tab where history_tab.operation = 'MERGE'
order by version&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The entries from Spark UI that have &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;scanning files for matches&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, show the tasks for scan phase. The entries that have&lt;/SPAN&gt; &lt;I&gt;&lt;SPAN&gt;rewriting N files&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;, show the tasks for the rewrite phase.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;No Optimizations&lt;/SPAN&gt;&lt;/H2&gt;
&lt;H3&gt;&lt;SPAN&gt;SparkUI&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;We can see that all 500 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; were rewritten, but they are not fully rewritten/overridden, it means that 500 deletion vectors are created.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_7-1740542713361.png" style="width: 747px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15069i636E068D5221C141/image-dimensions/747x279?v=v2" width="747" height="279" role="button" title="MuraliTalluri_7-1740542713361.png" alt="MuraliTalluri_7-1740542713361.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;&amp;nbsp;Delta history - merge metrics&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Scan phase took ~57s vs total merge time ~82s.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_8-1740542713258.png" style="width: 600px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15067iEA7EB05A7927F1B2/image-dimensions/600x75?v=v2" width="600" height="75" role="button" title="MuraliTalluri_8-1740542713258.png" alt="MuraliTalluri_8-1740542713258.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Liquid Clustering&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;H3&gt;&lt;SPAN&gt;SparkUI&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Notice that only 8 files from &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; were rewritten.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_9-1740542713358.png" style="width: 732px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15072i172315A3F3BB15B0/image-dimensions/732x236?v=v2" width="732" height="236" role="button" title="MuraliTalluri_9-1740542713358.png" alt="MuraliTalluri_9-1740542713358.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Delta history - merge metrics&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Scan phase took ~44s vs total merge time ~63s. Note that scanTimeMs is slightly lower than previous run (57s), because its 430 vs 500 files are read during the scan phase.&amp;nbsp;&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;rewriteTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is 5s vs 9s, this is due to the fact that only 8 files were scanned/rewritten from the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table vs 500 from the previous scenario.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_10-1740542713259.png" style="width: 593px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15070i5E7DC6BBFF92622A/image-dimensions/593x76?v=v2" width="593" height="76" role="button" title="MuraliTalluri_10-1740542713259.png" alt="MuraliTalluri_10-1740542713259.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Liquid Clustering + Photon&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;H3&gt;&lt;SPAN&gt;SparkUI&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;I&gt;&lt;SPAN&gt;On Photon runtimes this view is compacted. &lt;/SPAN&gt;&lt;/I&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_11-1740542713353.png" style="width: 735px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15071iAAB703CDA4B6C4B5/image-dimensions/735x136?v=v2" width="735" height="136" role="button" title="MuraliTalluri_11-1740542713353.png" alt="MuraliTalluri_11-1740542713353.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Delta history - merge metrics&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;The scan phase duration was drastically cut, taking 7 seconds versus our previous 57 seconds and 44 seconds. This is due to how complimentary DFP and clustering are.. The rewrite phase took 4s, which is slightly faster than the previous scenario because it is operating on the same amount of data, and is using Photon for the speed boost.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_12-1740542713181.png" style="width: 638px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15073iB725FA8EB7A1AF8E/image-dimensions/638x85?v=v2" width="638" height="85" role="button" title="MuraliTalluri_12-1740542713181.png" alt="MuraliTalluri_12-1740542713181.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Gotchas with rewriteTimeMs&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;In all the scenarios above, we observed that &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;rewriteTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is much lower than &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;scanTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. While this may not always be the case, in this instance, the rewrite phase doesn’t involve rewriting the entire Delta file; instead, only deletion vectors are generated. If the portion of data modified in a Delta file reaches a level where read amplification exceeds rewrite amplification, the Delta file will be rewritten. In such scenarios you will see rewriteTimeMs higher.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Why is Merge metrics from Delta table history important?&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Having to tune the Merge for performance is very common. When you observe that your merge command is taking longer than expected, you can simply query the merge metrics from Delta history to identity one of these:&lt;/SPAN&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;How has each part of the merge been performing over time? Has its performance degraded over time? Is its performance cyclical or predictable?&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;What fraction of the merge time is going for the scan vs rewrite phase?&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;If &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;scanTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is taking longer, check to see if pruning has happened&lt;/SPAN&gt;&lt;SPAN&gt; and if we can make pruning better by clustering on &lt;/SPAN&gt;&lt;A href="https://youtu.be/yZmrpXJg-G8?si=49yetG6Frr4M1lGW&amp;amp;t=2151" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;meaningful merge keys&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;If the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;rewriteTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is taking longer, we can check what's going with Deletion vectors. We can compare &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numTargetDeletionVectorsAdded&lt;/SPAN&gt;&lt;/I&gt; &lt;SPAN&gt;vs &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numTargetRowsUpdated&lt;/SPAN&gt;&lt;/I&gt; &lt;SPAN&gt;vs &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;numTargetFilesAdded&lt;/SPAN&gt;&lt;/I&gt; &lt;SPAN&gt;to see if the slowness is because of Delta files being overwritten.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;&lt;SPAN&gt;For example if you run the above query for Delta history, you can see below.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_13-1740542713153.png" style="width: 651px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15074i09F56CE1F95AE49D/image-dimensions/651x146?v=v2" width="651" height="146" role="button" title="MuraliTalluri_13-1740542713153.png" alt="MuraliTalluri_13-1740542713153.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;It's easy to identify the merge trend, which part of the merge is taking longer etc..&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Note: If your source table used in the merge is a view (either created from a Dataframe or a query), it can add additional time as the source dataset needs to be materialized first. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;materializeSourceTimeMs&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; represents the duration of materialization of the source dataset in merge. If your source dataset is a Delta table, this duration can be negligible(milli sec).&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&amp;nbsp;&lt;/H1&gt;
&lt;H1&gt;&lt;SPAN&gt;How to Broadcast in Merge query&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;For this section, both the merge queries are run on a medium serverless warehouse, this means you expect LC + DFP + Photon, and we don’t emphasize on the rewrite phase of merge, as we are trying to optimize the scan phase.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Quick summary on input tables: &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 5B rows in 430 files (81GB), liquid clustered by &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source_data2&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 5M rows in 5 files (76MB), &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is the unique key in both the tables.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;No broadcast hint&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Lets start with the merge query below, lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query2&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;merge into
  target_data as tgt
using
  source_data2 as src
on tgt.user_id = src.user_id
when matched then update set *
when not matched then insert *&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Even though we are using Photon here, DFP doesn’t kick in during the scan phase. The size of the source table is 76MB, which is too large to automatically be broadcasted, so &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;ShuffleHash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; join is performed instead of &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;BroadcastHash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. DFP kicks in only when the join can be &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcasted&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. Since there is no DFP, file pruning is also not effective.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_14-1740542713310.png" style="width: 378px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15075i3A0F4DFA88FEFD73/image-dimensions/378x539?v=v2" width="378" height="539" role="button" title="MuraliTalluri_14-1740542713310.png" alt="MuraliTalluri_14-1740542713310.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_15-1740542713251.png" style="width: 331px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15078i93553E20A52F43BC/image-dimensions/331x507?v=v2" width="331" height="507" role="button" title="MuraliTalluri_15-1740542713251.png" alt="MuraliTalluri_15-1740542713251.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The query took 2 min 39s to finish, but most of the time went in the scan phase (2 min 29 s).&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_16-1740542713041.png" style="width: 718px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15076iD979466616278EA9/image-dimensions/718x66?v=v2" width="718" height="66" role="button" title="MuraliTalluri_16-1740542713041.png" alt="MuraliTalluri_16-1740542713041.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Broadcast hint&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Even though we are using Photon, and the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is liquid clustered, merge is still slow.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Now lets try to speed up the merge query by using broadcast hint on the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. We are trying to explicitly broadcast a 76MB (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) table, to avoid shuffling of the 81GB (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) table.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query3&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;merge into 
  target_data as tgt 
using (
  select /*+ BROADCAST */ * from source_data2
) as src 
on tgt.user_id = src.user_id
when matched then update set *
when not matched then insert *&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Now &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;BroadcastHash &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;join&lt;/SPAN&gt; &lt;SPAN&gt;is performed instead of &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;ShuffleHash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; join, DFP kicks in; file pruning is also very effective.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_17-1740542713168.png" style="width: 351px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15077iDA6D546D25109E0A/image-dimensions/351x486?v=v2" width="351" height="486" role="button" title="MuraliTalluri_17-1740542713168.png" alt="MuraliTalluri_17-1740542713168.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_18-1740542713331.png" style="width: 352px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15080iD88540B4EEE6CB2B/image-dimensions/352x476?v=v2" width="352" height="476" role="button" title="MuraliTalluri_18-1740542713331.png" alt="MuraliTalluri_18-1740542713331.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The query took 21s to finish (previously 2 min 39s), scan phase is much faster.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_19-1740542713260.png" style="width: 718px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15081iC3719D3FAF0C8432/image-dimensions/718x60?v=v2" width="718" height="60" role="button" title="MuraliTalluri_19-1740542713260.png" alt="MuraliTalluri_19-1740542713260.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;If the same query was run using &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;broadcast&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; hint on a classic warehouse, we should still see good improvement vs without hint. The improvement can be attributed to the fact that we are still avoiding &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;shufflehash&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; join, but DFP won’t kick in classic warehouses.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;We can see that merge execution time is reduced from 159 sec to 21 sec by &lt;/STRONG&gt;&lt;STRONG&gt;&lt;I&gt;broadcasting&lt;/I&gt;&lt;/STRONG&gt;&lt;STRONG&gt; the table source table, 7x improvement.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The goal is not to suggest broadcasting always while doing merge, it worked out in this case since by explicitly broadcasting a 76MB (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) table we avoided shuffling of the 81GB (&lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;) table. Another successful example can be broadcasting a 2GB &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table vs a 2TB target table. While broadcasting 2GB might sound unrealistic but still worth it considering the target table size. An unsuccessful example can be broadcasting a 2GB source table vs a 10GB target table.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Merge query - Left Anti&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;In this section we are going to cover a MERGE that has only &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;when not matched insert &lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;clause, and how to rewrite it to become faster. This kind of MERGE is more of an INSERT, but it checks to see if a row’s key already exist in the target.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Both the queries are run on a medium serverless warehouse.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Quick summary on input tables. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target_data&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 5B rows(81GB), liquid clustered by &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;. &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source_data1&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; has 8k rows(155 KB), &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;user_id&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; is the unique key in both the tables. There are 3,999 user_ids that exist in &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; but not in &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Merge query - only when not matched&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Lets start with the merge query below, lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query4&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;merge into
  target_data as tgt
using
  source_data1 as src
on tgt.user_id = src.user_id
when not matched then insert *&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;In this scenario the query optimizer recognizes that the user is just inserting data, and chooses to perform a left anti join between source and target.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_20-1740542713189.png" style="width: 352px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15079i7CA352160B028950/image-dimensions/352x517?v=v2" width="352" height="517" role="button" title="MuraliTalluri_20-1740542713189.png" alt="MuraliTalluri_20-1740542713189.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_21-1740542713303.png" style="width: 447px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15083i0434C69230BF09EE/image-dimensions/447x458?v=v2" width="447" height="458" role="button" title="MuraliTalluri_21-1740542713303.png" alt="MuraliTalluri_21-1740542713303.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;It took 48 sec to finish the query, and there is no scan phase, only rewrite.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_22-1740542713258.png" style="width: 574px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15082i32AABA9D07D6E328/image-dimensions/574x76?v=v2" width="574" height="76" role="button" title="MuraliTalluri_22-1740542713258.png" alt="MuraliTalluri_22-1740542713258.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Query profile shows that its &lt;/SPAN&gt;&lt;SPAN&gt;source left anti join target&lt;/SPAN&gt;&lt;SPAN&gt; and then &lt;/SPAN&gt;&lt;SPAN&gt;insert into target&lt;/SPAN&gt;&lt;SPAN&gt;.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Broadcast hints likely will not work. The left side may be small enough to be broadcasted, but you can only broadcast the right side of a left anti join. The right side is often too large to be broadcasted.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_23-1740542713363.png" style="width: 657px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15084iF1B6453D27E681F7/image-dimensions/657x424?v=v2" width="657" height="424" role="button" title="MuraliTalluri_23-1740542713363.png" alt="MuraliTalluri_23-1740542713363.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Refactoring merge query with joins&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;As discussed above, the &lt;/SPAN&gt;&lt;SPAN&gt;source left anti join target&lt;/SPAN&gt;&lt;SPAN&gt; can be very expensive, as it involves shuffling the &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table. Lets speed up the merge process by changing it to below.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Lets call this &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;query5&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt;.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;insert into target_data 
with matching as (
    select source_data1.user_id
    from target_data inner join source_data1 
    on target_data.user_id = source_data1.user_id
)
select
  source_data1.*
from source_data1 left anti join matching 
on source_data1.user_id = matching.user_id&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;With query5, you may have noticed that we aren’t performing a merge at all, and you’d be right. We rely on the nature of the type of this merge, it is just an insert, we do not rewrite any rows in the target. Instead of performing a left anti join, we first perform a broadcastable inner join between source and target to find any matching rows. Next, we anti join the source table with these matches. Finally, we safely insert rows resulting from both joins into the table.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_24-1740542713330.png" style="width: 381px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15085i7008605005CFD11C/image-dimensions/381x444?v=v2" width="381" height="444" role="button" title="MuraliTalluri_24-1740542713330.png" alt="MuraliTalluri_24-1740542713330.png" /&gt;&lt;/span&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="MuraliTalluri_25-1740542713346.png" style="width: 424px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/15086iF4A672A112B121B6/image-dimensions/424x421?v=v2" width="424" height="421" role="button" title="MuraliTalluri_25-1740542713346.png" alt="MuraliTalluri_25-1740542713346.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;In this case, we were able to broadcast the first join, leading to a runtime improvement of ~2x (43s vs&amp;nbsp; 23s).&lt;/STRONG&gt;&lt;/P&gt;
&lt;H1&gt;&amp;nbsp;&lt;/H1&gt;
&lt;H1&gt;&lt;SPAN&gt;Summary&lt;/SPAN&gt;&lt;/H1&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Your merge will be most performant if you are using Photon engine + your &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is liquid clustered by a merge key. This is due to DFP, and we always recommend using Photon for merge.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;While you can get merge metrics from Spark UI, we can always programmatically get them from Delta table history. This comes in very handy for identifying slowness/bottlenecks in merge.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;If your &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; table is not broadcasted during the scan phase of the merge, and depending on the size of your &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;source&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;target&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; tables, we can use broadcast hints to speed up the merge.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Merge statement with only &lt;/SPAN&gt;&lt;SPAN&gt;when not matched then insert&lt;/SPAN&gt;&lt;SPAN&gt; can be rewritten using joins and can be faster than merge most of the times.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H1&gt;&lt;SPAN&gt;Code for creating input tables&lt;/SPAN&gt;&lt;/H1&gt;
&lt;DIV&gt;&lt;LI-CODE lang="python"&gt;%pip install dbldatagen&lt;/LI-CODE&gt;&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;%sql
set spark.sql.shuffle.partitions = auto;
create catalog if not exists mt_blog;
create schema if not exists merge_demo;&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;import dbldatagen as dg
from pyspark.sql import functions as F
from pyspark.sql.types import IntegerType, StringType, DateType, LongType, TimestampType
from datetime import timedelta, datetime, date

merge_source_generator = (dg.DataGenerator(spark, name="merge_source", rows=5000000000, partitions=400)
                      .withColumn("user_id", LongType(), minValue=1, maxValue=5000000000, step=1, random=False, uniqueValues = 5000000000)
                      .withColumn("segment_id", IntegerType(), minValue=1, maxValue=10000000, step=1, random=False)
                      .withColumn("dt", DateType(), random=False)
                      .withColumn("event_ts", TimestampType(), begin="2010-01-01 01:00:00", end="2024-12-31 23:59:00", interval="1 second", random=False )
                      )

merge_source_df = merge_source_generator.build()  # build our dataset
merge_source_df.repartition(500, 'user_id').write.format('delta').mode('overwrite').saveAsTable('mt_blog.merge_demo.target_data')&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;%sql
alter table mt_blog.merge_demo.target_data cluster by (user_id);
optimize mt_blog.merge_demo.target_data;&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;merge_change_generator1 = (dg.DataGenerator(spark, name="merge_source", rows=8000, partitions=5)
                      .withColumn("user_id", LongType(), minValue=4900000000, maxValue=5100000000, step=25000, random=False, uniqueValues = 8000)
                      .withColumn("segment_id", IntegerType(), minValue=10000001, maxValue=11000000, step=1, random=True)
                      .withColumn("dt", DateType(), random=True)
                      .withColumn("event_ts", TimestampType(), begin="2025-01-01 01:00:00", end="2025-12-31 23:59:00", interval="1 second", random=False )
                      )

merge_change_df1 = merge_change_generator1.build()  # build our dataset
merge_change_df1.write.format('delta').mode('overwrite').saveAsTable('mt_blog.merge_demo.source_data1')&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;merge_change_generator2 = (dg.DataGenerator(spark, name="merge_source", rows=5000000, partitions=5)
                      .withColumn("user_id", LongType(), minValue=4800000000, maxValue=5300000000, step=100, random=False, uniqueValues = 5000000)
                      .withColumn("segment_id", IntegerType(), minValue=10000001, maxValue=11000000, step=1, random=True)
                      .withColumn("dt", DateType(), random=True)
                      .withColumn("event_ts", TimestampType(), begin="2025-01-01 01:00:00", end="2025-12-31 23:59:00", interval="1 second", random=False )
                      )

merge_change_df2 = merge_change_generator2.build()  # build our dataset
merge_change_df2.write.format('delta').mode('overwrite').saveAsTable('mt_blog.merge_demo.source_data2')&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
      <pubDate>Wed, 26 Feb 2025 12:42:24 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/merge-deep-dive/ba-p/111190</guid>
      <dc:creator>MuraliTalluri</dc:creator>
      <dc:date>2025-02-26T12:42:24Z</dc:date>
    </item>
    <item>
      <title>Re: Merge - Deep Dive</title>
      <link>https://community.databricks.com/t5/technical-blog/merge-deep-dive/bc-p/111522#M482</link>
      <description>&lt;P&gt;Thank for the blog. Interesting read, I got some pointers that I need to check if we can implement that.&lt;/P&gt;&lt;P&gt;Question: why is a MERGE statement so 'stupid'? I am used to SQL Server behaviour, so when there is nothing to update, it will not update. However the Databricks MERGE will always update, regardless if rows were changed or not. This is annoying with a SCD2 approach and when the source is delivering overlapping delta windows.&lt;/P&gt;&lt;P&gt;The work-a-round for this is to compare first all records (and do a NULL check) and then decide if it can be forwarded to the MERGE, or implement a hash on the non-key fields and compare that. It is cumbersome and I'd rather have Databricks handle this plumbing.&lt;/P&gt;</description>
      <pubDate>Sun, 02 Mar 2025 19:40:26 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/merge-deep-dive/bc-p/111522#M482</guid>
      <dc:creator>JohannesVink1</dc:creator>
      <dc:date>2025-03-02T19:40:26Z</dc:date>
    </item>
    <item>
      <title>Re: Merge - Deep Dive</title>
      <link>https://community.databricks.com/t5/technical-blog/merge-deep-dive/bc-p/113433#M501</link>
      <description>&lt;P&gt;&lt;SPAN&gt;Thanks for the post. I don't understand this statement:&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;EM&gt;Although classic warehouses support certain Photon operations, they don’t fully Photonize Merge. Also for this scenario the tables don’t have liquid clustering enabled.&amp;nbsp;&lt;/EM&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN&gt;Why is not fully Photonized if Photon is switched on? Or do you refer to Predictive I/O and other pro features which in my current understanding are not related to Photon?&lt;/SPAN&gt;&lt;/P&gt;</description>
      <pubDate>Mon, 24 Mar 2025 15:25:37 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/merge-deep-dive/bc-p/113433#M501</guid>
      <dc:creator>AlexanderW</dc:creator>
      <dc:date>2025-03-24T15:25:37Z</dc:date>
    </item>
  </channel>
</rss>

