Delta Live Tables (DLT) is most useful when you want automated, reliable, and declarative ETL pipelines with built-in data quality and orchestration. Below are the primary scenarios where DLT is the best choice.
Delta Live Tables (DLT) is Databricks' declarative ETL framework, renamed Lakeflow Declarative Pipelines in 2025 with the same Python and SQL concepts. Instead of writing jobs that read, transform, write and schedule each step, you declare the tables you want and the query that defines each one. The runtime builds the dependency graph, provisions compute, processes data incrementally, enforces data-quality rules, and maintains the tables. The list above summarises when that trade is worth making; the sections below show how it works and what the code looks like.
A pipeline contains two kinds of datasets. A streaming table processes each input record once and is appended to incrementally; it is the natural choice for ingestion with Auto Loader or Kafka. A materialized view stores the result of a query and is refreshed to reflect its sources, recomputing incrementally where possible. When an update runs, DLT resolves references between datasets into a DAG, starts the upstream tables first, and records each step in an event log that also powers the lineage graph in the UI. Pipelines run in triggered mode (process available data then stop) or continuous mode (keep running for low latency).
Expectations are named boolean constraints attached to a dataset. The default action keeps violating rows and counts them; expect_or_drop removes them; expect_or_fail stops the update so bad data never lands. Because results go to the event log, teams can chart quality over time and alert on regressions without a separate validation framework.
import dlt
from pyspark.sql.functions import col, count, to_date
@dlt.table(comment="Raw JSON events from cloud storage")
def bronze_events():
return (spark.readStream.format("cloudFiles")
.option("cloudFiles.format", "json")
.load("/Volumes/main/raw/events/"))
@dlt.table
@dlt.expect_or_drop("has_user", "user_id IS NOT NULL")
@dlt.expect("recent", "event_ts > '2020-01-01'")
def silver_events():
return dlt.read_stream("bronze_events").withColumn("event_date", to_date(col("event_ts")))
@dlt.table(comment="Daily counts for dashboards")
def gold_daily():
return dlt.read("silver_events").groupBy("event_date").agg(count("*").alias("events"))
# SCD Type 2 customer dimension from a CDC feed
dlt.create_streaming_table("dim_customer")
dlt.apply_changes(
target="dim_customer", source="customers_cdc",
keys=["customer_id"], sequence_by=col("updated_at"),
stored_as_scd_type=2)
-- Equivalent SQL for the silver step
CREATE OR REFRESH STREAMING TABLE silver_events (
CONSTRAINT has_user EXPECT (user_id IS NOT NULL) ON VIOLATION DROP ROW
) AS SELECT *, to_date(event_ts) AS event_date FROM STREAM(bronze_events);
The apply_changes call replaces a hand-written MERGE: it orders changes by updated_at, closes the previous version of a changed customer with an end timestamp, and inserts the new version.
| Aspect | DLT pipeline | Notebook + Workflows job |
|---|---|---|
| Style | Declarative | Imperative |
| Dependencies | Inferred from code | Configured task by task |
| Data quality | Built-in expectations | Custom code |
| Incremental processing | Built in | Manual checkpoints and MERGE |
| Flexibility | Constrained to pipeline semantics | Anything Spark can do |
| Best for | Standard medallion ETL, CDC | Complex custom logic, ML jobs |