Skip to content

Commit 631cdbd

Browse files
authored
planner: constant folding to isnull(not null column) (pingcap#62163)
ref pingcap#7973, close pingcap#62050
1 parent 54f20c3 commit 631cdbd

File tree

8 files changed

+170
-3
lines changed

8 files changed

+170
-3
lines changed

pkg/planner/core/casetest/rule/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ go_test(
1010
"rule_join_reorder_test.go",
1111
"rule_outer2inner_test.go",
1212
"rule_predicate_pushdown_test.go",
13+
"rule_predicate_simplification_test.go",
1314
],
1415
data = glob(["testdata/**"]),
1516
flaky = True,
16-
shard_count = 8,
17+
shard_count = 9,
1718
deps = [
1819
"//pkg/domain",
1920
"//pkg/expression",

pkg/planner/core/casetest/rule/main_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func TestMain(m *testing.M) {
3333
testDataMap.LoadTestSuiteData("testdata", "derive_topn_from_window")
3434
testDataMap.LoadTestSuiteData("testdata", "join_reorder_suite")
3535
testDataMap.LoadTestSuiteData("testdata", "predicate_pushdown_suite")
36+
testDataMap.LoadTestSuiteData("testdata", "predicate_simplification")
3637
opts := []goleak.Option{
3738
goleak.IgnoreTopFunction("github.com/golang/glog.(*fileSink).flushDaemon"),
3839
goleak.IgnoreTopFunction("github.com/bazelbuild/rules_go/go/tools/bzltestutil.RegisterTimeoutHandler.func1"),
@@ -66,3 +67,7 @@ func GetJoinReorderSuiteData() testdata.TestData {
6667
func GetPredicatePushdownSuiteData() testdata.TestData {
6768
return testDataMap["predicate_pushdown_suite"]
6869
}
70+
71+
func GetPredicateSimplificationSuiteData() testdata.TestData {
72+
return testDataMap["predicate_simplification"]
73+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2025 PingCAP, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package rule
16+
17+
import (
18+
"testing"
19+
20+
"github.com/pingcap/tidb/pkg/testkit"
21+
"github.com/pingcap/tidb/pkg/testkit/testdata"
22+
)
23+
24+
func TestPredicateSimplification(t *testing.T) {
25+
store := testkit.CreateMockStore(t)
26+
tk := testkit.NewTestKit(t, store)
27+
tk.MustExec("use test")
28+
tk.MustExec(`CREATE TABLE t1 (
29+
id VARCHAR(64) PRIMARY KEY
30+
);`)
31+
tk.MustExec(`CREATE TABLE t2 (
32+
c1 VARCHAR(64) NOT NULL,
33+
c2 VARCHAR(64) NOT NULL,
34+
c3 VARCHAR(64) NOT NULL,
35+
PRIMARY KEY (c1, c2, c3),
36+
KEY c3 (c3)
37+
);`)
38+
tk.MustExec(`CREATE TABLE t3 (
39+
c1 VARCHAR(64) NOT NULL,
40+
c2 VARCHAR(64) NOT NULL,
41+
c3 VARCHAR(64) NOT NULL,
42+
PRIMARY KEY (c1, c2, c3),
43+
KEY c3 (c3)
44+
);`)
45+
tk.MustExec(`CREATE TABLE t4 (
46+
c1 VARCHAR(64) NOT NULL,
47+
c2 VARCHAR(64) NOT NULL,
48+
c3 VARCHAR(64) NOT NULL,
49+
state VARCHAR(64) NOT NULL DEFAULT 'ACTIVE',
50+
PRIMARY KEY (c1, c2, c3),
51+
KEY c3 (c3)
52+
);`)
53+
tk.MustExec(`CREATE TABLE t5 (
54+
c1 VARCHAR(64) NOT NULL,
55+
c2 VARCHAR(64) NOT NULL,
56+
PRIMARY KEY (c1, c2)
57+
);`)
58+
// since the plan may differ under different planner mode, recommend to record explain result to json accordingly.
59+
var input []string
60+
var output []struct {
61+
SQL string
62+
Plan []string
63+
}
64+
suite := GetPredicateSimplificationSuiteData()
65+
suite.LoadTestCases(t, &input, &output)
66+
for i, tt := range input {
67+
testdata.OnRecord(func() {
68+
output[i].SQL = tt
69+
output[i].Plan = testdata.ConvertRowsToStrings(tk.MustQuery("explain format=brief " + tt).Rows())
70+
})
71+
res := tk.MustQuery("explain format=brief " + tt)
72+
res.Check(testkit.Rows(output[i].Plan...))
73+
}
74+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
{
3+
"name": "TestPredicateSimplification",
4+
"cases": [
5+
"SELECT i.id, ip_products.products FROM t1 AS i LEFT JOIN t4 ON i.id = t4.c3 LEFT JOIN (SELECT t4.c3, GROUP_CONCAT(DISTINCT t2.c3 ORDER BY t2.c3 ASC) AS products FROM t4 JOIN t3 ON t4.c1 = t3.c1 AND t4.c2 = t3.c2 LEFT JOIN t2 ON t4.c1 = t2.c1 AND t4.c2 = t2.c2 WHERE t3.c3 = 'production' AND t4.state = 'ACTIVE' GROUP BY t4.c3, t4.c1, t4.c2) AS ip_products ON t4.c3 = ip_products.c3 LEFT JOIN t5 ON i.id = t5.c1 AND t5.c2 = 'production' WHERE t4.state = 'ACTIVE' AND t5.c1 IS NULL GROUP BY i.id, ip_products.products HAVING FIND_IN_SET('info', products) ORDER BY i.id ASC LIMIT 500 OFFSET 5500;"
6+
]
7+
}
8+
]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"Name": "TestPredicateSimplification",
4+
"Cases": [
5+
{
6+
"SQL": "SELECT i.id, ip_products.products FROM t1 AS i LEFT JOIN t4 ON i.id = t4.c3 LEFT JOIN (SELECT t4.c3, GROUP_CONCAT(DISTINCT t2.c3 ORDER BY t2.c3 ASC) AS products FROM t4 JOIN t3 ON t4.c1 = t3.c1 AND t4.c2 = t3.c2 LEFT JOIN t2 ON t4.c1 = t2.c1 AND t4.c2 = t2.c2 WHERE t3.c3 = 'production' AND t4.state = 'ACTIVE' GROUP BY t4.c3, t4.c1, t4.c2) AS ip_products ON t4.c3 = ip_products.c3 LEFT JOIN t5 ON i.id = t5.c1 AND t5.c2 = 'production' WHERE t4.state = 'ACTIVE' AND t5.c1 IS NULL GROUP BY i.id, ip_products.products HAVING FIND_IN_SET('info', products) ORDER BY i.id ASC LIMIT 500 OFFSET 5500;",
7+
"Plan": [
8+
"TopN 8.00 root test.t1.id, offset:5500, count:500",
9+
"└─HashAgg 8.00 root group by:Column#16, test.t1.id, funcs:firstrow(test.t1.id)->test.t1.id, funcs:firstrow(Column#16)->Column#16",
10+
" └─Selection 8.00 root isnull(test.t5.c1)",
11+
" └─Projection 10.00 root test.t1.id, Column#16, test.t5.c1",
12+
" └─HashJoin 10.00 root inner join, equal:[eq(test.t4.c3, test.t4.c3)]",
13+
" ├─Selection(Build) 6.40 root find_in_set(\"info\", Column#16)",
14+
" │ └─HashAgg 8.00 root group by:test.t4.c1, test.t4.c2, test.t4.c3, funcs:group_concat(distinct test.t2.c3 order by test.t2.c3 separator \",\")->Column#16, funcs:firstrow(test.t4.c3)->test.t4.c3",
15+
" │ └─IndexJoin 15.62 root left outer join, inner:TableReader, outer key:test.t4.c1, test.t4.c2, inner key:test.t2.c1, test.t2.c2, equal cond:eq(test.t4.c1, test.t2.c1), eq(test.t4.c2, test.t2.c2)",
16+
" │ ├─IndexJoin(Build) 12.50 root inner join, inner:TableReader, outer key:test.t3.c1, test.t3.c2, inner key:test.t4.c1, test.t4.c2, equal cond:eq(test.t3.c1, test.t4.c1), eq(test.t3.c2, test.t4.c2)",
17+
" │ │ ├─IndexReader(Build) 10.00 root index:IndexRangeScan",
18+
" │ │ │ └─IndexRangeScan 10.00 cop[tikv] table:t3, index:c3(c3) range:[\"production\",\"production\"], keep order:false, stats:pseudo",
19+
" │ │ └─TableReader(Probe) 0.01 root data:Selection",
20+
" │ │ └─Selection 0.01 cop[tikv] eq(test.t4.state, \"ACTIVE\")",
21+
" │ │ └─TableRangeScan 10.00 cop[tikv] table:t4 range: decided by [eq(test.t4.c1, test.t3.c1) eq(test.t4.c2, test.t3.c2)], keep order:false, stats:pseudo",
22+
" │ └─TableReader(Probe) 12.50 root data:TableRangeScan",
23+
" │ └─TableRangeScan 12.50 cop[tikv] table:t2 range: decided by [eq(test.t2.c1, test.t4.c1) eq(test.t2.c2, test.t4.c2)], keep order:false, stats:pseudo",
24+
" └─IndexJoin(Probe) 12.50 root left outer join, inner:TableReader, outer key:test.t1.id, inner key:test.t5.c1, equal cond:eq(test.t1.id, test.t5.c1)",
25+
" ├─IndexJoin(Build) 12.50 root inner join, inner:TableReader, outer key:test.t4.c3, inner key:test.t1.id, equal cond:eq(test.t4.c3, test.t1.id)",
26+
" │ ├─TableReader(Build) 10.00 root data:Selection",
27+
" │ │ └─Selection 10.00 cop[tikv] eq(test.t4.state, \"ACTIVE\")",
28+
" │ │ └─TableFullScan 10000.00 cop[tikv] table:t4 keep order:false, stats:pseudo",
29+
" │ └─TableReader(Probe) 10.00 root data:TableRangeScan",
30+
" │ └─TableRangeScan 10.00 cop[tikv] table:i range: decided by [eq(test.t1.id, test.t4.c3)], keep order:false, stats:pseudo",
31+
" └─TableReader(Probe) 0.01 root data:Selection",
32+
" └─Selection 0.01 cop[tikv] eq(test.t5.c2, \"production\")",
33+
" └─TableRangeScan 12.50 cop[tikv] table:t5 range: decided by [eq(test.t5.c1, test.t1.id) eq(test.t5.c2, production)], keep order:false, stats:pseudo"
34+
]
35+
}
36+
]
37+
}
38+
]

pkg/planner/core/constraint/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ go_library(
77
visibility = ["//visibility:public"],
88
deps = [
99
"//pkg/expression",
10+
"//pkg/parser/ast",
11+
"//pkg/parser/mysql",
1012
"//pkg/planner/core/base",
1113
],
1214
)

pkg/planner/core/constraint/exprs.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
package constraint
1616

1717
import (
18+
"slices"
19+
1820
"github.com/pingcap/tidb/pkg/expression"
21+
"github.com/pingcap/tidb/pkg/parser/ast"
22+
"github.com/pingcap/tidb/pkg/parser/mysql"
1923
"github.com/pingcap/tidb/pkg/planner/core/base"
2024
)
2125

@@ -40,3 +44,31 @@ func DeleteTrueExprs(p base.LogicalPlan, conds []expression.Expression) []expres
4044
}
4145
return newConds
4246
}
47+
48+
// DeleteTrueExprsBySchema delete true expressions such as not(isnull(not null column)).
49+
// It is used in the predicate pushdown optimization to remove unnecessary conditions which will be pushed down to child operators.
50+
func DeleteTrueExprsBySchema(ctx expression.EvalContext, schema *expression.Schema, conds []expression.Expression) []expression.Expression {
51+
return slices.DeleteFunc(conds, func(item expression.Expression) bool {
52+
if expr, ok := item.(*expression.ScalarFunction); ok && expr.FuncName.L == ast.UnaryNot {
53+
if args := expr.GetArgs(); len(args) == 1 {
54+
// If the expression is `not(isnull(not null column))`, we can remove it.
55+
return isNullWithNotNullColumn(ctx, schema, args[0])
56+
}
57+
}
58+
return false
59+
})
60+
}
61+
62+
// isNullWithNotNullColumn checks if the expression is `isnull(not null column)`.
63+
func isNullWithNotNullColumn(ctx expression.EvalContext, schema *expression.Schema, expr expression.Expression) bool {
64+
if e, ok := expr.(*expression.ScalarFunction); ok && e.FuncName.L == ast.IsNull {
65+
if args := e.GetArgs(); len(args) == 1 {
66+
if col, ok := args[0].(*expression.Column); ok {
67+
if retrieveColumn := schema.RetrieveColumn(col); retrieveColumn != nil {
68+
return mysql.HasNotNullFlag(retrieveColumn.GetType(ctx).GetFlag())
69+
}
70+
}
71+
}
72+
}
73+
return false
74+
}

pkg/planner/core/operator/logicalop/logical_join.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/pingcap/tidb/pkg/parser/mysql"
2727
"github.com/pingcap/tidb/pkg/planner/cardinality"
2828
"github.com/pingcap/tidb/pkg/planner/core/base"
29+
"github.com/pingcap/tidb/pkg/planner/core/constraint"
2930
"github.com/pingcap/tidb/pkg/planner/core/cost"
3031
ruleutil "github.com/pingcap/tidb/pkg/planner/core/rule/util"
3132
"github.com/pingcap/tidb/pkg/planner/funcdep"
@@ -282,8 +283,14 @@ func (p *LogicalJoin) PredicatePushDown(predicates []expression.Expression, opt
282283
}
283284
leftCond = expression.RemoveDupExprs(leftCond)
284285
rightCond = expression.RemoveDupExprs(rightCond)
285-
leftRet, lCh := p.Children()[0].PredicatePushDown(leftCond, opt)
286-
rightRet, rCh := p.Children()[1].PredicatePushDown(rightCond, opt)
286+
evalCtx := p.SCtx().GetExprCtx().GetEvalCtx()
287+
children := p.Children()
288+
rightChild := children[1]
289+
leftChild := children[0]
290+
rightCond = constraint.DeleteTrueExprsBySchema(evalCtx, rightChild.Schema(), rightCond)
291+
leftCond = constraint.DeleteTrueExprsBySchema(evalCtx, leftChild.Schema(), leftCond)
292+
leftRet, lCh := leftChild.PredicatePushDown(leftCond, opt)
293+
rightRet, rCh := rightChild.PredicatePushDown(rightCond, opt)
287294
addSelection(p, lCh, leftRet, 0, opt)
288295
addSelection(p, rCh, rightRet, 1, opt)
289296
p.updateEQCond()

0 commit comments

Comments
 (0)