Skip to content

Commit 4523e07

Browse files
committed
Adress code review
1 parent d1b9dad commit 4523e07

7 files changed

Lines changed: 47 additions & 26 deletions

File tree

datafusion/common/src/config.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,12 +1124,20 @@ config_namespace! {
11241124
/// into the file scan phase.
11251125
pub enable_topk_dynamic_filter_pushdown: bool, default = true
11261126

1127-
/// When set to true, uncorrelated scalar subqueries are left in
1128-
/// the logical plan and executed by `ScalarSubqueryExec` during physical
1129-
/// execution. When set to false, all scalar subqueries (including
1130-
/// uncorrelated ones) are rewritten to left joins by the
1127+
/// When set to true, uncorrelated scalar subqueries are
1128+
/// left in the logical plan and executed by `ScalarSubqueryExec` during
1129+
/// physical execution. When set to false, all scalar subqueries
1130+
/// (including uncorrelated ones) are rewritten to left joins by the
11311131
/// `ScalarSubqueryToJoin` optimizer rule.
1132-
pub physical_uncorrelated_scalar_subquery: bool, default = true
1132+
///
1133+
/// Note disabling this option is not recommended. It restores
1134+
/// pre-PR-21240 behavior, which silently produces incorrect results for
1135+
/// multi-row subqueries and does not support scalar subqueries in
1136+
/// ORDER BY / JOIN ON / aggregate-function arguments. This option is
1137+
/// intended as a temporary escape hatch for distributed execution
1138+
/// frameworks and is planned to be removed in a future DataFusion
1139+
/// release.
1140+
pub enable_physical_uncorrelated_scalar_subquery: bool, default = true
11331141

11341142
/// When set to true, the optimizer will attempt to push down Join dynamic filters
11351143
/// into the file scan phase.

datafusion/core/src/physical_planner.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,15 +437,15 @@ impl DefaultPhysicalPlanner {
437437
session_state: &'a SessionState,
438438
) -> futures::future::BoxFuture<'a, Result<Arc<dyn ExecutionPlan>>> {
439439
Box::pin(async move {
440-
// When `physical_uncorrelated_scalar_subquery` is disabled, the
440+
// When `enable_physical_uncorrelated_scalar_subquery` is disabled, the
441441
// `ScalarSubqueryToJoin` optimizer rule rewrites all uncorrelated
442442
// scalar subqueries to joins, so none should reach this point.
443443
// Skip collection in that case to avoid creating a no-op
444444
// `ScalarSubqueryExec` wrapper.
445445
let all_subqueries = if session_state
446446
.config_options()
447447
.optimizer
448-
.physical_uncorrelated_scalar_subquery
448+
.enable_physical_uncorrelated_scalar_subquery
449449
{
450450
Self::collect_scalar_subqueries(logical_plan)
451451
} else {

datafusion/optimizer/src/scalar_subquery_to_join.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
//! [`ScalarSubqueryToJoin`] rewriting correlated scalar subquery filters to `JOIN`s
18+
//! [`ScalarSubqueryToJoin`] rewriting scalar subquery filters to `JOIN`s
1919
2020
use std::collections::{BTreeSet, HashMap};
2121
use std::sync::Arc;
@@ -36,9 +36,14 @@ use datafusion_expr::logical_plan::{JoinType, Subquery};
3636
use datafusion_expr::utils::conjunction;
3737
use datafusion_expr::{Expr, LogicalPlan, LogicalPlanBuilder, lit, not, when};
3838

39-
/// Optimizer rule that rewrites correlated scalar subquery filters to joins and
40-
/// places an additional projection on top of the filter, to preserve the
41-
/// original schema.
39+
/// Optimizer rule that rewrites scalar subquery filters to joins and places an
40+
/// additional projection on top of the filter to preserve the original schema.
41+
///
42+
/// When [`OptimizerOptions::enable_physical_uncorrelated_scalar_subquery`] is
43+
/// true (the default), only *correlated* scalar subqueries are rewritten here;
44+
/// uncorrelated ones are left for physical execution via `ScalarSubqueryExec`.
45+
/// When the option is false, all scalar subqueries — correlated and
46+
/// uncorrelated — are rewritten to left joins by this rule.
4247
#[derive(Default, Debug)]
4348
pub struct ScalarSubqueryToJoin {}
4449

@@ -93,7 +98,7 @@ impl OptimizerRule for ScalarSubqueryToJoin {
9398
let physical_uncorrelated = config
9499
.options()
95100
.optimizer
96-
.physical_uncorrelated_scalar_subquery;
101+
.enable_physical_uncorrelated_scalar_subquery;
97102
// Optimization: skip the rest of the rule and its copies if
98103
// there are no scalar subqueries this rule should rewrite
99104
if !contains_scalar_subquery_to_rewrite(
@@ -154,7 +159,7 @@ impl OptimizerRule for ScalarSubqueryToJoin {
154159
let physical_uncorrelated = config
155160
.options()
156161
.optimizer
157-
.physical_uncorrelated_scalar_subquery;
162+
.enable_physical_uncorrelated_scalar_subquery;
158163
// Optimization: skip the rest of the rule and its copies if there
159164
// are no scalar subqueries this rule should rewrite
160165
if !projection.expr.iter().any(|expr| {
@@ -246,7 +251,7 @@ impl OptimizerRule for ScalarSubqueryToJoin {
246251
/// Returns true if the expression contains a scalar subquery that this rule
247252
/// should rewrite to a join.
248253
///
249-
/// When `physical_uncorrelated_scalar_subquery` is true (the default) only
254+
/// When `enable_physical_uncorrelated_scalar_subquery` is true (the default) only
250255
/// correlated scalar subqueries are rewritten — uncorrelated ones are handled
251256
/// by the physical planner via `ScalarSubqueryExec`. When it is false, all
252257
/// scalar subqueries (correlated and uncorrelated) are rewritten.
@@ -316,9 +321,15 @@ impl TreeNodeRewriter for ExtractScalarSubQuery<'_> {
316321
/// where c.balance > o."avg(total)"
317322
/// ```
318323
///
324+
/// When [`OptimizerOptions::enable_physical_uncorrelated_scalar_subquery`] is
325+
/// false, this function also handles uncorrelated scalar subqueries, rewriting
326+
/// them as a `Left Join: Filter: Boolean(true)` instead of leaving them for
327+
/// `ScalarSubqueryExec`.
328+
///
319329
/// # Arguments
320330
///
321-
/// * `subquery` - The correlated scalar subquery to decorrelate.
331+
/// * `subquery` - The scalar subquery to rewrite (correlated, or uncorrelated
332+
/// when `enable_physical_uncorrelated_scalar_subquery` is false).
322333
/// * `outer_input` - The outer plan that the decorrelated subquery is
323334
/// left-joined onto — the input of the `Filter` or `Projection` node
324335
/// that contained the subquery.
@@ -338,7 +349,7 @@ fn build_join(
338349
) -> Result<Option<(LogicalPlan, HashMap<Column, Expr>)>> {
339350
// `build_join` also handles uncorrelated scalar subqueries (as a left
340351
// join with `Boolean(true)`) when the
341-
// `physical_uncorrelated_scalar_subquery` option is disabled.
352+
// `enable_physical_uncorrelated_scalar_subquery` option is disabled.
342353
let subquery_plan = subquery.subquery.as_ref();
343354
let mut pull_up = PullUpCorrelatedExpr::new().with_need_handle_count_bug(true);
344355
let decorrelated_subquery = subquery_plan.clone().rewrite(&mut pull_up).data()?;
@@ -1205,7 +1216,9 @@ mod tests {
12051216

12061217
let mut options = ConfigOptions::default();
12071218
options.optimizer.filter_null_join_keys = true;
1208-
options.optimizer.physical_uncorrelated_scalar_subquery = false;
1219+
options
1220+
.optimizer
1221+
.enable_physical_uncorrelated_scalar_subquery = false;
12091222
let context = crate::OptimizerContext::new_with_config_options(Arc::new(options));
12101223

12111224
let rule: Arc<dyn OptimizerRule + Send + Sync> =

datafusion/sqllogictest/test_files/information_schema.slt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ datafusion.optimizer.enable_distinct_aggregation_soft_limit true
303303
datafusion.optimizer.enable_dynamic_filter_pushdown true
304304
datafusion.optimizer.enable_join_dynamic_filter_pushdown true
305305
datafusion.optimizer.enable_leaf_expression_pushdown true
306+
datafusion.optimizer.enable_physical_uncorrelated_scalar_subquery true
306307
datafusion.optimizer.enable_piecewise_merge_join false
307308
datafusion.optimizer.enable_round_robin_repartition true
308309
datafusion.optimizer.enable_sort_pushdown true
@@ -320,7 +321,6 @@ datafusion.optimizer.hash_join_single_partition_threshold 1048576
320321
datafusion.optimizer.hash_join_single_partition_threshold_rows 131072
321322
datafusion.optimizer.join_reordering true
322323
datafusion.optimizer.max_passes 3
323-
datafusion.optimizer.physical_uncorrelated_scalar_subquery true
324324
datafusion.optimizer.prefer_existing_sort false
325325
datafusion.optimizer.prefer_existing_union false
326326
datafusion.optimizer.prefer_hash_join true
@@ -454,6 +454,7 @@ datafusion.optimizer.enable_distinct_aggregation_soft_limit true When set to tru
454454
datafusion.optimizer.enable_dynamic_filter_pushdown true When set to true attempts to push down dynamic filters generated by operators (TopK, Join & Aggregate) into the file scan phase. For example, for a query such as `SELECT * FROM t ORDER BY timestamp DESC LIMIT 10`, the optimizer will attempt to push down the current top 10 timestamps that the TopK operator references into the file scans. This means that if we already have 10 timestamps in the year 2025 any files that only have timestamps in the year 2024 can be skipped / pruned at various stages in the scan. The config will suppress `enable_join_dynamic_filter_pushdown`, `enable_topk_dynamic_filter_pushdown` & `enable_aggregate_dynamic_filter_pushdown` So if you disable `enable_topk_dynamic_filter_pushdown`, then enable `enable_dynamic_filter_pushdown`, the `enable_topk_dynamic_filter_pushdown` will be overridden.
455455
datafusion.optimizer.enable_join_dynamic_filter_pushdown true When set to true, the optimizer will attempt to push down Join dynamic filters into the file scan phase.
456456
datafusion.optimizer.enable_leaf_expression_pushdown true When set to true, the optimizer will extract leaf expressions (such as `get_field`) from filter/sort/join nodes into projections closer to the leaf table scans, and push those projections down towards the leaf nodes.
457+
datafusion.optimizer.enable_physical_uncorrelated_scalar_subquery true When set to true, uncorrelated scalar subqueries are left in the logical plan and executed by `ScalarSubqueryExec` during physical execution. When set to false, all scalar subqueries (including uncorrelated ones) are rewritten to left joins by the `ScalarSubqueryToJoin` optimizer rule. Note disabling this option is not recommended. It restores pre-PR-21240 behavior, which silently produces incorrect results for multi-row subqueries and does not support scalar subqueries in ORDER BY / JOIN ON / aggregate-function arguments. This option is intended as a temporary escape hatch for distributed execution frameworks and is planned to be removed in a future DataFusion release.
457458
datafusion.optimizer.enable_piecewise_merge_join false When set to true, piecewise merge join is enabled. PiecewiseMergeJoin is currently experimental. Physical planner will opt for PiecewiseMergeJoin when there is only one range filter.
458459
datafusion.optimizer.enable_round_robin_repartition true When set to true, the physical plan optimizer will try to add round robin repartitioning to increase parallelism to leverage more CPU cores
459460
datafusion.optimizer.enable_sort_pushdown true Enable sort pushdown optimization. When enabled, attempts to push sort requirements down to data sources that can natively handle them (e.g., by reversing file/row group read order). Returns **inexact ordering**: Sort operator is kept for correctness, but optimized input enables early termination for TopK queries (ORDER BY ... LIMIT N), providing significant speedup. Memory: No additional overhead (only changes read order). Future: Will add option to detect perfectly sorted data and eliminate Sort completely. Default: true
@@ -471,7 +472,6 @@ datafusion.optimizer.hash_join_single_partition_threshold 1048576 The maximum es
471472
datafusion.optimizer.hash_join_single_partition_threshold_rows 131072 The maximum estimated size in rows for one input side of a HashJoin will be collected into a single partition
472473
datafusion.optimizer.join_reordering true When set to true, the physical plan optimizer may swap join inputs based on statistics. When set to false, statistics-driven join input reordering is disabled and the original join order in the query is used.
473474
datafusion.optimizer.max_passes 3 Number of times that the optimizer will attempt to optimize the plan
474-
datafusion.optimizer.physical_uncorrelated_scalar_subquery true When set to true, uncorrelated scalar subqueries are left in the logical plan and executed by `ScalarSubqueryExec` during physical execution. When set to false, all scalar subqueries (including uncorrelated ones) are rewritten to left joins by the `ScalarSubqueryToJoin` optimizer rule.
475475
datafusion.optimizer.prefer_existing_sort false When true, DataFusion will opportunistically remove sorts when the data is already sorted, (i.e. setting `preserve_order` to true on `RepartitionExec` and using `SortPreservingMergeExec`) When false, DataFusion will maximize plan parallelism using `RepartitionExec` even if this requires subsequently resorting data using a `SortExec`.
476476
datafusion.optimizer.prefer_existing_union false When set to true, the optimizer will not attempt to convert Union to Interleave
477477
datafusion.optimizer.prefer_hash_join true When set to true, the physical plan optimizer will prefer HashJoin over SortMergeJoin. HashJoin can work more efficiently than SortMergeJoin but consumes more memory

datafusion/sqllogictest/test_files/subquery.slt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,10 +2093,10 @@ SELECT (SELECT v FROM (SELECT 1 AS v UNION ALL SELECT 2) AS t ORDER BY v LIMIT 1
20932093

20942094
#############
20952095
## End-to-end correctness coverage for the flag-off path.
2096-
## When `datafusion.optimizer.physical_uncorrelated_scalar_subquery` is false,
2096+
## When `datafusion.optimizer.enable_physical_uncorrelated_scalar_subquery` is false,
20972097
## uncorrelated scalar subqueries are rewritten to left joins by
20982098
## `ScalarSubqueryToJoin` instead of executed by `ScalarSubqueryExec`. This
2099-
## restores pre-PR-21240 behavior, which has three known shortcomings the
2099+
## restores pre-PR-21240 behavior, which has two known shortcomings the
21002100
## physical-execution path was built to fix: multi-row subqueries silently
21012101
## return wrong results, and uncorrelated scalar subqueries do not work in
21022102
## ORDER BY / JOIN ON / aggregate-function arguments. Those cases are
@@ -2105,7 +2105,7 @@ SELECT (SELECT v FROM (SELECT 1 AS v UNION ALL SELECT 2) AS t ORDER BY v LIMIT 1
21052105
#############
21062106

21072107
statement ok
2108-
set datafusion.optimizer.physical_uncorrelated_scalar_subquery = false;
2108+
set datafusion.optimizer.enable_physical_uncorrelated_scalar_subquery = false;
21092109

21102110
# Scalar subquery returning exactly one row → success
21112111
query I
@@ -2178,7 +2178,7 @@ SELECT 1 = (SELECT CAST(NULL AS INT));
21782178
NULL
21792179

21802180
statement ok
2181-
RESET datafusion.optimizer.physical_uncorrelated_scalar_subquery;
2181+
RESET datafusion.optimizer.enable_physical_uncorrelated_scalar_subquery;
21822182

21832183
statement count 0
21842184
DROP TABLE sq_values;

datafusion/sqllogictest/test_files/tpch/tpch.slt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ include ./answers/q*.slt.part
2323

2424
# test answers with uncorrelated scalar subqueries rewritten to joins
2525
statement ok
26-
set datafusion.optimizer.physical_uncorrelated_scalar_subquery = false;
26+
set datafusion.optimizer.enable_physical_uncorrelated_scalar_subquery = false;
2727

2828
include ./answers/q*.slt.part
2929

3030
statement ok
31-
reset datafusion.optimizer.physical_uncorrelated_scalar_subquery;
31+
reset datafusion.optimizer.enable_physical_uncorrelated_scalar_subquery;
3232

3333
# test answers with sort merge join
3434
statement ok

0 commit comments

Comments
 (0)