Skip to content

Commit f962686

Browse files
authored
fix: Fix bug with structurally equal correlated subqueries (#22313)
## Which issue does this PR close? - Closes #21210. ## Rationale for this change If a query contained two or more structurally equal correlated subqueries, we previously would produce incorrect query results. This was caused by using `Subquery` as a hashmap key; if we use the subquery alias instead, we can avoid the unintended key collision. Using the alias (a string) as the hashmap key is also safer and more efficient than hashing on a complex type like `Subquery`. ## What changes are included in this PR? * Fix for bug in `ScalarSubqueryToJoin` rewrite pass * SLT test ## Are these changes tested? Yes, with new test added. ## Are there any user-facing changes? No, aside from fixing the previously incorrect query results.
1 parent bbf9078 commit f962686

2 files changed

Lines changed: 30 additions & 19 deletions

File tree

datafusion/optimizer/src/scalar_subquery_to_join.rs

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -153,20 +153,17 @@ impl OptimizerRule for ScalarSubqueryToJoin {
153153
}
154154

155155
let mut all_subqueries = vec![];
156-
#[allow(clippy::allow_attributes, clippy::mutable_key_type)]
157-
// Expr contains Arc with interior mutability but is intentionally used as hash key
158-
let mut expr_to_rewrite_expr_map = HashMap::new();
159-
#[allow(clippy::allow_attributes, clippy::mutable_key_type)]
160-
// Expr contains Arc with interior mutability but is intentionally used as hash key
161-
let mut subquery_to_expr_map = HashMap::new();
162-
for expr in projection.expr.iter() {
163-
let (subqueries, rewrite_exprs) =
156+
let mut alias_to_index: HashMap<String, usize> = HashMap::new();
157+
let mut rewrite_exprs: Vec<Expr> =
158+
Vec::with_capacity(projection.expr.len());
159+
for (idx, expr) in projection.expr.iter().enumerate() {
160+
let (subqueries, rewrite_expr) =
164161
self.extract_subquery_exprs(expr, config.alias_generator())?;
165-
for (subquery, _) in &subqueries {
166-
subquery_to_expr_map.insert(subquery.clone(), expr.clone());
162+
for (_, alias) in &subqueries {
163+
alias_to_index.insert(alias.clone(), idx);
167164
}
168165
all_subqueries.extend(subqueries);
169-
expr_to_rewrite_expr_map.insert(expr, rewrite_exprs);
166+
rewrite_exprs.push(rewrite_expr);
170167
}
171168
assert_or_internal_err!(
172169
!all_subqueries.is_empty(),
@@ -180,10 +177,9 @@ impl OptimizerRule for ScalarSubqueryToJoin {
180177
{
181178
cur_input = optimized_subquery;
182179
if !expr_check_map.is_empty()
183-
&& let Some(expr) = subquery_to_expr_map.get(&subquery)
184-
&& let Some(rewrite_expr) = expr_to_rewrite_expr_map.get(expr)
180+
&& let Some(&idx) = alias_to_index.get(&alias)
185181
{
186-
let new_expr = rewrite_expr
182+
let new_expr = rewrite_exprs[idx]
187183
.clone()
188184
.transform_up(|expr| {
189185
// replace column references with entry in map, if it exists
@@ -197,7 +193,7 @@ impl OptimizerRule for ScalarSubqueryToJoin {
197193
}
198194
})
199195
.data()?;
200-
expr_to_rewrite_expr_map.insert(expr, new_expr);
196+
rewrite_exprs[idx] = new_expr;
201197
}
202198
} else {
203199
// if we can't handle all of the subqueries then bail for now
@@ -206,14 +202,13 @@ impl OptimizerRule for ScalarSubqueryToJoin {
206202
}
207203

208204
let mut proj_exprs = vec![];
209-
for expr in projection.expr.iter() {
205+
for (expr, new_expr) in projection.expr.iter().zip(rewrite_exprs) {
210206
let old_expr_name = expr.schema_name().to_string();
211-
let new_expr = expr_to_rewrite_expr_map.get(expr).unwrap();
212207
let new_expr_name = new_expr.schema_name().to_string();
213208
if new_expr_name != old_expr_name {
214-
proj_exprs.push(new_expr.clone().alias(old_expr_name))
209+
proj_exprs.push(new_expr.alias(old_expr_name))
215210
} else {
216-
proj_exprs.push(new_expr.clone());
211+
proj_exprs.push(new_expr);
217212
}
218213
}
219214
let new_plan = LogicalPlanBuilder::from(cur_input)

datafusion/sqllogictest/test_files/subquery.slt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,22 @@ SELECT t1_id, (SELECT count(*) FROM t2 WHERE t2.t2_int = t1.t1_int) from t1
851851
44 0
852852

853853

854+
#correlated_scalar_subquery_count_agg_duplicated_in_projection
855+
# Two structurally identical correlated COUNT subqueries in the same
856+
# projection must each receive the count-bug compensation, so unmatched
857+
# outer rows produce 0 (not NULL) on both sides.
858+
query III rowsort
859+
SELECT
860+
t1_id,
861+
(SELECT count(*) FROM t2 WHERE t2.t2_int = t1.t1_int) + 1 AS a,
862+
(SELECT count(*) FROM t2 WHERE t2.t2_int = t1.t1_int) + 2 AS b
863+
FROM t1
864+
----
865+
11 2 3
866+
22 1 2
867+
33 4 5
868+
44 1 2
869+
854870
#correlated_scalar_subquery_count_agg2
855871
query TT
856872
explain SELECT t1_id, (SELECT count(*) FROM t2 WHERE t2.t2_int = t1.t1_int) as cnt from t1

0 commit comments

Comments
 (0)