DatabricksZero to Advanced

Delta Lake is the table format under almost everything on Databricks. This module is hands-on with SQL and PySpark on the practice data.

  • Needs the orders table from Module 4 (run Module 4 first)
  • Demo tables are separate copies (orders_delta, orders_clustered, orders_cdf), so your orders table stays clean
  • Run this setup cell once per session
Python
from pyspark.sql import functions as F

catalog = spark.sql("SELECT current_catalog()").first()[0]
spark.sql(f"USE SCHEMA {catalog}.dbx_course")
spark.sql("CREATE OR REPLACE TABLE orders_delta AS SELECT * FROM orders")
print(spark.table("orders_delta").count())    # 2000

On Free Edition

All hands-on topics run on serverless compute. Table maintenance that Databricks runs for you (predictive optimization) works only on managed tables.

5.01Concept

What is Delta Lake?

By the end: explain what Delta Lake adds on top of Parquet files and why Databricks uses it by default.

  • Delta Lake = open-source table format and storage layer
  • Data stays in Parquet files. A transaction log adds reliability on top
  • Default table format on Databricks. CREATE TABLE without a format gives a Delta table
Delta tablewhat you querymade ofParquet data filesthe rowstracked by_delta_log/JSON commits + checkpoints
A Delta table is Parquet files plus a log that says which files are the table.
Gives you How
ACID transactions The log commits whole changes or nothing
Schema enforcement Writes that don't match are rejected
Time travel Old versions stay readable until cleanup
UPDATE, DELETE, MERGE Files are rewritten, the log records it
Streaming and batch on one table The table is both a source and a sink

Remember

Delta is not a different storage system. It's Parquet plus a log.

5.02Concept

Parquet vs Delta Lake

By the end: decide when a plain Parquet folder is enough and when you need Delta.

Parquet folderfiles onlyno transactionsno schema enforcementno update or deleteno historyDelta tabletransactionsschema enforcementMERGE, UPDATE, DELETEtime travelfile skipping
  • A half-finished write to a Parquet folder leaves partial files that readers see
  • To change one row in Parquet you rewrite the folder yourself
  • Parquet is fine for one-off exports and files you receive
  • Everything you query repeatedly belongs in a Delta table

Rule

Receive as CSV, JSON or Parquet. Store and serve as Delta.

5.03Concept

Delta Lake Architecture

By the end: describe how a Delta table is laid out and how a read finds the right files.

QueryRead the loglatest versionLive fileswith statsRead needed files
The log tells Spark which files make up the table and which can be skipped.
  • Data files: Parquet, optionally with deletion vectors
  • Log: ordered numbered commits (00000.json, 00001.json, ...), plus periodic checkpoints
  • File statistics (min, max, null counts) in the log let Spark skip files that can't match a filter
  • Managed tables: don't touch the files yourself. Use SQL or DataFrame APIs
SQL
%sql
DESCRIBE DETAIL orders_delta
  • Look at format, location, numFiles, sizeInBytes
5.04Hands-on on Free Edition

Delta Transaction Log

By the end: read a table's history and explain what each commit in the log records.

SQL
%sql
DESCRIBE HISTORY orders_delta
  • One row per commit, newest first
  • Key columns: version, timestamp, operation, operationParameters, operationMetrics
  • First run: version 0 is the CREATE OR REPLACE TABLE AS SELECT from setup
v0create tablev1UPDATEv2DELETEv3MERGE
Each change is one commit. The table is the result of replaying them.
  • A commit lists files added and files removed. Nothing is edited in place
  • Table history is kept for 30 days by default (delta.logRetentionDuration)
Python
display(spark.sql("DESCRIBE HISTORY orders_delta").select(
    "version", "timestamp", "operation", "operationMetrics"))
5.05Hands-on on Free Edition

ACID Transactions

By the end: explain the four ACID guarantees and see atomicity and concurrency rules at work.

Atomicall of a write or noneConsistentconstraints and schema holdIsolatedreaders see a whole versionDurablecommitted = stored
  • Atomic: a failed job leaves no half-written table
  • Isolated: readers see the last committed version, not a write in progress
  • Writers use optimistic concurrency: two writers touching the same files, the second commit can fail with a conflict exception and should retry
  • Appends from different jobs rarely conflict. Overlapping updates and deletes can
Python
# atomicity: this write fails part-way, so the table doesn't change
before = spark.table("orders_delta").count()
try:
    spark.sql("""
        INSERT INTO orders_delta
        SELECT order_id + 10000, customer_id, product_id,
               CASE WHEN order_id = 3 THEN raise_error('boom') ELSE quantity END,
               status, order_ts
        FROM orders WHERE order_id <= 5
    """)
except Exception as e:
    print("failed:", str(e).splitlines()[0])
print(before, spark.table("orders_delta").count())    # same number twice
  • raise_error fails the write while it runs. The rows before it are not kept
5.06Hands-on with caveats

Managed vs External Tables

By the end: choose between managed and external tables and say what `DROP TABLE` does to each.

ManagedUnity Catalog owns the filesstored with the schemadrop = data deleted after 7 daysoptimisation automaticExternalyou own the locationneeds external locationdrop = files stayyou clean up
  • Databricks calls managed tables the default and recommended type
  • Managed: DROP TABLE removes data after a recovery window (7 days by default). UNDROP TABLE works inside it
  • External: dropping removes only the metadata. You delete the files
  • Some features, like predictive optimization, only apply to managed tables
  • External tables need a storage credential and external location (Module 6)
SQL
%sql
DESCRIBE EXTENDED orders_delta
  • Look for Type (MANAGED) and Location

On Free Edition

You use managed tables. Custom storage locations aren't supported, so external tables are explanation only.

5.07Hands-on on Free Edition

Delta Table Creation and Management

By the end: create Delta tables with SQL and DataFrames and manage them with `ALTER TABLE`.

SQL
%sql
CREATE TABLE IF NOT EXISTS customers_delta (
  customer_id INT NOT NULL,
  name        STRING,
  country     STRING,
  signup_date DATE
)
COMMENT 'Practice customers'
Python
# from a DataFrame
(spark.table("customers")
      .write.mode("overwrite")
      .saveAsTable("customers_delta"))

# from a query
spark.sql("CREATE OR REPLACE TABLE big_orders AS SELECT * FROM orders WHERE quantity >= 4")
Task SQL
Add a column ALTER TABLE t ADD COLUMN note STRING
Rename a column ALTER TABLE t RENAME COLUMN a TO b (needs column mapping)
Add a constraint ALTER TABLE t ADD CONSTRAINT pos CHECK (quantity > 0)
Comment COMMENT ON TABLE t IS '...'
Table property ALTER TABLE t SET TBLPROPERTIES (k = 'v')
Clone CREATE TABLE copy DEEP CLONE t
  • CREATE OR REPLACE TABLE keeps the table's history. DROP + CREATE doesn't
  • NOT NULL and CHECK constraints are enforced on every write
SQL
%sql
ALTER TABLE orders_delta ADD CONSTRAINT qty_positive CHECK (quantity > 0 OR quantity IS NULL)
5.08Hands-on on Free Edition

UPDATE, DELETE, and MERGE

By the end: change rows with UPDATE, DELETE and MERGE and use MERGE for upserts.

SQL
%sql
UPDATE orders_delta SET status = 'cancelled' WHERE order_id = 1
SQL
%sql
DELETE FROM orders_delta WHERE quantity IS NULL
  • The 15 null-quantity rows are gone: count is now 1985
Source rowsthe changesMERGEmatch on keyMatchedupdate
Not matched rows are inserted. One MERGE = one atomic commit.

Upsert with MERGE:

SQL
%sql
CREATE OR REPLACE TEMP VIEW order_updates AS
SELECT order_id, customer_id, product_id, quantity, 'returned' AS status, order_ts
FROM orders_delta WHERE order_id <= 3
UNION ALL
SELECT 2001, 1, 1, 2, 'completed', current_timestamp()
SQL
%sql
MERGE INTO orders_delta AS t
USING order_updates AS s
ON t.order_id = s.order_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
  • Orders 1 to 3 are updated, order 2001 is inserted: count is 1986
  • Optional clause: WHEN NOT MATCHED BY SOURCE THEN DELETE (or UPDATE) for target rows missing from the source

One source row per target row

If several source rows match one target row, the MERGE can fail. Deduplicate the source on the key first (Module 4.7).

Python API

DeltaTable.forName(spark, "t").merge(...) also exists. Use the SQL form if the import isn't available on your compute.

5.09Hands-on on Free Edition

Schema Enforcement and Evolution

By the end: see schema enforcement reject a bad write and use schema evolution when you mean it.

Enforcementdefaultextra column = errorwrong type = errorEvolutionmergeSchemanew columns addedyou opt in
Python
extra = spark.table("orders").limit(3).withColumn("note", F.lit("hi"))
try:
    extra.write.mode("append").saveAsTable("orders_delta")
except Exception as e:
    print("rejected:", str(e).splitlines()[0])
  • Append with a new column is rejected by default: a typo can't silently add columns
  • Opt in to evolution when the change is intended:
Python
extra.write.mode("append").option("mergeSchema", "true").saveAsTable("orders_delta")
spark.table("orders_delta").printSchema()       # now has "note"
  • Or evolve explicitly: ALTER TABLE orders_delta ADD COLUMN note STRING
  • MERGE can evolve schema too, with automatic schema evolution enabled

Dropping columns

Renaming or dropping columns needs column mapping to be enabled. It's a one-way property change, so decide deliberately.

5.10Hands-on on Free Edition

Time Travel and Table History

By the end: query earlier versions of a table and restore one after a mistake.

SQL
%sql
DESCRIBE HISTORY orders_delta
SQL
%sql
SELECT COUNT(*) FROM orders_delta VERSION AS OF 0
  • Version 0 still has all 2000 original rows (if you reran setup, pick the version with 2000 rows from the history)
  • Same with a timestamp: TIMESTAMP AS OF '2026-10-19'
  • In PySpark: spark.read.option("versionAsOf", 0).table("orders_delta")
  • Short form: SELECT * FROM orders_delta@v0

Undo a mistake:

SQL
%sql
RESTORE TABLE orders_delta TO VERSION AS OF 0
  • Needs MODIFY on the table. The table is back to 2000 rows
  • RESTORE is itself a new commit: history isn't erased
  • It fails if VACUUM already deleted the files that version needs

Not a backup

Time travel is for short-term recovery. Defaults: history 30 days, data files 7 days. Longer retention costs storage.

5.11Hands-on on Free Edition

VACUUM and Data Retention

By the end: preview and run VACUUM safely and explain how retention settings affect time travel.

Old filesremoved by commitsolder than 7 daysVACUUMDeleted from storage
After VACUUM, versions that needed those files can't be read.
SQL
%sql
VACUUM orders_delta DRY RUN
  • DRY RUN lists what would be deleted. Right after creating the table it lists nothing: files are younger than 7 days
  • Default retention: 7 days for data files (delta.deletedFileRetentionDuration)
  • Log (history) retention: 30 days (delta.logRetentionDuration)
  • Predictive optimization runs VACUUM for managed tables
SQL
%sql
ALTER TABLE orders_delta SET TBLPROPERTIES (
  'delta.deletedFileRetentionDuration' = 'interval 14 days',
  'delta.logRetentionDuration'         = 'interval 30 days')

Don't lower retention below 7 days

A running job or a long query may still need the old files. Databricks has a safety check against short retention. Keep log retention at least as long as file retention.

5.12Hands-on on Free Edition

OPTIMIZE and File Compaction

By the end: compact small files with OPTIMIZE and explain why file layout matters.

Many small filesslow to open`OPTIMIZE`Fewer large filesfaster reads
Compaction rewrites small files into bigger ones. It's a new commit, so old files wait for VACUUM.
SQL
%sql
OPTIMIZE orders_delta
  • Output shows files removed and added
  • Each UPDATE, MERGE or small append can leave small files
  • Fewer, larger files = fewer tasks and less listing overhead
  • On managed tables with predictive optimization, Databricks does this for you
SQL
%sql
DESCRIBE DETAIL orders_delta
  • Compare numFiles before and after

Let the platform do it

Prefer predictive optimization on managed tables. Run OPTIMIZE by hand for external tables or one-off cleanups.

5.13Hands-on on Free Edition

Liquid Clustering

By the end: create a liquid clustered table and choose clustering keys.

Partitioningfixed folders by columnhard to changesmall-file riskLiquid clusteringCLUSTER BYchange keys any timeincremental
  • Databricks recommends liquid clustering for all new tables
  • It replaces partitioning and ZORDER. They can't be combined with it
SQL
%sql
CREATE OR REPLACE TABLE orders_clustered
CLUSTER BY (customer_id)
AS SELECT * FROM orders
SQL
%sql
OPTIMIZE orders_clustered
  • Clustering happens when OPTIMIZE runs (or automatically with predictive optimization)
  • Pick keys you filter or join on: up to 4, no complex types, columns need statistics
  • Change later without rewriting history: ALTER TABLE orders_clustered CLUSTER BY (order_ts)
  • Let Databricks choose: CLUSTER BY AUTO (needs predictive optimization)

Small tables

2,000 rows won't show any speed difference. The syntax is what you practise here.

5.14Hands-on on Free Edition

Change Data Feed (CDF)

By the end: read row-level changes from a table with change data feed.

SQL
%sql
CREATE OR REPLACE TABLE orders_cdf
TBLPROPERTIES (delta.enableChangeDataFeed = true)
AS SELECT * FROM orders WHERE order_id <= 5
SQL
%sql
UPDATE orders_cdf SET quantity = 9 WHERE order_id = 1
SQL
%sql
DELETE FROM orders_cdf WHERE order_id = 2
SQL
%sql
SELECT order_id, quantity, _change_type, _commit_version
FROM table_changes('orders_cdf', 0)
ORDER BY _commit_version, order_id
_change_typeinsertupdate_preimageupdate_postimagedelete
  • Version 0: five insert rows. Version 1: a pre-image and post-image of order 1. Version 2: a delete of order 2
  • Also: _commit_version and _commit_timestamp
  • PySpark: .option("readChangeFeed", "true").option("startingVersion", 0).table(...)
  • Use it to feed downstream tables with only what changed (Module 9, Module 21)

Retention and limits

Change records go away when old versions are cleaned up. Copy them to a table if you need them long-term. You can't read across a rename or drop of a column. Newer runtimes also have automatic CDF with no property.

5.15Hands-on on Free Edition

Delta Table Maintenance

By the end: run a routine maintenance pass on a table and know what runs automatically.

Automaticpredictive optimizationOPTIMIZEVACUUMstatisticsmanaged tablesBy handOPTIMIZEVACUUMANALYZE TABLEexternal tables, one-offs
SQL
%sql
ANALYZE TABLE orders_delta COMPUTE STATISTICS FOR ALL COLUMNS

Maintenance checklist per table:

  • Layout: liquid clustering on the columns you filter by
  • Files: OPTIMIZE (or predictive optimization)
  • Cleanup: VACUUM with at least 7 days retention
  • Statistics: kept current so queries can skip files
  • Retention: set logRetentionDuration and deletedFileRetentionDuration on purpose

Clean up the demo tables:

Python
for t in ["orders_delta", "orders_clustered", "orders_cdf", "customers_delta", "big_orders"]:
    spark.sql(f"DROP TABLE IF EXISTS {t}")

Managed tables

Use managed tables so Databricks maintains them. Dropped by mistake? UNDROP TABLE works for 7 days.

Recap

What you can do now

  • Delta = Parquet files + a transaction log
  • Create, update, delete and merge rows. Enforce or evolve the schema
  • Time travel and RESTORE for short-term recovery
  • OPTIMIZE, VACUUM, liquid clustering and change data feed
  • Managed tables get maintenance for free