diff --git a/pkg/planner/core/casetest/rule/rule_outer2inner_test.go b/pkg/planner/core/casetest/rule/rule_outer2inner_test.go index ca0ff6f69a2ed..308dfa24790af 100644 --- a/pkg/planner/core/casetest/rule/rule_outer2inner_test.go +++ b/pkg/planner/core/casetest/rule/rule_outer2inner_test.go @@ -36,6 +36,8 @@ func TestOuter2Inner(t *testing.T) { tk.MustExec("CREATE TABLE part(P_PARTKEY INTEGER,P_BRAND CHAR(10),P_CONTAINER CHAR(10))") tk.MustExec("CREATE TABLE d (pk int, col_blob blob, col_blob_key blob, col_varchar_key varchar(1) , col_date date, col_int_key int)") tk.MustExec("CREATE TABLE dd (pk int, col_blob blob, col_blob_key blob, col_date date, col_int_key int)") + tk.MustExec("create table t0 (a0 int, b0 char, c0 char(2))") + tk.MustExec("create table t11 (a1 int, b1 char, c1 char)") var input Input var output []struct { diff --git a/pkg/planner/core/casetest/rule/testdata/outer2inner_in.json b/pkg/planner/core/casetest/rule/testdata/outer2inner_in.json index a7fd1c5cc1d61..2e332efb168fc 100644 --- a/pkg/planner/core/casetest/rule/testdata/outer2inner_in.json +++ b/pkg/planner/core/casetest/rule/testdata/outer2inner_in.json @@ -7,6 +7,9 @@ "select * from t1 left outer join t2 on a1=a2 where not(b2 is null) -- another form of basic case of not null", "select * from t1 left outer join t2 on a1=a2 where c2 = 5 OR b2 < 55 -- case with A OR B (Both A and B are null filtering)", "select * from t1 left outer join t2 on a1=a2 where c2 = 5 AND b2 is null -- case with A AND B (A is null filtering and B is not)", + "select * from t1 left outer join t2 on a1=a2 where b2 is NULL AND c2 = 5 -- case with A AND B (A is null filtering and B is not)", + "select * from t1 left outer join t2 on a1=a2 where not (b2 is NULL OR c2 = 5) -- NOT case ", + "select * from t1 left outer join t2 on a1=a2 where not (b2 is NULL AND c2 = 5) -- NOT case ", "select * from t2 left outer join t1 on a1=a2 where b1+b1 > 2; -- expression evaluates to UNKNOWN/FALSE", "select * from t2 left outer join t1 on a1=a2 where coalesce(b1,2) > 2; -- false condition for b1=NULL", "select * from t2 left outer join t1 on a1=a2 where true and b1 = 5; -- AND with one branch is null filtering", @@ -21,6 +24,7 @@ "WITH cte AS ( SELECT alias1.col_date AS field1 FROM d AS alias1 LEFT JOIN dd AS alias2 ON alias1.col_blob_key=alias2.col_blob_key WHERE alias1.col_varchar_key IS NULL OR alias1.col_blob_key >= 'a') DELETE FROM outr1.*, outr2.* USING d AS outr1 LEFT OUTER JOIN dd AS outr2 ON (outr1.col_date=outr2.col_date) JOIN cte AS outrcte ON outr2.col_blob_key=outrcte.field1 -- nested complex case", "with cte as (select count(a2) as cnt,b2-5 as b3 from t1 left outer join t2 on a1=a2 group by b3) select * from cte where b3 > 1 -- aggregate case.", "select * from dd as outr1 WHERE outr1.col_blob IN (SELECT DISTINCT innr1.col_blob_key AS y FROM d AS innrcte left outer join dd AS innr1 ON innr1.pk = innrcte.col_date WHERE outr1.col_int_key > 6)", + "select * from t0 left outer join t11 on a0=a1 where t0.b0 in (t11.b1, t11.c1) -- each = in the in list is null filtering", "select * from t1 left outer join t2 on a1=a2 where b2 is null -- negative case with single predicate which is not null filtering", "select * from t1 left outer join t2 on a1=a2 where c2 = 5 OR b2 is null -- negative case with A OR B (A is null filtering and B is not)", "select * from t1 left outer join t2 on a1=a2 where not(b2 is not null) -- nested 'not' negative case", @@ -37,7 +41,13 @@ "SELECT * FROM ti LEFT JOIN (SELECT i FROM ti WHERE FALSE) AS d1 ON ti.i = d1.i WHERE NOT EXISTS (SELECT 1 FROM ti AS inner_t1 WHERE i = d1.i) -- anti semi join", "select count(*) from t1 where t1.a1+100 > ( select count(*) from t2 where t1.a1=t2.a2 and t1.b1=t2.b2) group by t1.b1 -- filter not filtering over derived outer join", "with cte as (select count(a2) as cnt,ifnull(b2,5) as b2 from t1 left outer join t2 on a1=a2 group by b2) select * from cte where b2 > 1 -- non null filter on group by", - "with cte as (select count(a2) as cnt,ifnull(b2,5) as b2 from t1 left outer join t2 on a1=a2 group by b2) select * from cte where cnt > 1 -- filter on aggregates not applicable" + "with cte as (select count(a2) as cnt,ifnull(b2,5) as b2 from t1 left outer join t2 on a1=a2 group by b2) select * from cte where cnt > 1 -- filter on aggregates not applicable", + "select * from t0 left outer join t11 on a0=a1 where t0.b0 in (t0.b0, t11.b1)", + "select * from t0 left outer join t11 on a0=a1 where '5' not in (t0.b0, t11.b1)", + "select * from t0 left outer join t11 on a0=a1 where '1' in (t0.b0, t11.b1)", + "select * from t0 left outer join t11 on a0=a1 where t0.b0 in ('5', t11.b1) -- some = in the in list is not null filtering", + "select * from t0 left outer join t11 on a0=a1 where '5' in (t0.b0, t11.b1) -- some = in the in list is not null filtering", + "select * from t1 left outer join t2 on a1=a2 where not (b2 is NOT NULL AND c2 = 5) -- NOT case " ] } ] diff --git a/pkg/planner/core/casetest/rule/testdata/outer2inner_out.json b/pkg/planner/core/casetest/rule/testdata/outer2inner_out.json index 4780b3b9504b8..42dc0d93a4847 100644 --- a/pkg/planner/core/casetest/rule/testdata/outer2inner_out.json +++ b/pkg/planner/core/casetest/rule/testdata/outer2inner_out.json @@ -67,6 +67,45 @@ " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo" ] }, + { + "SQL": "select * from t1 left outer join t2 on a1=a2 where b2 is NULL AND c2 = 5 -- case with A AND B (A is null filtering and B is not)", + "Plan": [ + "Projection 0.01 root test.t1.a1, test.t1.b1, test.t1.c1, test.t2.a2, test.t2.b2, test.t2.c2", + "└─HashJoin 0.01 root inner join, equal:[eq(test.t2.a2, test.t1.a1)]", + " ├─TableReader(Build) 0.01 root data:Selection", + " │ └─Selection 0.01 cop[tikv] eq(test.t2.c2, 5), isnull(test.t2.b2), not(isnull(test.t2.a2))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo", + " └─TableReader(Probe) 9990.00 root data:Selection", + " └─Selection 9990.00 cop[tikv] not(isnull(test.t1.a1))", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo" + ] + }, + { + "SQL": "select * from t1 left outer join t2 on a1=a2 where not (b2 is NULL OR c2 = 5) -- NOT case ", + "Plan": [ + "Projection 9990.00 root test.t1.a1, test.t1.b1, test.t1.c1, test.t2.a2, test.t2.b2, test.t2.c2", + "└─HashJoin 9990.00 root inner join, equal:[eq(test.t2.a2, test.t1.a1)]", + " ├─TableReader(Build) 7992.00 root data:Selection", + " │ └─Selection 7992.00 cop[tikv] and(not(isnull(test.t2.b2)), ne(test.t2.c2, 5)), not(isnull(test.t2.a2))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo", + " └─TableReader(Probe) 9990.00 root data:Selection", + " └─Selection 9990.00 cop[tikv] not(isnull(test.t1.a1))", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo" + ] + }, + { + "SQL": "select * from t1 left outer join t2 on a1=a2 where not (b2 is NULL AND c2 = 5) -- NOT case ", + "Plan": [ + "Projection 12483.33 root test.t1.a1, test.t1.b1, test.t1.c1, test.t2.a2, test.t2.b2, test.t2.c2", + "└─HashJoin 12483.33 root inner join, equal:[eq(test.t2.a2, test.t1.a1)]", + " ├─TableReader(Build) 9986.66 root data:Selection", + " │ └─Selection 9986.66 cop[tikv] not(isnull(test.t2.a2)), or(not(isnull(test.t2.b2)), ne(test.t2.c2, 5))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo", + " └─TableReader(Probe) 9990.00 root data:Selection", + " └─Selection 9990.00 cop[tikv] not(isnull(test.t1.a1))", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo" + ] + }, { "SQL": "select * from t2 left outer join t1 on a1=a2 where b1+b1 > 2; -- expression evaluates to UNKNOWN/FALSE", "Plan": [ @@ -297,6 +336,18 @@ " └─TableFullScan 99900000.00 cop[tikv] table:innr1 keep order:false, stats:pseudo" ] }, + { + "SQL": "select * from t0 left outer join t11 on a0=a1 where t0.b0 in (t11.b1, t11.c1) -- each = in the in list is null filtering", + "Plan": [ + "HashJoin 12487.50 root inner join, equal:[eq(test.t0.a0, test.t11.a1)], other cond:in(test.t0.b0, test.t11.b1, test.t11.c1)", + "├─TableReader(Build) 9990.00 root data:Selection", + "│ └─Selection 9990.00 cop[tikv] not(isnull(test.t11.a1))", + "│ └─TableFullScan 10000.00 cop[tikv] table:t11 keep order:false, stats:pseudo", + "└─TableReader(Probe) 9990.00 root data:Selection", + " └─Selection 9990.00 cop[tikv] not(isnull(test.t0.a0))", + " └─TableFullScan 10000.00 cop[tikv] table:t0 keep order:false, stats:pseudo" + ] + }, { "SQL": "select * from t1 left outer join t2 on a1=a2 where b2 is null -- negative case with single predicate which is not null filtering", "Plan": [ @@ -526,6 +577,78 @@ " └─Selection 9990.00 cop[tikv] not(isnull(test.t2.a2))", " └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo" ] + }, + { + "SQL": "select * from t0 left outer join t11 on a0=a1 where t0.b0 in (t0.b0, t11.b1)", + "Plan": [ + "Selection 9990.00 root in(test.t0.b0, test.t0.b0, test.t11.b1)", + "└─HashJoin 12487.50 root left outer join, equal:[eq(test.t0.a0, test.t11.a1)]", + " ├─TableReader(Build) 9990.00 root data:Selection", + " │ └─Selection 9990.00 cop[tikv] not(isnull(test.t11.a1))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t11 keep order:false, stats:pseudo", + " └─TableReader(Probe) 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t0 keep order:false, stats:pseudo" + ] + }, + { + "SQL": "select * from t0 left outer join t11 on a0=a1 where '5' not in (t0.b0, t11.b1)", + "Plan": [ + "Selection 9990.00 root not(in(\"5\", test.t0.b0, test.t11.b1))", + "└─HashJoin 12487.50 root left outer join, equal:[eq(test.t0.a0, test.t11.a1)]", + " ├─TableReader(Build) 9990.00 root data:Selection", + " │ └─Selection 9990.00 cop[tikv] not(isnull(test.t11.a1))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t11 keep order:false, stats:pseudo", + " └─TableReader(Probe) 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t0 keep order:false, stats:pseudo" + ] + }, + { + "SQL": "select * from t0 left outer join t11 on a0=a1 where '1' in (t0.b0, t11.b1)", + "Plan": [ + "Selection 9990.00 root in(\"1\", test.t0.b0, test.t11.b1)", + "└─HashJoin 12487.50 root left outer join, equal:[eq(test.t0.a0, test.t11.a1)]", + " ├─TableReader(Build) 9990.00 root data:Selection", + " │ └─Selection 9990.00 cop[tikv] not(isnull(test.t11.a1))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t11 keep order:false, stats:pseudo", + " └─TableReader(Probe) 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t0 keep order:false, stats:pseudo" + ] + }, + { + "SQL": "select * from t0 left outer join t11 on a0=a1 where t0.b0 in ('5', t11.b1) -- some = in the in list is not null filtering", + "Plan": [ + "Selection 9990.00 root in(test.t0.b0, \"5\", test.t11.b1)", + "└─HashJoin 12487.50 root left outer join, equal:[eq(test.t0.a0, test.t11.a1)]", + " ├─TableReader(Build) 9990.00 root data:Selection", + " │ └─Selection 9990.00 cop[tikv] not(isnull(test.t11.a1))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t11 keep order:false, stats:pseudo", + " └─TableReader(Probe) 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t0 keep order:false, stats:pseudo" + ] + }, + { + "SQL": "select * from t0 left outer join t11 on a0=a1 where '5' in (t0.b0, t11.b1) -- some = in the in list is not null filtering", + "Plan": [ + "Selection 9990.00 root in(\"5\", test.t0.b0, test.t11.b1)", + "└─HashJoin 12487.50 root left outer join, equal:[eq(test.t0.a0, test.t11.a1)]", + " ├─TableReader(Build) 9990.00 root data:Selection", + " │ └─Selection 9990.00 cop[tikv] not(isnull(test.t11.a1))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t11 keep order:false, stats:pseudo", + " └─TableReader(Probe) 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t0 keep order:false, stats:pseudo" + ] + }, + { + "SQL": "select * from t1 left outer join t2 on a1=a2 where not (b2 is NOT NULL AND c2 = 5) -- NOT case ", + "Plan": [ + "Selection 9990.00 root not(and(not(isnull(test.t2.b2)), eq(test.t2.c2, 5)))", + "└─HashJoin 12487.50 root left outer join, equal:[eq(test.t1.a1, test.t2.a2)]", + " ├─TableReader(Build) 9990.00 root data:Selection", + " │ └─Selection 9990.00 cop[tikv] not(isnull(test.t2.a2))", + " │ └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo", + " └─TableReader(Probe) 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo" + ] } ] } diff --git a/pkg/planner/core/rule_outer_to_inner_join.go b/pkg/planner/core/rule_outer_to_inner_join.go index a1de1f49e3557..83a8244993a12 100644 --- a/pkg/planner/core/rule_outer_to_inner_join.go +++ b/pkg/planner/core/rule_outer_to_inner_join.go @@ -19,7 +19,6 @@ import ( "context" "github.com/pingcap/tidb/pkg/expression" - "github.com/pingcap/tidb/pkg/parser/ast" "github.com/pingcap/tidb/pkg/planner/core/base" "github.com/pingcap/tidb/pkg/planner/util" "github.com/pingcap/tidb/pkg/planner/util/optimizetrace" @@ -87,7 +86,7 @@ func (p *LogicalJoin) ConvertOuterToInnerJoin(predicates []expression.Expression if p.JoinType == LeftOuterJoin || p.JoinType == RightOuterJoin { canBeSimplified := false for _, expr := range predicates { - isOk := isNullFiltered(p.SCtx(), innerTable.Schema(), expr) + isOk := util.IsNullRejected(p.SCtx(), innerTable.Schema(), expr) if isOk { canBeSimplified = true break @@ -149,51 +148,3 @@ func (s *LogicalProjection) ConvertOuterToInnerJoin(predicates []expression.Expr p.SetChildren(child) return p } - -// allConstants checks if only the expression has only constants. -func allConstants(ctx expression.BuildContext, expr expression.Expression) bool { - if expression.MaybeOverOptimized4PlanCache(ctx, []expression.Expression{expr}) { - return false // expression contains non-deterministic parameter - } - switch v := expr.(type) { - case *expression.ScalarFunction: - for _, arg := range v.GetArgs() { - if !allConstants(ctx, arg) { - return false - } - } - return true - case *expression.Constant: - return true - } - return false -} - -// isNullFiltered takes care of complex predicates like this: -// isNullFiltered(A OR B) = isNullFiltered(A) AND isNullFiltered(B) -// isNullFiltered(A AND B) = isNullFiltered(A) OR isNullFiltered(B) -func isNullFiltered(ctx base.PlanContext, innerSchema *expression.Schema, predicate expression.Expression) bool { - // The expression should reference at least one field in innerSchema or all constants. - if !expression.ExprReferenceSchema(predicate, innerSchema) && !allConstants(ctx.GetExprCtx(), predicate) { - return false - } - - switch expr := predicate.(type) { - case *expression.ScalarFunction: - if expr.FuncName.L == ast.LogicAnd { - if isNullFiltered(ctx, innerSchema, expr.GetArgs()[0]) { - return true - } - return isNullFiltered(ctx, innerSchema, expr.GetArgs()[0]) - } else if expr.FuncName.L == ast.LogicOr { - if !(isNullFiltered(ctx, innerSchema, expr.GetArgs()[0])) { - return false - } - return isNullFiltered(ctx, innerSchema, expr.GetArgs()[1]) - } else { - return util.IsNullRejected(ctx, innerSchema, expr) - } - default: - return util.IsNullRejected(ctx, innerSchema, predicate) - } -} diff --git a/pkg/planner/util/BUILD.bazel b/pkg/planner/util/BUILD.bazel index 50fa82be4a09a..ac7e7fe89109b 100644 --- a/pkg/planner/util/BUILD.bazel +++ b/pkg/planner/util/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "//pkg/parser/model", "//pkg/parser/mysql", "//pkg/planner/context", + "//pkg/planner/core/base", "//pkg/sessionctx/stmtctx", "//pkg/tablecodec", "//pkg/types", diff --git a/pkg/planner/util/null_misc.go b/pkg/planner/util/null_misc.go index 5549eb1697c9a..dbf5ee1393b01 100644 --- a/pkg/planner/util/null_misc.go +++ b/pkg/planner/util/null_misc.go @@ -18,76 +18,99 @@ import ( "github.com/pingcap/tidb/pkg/expression" "github.com/pingcap/tidb/pkg/parser/ast" "github.com/pingcap/tidb/pkg/planner/context" + "github.com/pingcap/tidb/pkg/planner/core/base" ) -// IsNullRejected check whether a condition is null-rejected -// A condition would be null-rejected in one of following cases: -// If it is a predicate containing a reference to an inner table that evaluates to UNKNOWN or FALSE -// when one of its arguments is NULL. -// If it is a conjunction containing a null-rejected condition as a conjunct. -// If it is a disjunction of null-rejected conditions. -func IsNullRejected(ctx context.PlanContext, schema *expression.Schema, expr expression.Expression) bool { - exprCtx := ctx.GetNullRejectCheckExprCtx() - expr = expression.PushDownNot(exprCtx, expr) - if expression.ContainOuterNot(expr) { - return false +// allConstants checks if only the expression has only constants. +func allConstants(ctx expression.BuildContext, expr expression.Expression) bool { + if expression.MaybeOverOptimized4PlanCache(ctx, []expression.Expression{expr}) { + return false // expression contains non-deterministic parameter } - sc := ctx.GetSessionVars().StmtCtx - for _, cond := range expression.SplitCNFItems(expr) { - if isNullRejectedSpecially(ctx, schema, expr) { - return true - } - - result := expression.EvaluateExprWithNull(exprCtx, schema, cond) - x, ok := result.(*expression.Constant) - if !ok { - continue - } - if x.Value.IsNull() { - return true - } else if isTrue, err := x.Value.ToBool(sc.TypeCtxOrDefault()); err == nil && isTrue == 0 { - return true + switch v := expr.(type) { + case *expression.ScalarFunction: + for _, arg := range v.GetArgs() { + if !allConstants(ctx, arg) { + return false + } } + return true + case *expression.Constant: + return true } return false } -// isNullRejectedSpecially handles some null-rejected cases specially, since the current in -// EvaluateExprWithNull is too strict for some cases, e.g. #49616. -func isNullRejectedSpecially(ctx context.PlanContext, schema *expression.Schema, expr expression.Expression) bool { - return specialNullRejectedCase1(ctx, schema, expr) // only 1 case now +// isNullRejectedInList checks null filter for IN list using OR logic. +// Reason is that null filtering through evaluation by isNullRejectedSimpleExpr +// has problems with IN list. For example, constant in (outer-table.col1, inner-table.col2) +// is not null rejecting since constant in (outer-table.col1, NULL) is not false/unknown. +func isNullRejectedInList(ctx base.PlanContext, expr *expression.ScalarFunction, innerSchema *expression.Schema) bool { + for i, arg := range expr.GetArgs() { + if i > 0 { + newArgs := make([]expression.Expression, 0, 2) + newArgs = append(newArgs, expr.GetArgs()[0]) + newArgs = append(newArgs, arg) + eQCondition, err := expression.NewFunction(ctx.GetExprCtx(), ast.EQ, expr.GetType(), newArgs...) + if err != nil { + return false + } + if !(isNullRejectedSimpleExpr(ctx, innerSchema, eQCondition)) { + return false + } + } + } + return true } -// specialNullRejectedCase1 is mainly for #49616. -// Case1 specially handles `null-rejected OR (null-rejected AND {others})`, then no matter what the result -// of `{others}` is (True, False or Null), the result of this predicate is null, so this predicate is null-rejected. -func specialNullRejectedCase1(ctx context.PlanContext, schema *expression.Schema, expr expression.Expression) bool { - isFunc := func(e expression.Expression, lowerFuncName string) *expression.ScalarFunction { - f, ok := e.(*expression.ScalarFunction) - if !ok { - return nil - } - if f.FuncName.L == lowerFuncName { - return f +// IsNullRejected takes care of complex predicates like this: +// IsNullRejected(A OR B) = IsNullRejected(A) AND IsNullRejected(B) +// IsNullRejected(A AND B) = IsNullRejected(A) OR IsNullRejected(B) +func IsNullRejected(ctx base.PlanContext, innerSchema *expression.Schema, predicate expression.Expression) bool { + predicate = expression.PushDownNot(ctx.GetNullRejectCheckExprCtx(), predicate) + if expression.ContainOuterNot(predicate) { + return false + } + + switch expr := predicate.(type) { + case *expression.ScalarFunction: + if expr.FuncName.L == ast.LogicAnd { + if IsNullRejected(ctx, innerSchema, expr.GetArgs()[0]) { + return true + } + return IsNullRejected(ctx, innerSchema, expr.GetArgs()[1]) + } else if expr.FuncName.L == ast.LogicOr { + if !(IsNullRejected(ctx, innerSchema, expr.GetArgs()[0])) { + return false + } + return IsNullRejected(ctx, innerSchema, expr.GetArgs()[1]) + } else if expr.FuncName.L == ast.In { + return isNullRejectedInList(ctx, expr, innerSchema) + } else { + return isNullRejectedSimpleExpr(ctx, innerSchema, expr) } - return nil + default: + return isNullRejectedSimpleExpr(ctx, innerSchema, predicate) } - orFunc := isFunc(expr, ast.LogicOr) - if orFunc == nil { +} + +// isNullRejectedSimpleExpr check whether a condition is null-rejected +// A condition would be null-rejected in one of following cases: +// If it is a predicate containing a reference to an inner table (null producing side) that evaluates +// to UNKNOWN or FALSE when one of its arguments is NULL. +func isNullRejectedSimpleExpr(ctx context.PlanContext, schema *expression.Schema, expr expression.Expression) bool { + // The expression should reference at least one field in innerSchema or all constants. + if !expression.ExprReferenceSchema(expr, schema) && !allConstants(ctx.GetExprCtx(), expr) { return false } - for i := 0; i < 2; i++ { - andFunc := isFunc(orFunc.GetArgs()[i], ast.LogicAnd) - if andFunc == nil { - continue - } - if !IsNullRejected(ctx, schema, orFunc.GetArgs()[1-i]) { - continue // the other side should be null-rejected: null-rejected OR (... AND ...) - } - for _, andItem := range expression.SplitCNFItems(andFunc) { - if IsNullRejected(ctx, schema, andItem) { - return true // hit the case in the comment: null-rejected OR (null-rejected AND ...) - } + exprCtx := ctx.GetNullRejectCheckExprCtx() + sc := ctx.GetSessionVars().StmtCtx + result := expression.EvaluateExprWithNull(exprCtx, schema, expr) + x, ok := result.(*expression.Constant) + if ok { + if x.Value.IsNull() { + return true + } else if isTrue, err := x.Value.ToBool(sc.TypeCtxOrDefault()); err == nil && isTrue == 0 { + return true } } return false