diff --git a/qpmodel/subquery.cs b/qpmodel/subquery.cs index 8ddec466..9ef0e50b 100644 --- a/qpmodel/subquery.cs +++ b/qpmodel/subquery.cs @@ -273,7 +273,7 @@ bool isUnresolvedColExpr(Expr e) // LogicNode_A // LogicNode_B // - LogicNode scalarToSingleJoin(LogicNode planWithSubExpr, ScalarSubqueryExpr scalarExpr) + LogicNode scalarToSingleJoin(LogicNode planWithSubExpr, ScalarSubqueryExpr scalarExpr, ref bool canReplace) { var newplan = planWithSubExpr; @@ -281,8 +281,8 @@ LogicNode scalarToSingleJoin(LogicNode planWithSubExpr, ScalarSubqueryExpr scala var nodeSubquery = scalarExpr.query_.logicPlan_; // make a single join - // nodeA - // + // nodeA + // // nodeSubquery // => // SingleJoin @@ -302,7 +302,7 @@ LogicNode scalarToSingleJoin(LogicNode planWithSubExpr, ScalarSubqueryExpr scala newplan = djoinOnRightAggregation(singleJoinNode, scalarExpr); break; case LogicFilter lf: - newplan = djoinOnRightFilter(singleJoinNode, scalarExpr); + newplan = djoinOnRightFilter(singleJoinNode, scalarExpr, ref canReplace); break; default: break; @@ -320,20 +320,26 @@ LogicNode scalarToSingleJoin(LogicNode planWithSubExpr, ScalarSubqueryExpr scala return newplan; } - Expr extractCurINExprFromNodeAFilter(LogicNode nodeA, InSubqueryExpr curInExpr, ExprRef markerFilter) + Expr extractCurINExprFromNodeAFilter(LogicNode nodeA, InSubqueryExpr curInExpr, ExprRef markerFilter, ref bool canReplace) { var nodeAFilter = nodeA.filter_; if (nodeAFilter != null) { - // a1 > @1 and a2 > @2 and a3 > 2, scalarExpr = @1 - // keeplist: a1 > @1 and a3 > 2 - // andlist after removal: a2 > @2 - // nodeAFilter = a1 > @1 and a3 > 2 + // OR-aware logic (same principle as existsToMarkJoin): + // For IN subqueries, the InSubqueryExpr appears as a direct conjunct + // (e.g., WHERE a1 IN (select ...)), so Equals works for non-OR. + // For OR conjuncts, only remove if no extra subqueries in the OR. // var andlist = nodeAFilter.FilterToAndList(); var keeplist = andlist.Where(x => x.VisitEachExists(e => e.Equals(curInExpr))).ToList(); - andlist.RemoveAll(x => x.VisitEachExists(e => e.Equals(curInExpr))); - if (andlist.Count == 0) + andlist.RemoveAll(x => + (!(x is LogicOrExpr) && x.VisitEachExists(e => e.Equals(curInExpr))) || + ((x is LogicOrExpr) && !hasAnyExtraSubqueryExprInOR(x, curInExpr))); + + // if there is any (#marker@1 or @2), the root should be replaced + canReplace = andlist.Find(x => (x is LogicOrExpr) && hasAnyExtraSubqueryExprInOR(x, curInExpr)) != null; + + if (andlist.Count == 0 || canReplace) nodeA.NullifyFilter(); else { @@ -347,7 +353,7 @@ Expr extractCurINExprFromNodeAFilter(LogicNode nodeA, InSubqueryExpr curInExpr, return nodeAFilter; } - LogicNode inToMarkJoin(LogicNode planWithSubExpr, InSubqueryExpr inExpr) + LogicNode inToMarkJoin(LogicNode planWithSubExpr, InSubqueryExpr inExpr, ref bool canReplace) { LogicNode nodeA = planWithSubExpr; @@ -359,7 +365,7 @@ LogicNode inToMarkJoin(LogicNode planWithSubExpr, InSubqueryExpr inExpr) // nullify nodeA's filter: the rest is push to top filter. However, // if nodeA is a Filter|MarkJoin, keep its mark filter. var markerFilter = new ExprRef(new MarkerExpr(nodeBFilter.tableRefs_, inExpr.subqueryid_), 0); - var nodeAFilter = extractCurINExprFromNodeAFilter(nodeA, inExpr, markerFilter); + var nodeAFilter = extractCurINExprFromNodeAFilter(nodeA, inExpr, markerFilter, ref canReplace); // consider SQL ...a1 in select b1 from... // a1 is outerExpr and b1 is selectExpr @@ -403,7 +409,7 @@ LogicJoin singleJoin2OuterJoin(LogicSingleJoin singJoinNode) } // D Xs (Filter(T)) => Filter(D Xs T) - LogicNode djoinOnRightFilter(LogicSingleJoin singleJoinNode, ScalarSubqueryExpr scalarExpr) + LogicNode djoinOnRightFilter(LogicSingleJoin singleJoinNode, ScalarSubqueryExpr scalarExpr, ref bool canReplace) { var nodeLeft = singleJoinNode.lchild_(); var nodeSubquery = singleJoinNode.rchild_(); @@ -419,15 +425,21 @@ LogicNode djoinOnRightFilter(LogicSingleJoin singleJoinNode, ScalarSubqueryExpr var nodeLeftFilter = nodeLeft.filter_; if (nodeLeftFilter != null) { - // a1 > @1 and a2 > @2 and a3 > 2, scalarExpr = @1 - // keeplist: a1 > @1 and a3 > 2 - // andlist after removal: a2 > @2 - // nodeAFilter = a1 > @1 and a3 > 2 + // OR-aware logic (same principle as existsToMarkJoin): + // For scalar subqueries, conjuncts CONTAIN the scalarExpr (e.g., + // a1 = @scalar), so use VisitEachExists for non-OR conjuncts. + // For OR conjuncts, only remove if no extra subqueries in the OR. // var andlist = nodeLeftFilter.FilterToAndList(); var keeplist = andlist.Where(x => x.VisitEachExists(e => e.Equals(scalarExpr))).ToList(); - andlist.RemoveAll(x => x.VisitEachExists(e => e.Equals(scalarExpr))); - if (andlist.Count == 0) + andlist.RemoveAll(x => + (!(x is LogicOrExpr) && x.VisitEachExists(e => e.Equals(scalarExpr))) || + ((x is LogicOrExpr) && !hasAnyExtraSubqueryExprInOR(x, scalarExpr))); + + // if there is any (expr with @1 or @2), the root should be replaced + canReplace = andlist.Find(x => (x is LogicOrExpr) && hasAnyExtraSubqueryExprInOR(x, scalarExpr)) != null; + + if (andlist.Count == 0 || canReplace) nodeLeft.NullifyFilter(); else { @@ -522,7 +534,8 @@ LogicNode djoinOnRightAggregation(LogicSingleJoin singleJoinNode, ScalarSubquery singleJoinNode.children_[1] = filterNode; // now we have convert it to a right filter plan - var newplan = djoinOnRightFilter(singleJoinNode, scalarExpr); + bool unusedCanReplace = false; + var newplan = djoinOnRightFilter(singleJoinNode, scalarExpr, ref unusedCanReplace); return newplan; } @@ -1082,10 +1095,10 @@ LogicNode oneSubqueryToJoin(LogicNode planWithSubExpr, SubqueryExpr subexpr, ref newplan = existsToMarkJoin(planWithSubExpr, se, ref canRepalce); break; case ScalarSubqueryExpr ss: - newplan = scalarToSingleJoin(planWithSubExpr, ss); + newplan = scalarToSingleJoin(planWithSubExpr, ss, ref canRepalce); break; case InSubqueryExpr si: - newplan = inToMarkJoin(planWithSubExpr, si); + newplan = inToMarkJoin(planWithSubExpr, si, ref canRepalce); break; default: break; diff --git a/test/regress/expect/subqueryd_or.txt b/test/regress/expect/subqueryd_or.txt new file mode 100644 index 00000000..d38b1da2 --- /dev/null +++ b/test/regress/expect/subqueryd_or.txt @@ -0,0 +1,44 @@ +select a1 from a where a2 in (select b2 from b where b2 = a1) or a2 > 2 +PhysicFilter (actual rows=1) + Output: a.a1[0] + Filter: ({#marker@1}[1] or a.a2[2]>2) + -> PhysicMarkJoin Left (actual rows=3) + Output: a.a1[0],{#marker@1}[2],a.a2[1] + Filter: (a.a2[1]=b.b2[2] and b.b2[2]=a.a1[0]) + -> PhysicScanTable a (actual rows=3) + Output: a.a1[0],a.a2[1] + -> PhysicScanTable b (actual rows=3, loops=3) + Output: b.b2[1] +2 + +select a1 from a where a2 not in (select b2 from b where b2 = a1) or a2 > 2 +PhysicFilter (actual rows=3) + Output: a.a1[0] + Filter: ({#marker@1}[1] or a.a2[2]>2) + -> PhysicMarkJoin Left (actual rows=3) + Output: a.a1[0],{#marker@1}[2],a.a2[1] + Filter: (a.a2[1]=b.b2[2] and b.b2[2]=a.a1[0]) + -> PhysicScanTable a (actual rows=3) + Output: a.a1[0],a.a2[1] + -> PhysicScanTable b (actual rows=3, loops=3) + Output: b.b2[1] +0 +1 +2 + +select a1, a3 from a where a.a1 = (select b1 from b where b2 = a2 and b3<4) or a2>1 +PhysicFilter (actual rows=3) + Output: a.a1[0],a.a3[1] + Filter: (a.a1[0]=b.b1[2] or a.a2[3]>1) + -> PhysicSingleJoin Left (actual rows=3) + Output: a.a1[0],a.a3[1],b.b1[3],a.a2[2] + Filter: b.b2[4]=a.a2[2] + -> PhysicScanTable a (actual rows=3) + Output: a.a1[0],a.a3[2],a.a2[1] + -> PhysicScanTable b (actual rows=2, loops=3) + Output: b.b1[0],b.b2[1] + Filter: b.b3[2]<4 +0,2 +1,3 +2,4 + diff --git a/test/regress/sql/subqueryd_or.sql b/test/regress/sql/subqueryd_or.sql new file mode 100644 index 00000000..c4654722 --- /dev/null +++ b/test/regress/sql/subqueryd_or.sql @@ -0,0 +1,13 @@ +-- Issue #271: OR handling in inToMarkJoin and scalarToSingleJoin +set enable_subquery_unnest = true; +set enable_dependent_join_pushdown = false; +set enable_neumann_full_decorrelation = false; + +-- correlated IN subquery in OR: IN matches nothing, a2>2 matches a1=2 +select a1 from a where a2 in (select b2 from b where b2 = a1) or a2 > 2; + +-- NOT IN with OR +select a1 from a where a2 not in (select b2 from b where b2 = a1) or a2 > 2; + +-- scalar subquery in OR +select a1, a3 from a where a.a1 = (select b1 from b where b2 = a2 and b3<4) or a2>1;