This is an automated email from the ASF dual-hosted git repository.
fhueske pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new 8fd8d9298ab [FLINK-40131][docs] Document LATERAL SNAPSHOT join (#28737)
8fd8d9298ab is described below
commit 8fd8d9298abf42c96f2023ff50fb6f5a3f75979b
Author: Fabian Hueske <[email protected]>
AuthorDate: Tue Jul 21 18:50:07 2026 +0200
[FLINK-40131][docs] Document LATERAL SNAPSHOT join (#28737)
* [FLINK-40131][docs] Document LATERAL SNAPSHOT join
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
.../docs/sql/functions/built-in-functions.md | 18 ++++
.../content.zh/docs/sql/reference/queries/joins.md | 107 ++++++++++++++++++++
.../docs/sql/functions/built-in-functions.md | 18 ++++
docs/content/docs/sql/reference/queries/joins.md | 111 +++++++++++++++++++++
4 files changed, 254 insertions(+)
diff --git a/docs/content.zh/docs/sql/functions/built-in-functions.md
b/docs/content.zh/docs/sql/functions/built-in-functions.md
index 4aaf786e860..6165b239fcb 100644
--- a/docs/content.zh/docs/sql/functions/built-in-functions.md
+++ b/docs/content.zh/docs/sql/functions/built-in-functions.md
@@ -132,6 +132,24 @@ JSON 函数使用符合 ISO/IEC TR 19075-6 SQL标准的 JSON 路径表达式。
{{< sql_functions_zh "bitmapagg" >}}
+Table Functions
+---------------
+
+Table functions take zero, one, or more values as input and return multiple
rows (a table) as the result. Most built-in table functions take a table as an
input argument.
+Table functions can be used in two ways: as stand-alone inputs, where they are
invoked just once, or in a `LATERAL` context, where they are invoked for each
row of an outer table.
+
+| Function | Description
|
+|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `TUMBLE(data => TABLE t, ...)` | Assigns each row of the `data`
table to a tumbling window specified by additional window columns
(`window_start`, `window_end`, `window_time`). See [Window TVF]({{< ref
"docs/sql/reference/queries/window-tvf" >}}#tumble) for the full list of
arguments, semantics, and usage. |
+| `HOP(data => TABLE t, ...)` | Assigns each row of the `data`
table to a hopping window specified by additional window columns
(`window_start`, `window_end`, `window_time`). See [Window TVF]({{< ref
"docs/sql/reference/queries/window-tvf" >}}#hop) for the full list of
arguments, semantics, and usage. |
+| `CUMULATE(data => TABLE t, ...)` | Assigns each row of the `data`
table to a cumulating window specified by additional window columns
(`window_start`, `window_end`, `window_time`). See [Window TVF]({{< ref
"docs/sql/reference/queries/window-tvf" >}}#cumulate) for the full list of
arguments, semantics, and usage. |
+| `SESSION(data => TABLE t, ...)` | Assigns each row of the `data`
table to a session window specified by additional window columns
(`window_start`, `window_end`, `window_time`). See [Window TVF]({{< ref
"docs/sql/reference/queries/window-tvf" >}}#session) for the full list of
arguments, semantics, and usage. |
+| `FROM_CHANGELOG(input => TABLE t [, ...])` | Converts an append-only table
with an explicit operation column into a dynamic table. See Changelog
Conversion for the full list of arguments, semantics, and usage.
|
+| `TO_CHANGELOG(input => TABLE t [, ...])` | Converts a dynamic table into
an append-only table with an explicit operation column. See Changelog
Conversion for the full list of arguments, semantics, and usage.
|
+| `SNAPSHOT(input => TABLE t [, ...])` | Returns the current state of a
dynamic table `t`. `SNAPSHOT` can only be used in a `LATERAL` context and not
as a stand-alone table function. See [LATERAL SNAPSHOT join]({{< ref
"docs/sql/reference/queries/joins" >}}#lateral-snapshot-join) for the full list
of arguments, the join semantics, and usage. |
+
+To implement your own table functions, see [user-defined table functions]({{<
ref "docs/dev/table/functions/udfs" >}}#table-functions).
+
时间间隔单位和时间点单位标识符
---------------------------------------
diff --git a/docs/content.zh/docs/sql/reference/queries/joins.md
b/docs/content.zh/docs/sql/reference/queries/joins.md
index a1e1db2caab..a97312e9100 100644
--- a/docs/content.zh/docs/sql/reference/queries/joins.md
+++ b/docs/content.zh/docs/sql/reference/queries/joins.md
@@ -295,6 +295,113 @@ WHERE
- SQL 中可以定义 temporal table DDL,但不能定义 temporal table 函数;
- temporal table DDL 和 temporal table function 都支持 temporal join 版本表,但只有
temporal table function 可以 temporal join 任何表/视图的最新版本(即"处理时间 Temporal Join")。
+LATERAL SNAPSHOT Join
+--------------
+
+{{< label Streaming >}} {{< label Batch >}}
+
+A `LATERAL SNAPSHOT` join is a *stream enrichment* join that augments an
append-only table with the current state of an updating table.
+As in the [temporal joins](#temporal-joins), the enriched (left) input is
called the *probe side* and the enriching (right) input is called the *build
side*.
+Every probe-side row is joined with the build-side state that is current at
the time the row is processed.
+
+For example, the following query enriches an append-only stream of `orders`
(the probe side) with the conversion rate from an updating `currency_rates`
table (the build side) that is current when the order is processed:
+
+```sql
+-- probe side: append-only stream of orders
+-- order_id | currency | amount | order_time
+-- ---------+----------+--------+-----------
+-- 1 | EUR | 10 | 10:15
+-- 2 | EUR | 10 | 10:31
+-- 3 | USD | 20 | 11:00
+
+-- build side: updating currency rates (upsert on currency)
+-- currency | rate | update_time
+-- ---------+------+------------
+-- EUR | 1.1 | 10:00
+-- USD | 1.0 | 10:00
+-- EUR | 1.2 | 10:30
+
+SELECT o.order_id, o.currency, o.amount, r.rate
+FROM orders AS o
+JOIN LATERAL TABLE(SNAPSHOT(input => TABLE currency_rates)) AS r
+ON o.currency = r.currency;
+
+order_id currency amount rate
+======== ======== ====== ====
+ 1 EUR 10 1.1 -- rate as of 10:00
+ 2 EUR 10 1.2 -- rate as of 10:30
+ 3 USD 20 1.0 -- rate as of 10:00
+```
+
+*Important:* The `LATERAL SNAPSHOT` join is non-deterministic. The order with
`order_id = 2` could also have been joined with the `10:00` version of `EUR`.
The result depends on the order in which the operator processes its inputs,
which cannot be controlled.
+
+**When to use it**
+
+The `LATERAL SNAPSHOT` join is designed for enrichment scenarios where the
other temporal joins are a poor fit:
+
+- **The build side does not receive continuous updates.** An event-time
temporal join only emits a joined row once the combined watermark of both
inputs has passed the event time of the probe-side row. A build side that does
not continuously produce records, and therefore does not advance its watermark,
stalls the join and lets probe-side state accumulate. A `LATERAL SNAPSHOT` join
keeps making progress even when the build side is idle.
+- **Low latency is required.** An event-time temporal join holds back
probe-side rows until the watermark catches up, which adds latency. After the
initial load phase, the `LATERAL SNAPSHOT` join immediately joins a probe-side
row when it arrives.
+- **The build side has no primary key.** Event-time and processing-time
temporal joins require the build side to have a primary key that appears in the
equi-join condition. A `LATERAL SNAPSHOT` join has no such requirement; neither
the probe side nor the build side needs a primary key.
+
+**How the join works**
+
+A `LATERAL SNAPSHOT` join operates in two phases to avoid joining probe-side
rows against an incomplete build side: it first loads the build side up to a
well-defined point in time (the *load phase*) before it starts joining (the
*join phase*).
+
+During the *load phase*, the operator accumulates the build-side changes into
state until the load-completion condition is met, without emitting any results
yet. Probe-side rows that arrive during the load phase are buffered. The load
phase completes when one of the following occurs:
+
+- the build-side watermark reaches a configured `load_completed_time`. This
time is either explicitly set by the user (`load_completed_condition =>
'user_time'`) or automatically set to the wall-clock time when the query is
compiled (`load_completed_condition => 'compile_time'`), or
+- as a fallback, the `load_completed_idle_timeout` elapses in processing time
without the build-side watermark advancing (which handles build sides that
become idle during start-up).
+
+When the load phase completes, the operator transitions to the *join phase*:
all buffered probe-side rows are joined against the current build-side state
and emitted.
+From then on, each probe-side row is joined and emitted immediately against
the build-side state that is current at that moment.
+Build-side updates continue to be applied to the state and become visible to
subsequent probe-side rows.
+
+The load phase is what distinguishes a `LATERAL SNAPSHOT` join from the
[processing-time temporal join](#processing-time-temporal-join), which has been
disabled for Flink SQL. The processing-time temporal join starts joining
immediately at query start, so early probe-side rows are joined against
whatever build-side data happens to have been loaded so far, producing missing
or stale results that depend on the order in which the inputs are read. By
first loading the build side, a `LATERAL [...]
+
+**Syntax**
+
+The build side is wrapped in the `SNAPSHOT` table function inside a `LATERAL
TABLE` clause. The outer (probe-side) table must be an append-only table.
+Both `INNER JOIN` and `LEFT [OUTER] JOIN` are supported. The join requires at
least one conjunctive equality predicate; additional non-equi predicates are
allowed in the `ON` clause.
+
+```sql
+SELECT [column_list]
+FROM probe_table
+[LEFT] JOIN LATERAL TABLE(
+ SNAPSHOT(
+ input => TABLE build_table,
+ [ load_completed_condition => <'compile_time' | 'user_time'>, ]
+ [ load_completed_time => <timestamp_ltz>, ]
+ [ load_completed_idle_timeout => <interval>, ]
+ [ state_ttl => <interval> ])) AS s
+ON probe_table.col = s.col
+```
+
+The `SNAPSHOT` function accepts the following arguments:
+
+| Argument | Type | Required | Description
|
+| --- | --- | ---
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `input` | TABLE | yes | The build-side table. It may use any changelog mode
(inserts, updates, and deletes). In streaming mode it must declare a
[watermark]({{< ref "docs/concepts/sql-table-concepts/time_attributes"
>}}#event-time).
|
+| `load_completed_condition` | STRING | no | Determines when the initial load
phase completes. One of `'compile_time'` (default) or `'user_time'`. With
`'compile_time'`, the load phase completes once the build-side watermark
reaches the wall-clock time at which the query was compiled. With
`'user_time'`, it completes once the build-side watermark reaches the explicit
`load_completed_time`. |
+| `load_completed_time` | TIMESTAMP_LTZ(3) | no | The build-side event time
that completes the load phase. Required when `load_completed_condition` is
`'user_time'` and must not be set otherwise.
|
+| `load_completed_idle_timeout` | INTERVAL | no | A processing-time fallback
to complete the load phase. The transition to the join phase happens when the
build-side watermark does not advance for more than the configured interval.
|
+| `state_ttl` | INTERVAL | no | Retention time for build-side state. Join keys
that are not accessed within this duration become eligible for eviction. Only
applied during the join phase. Defaults to the pipeline's [state TTL]({{< ref
"docs/dev/table/config" >}}#table-exec-state-ttl).
|
+
+`load_completed_condition`, `load_completed_time`,
`load_completed_idle_timeout`, and `state_ttl` only affect streaming execution
and are ignored in batch mode (see **Batch mode** below).
+
+**Result and state characteristics**
+
+The result is append-only and preserves the probe-side time attributes. A
build-side rowtime attribute that is projected into the output is materialized
as a regular `TIMESTAMP` and is no longer a time attribute. Probe-side
watermarks are forwarded downstream during the join phase; build-side
watermarks are consumed internally and are not propagated.
+
+Because rows are joined against the build-side state that is current at
processing time, the result is **not deterministic**. A given probe-side row
may be joined with different build-side versions across different runs,
depending on the relative timing of the two inputs. Probe and build-side inputs
can be configured with watermark alignment to keep the two inputs roughly
aligned on event time, so that a probe-side row tends to be joined with
build-side changes of a similar event time. T [...]
+
+The build-side state grows with the number of distinct build-side keys, and
during the load phase the buffered probe-side rows add to the state footprint
until the operator transitions to the join phase. Use `state_ttl` to bound the
build-side state for keys that are no longer updated or joined. You can reduce
the amount of data that is buffered and processed during the load phase by
configuring scan start offsets on the build and probe-side inputs, for example
with a `scan.startup.*` [d [...]
+
+**Batch mode**
+
+In batch mode, a `LATERAL SNAPSHOT` join is executed as a regular (`INNER` or
`LEFT`) join between the probe side and the complete build side. Batch
execution reads the entire build side before joining, so there is no load phase
and no incremental state build-up. The streaming-specific arguments
(`load_completed_condition`, `load_completed_time`,
`load_completed_idle_timeout`, and `state_ttl`) are accepted but have no
effect, and the build side does not need to declare a watermark.
+
+Because every probe-side row is joined against the final, complete build side,
the batch result is **deterministic**.
+
Lookup Join
--------------
diff --git a/docs/content/docs/sql/functions/built-in-functions.md
b/docs/content/docs/sql/functions/built-in-functions.md
index 42808103674..3946cd225ab 100644
--- a/docs/content/docs/sql/functions/built-in-functions.md
+++ b/docs/content/docs/sql/functions/built-in-functions.md
@@ -135,6 +135,24 @@ The aggregate functions take an expression across all the
rows as the input and
{{< sql_functions "bitmapagg" >}}
+Table Functions
+---------------
+
+Table functions take zero, one, or more values as input and return multiple
rows (a table) as the result. Most built-in table functions take a table as an
input argument.
+Table functions can be used in two ways: as stand-alone inputs, where they are
invoked just once, or in a `LATERAL` context, where they are invoked for each
row of an outer table.
+
+| Function | Description
|
+|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `TUMBLE(data => TABLE t, ...)` | Assigns each row of the `data`
table to a tumbling window specified by additional window columns
(`window_start`, `window_end`, `window_time`). See [Window TVF]({{< ref
"docs/sql/reference/queries/window-tvf" >}}#tumble) for the full list of
arguments, semantics, and usage. |
+| `HOP(data => TABLE t, ...)` | Assigns each row of the `data`
table to a hopping window specified by additional window columns
(`window_start`, `window_end`, `window_time`). See [Window TVF]({{< ref
"docs/sql/reference/queries/window-tvf" >}}#hop) for the full list of
arguments, semantics, and usage. |
+| `CUMULATE(data => TABLE t, ...)` | Assigns each row of the `data`
table to a cumulating window specified by additional window columns
(`window_start`, `window_end`, `window_time`). See [Window TVF]({{< ref
"docs/sql/reference/queries/window-tvf" >}}#cumulate) for the full list of
arguments, semantics, and usage. |
+| `SESSION(data => TABLE t, ...)` | Assigns each row of the `data`
table to a session window specified by additional window columns
(`window_start`, `window_end`, `window_time`). See [Window TVF]({{< ref
"docs/sql/reference/queries/window-tvf" >}}#session) for the full list of
arguments, semantics, and usage. |
+| `FROM_CHANGELOG(input => TABLE t [, ...])` | Converts an append-only table
with an explicit operation column into a dynamic table. See [Changelog
Conversion]({{< ref "docs/sql/reference/queries/changelog" >}}#from_changelog)
for the full list of arguments, semantics, and usage.
|
+| `TO_CHANGELOG(input => TABLE t [, ...])` | Converts a dynamic table into
an append-only table with an explicit operation column. See [Changelog
Conversion]({{< ref "docs/sql/reference/queries/changelog" >}}#to_changelog)
for the full list of arguments, semantics, and usage.
|
+| `SNAPSHOT(input => TABLE t [, ...])` | Returns the current state of a
dynamic table `t`. `SNAPSHOT` can only be used in a `LATERAL` context and not
as a stand-alone table function. See [LATERAL SNAPSHOT join]({{< ref
"docs/sql/reference/queries/joins" >}}#lateral-snapshot-join) for the full list
of arguments, the join semantics, and usage. |
+
+To implement your own table functions, see [user-defined table functions]({{<
ref "docs/dev/table/functions/udfs" >}}#table-functions).
+
Time Interval and Point Unit Specifiers
---------------------------------------
diff --git a/docs/content/docs/sql/reference/queries/joins.md
b/docs/content/docs/sql/reference/queries/joins.md
index d000a46b822..c323554d37b 100644
--- a/docs/content/docs/sql/reference/queries/joins.md
+++ b/docs/content/docs/sql/reference/queries/joins.md
@@ -300,6 +300,117 @@ The main difference between above Temporal Table DDL and
Temporal Table Function
- The temporal table DDL can be defined in SQL but temporal table function can
not;
- Both temporal table DDL and temporal table function support temporal join
versioned table, but only temporal table function can temporal join the latest
version of any table/view.
+LATERAL SNAPSHOT Join
+--------------
+
+{{< label Streaming >}} {{< label Batch >}}
+
+A `LATERAL SNAPSHOT` join is a *stream enrichment* join that augments an
append-only table with the current state of an updating table.
+As in the [temporal joins](#temporal-joins), the enriched (left) input is
called the *probe side* and the enriching (right) input is called the *build
side*.
+Every probe-side row is joined with the build-side state that is current at
the time the row is processed.
+
+For example, the following query enriches an append-only stream of `orders`
(the probe side) with the conversion rate from an updating `currency_rates`
table (the build side) that is current when the order is processed:
+
+```sql
+-- probe side: append-only stream of orders
+-- order_id | currency | amount | order_time
+-- ---------+----------+--------+-----------
+-- 1 | EUR | 10 | 10:15
+-- 2 | EUR | 10 | 10:31
+-- 3 | USD | 20 | 11:00
+
+-- build side: updating currency rates (upsert on currency)
+-- currency | rate | update_time
+-- ---------+------+------------
+-- EUR | 1.1 | 10:00
+-- USD | 1.0 | 10:00
+-- EUR | 1.2 | 10:30
+
+SELECT o.order_id, o.currency, o.amount, r.rate
+FROM orders AS o
+JOIN LATERAL TABLE(SNAPSHOT(input => TABLE currency_rates)) AS r
+ON o.currency = r.currency;
+
+order_id currency amount rate
+======== ======== ====== ====
+ 1 EUR 10 1.1 -- rate as of 10:00
+ 2 EUR 10 1.2 -- rate as of 10:30
+ 3 USD 20 1.0 -- rate as of 10:00
+```
+
+*Important:* The `LATERAL SNAPSHOT` join is non-deterministic. The order with
`order_id = 2` could also have been joined with the `10:00` version of `EUR`.
The result depends on the order in which the operator processes its inputs,
which cannot be controlled.
+
+**When to use it**
+
+The `LATERAL SNAPSHOT` join is designed for enrichment scenarios where the
other temporal joins are a poor fit:
+
+- **The build side does not receive continuous updates.** An event-time
temporal join only emits a joined row once the combined watermark of both
inputs has passed the event time of the probe-side row. A build side that does
not continuously produce records, and therefore does not advance its watermark,
stalls the join and lets probe-side state accumulate. A `LATERAL SNAPSHOT` join
keeps making progress even when the build side is idle.
+- **Low latency is required.** An event-time temporal join holds back
probe-side rows until the watermark catches up, which adds latency. After the
initial load phase, the `LATERAL SNAPSHOT` join immediately joins a probe-side
row when it arrives.
+- **The build side has no primary key.** Event-time and processing-time
temporal joins require the build side to have a primary key that appears in the
equi-join condition. A `LATERAL SNAPSHOT` join has no such requirement; neither
the probe side nor the build side needs a primary key.
+
+**How the join works**
+
+A `LATERAL SNAPSHOT` join operates in two phases to avoid joining probe-side
rows against an incomplete build side:
+it first loads the build side up to a well-defined point in time (the *load
phase*) before it starts joining (the *join phase*).
+
+During the *load phase*, the operator accumulates the build-side changes into
state until the load-completion condition is met, without emitting any results
yet.
+Probe-side rows that arrive during the load phase are buffered. The load phase
completes when one of the following occurs:
+
+- the build-side watermark reaches a configured `load_completed_time`. This
time is either explicitly set by the user (`load_completed_condition =>
'user_time'`) or automatically set to the wall-clock time when the query is
compiled (`load_completed_condition => 'compile_time'`), or
+- as a fallback, the `load_completed_idle_timeout` elapses in processing time
without the build-side watermark advancing (which handles build sides that
become idle during start-up).
+
+When the load phase completes, the operator transitions to the *join phase*:
all buffered probe-side rows are joined against the current build-side state
and emitted.
+From then on, each probe-side row is joined and emitted immediately against
the build-side state that is current at that moment.
+Build-side updates continue to be applied to the state and become visible to
subsequent probe-side rows.
+
+The load phase is what distinguishes a `LATERAL SNAPSHOT` join from the
[processing-time temporal join](#processing-time-temporal-join), which has been
disabled for Flink SQL.
+The processing-time temporal join starts joining immediately at query start,
so early probe-side rows are joined against whatever build-side data happens to
have been loaded so far, producing missing or stale results that depend on the
order in which the inputs are read.
+By first loading the build side, a `LATERAL SNAPSHOT` join avoids this problem.
+
+**Syntax**
+
+The build side is wrapped in the `SNAPSHOT` table function inside a `LATERAL
TABLE` clause. The outer (probe-side) table must be an append-only table.
+Both `INNER JOIN` and `LEFT [OUTER] JOIN` are supported. The join requires at
least one conjunctive equality predicate; additional non-equi predicates are
allowed in the `ON` clause.
+
+```sql
+SELECT [column_list]
+FROM probe_table
+[LEFT] JOIN LATERAL TABLE(
+ SNAPSHOT(
+ input => TABLE build_table,
+ [ load_completed_condition => <'compile_time' | 'user_time'>, ]
+ [ load_completed_time => <timestamp_ltz>, ]
+ [ load_completed_idle_timeout => <interval>, ]
+ [ state_ttl => <interval> ])) AS s
+ON probe_table.col = s.col
+```
+
+The `SNAPSHOT` function accepts the following arguments:
+
+| Argument | Type | Required | Description
|
+| --- | --- | ---
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `input` | TABLE | yes | The build-side table. It may use any [changelog
mode]({{< ref "docs/sql/reference/queries/changelog" >}}) (inserts, updates,
and deletes). In streaming mode it must declare a [watermark]({{< ref
"docs/concepts/sql-table-concepts/time_attributes" >}}#event-time).
|
+| `load_completed_condition` | STRING | no | Determines when the initial load
phase completes. One of `'compile_time'` (default) or `'user_time'`. With
`'compile_time'`, the load phase completes once the build-side watermark
reaches the wall-clock time at which the query was compiled. With
`'user_time'`, it completes once the build-side watermark reaches the explicit
`load_completed_time`. |
+| `load_completed_time` | TIMESTAMP_LTZ(3) | no | The build-side event time
that completes the load phase. Required when `load_completed_condition` is
`'user_time'` and must not be set otherwise.
|
+| `load_completed_idle_timeout` | INTERVAL | no | A processing-time fallback
to complete the load phase. The transition to the join phase happens when the
build-side watermark does not advance for more than the configured interval.
|
+| `state_ttl` | INTERVAL | no | Retention time for build-side state. Join keys
that are not accessed within this duration become eligible for eviction. Only
applied during the join phase. Defaults to the pipeline's [state TTL]({{< ref
"docs/dev/table/config" >}}#table-exec-state-ttl).
|
+
+`load_completed_condition`, `load_completed_time`,
`load_completed_idle_timeout`, and `state_ttl` only affect streaming execution
and are ignored in batch mode (see **Batch mode** below).
+
+**Result and state characteristics**
+
+The result is append-only and preserves the probe-side time attributes. A
build-side rowtime attribute that is projected into the output is materialized
as a regular `TIMESTAMP` and is no longer a time attribute. Probe-side
watermarks are forwarded downstream during the join phase; build-side
watermarks are consumed internally and are not propagated.
+
+Because rows are joined against the build-side state that is current at
processing time, the result is **not deterministic**. A given probe-side row
may be joined with different build-side versions across different runs,
depending on the relative timing of the two inputs. Probe and build-side inputs
can be configured with watermark alignment to keep the two inputs roughly
aligned on event time, so that a probe-side row tends to be joined with
build-side changes of a similar event time. T [...]
+
+The build-side state grows with the number of distinct build-side keys, and
during the load phase the buffered probe-side rows add to the state footprint
until the operator transitions to the join phase. Use `state_ttl` to bound the
build-side state for keys that are no longer updated or joined. You can reduce
the amount of data that is buffered and processed during the load phase by
configuring scan start offsets on the build and probe-side inputs, for example
with a `scan.startup.*` [d [...]
+
+**Batch mode**
+
+In batch mode, a `LATERAL SNAPSHOT` join is executed as a regular (`INNER` or
`LEFT`) join between the probe side and the complete build side. Batch
execution reads the entire build side before joining, so there is no load phase
and no incremental state build-up. The streaming-specific arguments
(`load_completed_condition`, `load_completed_time`,
`load_completed_idle_timeout`, and `state_ttl`) are accepted but have no
effect, and the build side does not need to declare a watermark.
+
+Because every probe-side row is joined against the final, complete build side,
the batch result is **deterministic**.
+
Lookup Join
--------------