This lesson on Delta Lake: ACID, Time Travel & Merges is hands-on and example-driven. You will be able to explain exactly how Delta Lake delivers ACID guarantees on top of plain cloud object storage, and use the transaction log, time travel, and MERGE in your own pipelines. By the end you can trace a write from staged Parquet files to an atomic commit, resolve concurrent write conflicts, and recover a table to an earlier version.
What You'll Be Able To Do
- Explain how Delta Lake delivers each of the four ACID guarantees on cloud object storage
- Trace a write through the stage, prepare, validate, commit, and cleanup steps
- Read a past table version using versionAsOf and timestampAsOf
- Resolve concurrent write conflicts using optimistic concurrency control
- Write a MERGE statement that upserts and deletes in a single transaction
- Diagnose small-file and transaction-log problems with OPTIMIZE and VACUUM
Detailed Concept Walkthrough
1. ACID Guarantees in Data Lakes
ACID — Atomicity, Consistency, Isolation, Durability — is the set of guarantees that makes concurrent reads and writes on one table safe. Delta Lake's whole selling point is bringing those guarantees to data lakes, which used to be the wild west.
- Mechanism: Atomicity means a transaction happens completely or not at all, like a bank transfer where the money either moves or stays put. Consistency means your declared rules — say, no customer can have a negative balance — hold before and after every transaction. Isolation keeps concurrent transactions in their own private bubbles so they never step on each other's toes, and Durability means a committed transaction stays committed even if the server crashes or gets hit by lightning.
- Under the Hood: Relational databases like Postgres, MySQL, and SQL Server have implemented these four properties for decades using locks and write-ahead logs. Cloud object storage has no lock manager and no in-place mutation, so Delta Lake had to rebuild the same guarantees on top of immutable files.
- Failure Mode Without ACID: Picture a lake with three simultaneous jobs: one writing customer data, one writing transaction records, one reading for analytics. Without ACID, Job A writes corrupted data, Job B overwrites everything, and Job C reads an inconsistent mix — reports are garbage and stakeholders are furious.
- Best Practice: Treat ACID as the reason you can run concurrent ETL against the same table at all. With Delta, all three jobs run simultaneously, each protected by isolation, and a failure rolls back cleanly instead of leaving half-written state behind.
-- Consistency is enforced on every write: schema plus constraints
CREATE TABLE customers (
customer_id INT NOT NULL,
balance DECIMAL(10,2)
) USING DELTA;
-- Rejected: violates the declared schema and the not-null rule
INSERT INTO customers VALUES ('abc', -10.00);
Key Takeaway: ACID is the contract that lets many writers and readers share one table without corrupting it.
2. Transaction Log and Checkpoints
Delta Lake never mutates your data files directly. Every change is recorded as a new JSON commit in the _delta_log folder, which acts as the table's single source of truth.
- Mechanism: Each transaction creates a new sequentially numbered JSON file — 00000000000000000000.json, 00000000000000000001.json, and so on — containing an array of actions. The main action types are add (this Parquet file joins the table, with path, size, record count, per-column min/max, and partition info), remove (this file leaves the table, with when and why), metadata (column names, types, partitioning scheme, format), and txn (an external system transaction reference).
- Under the Hood: The log is a ledger: to know the current state of the table you replay entries in order, exactly like reading a bank account statement from the first transaction. Reading millions of JSON entries on every query would be terrible, so Delta writes a checkpoint — a Parquet snapshot of the complete table state — every 10 commits by default.
- Execution Flow: A query reads the latest checkpoint to jump to a known-good state, replays only the JSON commits written since that checkpoint, prunes files using the column min/max statistics stored in the log, and finally reads only the relevant Parquet files. That last step is why queries stay fast even when the table holds millions of files.
- Best Practice: Checkpoint compaction combines multiple transaction log files into a single Parquet file for even faster scans. Never hand-edit or delete files inside _delta_log — you would be editing the table's source of truth, not just some metadata.
# Every commit is a new, sequentially numbered JSON file
ls _delta_log/
# 00000000000000000000.json
# 00000000000000000001.json
# 00000000000000000010.checkpoint.parquet <- full table state snapshot
Key Takeaway: The _delta_log is the table; the Parquet files are just data it points at.
3. Atomic Commits and Concurrency
A Delta write is a five-step pipeline that ends in one atomic append to the transaction log — that single append is what makes the whole operation all-or-nothing.
- Execution Flow: Stage writes the new Parquet files to a temporary location; Prepare builds the JSON entry for the transaction log; Validate checks the write against constraints and schema; Commit atomically appends the entry to the log; Cleanup marks the now-obsolete files for deletion.
- Under the Hood: The magic moment is the atomic append. Appending a single object to cloud storage is atomic, so either the entire transaction is recorded or none of it is — there is no halfway-committed state. If the job crashes mid-write, the log entry never appears and the abandoned temp Parquet files are simply ignored by the next reader, which is a clean rollback.
- Mechanism: When two writers race, Delta uses optimistic concurrency control. Writer A and Writer B both start; A finishes first and commits version 2; B tries to commit version 2, Delta sees that version number already exists, and B automatically retries as version 3. Both writes succeed, ordered, with no data corruption and no lost updates.
- Best Practice: Because conflicts are only detected at commit time, keep transactions short and avoid having many jobs rewrite the same partitions at once — retries preserve correctness but still cost wall-clock time.
# The commit is one atomic append to _delta_log — no partial writes
(df.write
.format("delta")
.mode("append")
.save("/mnt/lake/transactions"))
Key Takeaway: Atomicity comes from one atomic log append; conflicts are resolved by retrying at the next version.
4. Snapshot Isolation and Durability
Readers pin a version number and see a frozen snapshot of the table, while writers commit new versions independently — so nobody blocks anybody.
- Mechanism: When a read starts, Delta captures the current version from the transaction log, say version 50. A concurrent writer adds and removes files and commits version 51, but your read keeps looking at version 50. You see a snapshot frozen in time, completely unaffected by the concurrent write.
- Under the Hood: This is snapshot isolation, and its payoff is that readers never block writers and writers never block readers, with multiple writers able to work simultaneously. The trade-off is that you might read slightly stale data, which is an acceptable price in most data lake scenarios.
- Durability: Delta leans on the underlying cloud storage — S3, Azure Blob, or GCS replicate your objects across multiple data centers, with S3 offering eleven nines of durability. Delta adds a layer on top: the transaction log is the source of truth, so even if a Parquet file is corrupted or deleted you still have the log entry referencing it and can detect the problem.
- Best Practice: VACUUM cleans up old files but respects a retention period, 30 days by default. Inside that window you can recover with time travel using versionAsOf or timestampAsOf; outside it, the old files are gone for good.
# Pin a frozen snapshot by version number
spark.read.format("delta").option("versionAsOf", 50).load("/mnt/lake/transactions")
# Or travel back by timestamp
spark.read.format("delta").option("timestampAsOf", "2024-05-01").load("/mnt/lake/transactions")
Key Takeaway: Readers see a frozen version, writers commit new ones, and the log outlives any single Parquet file.
5. Write Patterns and MERGE
Append is cheap because it only adds files; update, delete, and merge are copy-on-write because Parquet files are immutable and must be rewritten.
- Mechanism: A simple append writes new records to one or more Parquet files and records a single ADD action. The query optimizer immediately sees the new files and no existing file is rewritten, so append is nearly always fast.
- Copy-on-Write: Update and delete are trickier because you must remove records from an existing file. Delta reads the existing Parquet file, filters out the deleted rows or applies the updates, writes the modified data to a new Parquet file, then records a REMOVE action for the old file and an ADD action for the new one — both in the same log entry, so the old file disappears and the new one appears atomically.
- MERGE: MERGE handles insert, update, and delete in a single operation. Delta identifies which existing records match the source data, rewrites those Parquet files with the updates or deletions, writes new Parquet files for the inserts, and records every file operation in one transaction log entry.
- Best Practice: Deletion vectors mark rows as deleted instead of rewriting whole files, making deletes and updates much cheaper. As tables grow, run OPTIMIZE to compact many small files into fewer large ones and keep the transaction log manageable.
-- Upsert and delete in one atomic transaction
MERGE INTO silver.customers AS t
USING staging.customers AS s
ON t.customer_id = s.customer_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;
Key Takeaway: Append only adds files; update, delete, and merge rewrite affected files and log the swap atomically.
Topics Covered in Delta Lake: ACID, Time Travel & Merges
- Intro and Promise (0:00 - 1:00) — Naveen introduces the lesson and promises a technical, under-the-hood look at how Delta Lake implements ACID.
- The Four ACID Properties (1:00 - 3:00) — Atomicity, Consistency, Isolation, and Durability are each explained with a bank-transfer style analogy.
- Why Data Lakes Need ACID (3:00 - 5:00) — A horror story of three concurrent ETL jobs corrupting a lake shows what breaks without transactions.
- Transaction Log Basics (5:00 - 8:00) — Delta records every change in the _delta_log folder and uses periodic checkpoints to avoid replaying the whole log.
- Atomicity and Concurrency (8:00 - 11:00) — The five-step write pipeline ends in an atomic log append, and racing writers are resolved by optimistic concurrency control.
- Consistency, Isolation, Durability (11:00 - 14:00) — Schema and constraint validation enforce consistency, snapshot isolation keeps readers and writers independent, and durability rides on cloud storage.
- Append, Copy-on-Write, MERGE (14:00 - 17:00) — Append only adds files, while update, delete, and merge rewrite affected Parquet files and log the swap in one entry.
- Checkpoints and Debugging (17:00 - 19:00) — Checkpoint compaction, OPTIMIZE, VACUUM, and DESCRIBE HISTORY are presented as fixes for common table problems.
- Advanced Delta Features (19:00 - 21:00) — Liquid clustering, Change Data Feed, dynamic SQL filters, Delta Sharing, and deletion vectors round out the lesson.
Spark Lakehouse Cheat Sheet
-
_delta_log— Transaction log directory holding every commit as JSONls _delta_log/00000000000000000000.json -
versionAsOf— Read a specific past version of the tablespark.read.format("delta").option("versionAsOf", 50).load(path) -
timestampAsOf— Read table state as of a point in timespark.read.format("delta").option("timestampAsOf", "2024-05-01").load(path) -
DESCRIBE HISTORY— List table versions, operations, and timestampsDESCRIBE HISTORY customers LIMIT 20; -
MERGE INTO— Upsert, update, and delete in one transactionMERGE INTO silver.customers AS t USING staging.customers AS s ON t.customer_id = s.customer_id WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *; -
VACUUM— Delete files older than the retention windowVACUUM customers RETAIN 168 HOURS; -
OPTIMIZE— Compact many small files into fewer large onesOPTIMIZE customers; -
Checkpoint— Parquet snapshot of full table state every 10 commitsls _delta_log/*.checkpoint.parquet
Comparison Table
| Operation | What Delta Writes | Cost |
|---|---|---|
| Append | New Parquet files plus ADD action | Cheapest, no rewrite |
| Update / Delete | Rewritten Parquet plus REMOVE and ADD | Rewrites whole affected file |
| MERGE | Rewritten matched files plus new inserts | One log entry, many file ops |
| Deletion vectors | Marks rows deleted, no rewrite | Fastest deletes, read-time merge |
Common Pitfalls
- Mistake: Assuming a failed write leaves partial data in the table. Avoid: check the log — no commit entry means readers ignore orphaned temp files.
- Mistake: Running VACUUM with zero retention to reclaim space. Avoid: keep the default 30-day retention so time travel still works.
- Mistake: Letting thousands of tiny files accumulate in a table. Avoid: run OPTIMIZE to compact small files into fewer large ones.
- Mistake: Expecting readers to see a writer's uncommitted changes. Avoid: snapshot isolation means readers see the version captured when the read started.
- Mistake: Treating concurrent write conflicts as data loss. Avoid: Delta retries the losing writer at the next version number automatically.
FAQs
- Why is appending to a JSON file considered atomic? Cloud object storage makes a single object PUT atomic, so the entire log entry lands or none of it does. That is why Delta commits by appending one new file rather than editing existing ones.
- Do readers block writers in Delta Lake? No. Snapshot isolation means a reader pins a version number and writers commit new versions independently, so readers never block writers and writers never block readers.
- What happens if two writers try to commit the same version? Optimistic concurrency control detects that the version number already exists and the losing writer automatically retries at the next version number. Both writes succeed, just ordered.
- How far back can I time travel? Any version still inside the retention window — 30 days by default. Once VACUUM removes the old files, those versions are no longer readable.
- Where does durability come from if Parquet files can be deleted? Cloud storage replicates objects across data centers, and the transaction log is the source of truth. Even a corrupted Parquet file can be detected because the log still references it.
- What is a checkpoint actually for? It is a Parquet snapshot of the complete table state written every 10 commits, so a query reads one checkpoint plus a few recent JSON commits instead of replaying the whole log.