DEV Community

Cover image for Your MERGE statements are scanning too much data
Aniket Abhishek Soni
Aniket Abhishek Soni

Posted on AI-assisted

Your MERGE statements are scanning too much data

The MERGE statement is a performance trap that lures engineers into thinking they are performing a simple row-level update, when they are actually triggering a full-table rewrite. If your MERGE jobs are consistently hitting high shuffle read/write metrics or taking longer as your table grows, you are likely failing to prune the search space before the shuffle occurs.

In Delta Lake, a MERGE operation is essentially a join between a source dataset and the target table. When you omit a filter on the target table—often called a "predicate pushdown" or "partition pruning"—Spark must scan every file in the target table to identify potential matches. On a multi-terabyte table, this is an expensive mistake.

The mechanics of the merge shuffle

When you run MERGE INTO target USING source ON target.id = source.id, Spark performs a full outer join behind the scenes. If you don't restrict the data, the engine cannot prune files using partition columns or Z-Ordering.

Consider a hypothetical table partitioned by processing_date. If you are merging data for 2023-10-01, you must include that date in your ON clause. Without it, the query planner must treat the entire table as a candidate for the update.

Even if you have an index or Z-Ordering on id, if the MERGE condition does not include the partition column, the engine may still need to perform a global shuffle to align records. Always structure your MERGE to include both the partition column and the business key in the ON clause:

# Illustrative example of constrained merge
target_table = "main.reporting.metrics"
source_df = spark.read.table("main.staging.incoming_data")

# Assume source_df is already filtered by processing_date
# Using both partition key and join key allows for effective pruning
(
    DeltaTable.forName(spark, target_table)
    .alias("t")
    .merge(
        source_df.alias("s"),
        "t.processing_date = s.processing_date AND t.id = s.id"
    )
    .whenMatchedUpdateAll()
    .whenNotMatchedInsertAll()
    .execute()
)
Enter fullscreen mode Exit fullscreen mode

a very large tunnel of lights in the middle of a room
Photo by Wei Shen on Unsplash

Leveraging Data Skipping and Z-Ordering

Data skipping is the first line of defense. Delta Lake maintains min/max statistics for columns in each Parquet file. When your MERGE filter includes these columns, the engine skips files where the range does not overlap with your source data.

However, data skipping only works if your data is physically clustered. If you merge data frequently, use ZORDER BY on your frequently joined columns. In Databricks, Liquid Clustering is the evolution of this, replacing the manual maintenance of Z-Ordering. If you are using Databricks Runtime 13.3 LTS or higher, migrate your large tables to use CLUSTER BY on your join keys. This physical layout allows the MERGE engine to skip significantly more files than standard partitioning alone.

The requirement: Auditability and schema enforcement

In regulated environments, you must prove that updates to a record are intentional and traceable. The MERGE command is powerful, but it can mask data quality issues or accidental overwrites if not properly governed.

The control: Constraining the merge scope

To maintain integrity, implement "source-side pruning." Never merge from an unconstrained source table. Always filter the source DataFrame to the smallest possible window before invoking the DeltaTable.merge() method. This reduces the shuffle size and limits the blast radius of the operation.

The control: Using column-level tracking

Enable Delta Lake's change data feed (CDF) on your tables. This provides an audit trail of exactly what the MERGE changed. Instead of guessing why a row value changed, you can query the CDF to see the "before" and "after" state of the record.

-- Enable CDF on the table
ALTER TABLE main.reporting.metrics SET TBLPROPERTIES (delta.enableChangeDataFeed = true);

-- Querying the changes
SELECT * FROM table_changes('main.reporting.metrics', 0, 100);
Enter fullscreen mode Exit fullscreen mode

The control: Restricting write privileges

In production environments, restrict who can execute MERGE operations. Use Unity Catalog to grant MODIFY permissions only to the service principals that own the pipeline. Avoid granting MODIFY to developers in production; instead, use INSERT or COPY INTO patterns where possible to avoid the complexity and overhead of MERGE.

a camera lens sitting on top of a refrigerator
Photo by Ahmad Fauzi on Unsplash

What this doesn't cover

This guidance focuses on the performance and governance of MERGE operations within the Delta Lake ecosystem. It does not address:

  • Handling schema evolution: MERGE can fail if the source schema is incompatible with the target. You must manage schema evolution via .option("mergeSchema", "true"), which introduces its own performance overhead.
  • Conflict resolution in streaming: If you are using MERGE inside a foreachBatch operation in Structured Streaming, you are subject to the concurrency limits of the underlying storage and the checkpointing mechanism.
  • External connectors: If you are merging data from an external JDBC source into Delta, the bottleneck is often the extraction phase, not the MERGE itself.

Conclusion

Performance in Delta Lake is rarely about "tuning" the engine and almost always about reducing the work the engine has to do. If your MERGE is slow, stop looking at cluster size and start looking at your file pruning. By ensuring your ON clauses align with your physical data layout—whether that is partitioning, Z-Ordering, or Liquid Clustering—you minimize the I/O and shuffle volume. Governance follows the same logic: by constraining the source and auditing the changes via CDF, you turn a black-box operation into a predictable, observable data pipeline.

Cover photo by Francisco Bricio on Unsplash.


This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.

Top comments (0)