Databricks DE ยท Topic 2.2
Four ways to write a Delta table โ pick by how much you're changing: row-level (MERGE), full/partition reload (INSERT OVERWRITE), rebuild (CREATE OR REPLACE), or derive (CTAS). This deep dive walks each with the mechanism and the code.
โธ click a statement
CREATE TABLE x AS SELECT โฆ) โ create a brand-new table from a query. Great for derived silver/gold tables.Match a source (incoming changes) against the target (Delta table) on a key, then apply up to three clause types โ all in one atomic commit.
MERGE INTO customers t -- target (the Delta table) USING customer_updates s -- source (incoming changes) ON t.id = s.id -- the match key WHEN MATCHED THEN UPDATE SET * -- in both โ update existing WHEN NOT MATCHED THEN INSERT * -- source only โ add new WHEN NOT MATCHED BY SOURCE THEN DELETE; -- target only โ remove gone rows
โธ click each clause
UPDATE or DELETE (with optional conditions; multiple matched clauses allowed, first match wins). The "update existing" path.INSERT. Unspecified columns get NULL. The "add new" path.UPDATE/DELETE. Cannot reference source columns (no source row exists). Requires DBR 12.2 LTS+. The "expire/remove gone rows" path.MERGE INTO customers t USING customer_updates s ON t.id = s.id WHEN MATCHED AND s.op = 'DELETE' THEN DELETE WHEN MATCHED AND s.op = 'UPDATE' THEN UPDATE SET name = s.name, tier = s.tier WHEN NOT MATCHED AND s.op <> 'DELETE' THEN INSERT (id, name, tier) VALUES (s.id, s.name, s.tier);
If more than one source row matches the same target row, the merge errors out. Dedup the source to one row per key first โ keep the latest by event time:
from pyspark.sql import functions as F, Window w = Window.partitionBy("id").orderBy(F.col("event_ts").desc()) deduped = (raw_updates .withColumn("rn", F.row_number().over(w)) .filter("rn = 1").drop("rn")) # one row per key โ safe to MERGE
MERGE rewrites the files holding matched rows. A predicate on the target lets Delta file-skip so it touches a fraction of the table.
MERGE INTO orders t USING daily_changes s ON t.id = s.id AND t.order_date >= current_date() - INTERVAL 7 DAYS -- prunes old files WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *;
Deletion vectors (merge-on-read, delta.enableDeletionVectors) cut cost further by marking rows deleted instead of rewriting whole files.
MERGE enforces schema by default; opt in to let new source columns flow in. The DeltaTable builder is the DataFrame-API equivalent.
SET spark.databricks.delta.schema.autoMerge.enabled = true; -- session-wide
from delta.tables import DeltaTable tgt = DeltaTable.forName(spark, "main.sales.customers") (tgt.alias("t") .merge(deduped.alias("s"), "t.id = s.id") .whenMatchedUpdateAll() .whenNotMatchedInsertAll() .whenNotMatchedBySourceDelete() .execute())
SCD2 closes the old row and inserts a new current row, so you keep full history. Step through what one MERGE does when customer C1 moves Silver โ Gold:
โธ step through the SCD2 merge
The SCD2 merge (two-pass source)
MERGE INTO dim_customer t USING ( -- pass 1: changed row keyed normally โ closes the old version SELECT id AS merge_key, * FROM customer_updates UNION ALL -- pass 2: same change, NULL key โ can't match โ forces an INSERT SELECT NULL AS merge_key, u.* FROM customer_updates u JOIN dim_customer d ON u.id = d.id AND d.is_current = true WHERE u.tier <> d.tier ) s ON t.id = s.merge_key AND t.is_current = true WHEN MATCHED AND t.tier <> s.tier THEN UPDATE SET is_current = false, end_date = current_date() -- close old WHEN NOT MATCHED THEN INSERT (id, tier, is_current, start_date, end_date) VALUES (s.id, s.tier, true, current_date(), NULL); -- open new
Atomically swap a table's (or a partition's) contents in one commit โ readers never see an empty/partial table.
-- Full table reload (replace everything atomically) INSERT OVERWRITE main.sales.orders SELECT * FROM staging.orders_full;
Targeted overwrite with replaceWhere (only one day)
(corrected_day.write.format("delta").mode("overwrite") .option("replaceWhere", "order_date = '2024-01-07'") # only this day is replaced .saveAsTable("main.sales.orders")) # Fails if any row's order_date != 2024-01-07 โ the constraint check protects you.
Dynamic partition overwrite (only the partitions present in new data)
spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic") (new_partitions.write.format("delta").mode("overwrite") .saveAsTable("main.sales.orders")) # untouched partitions stay as-is
You can't combine replaceWhere and partitionOverwriteMode=dynamic in one write โ pick one targeting strategy.
Redefine + repopulate in one atomic commit, keeping the table name and history lineage. Prefer it over DROP + CREATE, which loses history and leaves a window where the table doesn't exist.
CREATE OR REPLACE TABLE main.sales.dim_date AS SELECT d AS date_key, year(d) AS yr, month(d) AS mo FROM (SELECT explode(sequence(DATE'2020-01-01', DATE'2030-12-31', INTERVAL 1 DAY)) AS d);
CREATE TABLE AS SELECT builds a new table, inferring schema from the query โ the default for silver/gold derived tables.
CREATE TABLE main.sales.gold_daily_revenue AS SELECT order_date, sum(amount) AS revenue FROM main.sales.orders GROUP BY order_date; -- Want the structure but not the data? Copy the schema only: CREATE TABLE main.sales.orders_empty LIKE main.sales.orders;
| Rows touched | Atomic | Keeps history | Typical use | |
|---|---|---|---|---|
| MERGE | only matched files | โ | โ | CDC, SCD, upserts, dedup |
| INSERT OVERWRITE | all / a partition | โ | โ | full / partition reload |
| CREATE OR REPLACE | whole table | โ | โ (vs DROP+CREATE) | schema reset / rebuild |
| CTAS | new table | โ | n/a (new) | derived silver/gold table |
replaceWhere data must satisfy the predicate or the write errors; dynamic partition overwrite is per-partition โ a misplaced row can clobber a whole partition.WHEN NOT MATCHED BY SOURCE can't reference source columns and needs DBR 12.2 LTS+. OVERWRITE / REPLACE rewrite far more than a targeted MERGE โ don't use them for a few changed rows.spark.databricks.delta.schema.autoMerge.enabled.