Skip to content
Reliable Data Engineering
Lesson
Open in the interactive app

Databricks 2: Delta Lake Internals

Every Databricks interview probes Delta Lake: not just “it adds ACID to Parquet”, but how, and what happens when two jobs write at once, why VACUUM can break time travel, or when to choose liquid clustering. This module covers the internals behind those questions.


1. Anatomy of a Delta table

s3://lake/sales/orders/
├── _delta_log/
│   ├── 00000000000000000000.json     ← commit 0: protocol, metadata (schema, partitioning), add files
│   ├── 00000000000000000001.json     ← commit 1: add/remove actions
│   ├── ...
│   ├── 00000000000000000010.checkpoint.parquet   ← snapshot of table state every N commits
│   └── _last_checkpoint
├── part-00000-…-c000.snappy.parquet  ← data files (immutable)
├── part-00001-….snappy.parquet
└── deletion_vector_….bin             ← (if deletion vectors are enabled)

ACID from a log:


2. Optimistic concurrency control and conflicts

Writers don’t take locks. Each writer:

  1. Reads the latest snapshot (version N) and records what it read.
  2. Writes new data files.
  3. Tries to commit version N+1 by atomically creating N+1.json (put-if-absent semantics, or a commit coordinator on stores lacking it).
  4. If another writer already committed N+1, it checks for logical conflicts between its operation and the commits that won. If there are none, it retries as N+2. If there’s a conflict, it fails with a concurrency exception.
Operation AOperation BConflict?
Blind append (INSERT)Blind appendNo: both commit
AppendOPTIMIZEUsually no
UPDATE/DELETE/MERGE on partition PAppend to partition QNo (different data)
MERGE touching files FAnother MERGE/DELETE that removed or changed files in FYes: ConcurrentModificationException-family errors (e.g. ConcurrentDeleteReadException, ConcurrentAppendException)
Schema/metadata changeAnythingYes (metadata changed)

Reducing conflicts:


3. MERGE: what really happens

MERGE INTO silver.customers t
USING updates s
ON t.customer_id = s.customer_id
WHEN MATCHED AND s.op = 'D' THEN DELETE
WHEN MATCHED AND s.updated_at > t.updated_at THEN UPDATE SET *
WHEN NOT MATCHED AND s.op != 'D' THEN INSERT *
  1. Find touched files: join the source with the target to find which target files contain matching keys (using file stats and data skipping where possible).
  2. Rewrite: for copy-on-write, rewrite each touched file with updated/deleted rows applied, plus new files for inserts; commit remove for old files and add for new ones. With deletion vectors, deletes and updates mark old rows as deleted in a small bitmap file instead of rewriting the whole file (merge-on-read), so writes are much faster.

Performance rules:


4. Time travel, VACUUM and retention

SELECT * FROM orders VERSION AS OF 120;
SELECT * FROM orders TIMESTAMP AS OF '2024-05-01T00:00:00Z';
RESTORE TABLE orders TO VERSION AS OF 120;     -- rollback after a bad write
DESCRIBE HISTORY orders;                        -- who did what, with metrics

5. File layout: OPTIMIZE, Z-order, liquid clustering

Small files (from streaming, frequent small batches or over-partitioning) slow reads and inflate metadata.

Data skipping: readers skip files whose min/max stats exclude the filter. Stats are collected on the first 32 columns by default (delta.dataSkippingNumIndexedCols, or delta.dataSkippingStatsColumns), so put filter columns early or configure stats columns.

TechniqueHowProsCons
Hive-style partitioning (PARTITIONED BY date)Directory per valueCoarse pruning, easy retention/overwrite by partitionOnly for low-cardinality columns; over-partitioning → small files; fixed at creation
Z-order (OPTIMIZE … ZORDER BY (a, b))Rewrites files sorted by a space-filling curve on several columnsGood multi-column skippingFull rewrite of the data being optimised; not incremental; must re-run as data arrives
Liquid clustering (CLUSTER BY (a, b), or CLUSTER BY AUTO)Incremental clustering managed by Delta; keys can be changed without rewriting the tableIncremental, adapts to changing keys, works with row-level concurrency, recommended for new tablesRequires recent runtimes; not combinable with partitioning or Z-order on the same table

Current guidance: for new tables, use liquid clustering on the columns most used in filters and joins (or CLUSTER BY AUTO to let the platform pick keys from query patterns), and skip partitioning except for very large tables with clear date-based lifecycle needs.

Predictive optimisation (Unity Catalog managed tables) automatically runs OPTIMIZE, VACUUM and statistics collection when beneficial, removing most manual maintenance jobs.


6. Other features interviewers ask about

FeatureWhat it doesTypical use
Schema enforcementRejects writes whose schema doesn’t matchProtect curated tables
Schema evolution (mergeSchema, ALTER TABLE ADD COLUMN, column mapping for rename/drop)Controlled schema changesBronze ingestion, evolving sources
Constraints (NOT NULL, CHECK (amount >= 0))Commit fails if violatedEnforce invariants at write time
Generated columns / identity columnsDerived values (event_date GENERATED ALWAYS AS (CAST(ts AS DATE))), surrogate keysPartition columns derived from timestamps; dimension keys
Change Data Feed (delta.enableChangeDataFeed = true)Records row-level inserts, updates (pre/post images) and deletes per commitIncremental downstream processing, CDC out of the lakehouse
Deletion vectorsMarks deleted rows in bitmaps instead of rewriting filesFast DELETE/UPDATE/MERGE (merge-on-read)
Shallow cloneNew table pointing at the source’s files (metadata copy)Cheap test copies, experiments
Deep cloneFull copy of data and metadata, incrementally syncableBackups, DR copies, migrating tables
UniFormWrites Iceberg (and Hudi) metadata alongside DeltaLet Iceberg engines read Delta tables
Idempotent writes (txnAppId + txnVersion options)Skip a batch already committed by that writerExactly-once foreachBatch sinks

Interview questions

How does Delta Lake provide ACID transactions on object storage?

Through an ordered transaction log of JSON commits (plus periodic Parquet checkpoints). Each commit atomically adds and removes immutable data files by creating the next numbered log file (put-if-absent, or via a commit coordinator). Readers reconstruct a consistent snapshot from the log, so they never see partial writes. Writers use optimistic concurrency with conflict detection, and schema enforcement and constraints are checked before commit.

Two jobs MERGE into the same table and one fails with a concurrent modification error. Why, and how do you fix it?

Both read the same snapshot and their operations touched overlapping files (one removed or rewrote files the other read), so optimistic concurrency detected a logical conflict. Fixes: make the jobs touch disjoint data (partition/cluster by the dimension each job writes, and add explicit predicates in the ON clause), enable row-level concurrency (deletion vectors + liquid clustering), serialise writers to that table, and retry idempotent operations with backoff.

Why can VACUUM break time travel or running queries?

VACUUM deletes data files no longer referenced by the current snapshot that are older than the retention period. Older versions that reference those files become unreadable, and a long-running query or lagging stream reading an old snapshot can fail if its files disappear. Keep retention longer than the longest query or stream lag and your required time-travel window.

Z-order vs liquid clustering vs partitioning: what would you choose for a new 50 TB events table queried by date, customer and event type?

Liquid clustering on (event_date, customer_id, event_type), or CLUSTER BY AUTO, with predictive optimisation. It clusters incrementally, the keys can evolve, it supports row-level concurrency, and it avoids the small-file risk of partitioning by high-cardinality columns. Z-order requires repeated full rewrites. Directory partitioning by date only makes sense if you need partition-level lifecycle operations and partitions are large; it can’t be combined with liquid clustering.

How do you make MERGE faster on a large table?

Deduplicate the source to one row per key; add pruning predicates on clustering/partition columns; cluster the target on the merge key so matches touch few files; enable deletion vectors (and row-level concurrency) to avoid full file rewrites; use Photon; compact small files; and check DESCRIBE HISTORY metrics to confirm few files are rewritten per run.

What does Change Data Feed give you, and how is it different from time travel?

CDF records row-level changes per commit (_change_type = insert, update_preimage, update_postimage, delete, plus commit version and timestamp), so downstream jobs can read just the changes between versions and propagate them incrementally. Time travel gives full snapshots at a version, so you’d have to diff snapshots yourself. CDF must be enabled before the changes happen and is subject to retention.

Shallow vs deep clone?

A shallow clone copies only metadata and points at the source’s data files: instant and cheap, good for testing, but dependent on the source files (VACUUM on the source can break it). A deep clone copies data and metadata into an independent table, can be re-run to sync incrementally, and is suitable for backups, DR and migrations.