diff --git a/expression/util.go b/expression/util.go index a6d9ef0d6169d..7929d3384c78f 100644 --- a/expression/util.go +++ b/expression/util.go @@ -538,6 +538,41 @@ func PushDownNot(ctx sessionctx.Context, expr Expression) Expression { return newExpr } +// ContainOuterNot checks if there is an outer `not`. +func ContainOuterNot(expr Expression) bool { + return containOuterNot(expr, false) +} + +// containOuterNot checks if there is an outer `not`. +// Input `not` means whether there is `not` outside `expr` +// +// eg. +// not(0+(t.a == 1 and t.b == 2)) returns true +// not(t.a) and not(t.b) returns false +func containOuterNot(expr Expression, not bool) bool { + if f, ok := expr.(*ScalarFunction); ok { + switch f.FuncName.L { + case ast.UnaryNot: + return containOuterNot(f.GetArgs()[0], true) + case ast.IsTruthWithNull, ast.IsNull: + return containOuterNot(f.GetArgs()[0], not) + default: + if not { + return true + } + hasNot := false + for _, expr := range f.GetArgs() { + hasNot = hasNot || containOuterNot(expr, not) + if hasNot { + return hasNot + } + } + return hasNot + } + } + return false +} + // Contains tests if `exprs` contains `e`. func Contains(exprs []Expression, e Expression) bool { for _, expr := range exprs { diff --git a/planner/core/integration_test.go b/planner/core/integration_test.go index 847af9161c092..2e2076d55fa93 100644 --- a/planner/core/integration_test.go +++ b/planner/core/integration_test.go @@ -5150,3 +5150,29 @@ func (s *testIntegrationSuite) TestIndexMergeWithCorrelatedColumns(c *C) { } } + +func (s *testIntegrationSuite) TestIssue20510(c *C) { + tk := testkit.NewTestKit(c, s.store) + tk.MustExec("use test") + + tk.MustExec("drop table if exists t1, t2") + tk.MustExec("CREATE TABLE t1 (a int PRIMARY KEY, b int)") + tk.MustExec("CREATE TABLE t2 (a int PRIMARY KEY, b int)") + tk.MustExec("INSERT INTO t1 VALUES (1,1), (2,1), (3,1), (4,2)") + tk.MustExec("INSERT INTO t2 VALUES (1,2), (2,2)") + + tk.MustQuery("explain format=brief SELECT * FROM t1 LEFT JOIN t2 ON t1.a=t2.a WHERE not(0+(t1.a=30 and t2.b=1));").Check(testkit.Rows( + "Selection 8000.00 root not(plus(0, and(eq(test.t1.a, 30), eq(test.t2.b, 1))))", + "└─MergeJoin 10000.00 root left outer join, left key:test.t1.a, right key:test.t2.a", + " ├─TableReader(Build) 8000.00 root data:Selection", + " │ └─Selection 8000.00 cop[tikv] not(istrue_with_null(plus(0, and(eq(test.t2.a, 30), eq(test.t2.b, 1)))))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:true, stats:pseudo", + " └─TableReader(Probe) 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:true, stats:pseudo")) + tk.MustQuery("SELECT * FROM t1 LEFT JOIN t2 ON t1.a=t2.a WHERE not(0+(t1.a=30 and t2.b=1));").Check(testkit.Rows( + "1 1 1 2", + "2 1 2 2", + "3 1 ", + "4 2 ", + )) +} diff --git a/planner/core/rule_predicate_push_down.go b/planner/core/rule_predicate_push_down.go index feed34d7ee567..2d90c647933e1 100644 --- a/planner/core/rule_predicate_push_down.go +++ b/planner/core/rule_predicate_push_down.go @@ -376,6 +376,9 @@ func simplifyOuterJoin(p *LogicalJoin, predicates []expression.Expression) { // If it is a disjunction of null-rejected conditions. func isNullRejected(ctx sessionctx.Context, schema *expression.Schema, expr expression.Expression) bool { expr = expression.PushDownNot(ctx, expr) + if expression.ContainOuterNot(expr) { + return false + } sc := ctx.GetSessionVars().StmtCtx sc.InNullRejectCheck = true result := expression.EvaluateExprWithNull(ctx, schema, expr) diff --git a/planner/core/testdata/plan_suite_unexported_in.json b/planner/core/testdata/plan_suite_unexported_in.json index f32d4d4ab8123..06c90571e11fe 100644 --- a/planner/core/testdata/plan_suite_unexported_in.json +++ b/planner/core/testdata/plan_suite_unexported_in.json @@ -556,7 +556,9 @@ "select * from t t1 left join t t2 on t1.b = t2.b where not (t1.c > 1 or t2.c > 1);", "select * from t t1 left join t t2 on t1.b = t2.b where not (t1.c > 1 and t2.c > 1);", "select * from t t1 left join t t2 on t1.b > 1 where t1.c = t2.c;", - "select * from t t1 left join t t2 on true where t1.b <=> t2.b;" + "select * from t t1 left join t t2 on true where t1.b <=> t2.b;", + "select * from t t1 left join t t2 on t1.b = t2.b where not(0+(t1.c=1 and t2.c=2));", + "select * from t t1 left join t t2 on t1.b = t2.b where not(t1.c) and not(t2.c)" ] }, { diff --git a/planner/core/testdata/plan_suite_unexported_out.json b/planner/core/testdata/plan_suite_unexported_out.json index 391797fa59644..4fc39a9e7d4d0 100644 --- a/planner/core/testdata/plan_suite_unexported_out.json +++ b/planner/core/testdata/plan_suite_unexported_out.json @@ -991,6 +991,14 @@ { "Best": "Join{DataScan(t1)->DataScan(t2)}->Sel([nulleq(test.t.b, test.t.b)])->Projection", "JoinType": "left outer join" + }, + { + "Best": "Join{DataScan(t1)->DataScan(t2)}(test.t.b,test.t.b)->Sel([not(plus(0, and(eq(test.t.c, 1), eq(test.t.c, 2))))])->Projection", + "JoinType": "left outer join" + }, + { + "Best": "Join{DataScan(t1)->DataScan(t2)}(test.t.b,test.t.b)->Projection", + "JoinType": "inner join" } ] },