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
Module 5: Delta Lake Fundamentals
Delta tables: transactions, MERGE, time travel, maintenance and liquid clustering.
Topics in this module
- What is Delta Lake?
- Parquet vs Delta Lake
- Delta Lake Architecture
- Delta Transaction Log
- ACID Transactions
- Managed vs External Tables
- Delta Table Creation and Management
- UPDATE, DELETE, and MERGE
- Schema Enforcement and Evolution
- Time Travel and Table History
- VACUUM and Data Retention
- OPTIMIZE and File Compaction
- Liquid Clustering
- Change Data Feed (CDF)
- Delta Table Maintenance
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
orderstable from Module 4 (run Module 4 first) - Demo tables are separate copies (
orders_delta,orders_clustered,orders_cdf), so yourorderstable stays clean - Run this setup cell once per session
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.
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 TABLEwithout a format gives a Delta 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.
Parquet vs Delta Lake
By the end: decide when a plain Parquet folder is enough and when you need Delta.
- 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.
Delta Lake Architecture
By the end: describe how a Delta table is laid out and how a read finds the right files.
- 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
DESCRIBE DETAIL orders_delta
- Look at
format,location,numFiles,sizeInBytes
Delta Transaction Log
By the end: read a table's history and explain what each commit in the log records.
%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 SELECTfrom setup
- A commit lists files added and files removed. Nothing is edited in place
- Table history is kept for 30 days by default (
delta.logRetentionDuration)
display(spark.sql("DESCRIBE HISTORY orders_delta").select(
"version", "timestamp", "operation", "operationMetrics"))
ACID Transactions
By the end: explain the four ACID guarantees and see atomicity and concurrency rules at work.
- 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
# 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_errorfails the write while it runs. The rows before it are not kept
Managed vs External Tables
By the end: choose between managed and external tables and say what `DROP TABLE` does to each.
- Databricks calls managed tables the default and recommended type
- Managed:
DROP TABLEremoves data after a recovery window (7 days by default).UNDROP TABLEworks 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
DESCRIBE EXTENDED orders_delta
- Look for
Type(MANAGED) andLocation
On Free Edition
You use managed tables. Custom storage locations aren't supported, so external tables are explanation only.
Delta Table Creation and Management
By the end: create Delta tables with SQL and DataFrames and manage them with `ALTER TABLE`.
%sql
CREATE TABLE IF NOT EXISTS customers_delta (
customer_id INT NOT NULL,
name STRING,
country STRING,
signup_date DATE
)
COMMENT 'Practice customers'
# 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 TABLEkeeps the table's history.DROP+CREATEdoesn'tNOT NULLandCHECKconstraints are enforced on every write
%sql
ALTER TABLE orders_delta ADD CONSTRAINT qty_positive CHECK (quantity > 0 OR quantity IS NULL)
UPDATE, DELETE, and MERGE
By the end: change rows with UPDATE, DELETE and MERGE and use MERGE for upserts.
%sql
UPDATE orders_delta SET status = 'cancelled' WHERE order_id = 1
%sql
DELETE FROM orders_delta WHERE quantity IS NULL
- The 15 null-quantity rows are gone: count is now 1985
Upsert with MERGE:
%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
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(orUPDATE) 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.
Schema Enforcement and Evolution
By the end: see schema enforcement reject a bad write and use schema evolution when you mean it.
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:
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 MERGEcan 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.
Time Travel and Table History
By the end: query earlier versions of a table and restore one after a mistake.
%sql
DESCRIBE HISTORY orders_delta
%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
RESTORE TABLE orders_delta TO VERSION AS OF 0
- Needs
MODIFYon the table. The table is back to 2000 rows RESTOREis itself a new commit: history isn't erased- It fails if
VACUUMalready 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.
VACUUM and Data Retention
By the end: preview and run VACUUM safely and explain how retention settings affect time travel.
%sql
VACUUM orders_delta DRY RUN
DRY RUNlists 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
VACUUMfor managed tables
%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.
OPTIMIZE and File Compaction
By the end: compact small files with OPTIMIZE and explain why file layout matters.
%sql
OPTIMIZE orders_delta
- Output shows files removed and added
- Each
UPDATE,MERGEor 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
DESCRIBE DETAIL orders_delta
- Compare
numFilesbefore and after
Let the platform do it
Prefer predictive optimization on managed tables. Run OPTIMIZE by hand for external tables or one-off cleanups.
Liquid Clustering
By the end: create a liquid clustered table and choose clustering keys.
- Databricks recommends liquid clustering for all new tables
- It replaces partitioning and
ZORDER. They can't be combined with it
%sql
CREATE OR REPLACE TABLE orders_clustered
CLUSTER BY (customer_id)
AS SELECT * FROM orders
%sql
OPTIMIZE orders_clustered
- Clustering happens when
OPTIMIZEruns (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.
Change Data Feed (CDF)
By the end: read row-level changes from a table with change data feed.
%sql
CREATE OR REPLACE TABLE orders_cdf
TBLPROPERTIES (delta.enableChangeDataFeed = true)
AS SELECT * FROM orders WHERE order_id <= 5
%sql
UPDATE orders_cdf SET quantity = 9 WHERE order_id = 1
%sql
DELETE FROM orders_cdf WHERE order_id = 2
%sql
SELECT order_id, quantity, _change_type, _commit_version
FROM table_changes('orders_cdf', 0)
ORDER BY _commit_version, order_id
- Version 0: five
insertrows. Version 1: a pre-image and post-image of order 1. Version 2: adeleteof order 2 - Also:
_commit_versionand_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.
Delta Table Maintenance
By the end: run a routine maintenance pass on a table and know what runs automatically.
%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:
VACUUMwith at least 7 days retention - Statistics: kept current so queries can skip files
- Retention: set
logRetentionDurationanddeletedFileRetentionDurationon purpose
Clean up the demo tables:
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.
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
RESTOREfor short-term recovery OPTIMIZE,VACUUM, liquid clustering and change data feed- Managed tables get maintenance for free