<?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 Mastering Delta Lake MERGE Performance: Why It Slows Down and How to Fix It in Technical Blog</title>
    <link>https://community.databricks.com/t5/technical-blog/mastering-delta-lake-merge-performance-why-it-slows-down-and-how/ba-p/156305</link>
    <description>&lt;P&gt;&lt;SPAN&gt;If you've worked with Delta Lake at scale, you've encountered this: your MERGE operation that once completed in seconds now takes minutes or hours, as your table grows.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The culprit? Without optimization, MERGE operations scan far more data than necessary. Even when your table is properly laid out and your MERGE condition includes the key column.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Here's the part that's often overlooked: the key is to pre-scan your source data and inject literal values into the MERGE condition. This enables pruning that would otherwise be impossible—for both streaming and batch workloads.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;This article explains how MERGE executes at a high level, why the default behavior is problematic, and the strategies to fix it.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;HOW MERGE WORKS AT A HIGH LEVEL&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;A typical MERGE looks innocent enough:&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;target_delta.alias("target").merge(
    source=source_df.alias("source"),
    condition="target.order_id = source.order_id"
).whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;Delta Lake executes this through multiple phases:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Phase 1: Transaction Log Analysis&lt;/STRONG&gt;&lt;SPAN&gt; Spark reads the &lt;/SPAN&gt;&lt;SPAN&gt;_delta_log&lt;/SPAN&gt;&lt;SPAN&gt; to build the current table state—loading checkpoint files, reading commit JSONs, and extracting file paths, and column statistics.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Phase 2: File Selection (THE PROBLEM)&lt;/STRONG&gt;&lt;SPAN&gt; Delta determines which files to scan. By default, this is ALL files. We'll dive deep into why shortly.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Phase 3: Join Execution&lt;/STRONG&gt;&lt;SPAN&gt; Delta joins source and target to find matches. The join strategy is chosen by the optimizer based on data sizes and the MERGE clauses — you'll see &lt;/SPAN&gt;&lt;SPAN&gt;BroadcastHashJoin&lt;/SPAN&gt;&lt;SPAN&gt;, &lt;/SPAN&gt;&lt;SPAN&gt;SortMergeJoin&lt;/SPAN&gt;&lt;SPAN&gt;, or &lt;/SPAN&gt;&lt;SPAN&gt;ShuffledHashJoin&lt;/SPAN&gt;&lt;SPAN&gt; in the query plan, with join types like &lt;/SPAN&gt;&lt;SPAN&gt;LeftSemi&lt;/SPAN&gt;&lt;SPAN&gt;, &lt;/SPAN&gt;&lt;SPAN&gt;Inner&lt;/SPAN&gt;&lt;SPAN&gt;, or &lt;/SPAN&gt;&lt;SPAN&gt;LeftAnti&lt;/SPAN&gt;&lt;SPAN&gt; depending on whether the MERGE has matched updates, inserts, or both.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Phase 4: File Rewrite (Copy-on-Write)&lt;/STRONG&gt;&lt;SPAN&gt; By default, every affected file is rewritten entirely. Even updating 1 row in a 256MB file means reading 256MB and writing 256MB. This is write amplification. (With Deletion Vectors enabled, Delta can mark rows as deleted without full file rewrites—more on this later.)&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Phase 5: Transaction Commit&lt;/STRONG&gt;&lt;SPAN&gt; The transaction log is updated atomically with remove actions for old files and add actions for new files.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="dhruvkumar1_6-1778088130228.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/26715iF749AB70B3ABF2B1/image-size/medium?v=v2&amp;amp;px=400" role="button" title="dhruvkumar1_6-1778088130228.png" alt="dhruvkumar1_6-1778088130228.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;THE SCENARIO&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Consider an e-commerce orders table:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;500 million orders across 50 regions&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Organized by: &lt;/SPAN&gt;&lt;SPAN&gt;region&lt;/SPAN&gt;&lt;SPAN&gt; — e.g. &lt;/SPAN&gt;&lt;SPAN&gt;CLUSTER BY (region, event_time)&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Primary key: &lt;/SPAN&gt;&lt;SPAN&gt;order_id&lt;/SPAN&gt;&lt;SPAN&gt;, &lt;/SPAN&gt;&lt;SPAN&gt;region,event_time&lt;/SPAN&gt;&lt;SPAN&gt; (together they uniquely identify each event for a given order)&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Streaming updates from order management system (status changes, shipping updates, returns)&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;Your MERGE condition looks like this:&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;target.region = source.region
AND target.order_id = source.order_id
AND target.event_time = source.event_time&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;This is exactly how you should write it—matching on the key column and full primary key. Yet every micro-batch scans the entire table, even when processing just 5,000 order updates from 2 regions.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Why? This brings us back to Phase 2.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;THE ROOT CAUSE: PHASE 2 SCANS EVERYTHING BY DEFAULT&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;The MERGE condition above includes &lt;/SPAN&gt;&lt;SPAN&gt;region&lt;/SPAN&gt;&lt;SPAN&gt; — which is a clustering key. Intuitively, you'd expect Spark to:&lt;/SPAN&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Look at the source batch's region values&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Only scan files for those regions&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Skip everything else&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;&lt;SPAN&gt;But this doesn't happen.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;At query planning time (Phase 2), the source DataFrame hasn't been evaluated—Spark doesn't know what values it contains. The condition &lt;/SPAN&gt;&lt;SPAN&gt;target.region = source.region&lt;/SPAN&gt;&lt;SPAN&gt; references a column from an unevaluated DataFrame. Spark cannot push this down as a predicate because it has no concrete values to filter on.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The result: every file in your table gets scanned, regardless of how you structure your join condition — even if using &lt;/SPAN&gt;&lt;SPAN&gt;CLUSTER BY(region, ...)&lt;/SPAN&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;This is a common surprise for engineers: "My table is clustered/partitioned by region and I'm joining on region—why is it still scanning everything?"&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;THE SOLUTION: LITERAL VALUE INJECTION&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Pruning only works when the merge condition contains literal values that Spark can evaluate at plan time.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Same table, same primary key. But now we extract values first:&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# Pre-scan the source batch
regions = source_df.select("region").distinct().collect()
region_list = ", ".join([f"'{r.region}'" for r in regions])

# Inject literals into the condition
condition = f"""
    target.region IN ({region_list})
    AND target.region = source.region
    AND target.order_id = source.order_id
    AND target.event_time = source.event_time
"""&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;The &lt;/SPAN&gt;&lt;SPAN&gt;IN ({region_list})&lt;/SPAN&gt;&lt;SPAN&gt; clause contains literal strings like &lt;/SPAN&gt;&lt;SPAN&gt;IN ('us-east', 'eu-central')&lt;/SPAN&gt;&lt;SPAN&gt;. These are known at plan time. Spark can now skip data by looking at files where the min/max doesn’t overlap.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The rest of the condition handles the actual row matching. The literal clause handles the pruning.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Key insight:&lt;/STRONG&gt;&lt;SPAN&gt; You're not replacing the join condition—you're augmenting it with pruning hints that Spark can evaluate at plan time.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;HOW LITERAL INJECTION DRIVES PRUNING&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Your injected literals drive file skipping. The mechanism depends on your table layout, but the result is the same — Spark reads only the files that could contain matching rows:&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE&gt;
&lt;THEAD&gt;
&lt;TR&gt;
&lt;TH&gt;
&lt;P&gt;&lt;STRONG&gt;Table layout&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TH&gt;
&lt;TH&gt;
&lt;P&gt;&lt;STRONG&gt;What &lt;/STRONG&gt;&lt;STRONG&gt;target.region IN ('us-east-1', 'eu-central-1')&lt;/STRONG&gt;&lt;STRONG&gt; does&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TH&gt;
&lt;/TR&gt;
&lt;/THEAD&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;STRONG&gt;CLUSTER BY (region, ...)&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Skips files whose min/max on &lt;/SPAN&gt;&lt;SPAN&gt;region&lt;/SPAN&gt;&lt;SPAN&gt; don't overlap.&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;STRONG&gt;Go further with additional columns.&lt;/STRONG&gt;&lt;SPAN&gt; Delta Lake stores min/max statistics on columns of &lt;/SPAN&gt;&lt;STRONG&gt;numeric, string, date, and timestamp&lt;/STRONG&gt;&lt;SPAN&gt; types. Inject ranges on &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;any&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; column with statistics — not just the clustering/partition key:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;target.event_time &amp;gt;= '2024-03-25'&lt;/SPAN&gt;&lt;SPAN&gt; — skips files with only older data&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;target.order_id &amp;gt;= 'ORD-0050000' AND target.order_id &amp;lt;= 'ORD-0055000'&lt;/SPAN&gt;&lt;SPAN&gt; — skips files outside this ID range&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Combine multiple filters — the more selective, the more files skipped&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;With Liquid Clustering, all clustering keys (&lt;/SPAN&gt;&lt;SPAN&gt;region&lt;/SPAN&gt;&lt;SPAN&gt;, &lt;/SPAN&gt;&lt;SPAN&gt;event_time&lt;/SPAN&gt;&lt;SPAN&gt;, &lt;/SPAN&gt;&lt;SPAN&gt;order_id&lt;/SPAN&gt;&lt;SPAN&gt;) benefit because of non-overlapping ranges on all keys. With traditional partitioning, only the partition column gets file pruning — other columns rely on Z-ordering for tight file ranges.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Here's the full pattern — extract values from the source, inject them as literals on key columns:&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# Extract pruning values
regions = source_df.select("region").distinct().collect()
region_list = ", ".join([f"'{r.region}'" for r in regions])
time_bounds = source_df.agg(F.min("event_time"), F.max("event_time")).collect()[0]

# Inject literals — works for both liquid clustered and partitioned tables
condition = f"""
    target.region IN ({region_list})
    AND target.event_time &amp;gt;= '{time_bounds.min_time}'
    AND target.event_time &amp;lt;= '{time_bounds.max_time}'
    AND target.region = source.region
    AND target.order_id = source.order_id
    AND target.event_time = source.event_time
"""&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Important:&lt;/STRONG&gt;&lt;SPAN&gt; File-level statistics are only collected for the first 32 columns by default (controlled by &lt;/SPAN&gt;&lt;SPAN&gt;delta.dataSkippingNumIndexedCols&lt;/SPAN&gt;&lt;SPAN&gt;). For string types it only keeps the first 32 chars. For wide tables, either reorder your columns, increase this setting, or use &lt;/SPAN&gt;&lt;SPAN&gt;delta.dataSkippingStatsColumns&lt;/SPAN&gt;&lt;SPAN&gt; to specify columns by name.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Safety rule:&lt;/STRONG&gt;&lt;SPAN&gt; Only inject range filters on columns that are part of your MERGE key. Filtering on a column not in the MERGE condition risks skipping files that contain rows needing updates.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="dhruvkumar1_7-1778088130231.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/26717iCC1B208181B81D3C/image-size/medium?v=v2&amp;amp;px=400" role="button" title="dhruvkumar1_7-1778088130231.png" alt="dhruvkumar1_7-1778088130231.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;STREAMING VS BATCH: THE PRE-SCAN PATTERN&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;The literal injection technique requires two passes over your source data: one to extract pruning values, one for the MERGE itself. This has important implications for how you structure your pipeline.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Key requirement for streaming:&lt;/STRONG&gt;&lt;SPAN&gt; You &lt;/SPAN&gt;&lt;STRONG&gt;must&lt;/STRONG&gt;&lt;SPAN&gt; use &lt;/SPAN&gt;&lt;SPAN&gt;foreachBatch&lt;/SPAN&gt;&lt;SPAN&gt; mode. In a continuous streaming query (without &lt;/SPAN&gt;&lt;SPAN&gt;foreachBatch&lt;/SPAN&gt;&lt;SPAN&gt;), Spark builds a single execution plan for the entire stream — there's no hook to pre-scan a micro-batch and inject literals before the MERGE runs. The &lt;/SPAN&gt;&lt;SPAN&gt;foreachBatch&lt;/SPAN&gt;&lt;SPAN&gt; callback gives you a materialized DataFrame that you can query separately before passing it to the MERGE. Without &lt;/SPAN&gt;&lt;SPAN&gt;foreachBatch&lt;/SPAN&gt;&lt;SPAN&gt;, literal injection is not possible in streaming.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;For Streaming (foreachBatch):&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def merge_micro_batch(batch_df, batch_id):
    # batch_df is already materialized — safe to query twice
    regions = batch_df.select("region").distinct().collect()
    region_list = ", ".join([f"'{r.region}'" for r in regions])

    condition = f"""
        target.region IN ({region_list})
        AND target.region = source.region
        AND target.order_id = source.order_id
        AND target.event_time = source.event_time
    """


    target_delta.alias("target").merge(
        source=batch_df.alias("source"),
        condition=condition
    ).whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()

stream.writeStream.foreachBatch(merge_micro_batch).start()&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;The micro-batch is small and already bounded — recomputation is negligible. Caching is optional.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;For Batch:&lt;/STRONG&gt;&lt;SPAN&gt; Cache your source DataFrame first. Without caching, the extract step triggers one computation and the MERGE triggers a second. This double-read is wasteful and dangerous:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Warning — Data Consistency Risk:&lt;/STRONG&gt;&lt;SPAN&gt; If your source is non-deterministic (e.g., uses &lt;/SPAN&gt;&lt;SPAN&gt;current_timestamp()&lt;/SPAN&gt;&lt;SPAN&gt;, reads from a file source that changed between evaluations, or samples data), the MERGE might see different data than what you used for pruning. This can lead to &lt;/SPAN&gt;&lt;STRONG&gt;silent data loss or corruption&lt;/STRONG&gt;&lt;SPAN&gt; — rows that existed during pruning may vanish during the MERGE, or new rows may appear that don't match the pruning predicates. Always cache non-deterministic sources before extracting pruning literals.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Pattern:&lt;/STRONG&gt; &lt;SPAN&gt;source.cache()&lt;/SPAN&gt;&lt;SPAN&gt; → &lt;/SPAN&gt;&lt;SPAN&gt;collect()&lt;/SPAN&gt;&lt;SPAN&gt; → &lt;/SPAN&gt;&lt;SPAN&gt;merge()&lt;/SPAN&gt;&lt;SPAN&gt; → &lt;/SPAN&gt;&lt;SPAN&gt;source.unpersist()&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;A note on cost:&lt;/STRONG&gt;&lt;SPAN&gt; The &lt;/SPAN&gt;&lt;SPAN&gt;collect()&lt;/SPAN&gt;&lt;SPAN&gt; call introduces its own overhead — it materializes results and returns them to the driver, adding a round-trip. For very small source batches, this overhead may not be worth it if the target table is already well-clustered and the MERGE is fast. Weigh the cost of the &lt;/SPAN&gt;&lt;SPAN&gt;collect()&lt;/SPAN&gt;&lt;SPAN&gt; against the savings on the MERGE: if file pruning lets you skip scanning a large percentage of the target table, the driver round-trip is a bargain.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Bonus — Broadcast Hint:&lt;/STRONG&gt;&lt;SPAN&gt; For small source batches (common in streaming), adding a broadcast hint eliminates the shuffle join:&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;from pyspark.sql.functions import broadcast

target_delta.alias("target").merge(
    source=broadcast(batch_df).alias("source"),
    condition=condition
).whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;This pairs well with literal injection — pruning reduces the target side, broadcast eliminates the shuffle on the source side.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="dhruvkumar1_8-1778088130229.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/26718i040603F39A9E0F34/image-size/medium?v=v2&amp;amp;px=400" role="button" title="dhruvkumar1_8-1778088130229.png" alt="dhruvkumar1_8-1778088130229.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;PHYSICAL LAYOUT: MAKING LITERAL INJECTION EFFECTIVE&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Literal injection tells Spark &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;what&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; to filter. But how many files it actually skips depends on how your data is physically organized. If files contain randomly scattered values, even perfect literals won't skip much — every file's min/max range overlaps with everything.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;This is where physical layout matters: organize your data so that files contain tight, non-overlapping value ranges. Then your injected literals become surgically precise.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;You have two options — choose based on whether you're building new tables or optimizing existing ones.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;LIQUID CLUSTERING (RECOMMENDED)&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;STRONG&gt;For new tables, use Liquid Clustering.&lt;/STRONG&gt;&lt;SPAN&gt; It replaces both partitioning and Z-ordering with a single, unified mechanism.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;-- Everything you need: one line
CREATE TABLE orders (...) CLUSTER BY (region, event_time, order_id);&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;This is better than the traditional two-step approach:&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;-- Legacy: two separate mechanisms

CREATE TABLE orders (...) PARTITIONED BY (region);
OPTIMIZE orders ZORDER BY (event_time, order_id);&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Why it's better for MERGE:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;With &lt;/SPAN&gt;&lt;SPAN&gt;CLUSTER BY (region, event_time, order_id)&lt;/SPAN&gt;&lt;SPAN&gt;, all three columns participate in an optimized layout. Files are organized across the entire table — not within partition boundaries. When you inject literals on &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;any&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; of these columns, Spark uses all filters together to skip files:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;target.region IN ('us-east-1', 'eu-central-1')&lt;/SPAN&gt;&lt;SPAN&gt; — skips files for other regions&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;target.event_time &amp;gt;= '2024-03-25'&lt;/SPAN&gt;&lt;SPAN&gt; — skips files with older data&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Both filters combined — skips even more files than either alone&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Literal injection on a clustering key is functionally equivalent to partition pruning&lt;/STRONG&gt;&lt;SPAN&gt; — Spark skips all files whose min/max stats on that key don't match the literal. The difference is that it's file-level, there are no partition boundaries, and &lt;/SPAN&gt;&lt;I&gt;&lt;SPAN&gt;every&lt;/SPAN&gt;&lt;/I&gt;&lt;SPAN&gt; clustering key benefits equally from the same technique.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Key advantages:&lt;/STRONG&gt;&lt;/P&gt;
&lt;TABLE&gt;
&lt;THEAD&gt;
&lt;TR&gt;
&lt;TH&gt;
&lt;P&gt;&lt;STRONG&gt;Feature&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TH&gt;
&lt;TH&gt;
&lt;P&gt;&lt;STRONG&gt;Liquid Clustering&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TH&gt;
&lt;TH&gt;
&lt;P&gt;&lt;STRONG&gt;Partitioning + Z-Order&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TH&gt;
&lt;/TR&gt;
&lt;/THEAD&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Pruning mechanism&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;File-level stats on all clustering keys&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Partition pruning on partition column + file-level stats on Z-ordered columns within partitions&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Maintenance&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;OPTIMIZE&lt;/SPAN&gt;&lt;SPAN&gt; is incremental — only rewrites files with new/unclustered data&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;OPTIMIZE ZORDER BY&lt;/SPAN&gt;&lt;SPAN&gt; is also incremental — targets files with new/changed data, but re-sorts and rewrites all qualifying files to maintain Z-order&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Column flexibility&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;ALTER TABLE ... CLUSTER BY (new_cols)&lt;/SPAN&gt;&lt;SPAN&gt; — metadata-only change, no rewrite. New data uses new keys. Run &lt;/SPAN&gt;&lt;SPAN&gt;OPTIMIZE FULL&lt;/SPAN&gt;&lt;SPAN&gt; (DBR 16.0+) to recluster old data&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Changing partition columns requires full table rewrite. Changing Z-order columns takes effect on next &lt;/SPAN&gt;&lt;SPAN&gt;OPTIMIZE&lt;/SPAN&gt;&lt;SPAN&gt; for qualifying files&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Cardinality&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Handles automatically any cardinality. It also offers Hierarchical Clustering while lets you prioritize certain low-cardinality columns (like date or region) in Liquid Clustering so data is fully sorted by those columns first, then ZORDERed on the rest&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;High-cardinality partitioning creates millions of tiny directories. Z-ordering handles high cardinality well but only within partitions&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Cross-key optimization&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;It optimizes all keys simultaneously across the entire table&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Z-ordering limited to within partition boundaries — files can't be reorganized across partitions&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Automatic tuning&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;CLUSTER BY AUTO&lt;/SPAN&gt;&lt;SPAN&gt; (DBR 15.4+) — Predictive Optimization selects and evolves keys&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Manual column selection for both partition key and Z-order columns&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;STRONG&gt;Best practice:&lt;/STRONG&gt;&lt;SPAN&gt; Choose 1-4 columns that appear most frequently in your MERGE conditions and query filters. In our scenario: &lt;/SPAN&gt;&lt;SPAN&gt;CLUSTER BY (region, event_time)&lt;/SPAN&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;REDUCING WRITE AMPLIFICATION: FILE SIZE TUNING AND DELETION VECTORS&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Without Deletion Vectors, Delta Lake uses Copy-on-Write: to update a row, the entire file containing that row must be rewritten. This creates write amplification.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Example scenario:&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Your orders table has 256MB files, each containing ~500K rows&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;A MERGE updates 1,000 orders scattered across 100 files&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Result: 100 files x 256MB = 25.6GB read + 25.6GB written&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;For just 1,000 row changes, you've moved 51GB of data&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;With smaller files (64MB):&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Same 1,000 orders across 100 files&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Result: 100 files x 64MB = 6.4GB read + 6.4GB written&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;4x reduction in I/O for the same MERGE&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Trade-off:&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Smaller files (32-128 MB):&lt;/STRONG&gt;&lt;SPAN&gt; Lower write amplification, but more files to manage and more time spent id cloud APIs&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Larger files (256 MB - 1 GB):&lt;/STRONG&gt;&lt;SPAN&gt; Better read performance, but higher write amplification&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;For MERGE-heavy tables: Consider smaller file size or enable Deletion Vectors (Delta Lake 2.3+) if the join keys have high cardinality.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;What are Deletion Vectors?&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Normally, updating or deleting even a single row requires rewriting the entire file that contains it (Copy-on-Write). Deletion Vectors take a different approach: instead of rewriting files, they store a compact bitmap alongside each data file that marks which row positions have been invalidated.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;During a MERGE with Deletion Vectors enabled:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;For &lt;/SPAN&gt;&lt;STRONG&gt;deletes&lt;/STRONG&gt;&lt;SPAN&gt;, the row is simply marked as removed in the bitmap.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;For &lt;/SPAN&gt;&lt;STRONG&gt;updates&lt;/STRONG&gt;&lt;SPAN&gt;, the old row is marked as removed and the new row is written to a separate small file.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;The original data files remain untouched. This bypasses Copy-on-Write entirely — writes become much faster at the cost of slightly slower reads (readers must check the deletion vector to filter out invalidated rows). Periodic OPTIMIZE does two things: (1) merge small files (e.g., from new row inserts) into larger, optimally-sized files, and (2) rewrite files to physically remove rows marked as deleted via Deletion Vectors. For (2) specifically, the REORG command is also available as a dedicated alternative.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;HOW TO DIAGNOSE YOUR MERGE&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Don't guess—measure. Run &lt;/SPAN&gt;&lt;SPAN&gt;DESCRIBE HISTORY your_table&lt;/SPAN&gt;&lt;SPAN&gt; after any MERGE. The &lt;/SPAN&gt;&lt;SPAN&gt;operationMetrics&lt;/SPAN&gt;&lt;SPAN&gt; map tells you exactly what happened.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Three metrics that matter most:&lt;/STRONG&gt;&lt;/P&gt;
&lt;TABLE&gt;
&lt;THEAD&gt;
&lt;TR&gt;
&lt;TH&gt;
&lt;P&gt;&lt;STRONG&gt;Metric&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TH&gt;
&lt;TH&gt;
&lt;P&gt;&lt;STRONG&gt;What it tells you&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TH&gt;
&lt;TH&gt;
&lt;P&gt;&lt;STRONG&gt;Red flag&lt;/STRONG&gt;&lt;/P&gt;
&lt;/TH&gt;
&lt;/TR&gt;
&lt;/THEAD&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;scanTimeMs&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Time spent reading target files&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Dominates total time → improve pruning&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;rewriteTimeMs&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Time spent writing new files&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Dominates total time → enable DVs or shrink files&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;numTargetRowsCopied&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Unchanged rows rewritten alongside updated ones&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;TD&gt;
&lt;P&gt;&lt;SPAN&gt;Much larger than &lt;/SPAN&gt;&lt;SPAN&gt;numTargetRowsUpdated&lt;/SPAN&gt;&lt;SPAN&gt; → write amplification&lt;/SPAN&gt;&lt;/P&gt;
&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;STRONG&gt;How to act on them:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="dhruvkumar1_9-1778088130230.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/26719iB6E2F67E20404383/image-size/medium?v=v2&amp;amp;px=400" role="button" title="dhruvkumar1_9-1778088130230.png" alt="dhruvkumar1_9-1778088130230.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;SUMMARY&lt;/SPAN&gt;&lt;/H2&gt;
&lt;OL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;MERGE scans everything by default&lt;/STRONG&gt;&lt;SPAN&gt; because Spark cannot evaluate &lt;/SPAN&gt;&lt;SPAN&gt;source.column&lt;/SPAN&gt;&lt;SPAN&gt; values during planning.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Pre-scan your source&lt;/STRONG&gt;&lt;SPAN&gt; to extract literal values — partition keys, value ranges on key columns (timestamps, IDs, dates — any column with min/max stats).&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Inject these as literals&lt;/STRONG&gt;&lt;SPAN&gt; into your MERGE condition. This works identically for partitioned tables and Liquid Clustered tables.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Organize your data for effective pruning:&lt;/STRONG&gt;&lt;/LI&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="2"&gt;&lt;STRONG&gt;New tables:&lt;/STRONG&gt;&lt;SPAN&gt; Use Liquid Clustering — &lt;/SPAN&gt;&lt;SPAN&gt;CLUSTER BY (region, event_time, order_id)&lt;/SPAN&gt;&lt;SPAN&gt; unifies partition pruning + column stats pruning + Z-ordering into one mechanism. Literal injection on any clustering key drives file skipping.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="2"&gt;&lt;STRONG&gt;Existing partitioned tables:&lt;/STRONG&gt;&lt;SPAN&gt; Add Z-ordering on MERGE key columns to tighten file-level statistics within partitions.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Reduce write amplification&lt;/STRONG&gt;&lt;SPAN&gt; with Deletion Vectors (avoid full file rewrites) and appropriate file sizing.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;For streaming, use &lt;/STRONG&gt;&lt;STRONG&gt;foreachBatch&lt;/STRONG&gt;&lt;SPAN&gt; — it's mandatory for literal injection. For batch, cache your source to prevent data inconsistency.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG style="color: #1b3139; font-family: inherit;"&gt;Measure and monitor&lt;/STRONG&gt;&lt;SPAN&gt; — &lt;/SPAN&gt;&lt;SPAN&gt;DESCRIBE HISTORY&lt;/SPAN&gt;&lt;SPAN&gt; for spot checks, trended dashboards for regression&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;</description>
    <pubDate>Thu, 07 May 2026 13:35:56 GMT</pubDate>
    <dc:creator>dhruvkumar1</dc:creator>
    <dc:date>2026-05-07T13:35:56Z</dc:date>
    <item>
      <title>Mastering Delta Lake MERGE Performance: Why It Slows Down and How to Fix It</title>
      <link>https://community.databricks.com/t5/technical-blog/mastering-delta-lake-merge-performance-why-it-slows-down-and-how/ba-p/156305</link>
      <description>&lt;P data-local-id="3c490d4a254a" data-renderer-start-pos="1"&gt;Delta Lake MERGE scans all files by default — even with partitioning or Liquid Clustering. Learn the literal injection technique to dramatically reduce scan times.&lt;/P&gt;
&lt;UL class="ak-ul" data-indent-level="1" data-local-id="de017c10062f"&gt;
&lt;LI&gt;
&lt;P data-local-id="39aefec4b11c" data-renderer-start-pos="168"&gt;&lt;STRONG data-renderer-mark="true"&gt;MERGE scans everything by default&lt;/STRONG&gt; because column references in the MERGE condition are opaque at plan time — Spark can't push them down as filters. Pre-scanning your source and injecting literal values enables file pruning on both partitioned and Liquid Clustered tables.&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P data-local-id="e4e333a1fd46" data-renderer-start-pos="443"&gt;&lt;STRONG data-renderer-mark="true"&gt;Liquid Clustering unifies partition pruning and Z-ordering&lt;/STRONG&gt; into a single mechanism where literal injection on any clustering key drives file skipping — no partition boundaries, incremental maintenance, and automatic key selection on newer Databricks runtimes.&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P data-local-id="75a8d59962cc" data-renderer-start-pos="706"&gt;&lt;STRONG data-renderer-mark="true"&gt;Layer read and write optimizations together:&lt;/STRONG&gt; literal injection combined with physical layout optimization reduces files scanned, while Deletion Vectors eliminate full file rewrites — use DESCRIBE HISTORY metrics to diagnose and monitor.&lt;/P&gt;
&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Thu, 07 May 2026 13:35:56 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/mastering-delta-lake-merge-performance-why-it-slows-down-and-how/ba-p/156305</guid>
      <dc:creator>dhruvkumar1</dc:creator>
      <dc:date>2026-05-07T13:35:56Z</dc:date>
    </item>
    <item>
      <title>Re: Mastering Delta Lake MERGE Performance: Why It Slows Down and How to Fix It</title>
      <link>https://community.databricks.com/t5/technical-blog/mastering-delta-lake-merge-performance-why-it-slows-down-and-how/bc-p/156563#M1056</link>
      <description>&lt;P&gt;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/227759"&gt;@dhruvkumar1&lt;/a&gt;&amp;nbsp;Hey, thanks for the article.&amp;nbsp; I was under the impression that if you have photon enabled then it uses dynamic file pruning to trim the target down for the merge.&amp;nbsp; Is this incorrect?&lt;/P&gt;&lt;P&gt;&lt;A href="https://learn.microsoft.com/en-gb/azure/databricks/optimizations/dynamic-file-pruning" target="_blank"&gt;https://learn.microsoft.com/en-gb/azure/databricks/optimizations/dynamic-file-pruning&lt;/A&gt;&lt;/P&gt;</description>
      <pubDate>Mon, 11 May 2026 10:49:54 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/mastering-delta-lake-merge-performance-why-it-slows-down-and-how/bc-p/156563#M1056</guid>
      <dc:creator>mdungey</dc:creator>
      <dc:date>2026-05-11T10:49:54Z</dc:date>
    </item>
  </channel>
</rss>

