Cracking ML Interviews

Open in the interactive study hub

Delta Lake

Delta Lake is an open-source storage layer that brings ACID transactions, schema enforcement, time travel, and scalable metadata to data lakes on cloud object storage (S3, GCS, ADLS). It's the backbone of the Lakehouse architecture.


What is Delta Lake?

Before Delta Lake, data lakes had serious problems:

Delta Lake solves all of these on top of Parquet files in S3.

Data Lake (raw S3/GCS) + Delta Lake = Lakehouse
                                        ↓
                           ACID + Schema + Time Travel
                           + Fast reads + Streaming + ML

Core Features

Feature Description
ACID Transactions Atomic commits: either all changes succeed or none
Schema Enforcement Reject writes that don't match the table schema
Schema Evolution Safely add new columns
Time Travel Query data as it was at any point in time
Upserts (MERGE) Update or insert based on conditions
Deletes Delete specific rows (GDPR compliance)
Streaming + Batch Read and write to same table from batch and stream
Data Lineage Track what changed, when, and how
Optimize/Compaction Combine small files, Z-order for faster queries
Change Data Feed Read only what changed since last read

Getting Started

pip install delta-spark pyspark
from pyspark.sql import SparkSession

# Match Delta to Spark: Delta 3.x targets Spark 3.5, Delta 4.x targets Spark 4.0
spark = SparkSession.builder \
    .appName("DeltaLake") \
    .master("local[*]") \
    .config("spark.jars.packages", "io.delta:delta-spark_2.12:3.3.0") \
    .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
    .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") \
    .getOrCreate()

Writing and Reading

from delta.tables import DeltaTable
from pyspark.sql import functions as F

# Create a Delta table
data = [(1, "Alice", 85.0, "2025-01-01"),
        (2, "Bob", 92.5, "2025-01-01")]
df = spark.createDataFrame(data, ["id", "name", "score", "date"])

# Write as Delta
df.write.format("delta").mode("overwrite").save("/data/scores")

# Read Delta table
scores = spark.read.format("delta").load("/data/scores")
scores.show()

# Create table in metastore (SQL access)
df.write.format("delta").saveAsTable("scores")

# Or use SQL
spark.sql("""
    CREATE TABLE IF NOT EXISTS scores (
        id INT,
        name STRING,
        score DOUBLE,
        date DATE
    )
    USING DELTA
    LOCATION 's3://my-bucket/delta/scores/'
""")

ACID Transactions

# Concurrent writers are safe: Delta uses optimistic concurrency control

# Writer 1: append new records
new_data = spark.createDataFrame([(3, "Charlie", 78.3, "2025-01-02")], ["id", "name", "score", "date"])
new_data.write.format("delta").mode("append").save("/data/scores")

# Writer 2: update existing records
# The table never ends up corrupted: if the two commits truly conflict (e.g. the
# update read files the append changed), one fails with a concurrent-modification
# error and must retry; non-conflicting commits both succeed

Schema Enforcement & Evolution

# Schema enforcement: this will FAIL if schema doesn't match
try:
    bad_df = spark.createDataFrame([(4, "Dave")], ["id", "name"])
    bad_df.write.format("delta").mode("append").save("/data/scores")
except Exception as e:
    print("Schema mismatch blocked:", e)

# Schema evolution: allow adding new columns
spark.conf.set("spark.databricks.delta.schema.autoMerge.enabled", "true")

new_df = spark.createDataFrame(
    [(5, "Eve", 88.0, "2025-01-03", "Math")],
    ["id", "name", "score", "date", "subject"]  # new column: subject
)
new_df.write.format("delta").mode("append") \
    .option("mergeSchema", "true") \
    .save("/data/scores")

Time Travel

One of Delta Lake's killer features.

dt = DeltaTable.forPath(spark, "/data/scores")

# View table history
dt.history().show()
# version | timestamp | operation | operationParameters | ...

# Query specific version
spark.read.format("delta") \
    .option("versionAsOf", 0) \
    .load("/data/scores") \
    .show()

# Query at a specific timestamp
spark.read.format("delta") \
    .option("timestampAsOf", "2025-01-01 00:00:00") \
    .load("/data/scores") \
    .show()

# SQL time travel
spark.sql("SELECT * FROM scores VERSION AS OF 0")
spark.sql("SELECT * FROM scores TIMESTAMP AS OF '2025-01-01'")

Updates, Deletes, and MERGE (Upserts)

from delta.tables import DeltaTable

dt = DeltaTable.forPath(spark, "/data/scores")

# Update specific rows
dt.update(
    condition=F.col("name") == "Alice",
    set={"score": F.lit(95.0)}
)

# Delete rows
dt.delete(condition=F.col("score") < 60)

# MERGE (Upsert): the most powerful operation
updates = spark.createDataFrame([
    (1, "Alice", 99.0, "2025-01-05"),   # update existing
    (6, "Frank", 81.5, "2025-01-05"),   # new record
], ["id", "name", "score", "date"])

dt.alias("target").merge(
    updates.alias("source"),
    "target.id = source.id"
).whenMatchedUpdate(set={
    "score": "source.score",
    "date": "source.date"
}).whenNotMatchedInsert(values={
    "id": "source.id",
    "name": "source.name",
    "score": "source.score",
    "date": "source.date"
}).execute()

# Full upsert in SQL
spark.sql("""
    MERGE INTO scores AS target
    USING updates AS source
    ON target.id = source.id
    WHEN MATCHED THEN
        UPDATE SET target.score = source.score
    WHEN NOT MATCHED THEN
        INSERT (id, name, score, date) VALUES (source.id, source.name, source.score, source.date)
    WHEN NOT MATCHED BY SOURCE THEN
        DELETE
""")

Partitioning

# Write with partitioning
df.write.format("delta") \
    .partitionBy("date", "region") \
    .mode("overwrite") \
    .save("/data/sales")

# Partition pruning happens automatically on filter
sales = spark.read.format("delta").load("/data/sales")
sales.filter(F.col("date") == "2025-01-01").count()   # only reads 2025-01-01 partition

Optimize & Z-Order

from delta.tables import DeltaTable

dt = DeltaTable.forPath(spark, "/data/orders")

# Compact small files (bin-packing)
dt.optimize().executeCompaction()

# Z-Order: co-locate related data for faster filtering
dt.optimize().executeZOrderBy("customer_id", "date")

# After Z-Order: queries filtering on customer_id or date are much faster
spark.sql("OPTIMIZE orders ZORDER BY (customer_id, date)")

# Vacuum: remove old files to save storage
# (default retainDurationHours=168, i.e., 7 days for time travel)
dt.vacuum()           # keep 7 days of history
dt.vacuum(24)         # keep only 24 hours (less time travel); retention below 168h is
                      # rejected unless spark.databricks.delta.retentionDurationCheck.enabled=false

Streaming with Delta Lake

# Streaming write to Delta
query = df_stream.writeStream \
    .format("delta") \
    .outputMode("append") \
    .option("checkpointLocation", "/checkpoints/orders") \
    .table("orders")

# Streaming read from Delta (reads new changes as they arrive)
# Change Data Feed (requires delta.enableChangeDataFeed = true on the table)
changes = spark.readStream \
    .format("delta") \
    .option("readChangeFeed", "true") \
    .option("startingVersion", 0) \
    .table("orders")

Change Data Feed (CDF)

# Enable CDF on a table
spark.sql("""
    ALTER TABLE orders
    SET TBLPROPERTIES (delta.enableChangeDataFeed = true)
""")

# Read changes since version 5
cdf = spark.read.format("delta") \
    .option("readChangeFeed", "true") \
    .option("startingVersion", 5) \
    .table("orders")

cdf.select("_change_type", "order_id", "amount").show()
# _change_type: insert / update_preimage / update_postimage / delete

Delta Lake + dbt

# dbt/profiles.yml (Databricks)
my_profile:
  target: prod
  outputs:
    prod:
      type: databricks
      catalog: my_catalog    # Unity Catalog
      schema: analytics
      host: "{{ env_var('DBT_DATABRICKS_HOST') }}"
      http_path: "{{ env_var('DBT_HTTP_PATH') }}"
      token: "{{ env_var('DATABRICKS_TOKEN') }}"
-- dbt model using Delta features
{{ config(
    materialized='incremental',
    incremental_strategy='merge',
    unique_key='order_id',
    file_format='delta',
    post_hook="OPTIMIZE {{ this }} ZORDER BY (customer_id)"
) }}

SELECT * FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}

Delta Lake Architecture (Lakehouse)

Raw Files (S3/GCS/ADLS)
        ↓
Delta Lake Tables (Bronze/Silver/Gold)
        ↓
┌───────────────────────────────────────┐
│         Compute Engines               │
│  Apache Spark    Apache Flink         │
│  Trino/Presto    DuckDB               │
│  Snowflake       Databricks SQL       │
└───────────────────────────────────────┘
        ↓
BI Tools / ML Models / Data Apps

Delta Lake in 2026

Feature Description
Delta Lake 4.0 Release line built on Apache Spark 4.0 (adds variant type, type widening, and more)
Liquid Clustering Recommended over partitioning + Z-Order for new tables; incremental layout optimization
Deletion Vectors Soft-deletes without rewriting files (faster deletes)
Variant type Native semi-structured data type for JSON
Universal Format (UniForm) Delta tables readable as Iceberg (and Hudi) once enabled via table properties
Delta Kernel / delta-rs Libraries for reading and writing Delta without Spark (connectors, Python via deltalake)
DuckDB + Delta SELECT * FROM delta_scan('s3://...'): no Spark needed

Liquid Clustering (Replaces Partitioning)

-- Create table with liquid clustering (Databricks and open-source Delta)
CREATE TABLE orders (
    order_id    BIGINT,
    customer_id BIGINT,
    order_date  DATE
)
USING DELTA
CLUSTER BY (customer_id, order_date);

-- Clustering is applied incrementally when OPTIMIZE runs
-- (Databricks can run it for you via predictive optimization / CLUSTER BY AUTO)
-- Better than partitioning for high-cardinality columns

Cheat Sheet

# Read
df = spark.read.format("delta").load("path/")
df = spark.read.format("delta").option("versionAsOf", 5).load("path/")
df = spark.table("my_delta_table")

# Write
df.write.format("delta").mode("overwrite").save("path/")
df.write.format("delta").mode("append").save("path/")
df.write.format("delta").mode("overwrite").option("overwriteSchema", "true").save("path/")

# DeltaTable API
from delta.tables import DeltaTable
dt = DeltaTable.forPath(spark, "path/")
dt = DeltaTable.forName(spark, "my_table")

dt.history().show()                    # version history
dt.toDF().show()                       # as DataFrame
dt.update(condition, set_dict)         # update rows
dt.delete(condition)                   # delete rows
dt.merge(source, condition)            # upsert

# Maintenance
dt.optimize().executeCompaction()
dt.optimize().executeZOrderBy("col1", "col2")
dt.vacuum()
dt.vacuum(retentionHours=24)

# SQL
spark.sql("DESCRIBE HISTORY my_table")
spark.sql("OPTIMIZE my_table ZORDER BY (customer_id)")
spark.sql("VACUUM my_table RETAIN 168 HOURS")
spark.sql("ALTER TABLE my_table SET TBLPROPERTIES (delta.enableChangeDataFeed = true)")

View or edit this guide on GitHub