DatabricksZero to Advanced

Declare the tables, let Databricks run the pipeline. You write what each table should contain. Lakeflow works out order, incremental processing, retries and monitoring.

  • Product name: Lakeflow Spark Declarative Pipelines (earlier "Delta Live Tables", DLT). Code uses from pyspark import pipelines as dp
  • Pipeline code is files in a pipeline, not a notebook you run cell by cell. The sections below show the file contents
  • Free Edition allows one active pipeline at a time. Reuse a single pipeline and add files to it
  • Topics 4–9 build one pipeline: add each snippet to a file in transformations, then run the whole pipeline (datasets can be defined in any order)
  • Run this setup cell in a normal notebook first: it lands two batches of JSON files and a small CDC feed
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(f"CREATE SCHEMA IF NOT EXISTS {catalog}.dbx_pipeline")

landing = f"/Volumes/{catalog}/dbx_course/raw/pipeline_landing/orders"

def land(batch, lo, hi):
    (spark.table("orders").filter(F.col("order_id").between(lo, hi))
          .coalesce(1).write.mode("overwrite").json(f"{landing}/batch_{batch}"))

land(1, 1, 1000)
print(landing)

# a small change feed for customers (used in topic 9)
spark.sql("""
  CREATE OR REPLACE TABLE customers_feed AS
  SELECT customer_id, name, country, 'INSERT' AS operation, 1 AS seq FROM customers
""")
9.01Concept

Introduction to Lakeflow

By the end: say what Lakeflow pipelines are and when to use one.

Your codetables and queriesPipelineplans, orders, runsTablesin Unity Catalog
You declare the result. The pipeline decides how to produce it.
  • A pipeline = a set of datasets (tables, views) plus the queries that fill them
  • It handles order (from dependencies), parallelism, retries, checkpoints and incremental processing
  • Writes managed tables in Unity Catalog. Runs on serverless compute by default
Use a pipeline when Use a plain job when
Layered ETL (bronze, silver, gold) One-off or very custom logic
You want built-in quality rules and CDC You call external systems and APIs a lot
Incremental load without writing checkpoint code You need full control of every write

Names

Older material says DLT / Delta Live Tables. Same product line, now Lakeflow Spark Declarative Pipelines. Python moved from import dlt to from pyspark import pipelines as dp.

9.02Concept

Spark Declarative Pipelines

By the end: list the dataset types and what each one is for.

Streaming tableappend-only, processes each new row onceingestion, bronze, silverMaterialized viewresult of a query, kept up to datejoins, aggregates, goldViewnot storedsteps inside the pipeline
Streaming table Materialized view
Source stream (new rows only) batch query (whole result)
Updated by processing new data refresh (incremental when possible)
Python @dp.table @dp.materialized_view
SQL CREATE STREAMING TABLE CREATE MATERIALIZED VIEW
  • Flow = one query that writes into a table. A table has a default flow
  • Sink = a streaming target outside the pipeline (Delta, Kafka, Event Hubs)
  • Each dataset is defined once in a workspace. Exception: streaming tables can take several append flows (topic 7)

Remember

Streaming table for new data coming in. Materialized view for a result derived from other tables.

9.03Hands-on on Free Edition

Lakeflow Pipelines Editor

By the end: create a pipeline in the Lakeflow Pipelines Editor and run it.

New ETL pipelineName + schemaCode in transformationsDry run, Run
The editor is an IDE for pipelines.

Create it:

  1. New → ETL pipeline
  2. Name it dbx_orders. Default catalog: your catalog. Default schema: dbx_pipeline
  3. Choose Python (or SQL) for the starter file in transformations
  4. Leave compute as serverless (the default)
Folder Holds
transformations Source files that define tables
explorations Notebooks and queries. Not run by the pipeline
utilities Python modules you can import
  • Editor parts: file browser, code editor, pipeline graph, data preview, issues, event log
  • Dry run checks code and names without writing data
  • Run file updates only the tables in the open file. Run pipeline updates everything
  • Run the whole pipeline before you call it done: Run file can hide errors in other files

Develop with dry run

Dry run first, run pipeline second. Errors in names and columns show up without touching data.

9.04Hands-on on Free Edition

Streaming Tables

By the end: define a streaming table that ingests files with Auto Loader.

Put this in transformations/orders.py:

Python
from pyspark import pipelines as dp
from pyspark.sql import functions as F

@dp.table(comment="Raw orders as received")
def orders_bronze():
    return (
        spark.readStream.format("cloudFiles")
        .option("cloudFiles.format", "json")
        .option("cloudFiles.inferColumnTypes", "true")
        .option("pathGlobFilter", "*.json")
        .load(spark.conf.get("course.landing"))
        .withColumn("_ingested_at", F.current_timestamp())
    )

SQL equivalent in transformations/orders.sql:

SQL
CREATE OR REFRESH STREAMING TABLE orders_bronze
COMMENT 'Raw orders as received'
AS SELECT *, current_timestamp() AS _ingested_at
FROM STREAM read_files('${course.landing}', format => 'json');
  • The function returns a streaming DataFrame. Don't call collect(), count(), saveAsTable() or toTable() inside it
  • The table name is the function name (or name=)
  • Schema and checkpoints are managed for you: no schemaLocation or checkpointLocation
  • The path comes from a pipeline parameter, course.landing (topic 11)
  • @dp.table(...) options: comment, cluster_by, partition_cols, schema, table_properties

Dataset code runs more than once

The pipeline may evaluate your function several times. Keep it to the query. No writes, prints with side effects, or counting.

9.05Hands-on on Free Edition

Materialized Views

By the end: define a materialized view that stays up to date.

Python
@dp.materialized_view(comment="Revenue per day, valid completed or shipped orders")
def gold_daily_revenue():
    o = spark.read.table("orders_silver").filter("status IN ('completed', 'shipped')")
    p = spark.read.table("dbx_course.products")
    return (
        o.join(p, "product_id")
         .groupBy("order_date")
         .agg(F.count("*").alias("orders"),
              F.round(F.sum(F.col("quantity") * F.col("unit_price")), 2).alias("revenue"))
    )
SQL
CREATE MATERIALIZED VIEW gold_daily_revenue AS
SELECT order_date, COUNT(*) AS orders, ROUND(SUM(quantity * unit_price), 2) AS revenue
FROM orders_silver o JOIN dbx_course.products p USING (product_id)
WHERE status IN ('completed', 'shipped')
GROUP BY order_date;
  • Written as a batch query: spark.read.table, not readStream
  • Each update brings it in line with the sources, incrementally if Databricks can
  • Table names without a schema resolve to the pipeline's default catalog and schema
  • Other tables need a schema (dbx_course.products)
  • refresh_policy: auto (default), incremental, incremental_strict, full

Aggregates are materialized views

Streaming tables for append-only layers, materialized views for joins and aggregates. They recompute correctly when data is corrected.

9.06Concept

Declarative Transformations

By the end: describe the code as declarations and see the pipeline graph.

  • You write what: a dataset and the query that defines it
  • You don't write how: order, checkpoints, retries, upserts
  • Read another dataset by name. That one line creates a dependency edge
Python
@dp.table
def orders_silver():
    return (
        spark.readStream.table("orders_bronze")          # reads the table above
        .withColumn("status", F.lower(F.trim("status")))
        .withColumn("order_date", F.to_date("order_ts"))
        .dropDuplicates(["order_id"])
    )
orders_bronzeorders_silvergold_daily_revenue
The pipeline graph is built from the names your queries read.
  • Streaming reads use spark.readStream.table(...). Batch reads use spark.read.table(...)
  • Streaming dropDuplicates without a watermark keeps state for every key. Fine at this size, costly at scale (Module 11)
  • Use views for steps you don't want to publish (below)
  • No pivot() (needs the full data up front). Use conditional aggregation
9.07Hands-on on Free Edition

Pipeline Dependencies and Flows

By the end: wire datasets with views, private tables and multiple append flows.

Python
@dp.view                                     # not stored, only inside the pipeline
def valid_orders():
    return spark.readStream.table("orders_bronze").filter("quantity > 0")

@dp.table(private=True)                      # stored, but not published to the catalog
def orders_enriched():
    return spark.readStream.table("valid_orders")

Several sources into one table:

Python
dp.create_streaming_table("all_orders")

@dp.append_flow(target="all_orders", name="web")
def from_web():
    return spark.readStream.table("web_orders")

@dp.append_flow(target="all_orders", name="shop")
def from_shop():
    return spark.readStream.table("shop_orders")

@dp.append_flow(target="all_orders", name="history_backfill", once=True)
def backfill():
    return spark.read.table("old_orders")        # batch, loaded one time
  • A flow reads a source, applies logic, writes to a target
  • Append flows add new sources without a UNION
  • once=True loads history one time (batch source). It reruns only on a full refresh
  • The pipeline runs independent flows in parallel
9.08Hands-on on Free Edition

Data Quality Expectations

By the end: add data quality expectations and choose what happens on failure.

Python
@dp.table
@dp.expect("has_customer", "customer_id IS NOT NULL")             # keep row, log it
@dp.expect_or_drop("positive_qty", "quantity > 0")                # drop row, log it
@dp.expect_or_fail("has_order_id", "order_id IS NOT NULL")        # stop the update
def orders_clean():
    return spark.readStream.table("orders_silver")
SQL
CREATE OR REFRESH STREAMING TABLE orders_clean (
  CONSTRAINT has_customer EXPECT (customer_id IS NOT NULL),
  CONSTRAINT positive_qty EXPECT (quantity > 0) ON VIOLATION DROP ROW,
  CONSTRAINT has_order_id EXPECT (order_id IS NOT NULL) ON VIOLATION FAIL UPDATE
) AS SELECT * FROM STREAM orders_silver;
Decorator On violation
expect Keep the row, record the count
expect_or_drop Drop the row, record the count
expect_or_fail Stop the update
expect_all, expect_all_or_drop, expect_all_or_fail Same, with a dict of rules
Python
rules = {"has_customer": "customer_id IS NOT NULL", "has_ts": "order_ts IS NOT NULL"}

@dp.table
@dp.expect_all_or_drop(rules)
def orders_checked():
    return spark.readStream.table("orders_silver")
  • Decorators go after @dp.table and above the function
  • Descriptions must be unique per dataset
  • Counts of passed, failed and dropped rows go to the event log (topic 12)
  • Dropped rows vanish. To keep them, route failures to a quarantine table with the inverse condition

Start with warn, then tighten

Begin with expect to see failure rates. Move to drop or fail when you trust the rule.

9.09Hands-on on Free Edition

CDC Processing with Lakeflow

By the end: apply a change feed to a table with AUTO CDC, as SCD 1 or SCD 2.

Change feedinserts, updates, deletesAUTO CDCkeys + sequenceTarget tablecurrent state or history
Out-of-order events are put in the right order using the sequence column.
Python
@dp.view
def customer_changes():
    return spark.readStream.table("dbx_course.customers_feed")

dp.create_streaming_table("customers_current")

dp.create_auto_cdc_flow(
    target="customers_current",
    source="customer_changes",
    keys=["customer_id"],
    sequence_by="seq",
    apply_as_deletes=F.expr("operation = 'DELETE'"),
    except_column_list=["operation", "seq"],
    stored_as_scd_type=1,
)
SQL
CREATE OR REFRESH STREAMING TABLE customers_history;

CREATE FLOW customers_cdc AS AUTO CDC INTO customers_history
FROM STREAM(dbx_course.customers_feed)
KEYS (customer_id)
APPLY AS DELETE WHEN operation = 'DELETE'
SEQUENCE BY seq
COLUMNS * EXCEPT (operation, seq)
STORED AS SCD TYPE 2;
SCD type 1 SCD type 2
Result Latest row per key Every version
Extra columns none __START_AT, __END_AT
Current row all rows __END_AT IS NULL
Deletes row removed version ended
  • Add changes by inserting rows into customers_feed with a higher seq, then update the pipeline
  • sequence_by handles late and out-of-order events: the highest sequence wins
  • The source must be a stream. The target is a streaming table created first
  • This replaces the hand-written SCD 2 MERGE from Module 8.10
  • apply_changes() is the older name for create_auto_cdc_flow()

Free Edition

Run this as part of the one allowed pipeline. Add the file, do a dry run, then update.

9.10Concept

Incremental Refresh

By the end: understand when updates are incremental and when they are full.

Streaming tablereads only new source rowscheckpoint tracks progressMaterialized viewrecomputes only what changed, if possibleelse a full recomputeFull refreshclears and rebuildsuse rarely
  • Normal refresh: streaming tables process new records. Materialized views use an incremental refresh when cheaper
  • Full refresh: streaming tables clear data and checkpoints and reprocess the source. Source files that no longer exist are lost
  • Incremental refresh of materialized views needs serverless compute
  • Techniques seen in the event log: ROW_BASED, GROUP_AGGREGATE, WINDOW_FUNCTION, APPEND_ONLY, PARTITION_OVERWRITE, FULL_RECOMPUTE, NO_OP
  • Float SUM/AVG can force full recompute: cast to DECIMAL
  • WITH RECURSIVE and non-deterministic functions (except time functions in WHERE) fall back to full
SQL
-- which technique was used?
SELECT timestamp, message
FROM event_log(TABLE(<catalog>.dbx_pipeline.gold_daily_revenue))
WHERE event_type = 'planning_information'
ORDER BY timestamp DESC

Full refresh on a streaming table

It reprocesses from the source. If the source has aged out (old files, expired Kafka data), the rows are gone for good. Protect tables with pipelines.reset.allowed = false.

9.11Hands-on on Free Edition

Pipeline Parameters

By the end: pass values into a pipeline with parameters and configuration.

Set it in pipeline Settings → Configuration:

Key Value
course.landing /Volumes/<catalog>/dbx_course/raw/pipeline_landing/orders
course.min_qty 1

Read it:

Python
min_qty = int(spark.conf.get("course.min_qty"))      # always a string

@dp.table
def orders_valid():
    return spark.readStream.table("orders_bronze").filter(F.col("quantity") >= min_qty)
SQL
SELECT * FROM STREAM orders_bronze WHERE quantity >= ${course.min_qty};
  • Configuration keys are read with spark.conf.get("key") in Python and ${key} in SQL
  • Values are always strings: cast them
  • Use dotted names (course.min_qty) so they don't clash with Spark settings
  • Newer pipeline parameters are set per update and used with :name in SQL only, e.g. USE CATALOG IDENTIFIER(:source_catalog)
  • Per-environment values (dev, prod) belong here: same code, different configuration

No hard-coded paths

Paths, catalogs and thresholds are configuration. Then one pipeline definition serves every environment.

9.12Hands-on on Free Edition

Pipeline Monitoring and Event Logs

By the end: monitor an update and query the event log.

  • The pipeline page shows each update, its graph, row counts and data quality numbers
  • Click a table in the graph for flow metrics, expectation results and the schema
  • The event log is a Delta table with every event of every update
SQL
-- by pipeline id (copy it from the pipeline settings)
SELECT timestamp, event_type, message
FROM event_log('<pipeline-id>')
ORDER BY timestamp DESC
LIMIT 20
event_type What it tells you
create_update, update_progress Update started, state changes
flow_progress Per-flow metrics, data quality counts
flow_definition Which datasets feed which (lineage)
planning_information Refresh technique used
user_action Who created or started the pipeline
  • Expectation results are in details:flow_progress.data_quality.expectations: passed_records, failed_records; dropped_records is next to it
  • By default the log is a hidden table, readable by the run-as user. Publish it to a named table in Advanced settings → event log
  • Put a view over the published log before granting access to others

Don't delete the log

Deleting the event log table or its schema can make later updates fail.

9.13Concept

Pipeline Recovery and Troubleshooting

By the end: recover from a failed update and find the cause.

  • Read the issues panel and the event log error first: it names the failing flow and the cause
  • Refresh failed tables reruns the failed tables and what depends on them
  • Select tables for refresh reruns a chosen subset. Excluded tables aren't refreshed, even if downstream
  • Full refresh selection reprocesses data for chosen tables
  • Validate (dry run) finds name and column errors without writing data
Symptom Likely cause
Table not found Name resolved in the wrong default catalog or schema
Dataset defined twice Same table name in two files or pipelines
expect_or_fail stopped the update A rule failed. Check data quality in the log
Schema change error from a source Auto Loader evolution (Module 7.7). Rerun or full refresh
Works in Run file, fails in Run pipeline Another file has an error
  • Development runs (UI) reuse compute and don't retry, so errors show fast
  • Job-triggered runs retry and restart for recoverable errors: use them for production (Module 10)

Fix forward

Fix the code, then refresh only what failed. Use full refresh as the exception.

Recap

What you can do now

  • Pipelines, dataset types, the editor and the dependency graph
  • Streaming tables with Auto Loader, materialized views and flows
  • Expectations, AUTO CDC (SCD 1 and 2) and incremental refresh
  • Parameters, event log monitoring and recovery