diff --git a/pkg/planner/core/integration_test.go b/pkg/planner/core/integration_test.go index b572bec656c94..3a99af79976b8 100644 --- a/pkg/planner/core/integration_test.go +++ b/pkg/planner/core/integration_test.go @@ -111,7 +111,7 @@ func TestAggPushDownEngine(t *testing.T) { " └─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo")) } -func TestIssue15110(t *testing.T) { +func TestIssue15110And49616(t *testing.T) { store := testkit.CreateMockStore(t) tk := testkit.NewTestKit(t, store) tk.MustExec("use test") @@ -145,6 +145,15 @@ func TestIssue15110(t *testing.T) { tk.MustExec("set @@session.tidb_isolation_read_engines = 'tiflash'") tk.MustExec("explain format = 'brief' SELECT count(*) FROM crm_rd_150m dataset_48 WHERE (CASE WHEN (month(dataset_48.customer_first_date)) <= 30 THEN '新客' ELSE NULL END) IS NOT NULL;") + + // for #49616 + tk.MustExec(`use test`) + tk.MustExec("set @@session.tidb_isolation_read_engines = 'tikv'") + tk.MustExec(`create table t1 (k int, a int)`) + tk.MustExec(`create table t2 (k int, b int, key(k))`) + tk.MustHavePlan(`select /*+ tidb_inlj(t2, t1) */ * + from t2 left join t1 on t1.k=t2.k + where a>0 or (a=0 and b>0)`, `IndexJoin`) } func TestKeepOrderHintWithBinding(t *testing.T) { diff --git a/pkg/planner/core/rule_predicate_push_down.go b/pkg/planner/core/rule_predicate_push_down.go index d79226e9cdae3..a960e5aa97490 100644 --- a/pkg/planner/core/rule_predicate_push_down.go +++ b/pkg/planner/core/rule_predicate_push_down.go @@ -440,6 +440,10 @@ func isNullRejected(ctx sessionctx.Context, schema *expression.Schema, expr expr sc.InNullRejectCheck = false }() for _, cond := range expression.SplitCNFItems(expr) { + if isNullRejectedSpecially(ctx, schema, expr) { + return true + } + result := expression.EvaluateExprWithNull(ctx, schema, cond) x, ok := result.(*expression.Constant) if !ok { @@ -454,6 +458,47 @@ func isNullRejected(ctx sessionctx.Context, schema *expression.Schema, expr expr 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 sessionctx.Context, schema *expression.Schema, expr expression.Expression) bool { + return specialNullRejectedCase1(ctx, schema, expr) // only 1 case now +} + +// 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 sessionctx.Context, 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 + } + return nil + } + orFunc := isFunc(expr, ast.LogicOr) + if orFunc == nil { + 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 ...) + } + } + } + return false +} + // PredicatePushDown implements LogicalPlan PredicatePushDown interface. func (p *LogicalExpand) PredicatePushDown(predicates []expression.Expression, opt *logicalOptimizeOp) (ret []expression.Expression, retPlan LogicalPlan) { // Note that, grouping column related predicates can't be pushed down, since grouping column has nullability change after Expand OP itself.