diff --git a/executor/executor.go b/executor/executor.go index dd7a82dc1fadb..eb7b7930b7804 100644 --- a/executor/executor.go +++ b/executor/executor.go @@ -1743,12 +1743,14 @@ func ResetContextOfStmt(ctx sessionctx.Context, s ast.StmtNode) (err error) { sc.OriginalSQL = s.Text() if explainStmt, ok := s.(*ast.ExplainStmt); ok { sc.InExplainStmt = true + sc.InExplainAnalyzeStmt = explainStmt.Analyze sc.IgnoreExplainIDSuffix = (strings.ToLower(explainStmt.Format) == types.ExplainFormatBrief) sc.InVerboseExplain = strings.ToLower(explainStmt.Format) == types.ExplainFormatVerbose s = explainStmt.Stmt } if explainForStmt, ok := s.(*ast.ExplainForStmt); ok { sc.InExplainStmt = true + sc.InExplainAnalyzeStmt = true sc.InVerboseExplain = strings.ToLower(explainForStmt.Format) == types.ExplainFormatVerbose } // TODO: Many same bool variables here. diff --git a/expression/expression.go b/expression/expression.go index 3566b2ccd84e3..f40a6325fa737 100644 --- a/expression/expression.go +++ b/expression/expression.go @@ -47,6 +47,7 @@ const ( columnFlag byte = 1 scalarFunctionFlag byte = 3 parameterFlag byte = 4 + ScalarSubQFlag byte = 5 ) // EvalAstExpr evaluates ast expression directly. diff --git a/planner/core/common_plans.go b/planner/core/common_plans.go index 6fe06a63ddc3b..134decdc2f702 100644 --- a/planner/core/common_plans.go +++ b/planner/core/common_plans.go @@ -1303,6 +1303,10 @@ func (e *Explain) RenderResult() error { if err != nil { return err } + err = e.explainPlanInRowFormatScalarSubquery() + if err != nil { + return err + } } case types.ExplainFormatDOT: if physicalPlan, ok := e.TargetPlan.(PhysicalPlan); ok { @@ -1338,6 +1342,25 @@ func (e *Explain) explainPlanInRowFormatCTE() (err error) { return } +func (e *Explain) explainPlanInRowFormatScalarSubquery() (err error) { + if e.ctx == nil { + return nil + } + for _, subQ := range e.ctx.GetSessionVars().MapScalarSubQ { + scalarQ, ok := subQ.(*ScalarSubqueryEvalCtx) + if !ok { + continue + } + e.prepareOperatorInfo(scalarQ, "root", "", "", true) + childIndent := texttree.Indent4Child("", true) + err = e.explainPlanInRowFormat(scalarQ.scalarSubQuery, "root", "", childIndent, true) + if err != nil { + return + } + } + return +} + // explainPlanInRowFormat generates explain information for root-tasks. func (e *Explain) explainPlanInRowFormat(p Plan, taskType, driverSide, indent string, isLastChild bool) (err error) { e.prepareOperatorInfo(p, taskType, driverSide, indent, isLastChild) diff --git a/planner/core/expression_rewriter.go b/planner/core/expression_rewriter.go index abf8e4ae4c392..30a475195e728 100644 --- a/planner/core/expression_rewriter.go +++ b/planner/core/expression_rewriter.go @@ -832,6 +832,33 @@ func (er *expressionRewriter) handleExistSubquery(ctx context.Context, v *ast.Ex er.err = err return v, true } + if er.b.ctx.GetSessionVars().StmtCtx.InExplainStmt && !er.b.ctx.GetSessionVars().StmtCtx.InExplainAnalyzeStmt && er.b.ctx.GetSessionVars().ExplainNonEvaledSubQuery { + newColID := er.b.ctx.GetSessionVars().AllocPlanColumnID() + subqueryCtx := ScalarSubqueryEvalCtx{ + scalarSubQuery: physicalPlan, + ctx: ctx, + is: er.b.is, + outputColIDs: []int64{newColID}, + }.Init(er.b.ctx, np.SelectBlockOffset()) + scalarSubQ := &ScalarSubQueryExpr{ + scalarSubqueryColID: newColID, + evalCtx: subqueryCtx, + } + scalarSubQ.RetType = np.Schema().Columns[0].GetType() + scalarSubQ.SetCoercibility(np.Schema().Columns[0].Coercibility()) + er.b.ctx.GetSessionVars().RegisterScalarSubQ(subqueryCtx) + if v.Not { + notWrapped, err := expression.NewFunction(er.b.ctx, ast.UnaryNot, types.NewFieldType(mysql.TypeTiny), scalarSubQ) + if err != nil { + er.err = err + return v, true + } + er.ctxStackAppend(notWrapped, types.EmptyName) + return v, true + } + er.ctxStackAppend(scalarSubQ, types.EmptyName) + return v, true + } row, err := EvalSubqueryFirstRow(ctx, physicalPlan, er.b.is, er.b.ctx) if err != nil { er.err = err @@ -1018,6 +1045,40 @@ func (er *expressionRewriter) handleScalarSubquery(ctx context.Context, v *ast.S er.err = err return v, true } + if er.b.ctx.GetSessionVars().StmtCtx.InExplainStmt && !er.b.ctx.GetSessionVars().StmtCtx.InExplainAnalyzeStmt && er.b.ctx.GetSessionVars().ExplainNonEvaledSubQuery { + subqueryCtx := ScalarSubqueryEvalCtx{ + scalarSubQuery: physicalPlan, + ctx: ctx, + is: er.b.is, + }.Init(er.b.ctx, np.SelectBlockOffset()) + newColIDs := make([]int64, 0, np.Schema().Len()) + newScalarSubQueryExprs := make([]expression.Expression, 0, np.Schema().Len()) + for _, col := range np.Schema().Columns { + newColID := er.b.ctx.GetSessionVars().AllocPlanColumnID() + scalarSubQ := &ScalarSubQueryExpr{ + scalarSubqueryColID: newColID, + evalCtx: subqueryCtx, + } + scalarSubQ.RetType = col.RetType + scalarSubQ.SetCoercibility(col.Coercibility()) + newColIDs = append(newColIDs, newColID) + newScalarSubQueryExprs = append(newScalarSubQueryExprs, scalarSubQ) + } + subqueryCtx.outputColIDs = newColIDs + + er.b.ctx.GetSessionVars().RegisterScalarSubQ(subqueryCtx) + if len(newScalarSubQueryExprs) == 1 { + er.ctxStackAppend(newScalarSubQueryExprs[0], types.EmptyName) + } else { + rowFunc, err := er.newFunction(ast.RowFunc, newScalarSubQueryExprs[0].GetType(), newScalarSubQueryExprs...) + if err != nil { + er.err = err + return v, true + } + er.ctxStack = append(er.ctxStack, rowFunc) + } + return v, true + } row, err := EvalSubqueryFirstRow(ctx, physicalPlan, er.b.is, er.b.ctx) if err != nil { er.err = err diff --git a/planner/core/initialize.go b/planner/core/initialize.go index 5a32d0294e952..c83c201621ffb 100644 --- a/planner/core/initialize.go +++ b/planner/core/initialize.go @@ -555,3 +555,9 @@ func (p PhysicalCTETable) Init(ctx sessionctx.Context, stats *property.StatsInfo p.stats = stats return &p } + +// Init initializes ScalarSubqueryEvalCtx +func (p ScalarSubqueryEvalCtx) Init(ctx sessionctx.Context, offset int) *ScalarSubqueryEvalCtx { + p.basePlan = newBasePlan(ctx, plancodec.TypeScalarSubQuery, offset) + return &p +} diff --git a/planner/core/scalar_subq_expression.go b/planner/core/scalar_subq_expression.go new file mode 100644 index 0000000000000..8909b632d4f21 --- /dev/null +++ b/planner/core/scalar_subq_expression.go @@ -0,0 +1,322 @@ +// Copyright 2023 PingCAP, Ins. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package core + +import ( + "context" + "fmt" + "strings" + + "github.com/pingcap/errors" + "github.com/pingcap/tidb/expression" + "github.com/pingcap/tidb/infoschema" + "github.com/pingcap/tidb/sessionctx" + "github.com/pingcap/tidb/sessionctx/stmtctx" + "github.com/pingcap/tidb/types" + "github.com/pingcap/tidb/types/json" + "github.com/pingcap/tidb/util/chunk" + "github.com/pingcap/tidb/util/codec" +) + +// ScalarSubqueryEvalCtx store the plan for the subquery, used by ScalarSubQueryExpr. +type ScalarSubqueryEvalCtx struct { + basePlan + + // The context for evaluating the subquery. + scalarSubQuery PhysicalPlan + ctx context.Context + is infoschema.InfoSchema + evalErr error + evaled bool + + outputColIDs []int64 + colsData []types.Datum +} + +func (ssctx *ScalarSubqueryEvalCtx) getColVal(colID int64) (*types.Datum, error) { + err := ssctx.selfEval() + if err != nil { + return nil, err + } + for i, id := range ssctx.outputColIDs { + if id == colID { + return &ssctx.colsData[i], nil + } + } + return nil, errors.Errorf("Could not found the ScalarSubQueryExpr#%d in the ScalarSubquery_%d", colID, ssctx.ID()) +} + +func (ssctx *ScalarSubqueryEvalCtx) selfEval() error { + if ssctx.evaled { + return ssctx.evalErr + } + ssctx.evaled = true + row, err := EvalSubqueryFirstRow(ssctx.ctx, ssctx.scalarSubQuery, ssctx.is, ssctx.SCtx()) + if err != nil { + ssctx.evalErr = err + return err + } + ssctx.colsData = row + return nil +} + +// ScalarSubQueryExpr is a expression placeholder for the non-correlated scalar subqueries which can be evaluated during optimizing phase. +// TODO: The methods related with evaluate the function will be revised in next step. +type ScalarSubQueryExpr struct { + scalarSubqueryColID int64 + + // The context for evaluating the subquery. + evalCtx *ScalarSubqueryEvalCtx + evalErr error + evaled bool + + hashcode []byte + + expression.Constant +} + +func (s *ScalarSubQueryExpr) selfEvaluate() error { + colVal, err := s.evalCtx.getColVal(s.scalarSubqueryColID) + if err != nil { + s.evalErr = err + s.Constant = *expression.NewNull() + return err + } + s.Constant.Value = *colVal + s.evaled = true + return nil +} + +// Eval implements the Expression interface. +func (s *ScalarSubQueryExpr) Eval(_ chunk.Row) (types.Datum, error) { + if s.evaled { + return s.Value, nil + } + if s.evalErr != nil { + return s.Value, s.evalErr + } + err := s.selfEvaluate() + return s.Value, err +} + +// EvalInt returns the int64 representation of expression. +func (*ScalarSubQueryExpr) EvalInt(_ sessionctx.Context, _ chunk.Row) (val int64, isNull bool, err error) { + return 0, false, errors.Errorf("Evaluation methods is not implemented for ScalarSubQueryExpr") +} + +// EvalReal returns the float64 representation of expression. +func (*ScalarSubQueryExpr) EvalReal(_ sessionctx.Context, _ chunk.Row) (val float64, isNull bool, err error) { + return 0, false, errors.Errorf("Evaluation methods is not implemented for ScalarSubQueryExpr") +} + +// EvalString returns the string representation of expression. +func (*ScalarSubQueryExpr) EvalString(_ sessionctx.Context, _ chunk.Row) (val string, isNull bool, err error) { + return "", false, errors.Errorf("Evaluation methods is not implemented for ScalarSubQueryExpr") +} + +// EvalDecimal returns the decimal representation of expression. +func (*ScalarSubQueryExpr) EvalDecimal(_ sessionctx.Context, _ chunk.Row) (val *types.MyDecimal, isNull bool, err error) { + return nil, false, errors.Errorf("Evaluation methods is not implemented for ScalarSubQueryExpr") +} + +// EvalTime returns the DATE/DATETIME/TIMESTAMP representation of expression. +func (*ScalarSubQueryExpr) EvalTime(_ sessionctx.Context, _ chunk.Row) (val types.Time, isNull bool, err error) { + return types.ZeroTime, false, errors.Errorf("Evaluation methods is not implemented for ScalarSubQueryExpr") +} + +// EvalDuration returns the duration representation of expression. +func (*ScalarSubQueryExpr) EvalDuration(_ sessionctx.Context, _ chunk.Row) (val types.Duration, isNull bool, err error) { + return types.ZeroDuration, false, errors.Errorf("Evaluation methods is not implemented for ScalarSubQueryExpr") +} + +// EvalJSON returns the JSON representation of expression. +func (*ScalarSubQueryExpr) EvalJSON(_ sessionctx.Context, _ chunk.Row) (val json.BinaryJSON, isNull bool, err error) { + return json.BinaryJSON{}, false, errors.Errorf("Evaluation methods is not implemented for ScalarSubQueryExpr") +} + +// GetType implements the Expression interface. +func (s *ScalarSubQueryExpr) GetType() *types.FieldType { + return s.RetType +} + +// Clone copies an expression totally. +func (s *ScalarSubQueryExpr) Clone() expression.Expression { + ret := *s + ret.RetType = s.RetType.Clone() + return &ret +} + +// Equal implements the Expression interface. +func (s *ScalarSubQueryExpr) Equal(_ sessionctx.Context, e expression.Expression) bool { + anotherS, ok := e.(*ScalarSubQueryExpr) + if !ok { + return false + } + if s.scalarSubqueryColID == anotherS.scalarSubqueryColID { + return true + } + return false +} + +// IsCorrelated implements the Expression interface. +func (*ScalarSubQueryExpr) IsCorrelated() bool { + return false +} + +// ConstItem implements the Expression interface. +func (*ScalarSubQueryExpr) ConstItem(_ *stmtctx.StatementContext) bool { + return true +} + +// Decorrelate implements the Expression interface. +func (s *ScalarSubQueryExpr) Decorrelate(_ *expression.Schema) expression.Expression { + return s +} + +// ResolveIndices implements the Expression interface. +func (s *ScalarSubQueryExpr) ResolveIndices(_ *expression.Schema) (expression.Expression, error) { + return s, nil +} + +// ResolveIndicesByVirtualExpr implements the Expression interface. +func (s *ScalarSubQueryExpr) ResolveIndicesByVirtualExpr(_ *expression.Schema) (expression.Expression, bool) { + return s, false +} + +// RemapColumn implements the Expression interface. +func (s *ScalarSubQueryExpr) RemapColumn(_ map[int64]*expression.Column) (expression.Expression, error) { + return s, nil +} + +// ExplainInfo implements the Expression interface. +func (s *ScalarSubQueryExpr) ExplainInfo() string { + return s.String() +} + +// ExplainNormalizedInfo implements the Expression interface. +func (s *ScalarSubQueryExpr) ExplainNormalizedInfo() string { + return s.String() +} + +// HashCode implements the Expression interface. +func (s *ScalarSubQueryExpr) HashCode(_ *stmtctx.StatementContext) []byte { + if len(s.hashcode) != 0 { + return s.hashcode + } + s.hashcode = make([]byte, 0, 9) + s.hashcode = append(s.hashcode, expression.ScalarSubQFlag) + s.hashcode = codec.EncodeInt(s.hashcode, s.scalarSubqueryColID) + return s.hashcode +} + +// String implements the Stringer interface. +func (s *ScalarSubQueryExpr) String() string { + builder := &strings.Builder{} + fmt.Fprintf(builder, "ScalarQueryCol#%d", s.scalarSubqueryColID) + return builder.String() +} + +// MarshalJSON implements the goJSON.Marshaler interface. +func (s *ScalarSubQueryExpr) MarshalJSON() ([]byte, error) { + if s.evalErr != nil { + return nil, s.evalErr + } + if s.evaled { + return s.Constant.MarshalJSON() + } + err := s.selfEvaluate() + if err != nil { + return nil, err + } + return s.Constant.MarshalJSON() +} + +// ReverseEval evaluates the only one column value with given function result. +func (s *ScalarSubQueryExpr) ReverseEval(_ *stmtctx.StatementContext, _ types.Datum, _ types.RoundingType) (val types.Datum, err error) { + if s.evalErr != nil { + return s.Value, s.evalErr + } + if s.evaled { + return s.Value, nil + } + err = s.selfEvaluate() + if err != nil { + return s.Value, err + } + return s.Value, nil +} + +// SupportReverseEval implements the Expression interface. +func (*ScalarSubQueryExpr) SupportReverseEval() bool { + return true +} + +// VecEvalInt evaluates this expression in a vectorized manner. +func (*ScalarSubQueryExpr) VecEvalInt(_ sessionctx.Context, _ *chunk.Chunk, _ *chunk.Column) error { + return errors.Errorf("ScalarSubQueryExpr doesn't implement the vec eval yet") +} + +// VecEvalReal evaluates this expression in a vectorized manner. +func (*ScalarSubQueryExpr) VecEvalReal(_ sessionctx.Context, _ *chunk.Chunk, _ *chunk.Column) error { + return errors.Errorf("ScalarSubQueryExpr doesn't implement the vec eval yet") +} + +// VecEvalString evaluates this expression in a vectorized manner. +func (*ScalarSubQueryExpr) VecEvalString(_ sessionctx.Context, _ *chunk.Chunk, _ *chunk.Column) error { + return errors.Errorf("ScalarSubQueryExpr doesn't implement the vec eval yet") +} + +// VecEvalDecimal evaluates this expression in a vectorized manner. +func (*ScalarSubQueryExpr) VecEvalDecimal(_ sessionctx.Context, _ *chunk.Chunk, _ *chunk.Column) error { + return errors.Errorf("ScalarSubQueryExpr doesn't implement the vec eval yet") +} + +// VecEvalTime evaluates this expression in a vectorized manner. +func (*ScalarSubQueryExpr) VecEvalTime(_ sessionctx.Context, _ *chunk.Chunk, _ *chunk.Column) error { + return errors.Errorf("ScalarSubQueryExpr doesn't implement the vec eval yet") +} + +// VecEvalDuration evaluates this expression in a vectorized manner. +func (*ScalarSubQueryExpr) VecEvalDuration(_ sessionctx.Context, _ *chunk.Chunk, _ *chunk.Column) error { + return errors.Errorf("ScalarSubQueryExpr doesn't implement the vec eval yet") +} + +// VecEvalJSON evaluates this expression in a vectorized manner. +func (*ScalarSubQueryExpr) VecEvalJSON(_ sessionctx.Context, _ *chunk.Chunk, _ *chunk.Column) error { + return errors.Errorf("ScalarSubQueryExpr doesn't implement the vec eval yet") +} + +// Vectorized returns whether the expression can be vectorized. +func (*ScalarSubQueryExpr) Vectorized() bool { + return true +} + +// Schema implements the Plan interface. +func (*ScalarSubqueryEvalCtx) Schema() *expression.Schema { + return nil +} + +// ExplainInfo implements the Plan interface. +func (ssctx *ScalarSubqueryEvalCtx) ExplainInfo() string { + builder := &strings.Builder{} + fmt.Fprintf(builder, "Output: ") + for i, id := range ssctx.outputColIDs { + fmt.Fprintf(builder, "ScalarQueryCol#%d", id) + if i+1 != len(ssctx.outputColIDs) { + fmt.Fprintf(builder, ",") + } + } + return builder.String() +} diff --git a/planner/core/scalar_subq_test.go b/planner/core/scalar_subq_test.go new file mode 100644 index 0000000000000..71e82c179f9b1 --- /dev/null +++ b/planner/core/scalar_subq_test.go @@ -0,0 +1,90 @@ +// Copyright 2022 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package core_test + +import ( + . "github.com/pingcap/check" + "github.com/pingcap/tidb/util/testkit" + "github.com/pingcap/tidb/util/testleak" +) + +func (s *testPlanSuite) TestExplainNonEvaledSubquery(c *C) { + defer testleak.AfterTest(c)() + store, dom, err := newStoreWithBootstrap() + c.Assert(err, IsNil) + defer func() { + dom.Close() + store.Close() + }() + var ( + input []struct { + SQL string + IsExplainAnalyze bool + HasErr bool + } + output []struct { + SQL string + Plan []string + Error string + } + ) + s.testData.GetTestCases(c, &input, &output) + tk := testkit.NewTestKit(c, store) + + tk.MustExec("use test") + tk.MustExec("create table t1(a int, b int, c int)") + tk.MustExec("create table t2(a int, b int, c int)") + tk.MustExec("create table t3(a varchar(5), b varchar(5), c varchar(5))") + tk.MustExec("set @@tidb_opt_enable_non_eval_scalar_subquery=true") + + cutExecutionInfoFromExplainAnalyzeOutput := func(rows [][]interface{}) [][]interface{} { + // The columns are id, estRows, actRows, task type, access object, execution info, operator info, memory, disk + // We need to cut the unstable output of execution info, memory and disk. + for i := range rows { + rows[i] = rows[i][:6] // cut the final memory and disk. + rows[i] = append(rows[i][:5], rows[i][6:]...) + } + return rows + } + + for i, ts := range input { + s.testData.OnRecord(func() { + output[i].SQL = ts.SQL + if ts.HasErr { + err := tk.ExecToErr(ts.SQL) + c.Assert(err, IsNil, Commentf("Failed at case #%d", i)) + output[i].Error = err.Error() + output[i].Plan = nil + } else { + rows := tk.MustQuery(ts.SQL).Rows() + if ts.IsExplainAnalyze { + rows = cutExecutionInfoFromExplainAnalyzeOutput(rows) + } + output[i].Plan = s.testData.ConvertRowsToStrings(rows) + output[i].Error = "" + } + }) + if ts.HasErr { + err := tk.ExecToErr(ts.SQL) + c.Assert(err, IsNil, Commentf("Failed at case #%d", i)) + } else { + rows := tk.MustQuery(ts.SQL).Rows() + if ts.IsExplainAnalyze { + rows = cutExecutionInfoFromExplainAnalyzeOutput(rows) + } + compareStringSlice(c, s.testData.ConvertRowsToStrings(rows), s.testData.ConvertRowsToStrings(testkit.Rows(output[i].Plan...))) + } + } +} diff --git a/planner/core/testdata/plan_suite_in.json b/planner/core/testdata/plan_suite_in.json index 09233f05dd121..bcfb8a8381dd2 100644 --- a/planner/core/testdata/plan_suite_in.json +++ b/planner/core/testdata/plan_suite_in.json @@ -746,5 +746,64 @@ "cases": [ "select * from t where t.a < 3 and t.a < 3" ] + }, + { + "name": "TestExplainNonEvaledSubquery", + "cases": [ + // Test normal non-correlated scalar sub query. + { + "SQL": "explain format = 'brief' select * from t1 where a = (select a from t2 limit 1)", + "IsExplainAnalyze": false, + "HasErr": false + }, + { + "SQL": "explain analyze select * from t1 where a = (select a from t2 limit 1)", + "IsExplainAnalyze": true, + "HasErr": false + }, + // Test EXISTS non-correlated scalar sub query. + { + "SQL": "explain format = 'brief' select * from t1 where exists(select 1 from t2 where a = 1)", + "IsExplainAnalyze": false, + "HasErr": false + }, + { + "SQL": "explain analyze select * from t1 where exists(select 1 from t2 where a = 1)", + "IsExplainAnalyze": true, + "HasErr": false + }, + { + "SQL": "explain format = 'brief' select * from t1 where not exists(select 1 from t2 where a = 1)", + "IsExplainAnalyze": false, + "HasErr": false + }, + { + "SQL": "explain analyze select * from t1 where not exists(select 1 from t2 where a = 1)", + "IsExplainAnalyze": true, + "HasErr": false + }, + // Test with constant propagation. + { + "SQL": "explain format = 'brief' select * from t1 where exists(select 1 from t2 where a = (select a from t3 limit 1) and b = a);", + "IsExplainAnalyze": false, + "HasErr": false + }, + { + "SQL": "explain analyze select * from t1 where exists(select 1 from t2 where a = (select a from t3 limit 1) and b = a);", + "IsExplainAnalyze": true, + "HasErr": false + }, + // Test multiple returns. + { + "SQL": "explain format = 'brief' select * from t1 where (a, b) = (select a, b from t2 limit 1)", + "IsExplainAnalyze": false, + "HasErr": false + }, + { + "SQL": "explain analyze select * from t1 where (a, b) = (select a, b from t2 limit 1)", + "IsExplainAnalyze": true, + "HasErr": false + } + ] } ] diff --git a/planner/core/testdata/plan_suite_out.json b/planner/core/testdata/plan_suite_out.json index 7a788eaad3912..b0b983ca157ed 100644 --- a/planner/core/testdata/plan_suite_out.json +++ b/planner/core/testdata/plan_suite_out.json @@ -2677,5 +2677,122 @@ ] } ] + }, + { + "Name": "TestExplainNonEvaledSubquery", + "Cases": [ + { + "SQL": "explain format = 'brief' select * from t1 where a = (select a from t2 limit 1)", + "Plan": [ + "Selection 8000.00 root eq(test.t1.a, ScalarQueryCol#9)", + "└─TableReader 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo", + "ScalarSubQuery N/A root Output: ScalarQueryCol#9", + "└─MaxOneRow 1.00 root ", + " └─Limit 1.00 root offset:0, count:1", + " └─TableReader 1.00 root data:Limit", + " └─Limit 1.00 cop[tikv] offset:0, count:1", + " └─TableFullScan 1.00 cop[tikv] table:t2 keep order:false, stats:pseudo" + ], + "Error": "" + }, + { + "SQL": "explain analyze select * from t1 where a = (select a from t2 limit 1)", + "Plan": [ + "TableDual_18 8000.00 0 root " + ], + "Error": "" + }, + { + "SQL": "explain format = 'brief' select * from t1 where exists(select 1 from t2 where a = 1)", + "Plan": [ + "Selection 8000.00 root ScalarQueryCol#10", + "└─TableReader 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo", + "ScalarSubQuery N/A root Output: ScalarQueryCol#10", + "└─TableReader 10.00 root data:Selection", + " └─Selection 10.00 cop[tikv] eq(test.t2.a, 1)", + " └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo" + ], + "Error": "" + }, + { + "SQL": "explain analyze select * from t1 where exists(select 1 from t2 where a = 1)", + "Plan": [ + "TableDual_12 0.00 0 root " + ], + "Error": "" + }, + { + "SQL": "explain format = 'brief' select * from t1 where not exists(select 1 from t2 where a = 1)", + "Plan": [ + "Selection 8000.00 root not(ScalarQueryCol#10)", + "└─TableReader 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo", + "ScalarSubQuery N/A root Output: ScalarQueryCol#10", + "└─TableReader 10.00 root data:Selection", + " └─Selection 10.00 cop[tikv] eq(test.t2.a, 1)", + " └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo" + ], + "Error": "" + }, + { + "SQL": "explain analyze select * from t1 where not exists(select 1 from t2 where a = 1)", + "Plan": [ + "TableReader_12 10000.00 0 root ", + "└─TableFullScan_11 10000.00 0 cop[tikv] table:t1" + ], + "Error": "" + }, + { + "SQL": "explain format = 'brief' select * from t1 where exists(select 1 from t2 where a = (select a from t3 limit 1) and b = a);", + "Plan": [ + "Selection 8000.00 root ScalarQueryCol#15", + "└─TableReader 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo", + "ScalarSubQuery N/A root Output: ScalarQueryCol#13", + "└─MaxOneRow 1.00 root ", + " └─Limit 1.00 root offset:0, count:1", + " └─TableReader 1.00 root data:Limit", + " └─Limit 1.00 cop[tikv] offset:0, count:1", + " └─TableFullScan 1.00 cop[tikv] table:t3 keep order:false, stats:pseudo", + "ScalarSubQuery N/A root Output: ScalarQueryCol#15", + "└─Selection 6400.00 root eq(cast(test.t2.a, double BINARY), cast(ScalarQueryCol#13, double BINARY)), eq(cast(test.t2.b, double BINARY), cast(ScalarQueryCol#13, double BINARY))", + " └─TableReader 8000.00 root data:Selection", + " └─Selection 8000.00 cop[tikv] eq(test.t2.b, test.t2.a)", + " └─TableFullScan 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo" + ], + "Error": "" + }, + { + "SQL": "explain analyze select * from t1 where exists(select 1 from t2 where a = (select a from t3 limit 1) and b = a);", + "Plan": [ + "TableDual_23 0.00 0 root " + ], + "Error": "" + }, + { + "SQL": "explain format = 'brief' select * from t1 where (a, b) = (select a, b from t2 limit 1)", + "Plan": [ + "Selection 8000.00 root eq(test.t1.a, ScalarQueryCol#9), eq(test.t1.b, ScalarQueryCol#10)", + "└─TableReader 10000.00 root data:TableFullScan", + " └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo", + "ScalarSubQuery N/A root Output: ScalarQueryCol#9,ScalarQueryCol#10", + "└─MaxOneRow 1.00 root ", + " └─Limit 1.00 root offset:0, count:1", + " └─TableReader 1.00 root data:Limit", + " └─Limit 1.00 cop[tikv] offset:0, count:1", + " └─TableFullScan 1.00 cop[tikv] table:t2 keep order:false, stats:pseudo" + ], + "Error": "" + }, + { + "SQL": "explain analyze select * from t1 where (a, b) = (select a, b from t2 limit 1)", + "Plan": [ + "TableDual_18 8000.00 0 root " + ], + "Error": "" + } + ] } ] diff --git a/planner/optimize.go b/planner/optimize.go index 842e567633e21..9420a082500be 100644 --- a/planner/optimize.go +++ b/planner/optimize.go @@ -328,6 +328,7 @@ func optimize(ctx context.Context, sctx sessionctx.Context, node ast.Node, is in // build logical plan sctx.GetSessionVars().PlanID = 0 sctx.GetSessionVars().PlanColumnID = 0 + sctx.GetSessionVars().MapScalarSubQ = nil hintProcessor := &hint.BlockHintProcessor{Ctx: sctx} node.Accept(hintProcessor) diff --git a/sessionctx/stmtctx/stmtctx.go b/sessionctx/stmtctx/stmtctx.go index a1af361f63438..0d169228e5de8 100644 --- a/sessionctx/stmtctx/stmtctx.go +++ b/sessionctx/stmtctx/stmtctx.go @@ -74,6 +74,7 @@ type StatementContext struct { InSelectStmt bool InLoadDataStmt bool InExplainStmt bool + InExplainAnalyzeStmt bool InCreateOrAlterStmt bool InPreparedPlanBuilding bool IgnoreTruncate bool diff --git a/sessionctx/variable/session.go b/sessionctx/variable/session.go index ae6d14df23f6b..daa333d2390aa 100644 --- a/sessionctx/variable/session.go +++ b/sessionctx/variable/session.go @@ -535,6 +535,9 @@ type SessionVars struct { // PlanColumnID is the unique id for column when building plan. PlanColumnID int64 + // MapScalarSubQ maps the scalar sub queries from its ID to its struct. + MapScalarSubQ []interface{} + // User is the user identity with which the session login. User *auth.UserIdentity @@ -596,6 +599,8 @@ type SessionVars struct { // AllowDistinctAggPushDown can be set true to allow agg with distinct push down to tikv/tiflash. AllowDistinctAggPushDown bool + ExplainNonEvaledSubQuery bool + // MultiStatementMode permits incorrect client library usage. Not recommended to be turned on. MultiStatementMode int @@ -1407,6 +1412,11 @@ func (s *SessionVars) AllocPlanColumnID() int64 { return s.PlanColumnID } +// RegisterScalarSubQ register a scalar sub query into the map. This will be used for EXPLAIN. +func (s *SessionVars) RegisterScalarSubQ(scalarSubQ interface{}) { + s.MapScalarSubQ = append(s.MapScalarSubQ, scalarSubQ) +} + // GetCharsetInfo gets charset and collation for current context. // What character set should the server translate a statement to after receiving it? // For this, the server uses the character_set_connection and collation_connection system variables. diff --git a/sessionctx/variable/sysvar.go b/sessionctx/variable/sysvar.go index 11bb4b6adac0a..472e78d8cf31f 100644 --- a/sessionctx/variable/sysvar.go +++ b/sessionctx/variable/sysvar.go @@ -410,6 +410,10 @@ var defaultSysVars = []*SysVar{ s.EnableCorrelationAdjustment = TiDBOptOn(val) return nil }}, + {Scope: ScopeGlobal | ScopeSession, Name: TiDBOptExplainNoEvaledSubQuery, Value: BoolToOnOff(DefTiDBOptExplainEvaledSubquery), Type: TypeBool, SetSession: func(s *SessionVars, val string) error { + s.ExplainNonEvaledSubQuery = TiDBOptOn(val) + return nil + }}, {Scope: ScopeGlobal | ScopeSession, Name: TiDBOptCorrelationExpFactor, Value: strconv.Itoa(DefOptCorrelationExpFactor), Type: TypeUnsigned, MinValue: 0, MaxValue: math.MaxInt32, SetSession: func(s *SessionVars, val string) error { s.CorrelationExpFactor = int(tidbOptInt64(val, DefOptCorrelationExpFactor)) return nil diff --git a/sessionctx/variable/tidb_vars.go b/sessionctx/variable/tidb_vars.go index 5f5b42cd66fd7..6bd9f9b16247c 100644 --- a/sessionctx/variable/tidb_vars.go +++ b/sessionctx/variable/tidb_vars.go @@ -56,6 +56,8 @@ const ( // It's unit is bytes, if the size of small table is larger than it, we will not use bcj. TiDBBCJThresholdSize = "tidb_broadcast_join_threshold_size" + TiDBOptExplainNoEvaledSubQuery = "tidb_opt_enable_non_eval_scalar_subquery" + // tidb_broadcast_join_threshold_count is used to limit the count of small table for mpp broadcast join. // If we can't estimate the size of one side of join child, we will check if its row number exceeds this limitation. TiDBBCJThresholdCount = "tidb_broadcast_join_threshold_count" @@ -676,6 +678,7 @@ const ( DefOptMPPOuterJoinFixedBuildSide = false DefOptWriteRowID = false DefOptEnableCorrelationAdjustment = true + DefTiDBOptExplainEvaledSubquery = false DefOptLimitPushDownThreshold = 100 DefOptCorrelationThreshold = 0.9 DefOptCorrelationExpFactor = 1 diff --git a/util/plancodec/id.go b/util/plancodec/id.go index b6f699c3b9837..d04b811bcae7f 100644 --- a/util/plancodec/id.go +++ b/util/plancodec/id.go @@ -127,6 +127,8 @@ const ( TypeCTE = "CTEFullScan" // TypeCTEDefinition is the type of CTE definition TypeCTEDefinition = "CTE" + // TypeScalarSubQuery is the type of ScalarQuery + TypeScalarSubQuery = "ScalarSubQuery" ) // plan id. @@ -184,6 +186,7 @@ const ( typeCTE int = 50 typeCTEDefinition int = 51 typeCTETable int = 52 + TypeScalarSubQueryID int = 60 ) // TypeStringToPhysicalID converts the plan type string to plan id. @@ -293,6 +296,8 @@ func TypeStringToPhysicalID(tp string) int { return typeCTEDefinition case TypeCTETable: return typeCTETable + case TypeScalarSubQuery: + return TypeScalarSubQueryID } // Should never reach here. return 0 @@ -403,6 +408,8 @@ func PhysicalIDToTypeString(id int) string { return TypeCTEDefinition case typeCTETable: return TypeCTETable + case TypeScalarSubQueryID: + return TypeScalarSubQuery } // Should never reach here.