Databricks DE ยท Topic 2.1

Delta Tables & the Transaction Log

Delta Lake turns plain Parquet files into a reliable table by adding a file-based transaction log โ€” the source of ACID, schema enforcement, fast scans, and version history. This deep dive walks each sub-topic with the mechanism and the code.

_delta_log ACID on object storage Optimistic concurrency Data skipping Time travel

The one-liner

Delta Lake = immutable Parquet data files + a _delta_log that records every change in order. The table is the log, not the files on disk. That log is what makes object-storage files behave like a reliable warehouse table. On Databricks, every table is Delta by default โ€” no USING DELTA needed.
๐Ÿฆ Analogy: the transaction log is a bank ledger. Parquet files are the cash in the vault; the ledger records every deposit/withdrawal in order โ€” so the balance is always correct, auditable, and replayable, even with many tellers writing at once.

What the log gives you โ€” click each

โ–ธ click a superpower

๐Ÿ”’
ACID
๐Ÿ“
Schema enforcement
โšก
Data skipping
๐Ÿ•ฐ๏ธ
Version history
ACID transactions. Commits are atomic (all-or-nothing) and isolated via optimistic concurrency โ€” concurrent writers can't corrupt each other. The log's ordering protocol serializes commits.
Schema enforcement. Delta validates the DataFrame schema on write and rejects mismatches, protecting downstream consumers. (Evolution to add columns is a separate opt-in.)
Data skipping. Every file's min/max/nullCount stats live in the log, so filtered queries skip whole Parquet files without opening them.
Version history. Each write is a new version recorded in the log โ€” enabling time travel, audit, and one-command rollback.

Why it matters

๐Ÿ“ฆ Real-world use case: a streaming job and a nightly batch both write the same orders table; the log serializes their commits so neither reader sees a half-written result and the table is never corrupted.

How it works โ€” deep dive

1 ยท Physical layout: Parquet + _delta_log/

A Delta table on disk is just data files + a log directory. Data files are immutable โ€” an "update" writes new files and tombstones old ones in the log.

/orders/                                  # table root
โ”œโ”€โ”€ part-00000-....snappy.parquet         # immutable Parquet data
โ”œโ”€โ”€ part-00001-....snappy.parquet
โ””โ”€โ”€ _delta_log/                           # the transaction log
    โ”œโ”€โ”€ 00000000000000000000.json         # commit 0 (atomic unit)
    โ”œโ”€โ”€ 00000000000000000001.json         # commit 1
    โ”œโ”€โ”€ 00000000000000000010.checkpoint.parquet  # written periodically
    โ””โ”€โ”€ _last_checkpoint                   # pointer to newest checkpoint

Inspect the log from SQL

DESCRIBE HISTORY main.sales.orders;   -- one row per commit: version, op, user, metrics
DESCRIBE DETAIL  main.sales.orders;   -- numFiles, sizeInBytes, location, format

2 ยท What lives inside a commit (the "actions")

Each JSON commit is a list of actions โ€” the diff applied at that version. The mental shift: a write is not an edit, it's a set of add/remove records appended to the log.

ActionMeaning
addData file added โ€” with size + column stats (min/max/nullCount).
removeFile logically removed (tombstone; stays on disk until VACUUM).
metaDataSchema, partitioning, table properties.
protocolReader/writer protocol versions (which features are required).
commitInfoAudit metadata: operation, timestamp, user, metrics.
txnIdempotency marker for streaming (set-once per batch).

3 ยท Snapshot reconstruction โ€” how a write becomes a version

To read the table, Delta nets add minus remove across the log (starting from the newest checkpoint) to compute the exact live file list. Checkpoints (cadence set by delta.checkpointInterval โ€” default is runtime-dependent: 100 on recent Databricks Runtimes, 10 in OSS Delta) keep this O(recent commits).

โ–ธ click a version

v0
CREATE
โ–ธ
v1
INSERT
โ–ธ
v2
MERGE
โ–ธ
v3
DELETE
v0 โ€” table created. First commit ...0000.json records the schema (metaData) + protocol.
v1 โ€” rows inserted. Commit lists add actions for the new Parquet files. Readers now compute the v1 snapshot.
v2 โ€” MERGE/upsert. Commit records remove + add atomically. No reader ever sees a half-applied merge.
v3 โ€” delete. Files tombstoned via remove. Old files stay on disk until VACUUM โ€” so you can still time-travel back to v2 until then.

4 ยท ACID via atomic commits + optimistic concurrency (OCC)

Writers don't lock. Each writer reads a snapshot, does its work, then tries to commit the next version number with a put-if-absent write. If another writer got there first, the loser detects the conflict and retries. Walk the flow:

โ–ธ step through two concurrent writers

STEP 1 / 5
Both writers read snapshot v7. Writer A (append US orders) and Writer B (append EU orders) each see the current version is 7 and start their work independently โ€” no locks taken.

Avoid conflicts by scoping writers to disjoint partitions

# Each job writes only its own region โ€” OCC won't treat them as conflicting
(df_us.write.format("delta").mode("append")
   .option("replaceWhere", "region = 'US'")   # disjoint predicate
   .saveAsTable("main.sales.orders"))
# A parallel job using replaceWhere "region = 'EU'" commits cleanly in parallel.

Conflict types interviewers probe: ConcurrentAppendException (overlapping partition/predicate), ConcurrentDeleteReadException / ConcurrentDeleteDeleteException (a file you read/deleted was deleted), MetadataChangedException (schema changed under you).

5 ยท Schema enforcement (write-time validation)

Delta validates the incoming schema on every write and rejects mismatches. Enforcement rejects; evolution (opt-in) adds columns โ€” don't confuse them.

# This APPEND fails fast โ€” `bonus` is not a column in the target table
bad = spark.createDataFrame([(1, "ACME", 99.0)], ["id", "name", "bonus"])
bad.write.format("delta").mode("append").saveAsTable("main.sales.customers")
# AnalysisException: A schema mismatch detected when writing to the Delta table.

# Opt in to evolution only when you *intend* to add the column:
(bad.write.format("delta").mode("append")
    .option("mergeSchema", "true")            # adds `bonus` to the table
    .saveAsTable("main.sales.customers"))

6 ยท Data skipping via column statistics

Each add stores min/max/nullCount for the first 32 columns (delta.dataSkippingNumIndexedCols). A filtered query reads those stats from the log and skips whole Parquet files whose range can't match โ€” often 10โ€“100ร— less data scanned. OPTIMIZE/ZORDER/liquid clustering (Topic 3) tighten those ranges to make skipping far more effective.

-- Delta reads file-level min/max from the log and skips non-matching files
SELECT * FROM main.sales.orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-01-07';

7 ยท Version history & time travel

Because the log keeps every commit, you can read the table as of any past version/timestamp (within retention) and roll back with one command. Compare the two APIs:

Spark SQL
PySpark
SELECT * FROM main.sales.orders VERSION AS OF 12;            -- by version
SELECT * FROM main.sales.orders TIMESTAMP AS OF '2024-01-15';  -- by time

-- Roll a bad load back (writes a NEW commit; no data loss)
RESTORE TABLE main.sales.orders TO VERSION AS OF 12;
df = (spark.read
        .option("versionAsOf", 12)        # or .option("timestampAsOf", "2024-01-15")
        .table("main.sales.orders"))

# Restore via the Delta table API
from delta.tables import DeltaTable
DeltaTable.forName(spark, "main.sales.orders").restoreToVersion(12)

How to do it โ€” from scratch

catalog, schema = "main", "de_demo"
spark.sql(f"CREATE SCHEMA IF NOT EXISTS {catalog}.{schema}")
spark.sql(f"USE {catalog}.{schema}")

# Delta is the default โ€” no USING DELTA needed
spark.sql("""
  CREATE TABLE IF NOT EXISTS orders (
    order_id BIGINT, customer STRING,
    amount DECIMAL(10,2), order_date DATE
  )
  TBLPROPERTIES (delta.checkpointInterval = 10)   -- explicitly set checkpoint cadence
""")

# Each write = a new version = a new JSON commit in _delta_log/
spark.sql("INSERT INTO orders VALUES (1,'ACME', 250.00, DATE'2024-01-03')")
spark.sql("DESCRIBE HISTORY orders").show(truncate=False)

Delta vs. plain Parquet/CSV "tables"

CapabilityPlain Parquet/CSV dirDelta table
Atomic multi-file writesโœ— partial output visibleโœ“ all-or-nothing commit
Concurrent writersโœ— corruption / lost dataโœ“ OCC + conflict detection
Schema enforcementโœ— anything landsโœ“ validated on write
Update / delete / upsertโœ— rewrite by handโœ“ MERGE / UPDATE / DELETE
Time travel / auditโœ— noneโœ“ DESCRIBE HISTORY, VERSION AS OF
Fast filtered readsโœ— scan everythingโœ“ file-skipping via log stats

Uses, edge cases & limitations

Common gotchas