This is an automated email from the ASF dual-hosted git repository.
peter-toth pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 190ea9b3f0ac [SPARK-58562][SQL][DOC] Document subplan merging in the
SQL performance tuning guide
190ea9b3f0ac is described below
commit 190ea9b3f0acc4f9ae2ba3db51607c87f9c31b85
Author: Peter Toth <[email protected]>
AuthorDate: Wed Aug 5 14:48:41 2026 +0200
[SPARK-58562][SQL][DOC] Document subplan merging in the SQL performance
tuning guide
### What changes were proposed in this pull request?
This PR adds a "Merging Subplans" section to
`docs/sql-performance-tuning.md`, between "Join Strategy Hints" and "Adaptive
Query Execution". Docs only, no code change.
The section covers:
- what `MergeSubplans` does and which subplans qualify (non-correlated
deterministic scalar subqueries and non-grouping aggregates), with a small
example that merges by default,
- how a merged subplan looks in `EXPLAIN` (a subquery whose single output
column is `mergedValue`, shared via `ReusedSubquery`),
- when two subplans are merged, and that a query still carrying a
non-inlined `WITH` clause is skipped,
- why the one-sided filter case is always beneficial while the symmetric
one is not, with the TPC-DS numbers measured in SPARK-40193 and SPARK-56677 and
guidance on when to consider enabling it,
- the 4 configs under
`spark.sql.optimizer.mergeSubplans.filterPropagation.*` in a config table, and
how to turn the rule off via `spark.sql.optimizer.excludedRules`.
### Why are the changes needed?
The optimization and its configs are not described anywhere in the docs
today. The 4 configs are public, so they show up in the generated SQL config
table in `configuration.md`, but nothing tells users what the optimization
does, which query shapes it applies to, or what the trade-offs of the 3
non-default configs are. The only other mention in `docs/` is the
migration-guide note about the rule's package move.
This matters most for symmetric filter propagation. It is disabled by
default because merging two filtered scans widens the scan filter to `OR(f1,
f2)`, which can lose partition or file pruning. But on queries that compute
several differently filtered aggregates over the same table it is a big win: q9
and q28 got about 3.5x faster in TPC-DS runs, and together with propagation
through joins q88 got about 7x and q90 about 2x faster. Users have no way to
discover that today.
The section also makes the plan shape discoverable: a merged subplan shows
up as a subquery with a `mergedValue` column, which is otherwise hard to trace
back to its source.
### Does this PR introduce _any_ user-facing change?
No, documentation only.
### How was this patch tested?
No tests, docs only. I verified every statement against the implementation
on master: the config keys, defaults and versions against `SQLConf`, the merge
conditions and the join nullability rules against `PlanMerger`, the skipped-CTE
and candidate conditions against `MergeSubplans`, the `SCAN_MERGING` opt-in
against `TableCapability` and its (test-only) implementors, and the rule name
used in `excludedRules` against `Rule.ruleName` and the non-excludable rule
list. The `mergedValue` o [...]
The added table follows the same `spark-config` markup as the other tables
on the page.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)
Closes #57765 from peter-toth/SPARK-58562-document-plan-merging.
Authored-by: Peter Toth <[email protected]>
Signed-off-by: Peter Toth <[email protected]>
---
docs/sql-performance-tuning.md | 72 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+)
diff --git a/docs/sql-performance-tuning.md b/docs/sql-performance-tuning.md
index 3ba3a6c749ca..e0cba9362736 100644
--- a/docs/sql-performance-tuning.md
+++ b/docs/sql-performance-tuning.md
@@ -258,6 +258,78 @@ SELECT /*+ BROADCAST(r) */ * FROM src s JOIN records r ON
s.key = r.key
For more details please refer to the documentation of [Join
Hints](sql-ref-syntax-qry-select-hints.html#join-hints).
+## Merging Subplans
+
+Spark merges subplans that return a single row and read the same input, so
that input is scanned once instead of once per subplan. The candidates are
non-correlated deterministic scalar subqueries and non-grouping aggregates
(aggregates without `GROUP BY`). The merged subplan is evaluated once and
outputs a single struct, and each original site reads its own field out of that
struct. This optimization is enabled by default.
+
+For example, in the following query both subqueries scan `store_sales`:
+
+```sql
+SELECT
+ (SELECT min(ss_net_paid) FROM store_sales),
+ (SELECT max(ss_net_paid) FROM store_sales)
+```
+
+They are merged into one aggregate that computes `min` and `max` together, so
`store_sales` is read once. In `EXPLAIN` output a merged subplan shows up as a
subquery whose single output column is named `mergedValue`, and the sites that
share it as `ReusedSubquery`.
+
+Two subplans are merged when their plans match node by node: `Project` lists
are unioned, `Aggregate`s must have the same grouping and use the same
aggregation implementation (so a `min` is not merged with a `collect_list`),
`Filter`s must have the same condition, `Join`s must have the same type,
condition and hints, and the leaves must read the same input. Subplans that
differ only in their `WHERE` conditions can be merged as well, by turning each
side's condition into a boolean column [...]
+
+When only one of the two subplans has a filter, merging is always beneficial,
because the unfiltered side reads all the data anyway. This case is on by
default, unless the filter has to cross a `Join` to reach the aggregate, which
needs the through-join configuration below. When both sides have a filter (the
symmetric case), the merged scan filter becomes `OR(f1, f2)`, which is less
selective than either original filter and can therefore read more data - for
example when the filters prun [...]
+
+Still, it is worth considering on queries that compute several differently
filtered aggregates over the same table, which is a common analytical shape:
+
+```sql
+SELECT
+ (SELECT avg(ss_net_paid) FROM store_sales WHERE ss_quantity BETWEEN 1 AND
20),
+ (SELECT avg(ss_net_paid) FROM store_sales WHERE ss_quantity BETWEEN 21 AND
40)
+```
+
+In TPC-DS benchmark runs, enabling symmetric filter propagation made `q9` and
`q28` about 3.5x faster, and enabling it together with propagation through
joins made `q88` about 7x and `q90` about 2x faster. See
[SPARK-40193](https://issues.apache.org/jira/browse/SPARK-40193) and
[SPARK-56677](https://issues.apache.org/jira/browse/SPARK-56677) for these
measurements. The trade-off depends on the tables: the gain is largest when the
differing filters are on columns the data source cannot pr [...]
+
+<table class="spark-config">
+ <thead><tr><th>Property Name</th><th>Default</th><th>Meaning</th><th>Since
Version</th></tr></thead>
+ <tr>
+
<td><code>spark.sql.optimizer.mergeSubplans.filterPropagation.enabled</code></td>
+ <td>true</td>
+ <td>
+ When true, subplans that differ only in their filter conditions can be
merged by propagating the filters up to the enclosing non-grouping aggregates.
This is the umbrella configuration of the three below: none of them has any
effect when this is false. Subplans with the same filter condition are merged
regardless of this configuration.
+ </td>
+ <td>4.2.0</td>
+ </tr>
+ <tr>
+
<td><code>spark.sql.optimizer.mergeSubplans.filterPropagation.symmetricFilterPropagation.enabled</code></td>
+ <td>false</td>
+ <td>
+ When true, two non-grouping aggregate subplans that both have a filter
condition can also be merged. Disabled by default because the merged filter is
widened to <code>OR(f1, f2)</code>, which may read more data than the two
original filters, especially on heavily partitioned or file-pruned tables.
+ </td>
+ <td>4.2.0</td>
+ </tr>
+ <tr>
+
<td><code>spark.sql.optimizer.mergeSubplans.filterPropagation.throughJoin.enabled</code></td>
+ <td>false</td>
+ <td>
+ When true, filter conditions can also propagate through
<code>Join</code> nodes, which lets subplans that differ only in their filter
conditions and share a common join be merged. When false, no filter is
propagated across a join, not even when only one of the two subplans has a
filter. A filter only propagates from the preserved side of the join: the left
side of <code>LEFT OUTER</code>/<code>LEFT SEMI</code>/<code>LEFT ANTI</code>,
the right side of <code>RIGHT OUTER</code>, or e [...]
+ </td>
+ <td>4.2.0</td>
+ </tr>
+ <tr>
+
<td><code>spark.sql.optimizer.mergeSubplans.filterPropagation.dsv2SymmetricFilterPropagation.enabled</code></td>
+ <td>false</td>
+ <td>
+ When true, two DataSource V2 scans that pushed the same strictly
enforced filters but carry different best-effort (post-scan) filters can be
merged even when
<code>spark.sql.optimizer.mergeSubplans.filterPropagation.symmetricFilterPropagation.enabled</code>
is false. In this case widening cannot change the set of rows the scan is
required to return, as the strict filters are re-pushed unchanged and the
enclosing <code>Filter</code> re-checks the rest above the scan. This applies
on [...]
+ </td>
+ <td>4.3.0</td>
+ </tr>
+</table>
+
+Subplan merging can be turned off entirely by adding the rule to
`spark.sql.optimizer.excludedRules`:
+
+```
+spark.sql.optimizer.excludedRules=org.apache.spark.sql.execution.planmerging.MergeSubplans
+```
+
+Use exactly that name: the rule was called `MergeScalarSubqueries` before
Spark 4.2 and sat in a different package before Spark 4.3, and unknown names in
`spark.sql.optimizer.excludedRules` are silently ignored, so an older name
carried over from a previous version does not turn the rule off. See the [SQL
migration guide](sql-migration-guide.html) for the old names.
+
## Adaptive Query Execution
Adaptive Query Execution (AQE) is an optimization technique in Spark SQL that
makes use of the runtime statistics to choose the most efficient query
execution plan, which is enabled by default since Apache Spark 3.2.0. Spark SQL
can turn on and off AQE by `spark.sql.adaptive.enabled` as an umbrella
configuration.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]