On this page
Recompute the recent rows of a streaming table¶
Some data keeps changing for a while after it lands. Orders are corrected, cancelled and re-priced for a few days after they are placed, late records arrive, and an upstream system reprocesses yesterday. After that the rows settle. You want a table that keeps the whole history, where the recent days are recomputed from the source on every refresh and the older days are left alone.
That is what a Databricks REPLACE WHERE flow does, and mode: replace_where
generates one.
Recompute a window, keep the history¶
You give the write a SQL predicate, such as “orders from the last 7 days”. On every refresh the REPLACE WHERE flow deletes the target rows that match the predicate and recomputes them by re-running the source query for that range. Rows that don’t match are left exactly as they are. The source is read in batch, so you process new rows without managing streaming concepts such as watermarks.
A worked example. The predicate is order_date >= date_add(current_date(),
-7), and the refresh runs on 2026-06-10, so it covers 2026-06-03 onwards:
BEFORE — target rows
order_id order_date amount
101 2026-05-20 40
205 2026-06-05 12
206 2026-06-06 30
207 2026-06-08 18
SOURCE — what the bronze table holds now
order_id order_date amount
101 2026-05-20 45 # corrected, but outside the predicate
205 2026-06-05 12
207 2026-06-08 20 # corrected
208 2026-06-10 25 # new
# 206 was deleted upstream
AFTER — target rows, via REPLACE WHERE
order_id order_date amount
101 2026-05-20 40 # untouched: outside the predicate
205 2026-06-05 12
207 2026-06-08 20
208 2026-06-10 25
# 206 is gone: the rows inside the predicate were recomputed from the source
Inside the predicate the target now matches the source: 207 was corrected,
208 was added and 206 was deleted, with no delete event needed. Outside
it, nothing changed, including the correction to order 101. That is the
trade-off: once a row is older than the window, the table stops following the
source for it. Pick a window that covers how long your data keeps changing.
The batch read is what sets it apart from its streaming sibling,
mode: replace:
Aspect |
|
|
|---|---|---|
Rows replaced |
Every target row that matches a predicate |
The target rows of each key that a batch carries, when the batch’s
|
Source read |
Batch; the predicate’s range is recomputed |
Stream; the new rows are applied |
Rows outside |
Never touched |
Keys absent from the batch are never touched |
Before you start¶
You need a Lakehouse Plumber project (see the Get Started course for
lhp init) and a source table. This guide reads a bronze orders table
that another pipeline lands, with an order_date column for the predicate.
Three rules are worth knowing before you write it:
The source is read in batch. A streaming source is rejected.
The target is served by this single flow. No other write action can use its table, and Lakehouse Plumber always creates the table for you.
Databricks refreshes the flow incrementally only on serverless compute.
Declare the REPLACE WHERE write¶
The flowgroup has two actions: a load that reads the orders in batch into a
view, and a write whose write_target is a streaming_table in
mode: replace_where. Create pipelines/orders_recent.yaml:
# Keep a silver table of orders whose last 7 days are recomputed on every refresh.
# Action chain: load (delta, batch read of the bronze orders) -> write (streaming_table, mode: replace_where).
#
# On each refresh the REPLACE WHERE flow deletes the target rows that match the
# predicate (the last 7 days by order_date) and recomputes them from the source.
# Rows outside the predicate (older orders) are left as they are.
pipeline: silver_orders
flowgroup: orders_recent
actions:
# 1. Load: a batch read of the bronze orders table. A REPLACE WHERE flow reads
# its source in batch, so this load must not stream.
- name: load_orders
type: load
readMode: batch
source:
type: delta
catalog: "${catalog}"
schema: "${bronze_schema}"
table: orders
target: v_orders
# 2. Write: recompute the last 7 days of orders on every refresh. The change
# data feed is turned on so a downstream pipeline can follow the table
# (see pipelines/orders_recent_mirror.yaml).
- name: write_orders_recent
type: write
source: v_orders
write_target:
type: streaming_table
catalog: "${catalog}"
schema: "${silver_schema}"
table: orders_recent
mode: replace_where
table_properties:
delta.enableChangeDataFeed: "true"
replace_where_config:
replace_where: "order_date >= date_add(current_date(), -7)"
readMode: batchon the load makesv_ordersa batch view. Adeltaload reads in batch by default; it is spelled out here because a streaming view would be rejected.mode: replace_whereis the switch.replace_where_config.replace_whereis the predicate, a SQL string that Lakehouse Plumber writes into the generated code as it is.${...}tokens in it are resolved per environment first.table_propertiesturns on the change data feed. Nothing in this pipeline needs it; a downstream pipeline uses it at the end of this guide.
Generate the pipeline¶
Validate first, then generate:
$ lhp validate --env dev
✓ discover (0.00s)
✓ preflight (0.00s)
✓ silver_orders 0 files
✓ validate (0.29s)
1 validated · 0.3s
$ lhp generate --env dev
✓ discover (0.00s)
✓ preflight (0.00s)
✓ silver_orders 1 file
✓ generate (0.31s)
✓ format (0.01s)
✓ monitoring (0.00s)
1 pipeline generated · 1 file · 0.3s
validate resolves the tokens and checks the write: that the
replace_where_config block holds a non-blank predicate, that the view it
reads is a batch view, and that no other write action in the pipeline targets
the table.
Read what Lakehouse Plumber wrote¶
Open generated/dev/silver_orders/orders_recent.py:
# Generated by LakehousePlumber
# Pipeline: silver_orders
# FlowGroup: orders_recent
from pyspark import pipelines as dp
# Pipeline Configuration
PIPELINE_ID = "silver_orders"
FLOWGROUP_ID = "orders_recent"
# ============================================================================
# SOURCE VIEWS
# ============================================================================
@dp.temporary_view()
def v_orders():
"""Delta source: dev_catalog.bronze.orders"""
df = spark.read.table("dev_catalog.bronze.orders")
return df
# ============================================================================
# TARGET TABLES
# ============================================================================
@dp.table(
name="dev_catalog.silver.orders_recent",
comment="Streaming table: orders_recent",
table_properties={
"delta.enableChangeDataFeed": "true",
"pipelines.reset.allowed": "false",
},
replace_where="order_date >= date_add(current_date(), -7)",
)
def orders_recent():
"""Replace-where flow to dev_catalog.silver.orders_recent"""
df = spark.read.table("v_orders")
return df
The target is one @dp.table statement, which Databricks reads as both the
streaming table and its REPLACE WHERE flow. There is no
dp.create_streaming_table and no flow decorator. The predicate is the
replace_where= argument, exactly as you wrote it, and the flow reads its
view with spark.read. table_properties holds your change-data-feed
setting and one more that you did not write, pipelines.reset.allowed.
Keep the history safe¶
A full refresh of a REPLACE WHERE flow re-runs the source query for the
current predicate only. Without protection it would delete every row older
than 7 days. Lakehouse Plumber therefore sets pipelines.reset.allowed to
"false" on every REPLACE WHERE table, unless you set it yourself:
A full refresh of the whole pipeline still runs. The table keeps its older rows, and only the last 7 days are recomputed.
A full refresh that selects this table fails the update, because the table is not resettable.
When you do want to start the table over, see the reset recipe in the write reference.
Keep refreshes incremental¶
Databricks can refresh the flow incrementally, processing only what changed inside the predicate instead of recomputing all of it. That needs serverless compute, a deterministic predicate on columns of the source table, and a deterministic query. Two things in your YAML affect it:
The predicate. A moving lower bound, like the one above, stays eligible. Widening the predicate costs one full recomputation of the window, and so can a moving upper bound.
Operational metadata. A metadata column computed with a non-deterministic function, such as
F.current_timestamp(), makes every refresh recompute the whole window.lhp validatewarns about it withLHP-VAL-073.
The write reference explains how to check in the event log which way each refresh ran.
Read the table downstream¶
Every refresh rewrites rows inside the predicate, so the table is not an append-only source. Suppose a gold pipeline streams it, as it would any other table:
pipeline: gold_orders
flowgroup: orders_recent_copy
actions:
- name: load_orders_recent
type: load
readMode: stream
source:
type: delta
catalog: "${catalog}"
schema: "${silver_schema}"
table: orders_recent
target: v_orders_recent
- name: write_orders_recent_copy
type: write
source: v_orders_recent
write_target:
type: streaming_table
catalog: "${catalog}"
schema: "${gold_schema}"
table: orders_recent_copy
On Databricks that stream would fail as soon as a refresh rewrote a row, and
with skipChangeCommits it would silently skip the rewritten rows instead.
lhp validate stops it first:
$ lhp validate --env dev
✓ discover (0.00s)
✓ preflight (0.00s)
✗ gold_orders LHP-VAL-072 Streaming read of a REPLACE WHERE table
✓ silver_orders 0 files
✗ validate (0.32s)
1 validated · 1 failed · 0.3s
gold_orders LHP-VAL-072 Streaming read of a REPLACE WHERE table
Re-run with details: lhp validate --env dev --show-details
The check covers the whole run, so it catches a reader in another pipeline.
In the --show-details panel, the context line names only the pipeline. The
message names the read, and the write behind it, as
pipeline.flowgroup.action:
gold_orders.orders_recent_copy.load_orders_recent streams from 'dev_catalog.silver.orders_recent', a REPLACE WHERE table written by silver_orders.orders_recent.write_orders_recent. Each refresh rewrites rows, which a streaming read cannot follow (skipChangeCommits silently drops data). Use readMode: batch, or stream its change feed with options.readChangeFeed: "true" (see docs).
A batch read fixes it. To keep a streaming copy instead, follow the table’s
change data feed, which records each rewrite as delete and insert
events, and apply those events with AUTO CDC. Replace the file with
pipelines/orders_recent_mirror.yaml:
# Mirror the REPLACE WHERE table silver.orders_recent into gold, through its change data feed.
# Action chain: load (delta, change-feed stream of the REPLACE WHERE table) -> write (streaming_table, mode: cdc).
#
# A plain streaming read of a REPLACE WHERE table fails validation (LHP-VAL-072):
# every refresh rewrites rows, which a stream cannot follow. The change feed
# carries each rewrite as delete and insert events, which AUTO CDC applies.
pipeline: gold_orders
flowgroup: orders_recent_mirror
actions:
# 1. Load: stream the change feed of the REPLACE WHERE table. The where_clause
# drops update pre-images, so a stale pre-image can never win the tie-break.
- name: load_orders_recent_changes
type: load
readMode: stream
source:
type: delta
catalog: "${catalog}"
schema: "${silver_schema}"
table: orders_recent
options:
readChangeFeed: "true"
where_clause:
- "_change_type != 'update_preimage'"
target: v_orders_recent_changes
# 2. Write: apply the change events onto order_id. Within one commit a key can
# have both a delete and an insert; sorting by _change_type after
# _commit_version makes the insert win.
- name: write_orders_recent_mirror
type: write
source: v_orders_recent_changes
write_target:
type: streaming_table
catalog: "${catalog}"
schema: "${gold_schema}"
table: orders_recent_mirror
mode: cdc
cdc_config:
keys: ["order_id"]
sequence_by: ["_commit_version", "_change_type"]
scd_type: 1
apply_as_deletes: "_change_type = 'delete'"
except_column_list: ["_change_type", "_commit_version", "_commit_timestamp"]
readChangeFeed: "true"streams the table’s change events instead of its rows. Lakehouse Plumber does not report a change-feed read asLHP-VAL-072.sequence_by: ["_commit_version", "_change_type"]is required. A rewritten row has adeleteand aninsertin the same commit. Ordered by_commit_versionalone, AUTO CDC can keep thedeleteand drop the row from the copy without any error; adding_change_typemakes theinsertwin.The
where_clausedrops update pre-images, andexcept_column_listkeeps the change-feed columns out of the copy.
Validate and generate again; both pipelines pass:
$ lhp validate --env dev
✓ discover (0.00s)
✓ preflight (0.00s)
✓ gold_orders 0 files
✓ silver_orders 0 files
✓ validate (0.32s)
2 validated · 0.3s
$ lhp generate --env dev
✓ discover (0.00s)
✓ preflight (0.00s)
✓ gold_orders 1 file
✓ silver_orders 1 file
✓ generate (0.34s)
✓ format (0.01s)
✓ monitoring (0.00s)
2 pipelines generated · 2 files · 0.3s
The mirror’s flow is an AUTO CDC flow over the change feed:
# CDC flow: f_orders_recent_mirror
dp.create_auto_cdc_flow(
target="dev_catalog.gold.orders_recent_mirror",
source="v_orders_recent_changes",
name="f_orders_recent_mirror",
keys=["order_id"],
sequence_by=struct("_commit_version", "_change_type"),
stored_as_scd_type=1,
except_column_list=["_change_type", "_commit_version", "_commit_timestamp"],
apply_as_deletes="_change_type = 'delete'",
)
What you just did¶
A short replace_where_config block compiled to one @dp.table with a
replace_where= predicate, a batch read and a reset guard. You declared
which rows to keep recomputing; Databricks deletes and recomputes them on
each refresh, and Lakehouse Plumber checked the setup around it: a batch
source, a single flow, and downstream pipelines that follow the table through
its change feed instead of streaming its rows.
What’s next¶
Replace by key from a stream instead. When the source resends complete sets of rows per key, use
mode: replace(REPLACE USING), covered in Replace all rows that match a grouping key.See the field reference. The rules, the reset recipe, the incremental refresh conditions,
LHP-VAL-072andLHP-VAL-073and backfills are described in the write action reference.
Syntax and options¶
See Replace rows matching a predicate for all supported settings and defaults.