Files
tidb/planner/optimize.go
2019-11-13 16:22:48 +08:00

316 lines
11 KiB
Go

// Copyright 2018 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,
// See the License for the specific language governing permissions and
// limitations under the License.
package planner
import (
"context"
"fmt"
"math"
"strings"
"github.com/pingcap/errors"
"github.com/pingcap/parser"
"github.com/pingcap/parser/ast"
"github.com/pingcap/parser/format"
"github.com/pingcap/tidb/bindinfo"
"github.com/pingcap/tidb/domain"
"github.com/pingcap/tidb/infoschema"
"github.com/pingcap/tidb/metrics"
"github.com/pingcap/tidb/planner/cascades"
plannercore "github.com/pingcap/tidb/planner/core"
"github.com/pingcap/tidb/privilege"
"github.com/pingcap/tidb/sessionctx"
"github.com/pingcap/tidb/types"
driver "github.com/pingcap/tidb/types/parser_driver"
"github.com/pingcap/tidb/util/logutil"
"go.uber.org/zap"
)
// Optimize does optimization and creates a Plan.
// The node must be prepared first.
func Optimize(ctx context.Context, sctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (plannercore.Plan, types.NameSlice, error) {
fp := plannercore.TryFastPlan(sctx, node)
if fp != nil {
if !isPointGetWithoutDoubleRead(sctx, fp) {
sctx.PrepareTxnFuture(ctx)
}
return fp, fp.OutputNames(), nil
}
sctx.PrepareTxnFuture(ctx)
bestPlan, names, _, err := optimize(ctx, sctx, node, is)
if err != nil {
return nil, nil, err
}
if !(sctx.GetSessionVars().UsePlanBaselines || sctx.GetSessionVars().EvolvePlanBaselines) {
return bestPlan, names, nil
}
stmtNode, ok := node.(ast.StmtNode)
if !ok {
return bestPlan, names, nil
}
bindRecord, scope := getBindRecord(sctx, stmtNode)
if bindRecord == nil {
return bestPlan, names, nil
}
bestPlanHint := plannercore.GenHintsFromPhysicalPlan(bestPlan)
binding := bindRecord.FindBinding(bestPlanHint)
// If the best bestPlan is in baselines, just use it.
if binding != nil && binding.Status == bindinfo.Using {
return bestPlan, names, nil
}
bestCostAmongHints := math.MaxFloat64
var bestPlanAmongHints plannercore.Plan
originHints := bindinfo.CollectHint(stmtNode)
// Try to find the best binding.
for _, binding := range bindRecord.Bindings {
if binding.Status != bindinfo.Using {
continue
}
metrics.BindUsageCounter.WithLabelValues(scope).Inc()
bindinfo.BindHint(stmtNode, binding.Hint)
plan, _, cost, err := optimize(ctx, sctx, node, is)
if err != nil {
binding.Status = bindinfo.Invalid
handleInvalidBindRecord(ctx, sctx, scope, bindinfo.BindRecord{
OriginalSQL: bindRecord.OriginalSQL,
Db: bindRecord.Db,
Bindings: []bindinfo.Binding{binding},
})
continue
}
if cost < bestCostAmongHints {
bestCostAmongHints = cost
bestPlanAmongHints = plan
}
}
// If there is already a evolution task, we do not need to handle it again.
if sctx.GetSessionVars().EvolvePlanBaselines && binding == nil {
handleEvolveTasks(ctx, sctx, bindRecord, stmtNode, bestPlanHint)
}
// Restore the hint to avoid changing the stmt node.
bindinfo.BindHint(stmtNode, originHints)
if sctx.GetSessionVars().UsePlanBaselines && bestPlanAmongHints != nil {
return bestPlanAmongHints, names, nil
}
return bestPlan, names, nil
}
func optimize(ctx context.Context, sctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (plannercore.Plan, types.NameSlice, float64, error) {
// build logical plan
sctx.GetSessionVars().PlanID = 0
sctx.GetSessionVars().PlanColumnID = 0
hintProcessor := &plannercore.BlockHintProcessor{Ctx: sctx}
node.Accept(hintProcessor)
builder := plannercore.NewPlanBuilder(sctx, is, hintProcessor)
p, err := builder.Build(ctx, node)
if err != nil {
return nil, nil, 0, err
}
sctx.GetSessionVars().StmtCtx.Tables = builder.GetDBTableInfo()
activeRoles := sctx.GetSessionVars().ActiveRoles
// Check privilege. Maybe it's better to move this to the Preprocess, but
// we need the table information to check privilege, which is collected
// into the visitInfo in the logical plan builder.
if pm := privilege.GetPrivilegeManager(sctx); pm != nil {
if err := plannercore.CheckPrivilege(activeRoles, pm, builder.GetVisitInfo()); err != nil {
return nil, nil, 0, err
}
}
if err := plannercore.CheckTableLock(sctx, is, builder.GetVisitInfo()); err != nil {
return nil, nil, 0, err
}
// Handle the execute statement.
if execPlan, ok := p.(*plannercore.Execute); ok {
err := execPlan.OptimizePreparedPlan(ctx, sctx, is)
return p, p.OutputNames(), 0, err
}
names := p.OutputNames()
// Handle the non-logical plan statement.
logic, isLogicalPlan := p.(plannercore.LogicalPlan)
if !isLogicalPlan {
return p, names, 0, nil
}
// Handle the logical plan statement, use cascades planner if enabled.
if sctx.GetSessionVars().EnableCascadesPlanner {
finalPlan, cost, err := cascades.DefaultOptimizer.FindBestPlan(sctx, logic)
return finalPlan, names, cost, err
}
finalPlan, cost, err := plannercore.DoOptimize(ctx, builder.GetOptFlag(), logic)
return finalPlan, names, cost, err
}
func extractSelectAndNormalizeDigest(stmtNode ast.StmtNode) (*ast.SelectStmt, string, string) {
switch x := stmtNode.(type) {
case *ast.ExplainStmt:
switch x.Stmt.(type) {
case *ast.SelectStmt:
normalizeExplainSQL := parser.Normalize(x.Text())
idx := strings.Index(normalizeExplainSQL, "select")
normalizeSQL := normalizeExplainSQL[idx:]
hash := parser.DigestHash(normalizeSQL)
return x.Stmt.(*ast.SelectStmt), normalizeSQL, hash
}
case *ast.SelectStmt:
normalizedSQL, hash := parser.NormalizeDigest(x.Text())
return x, normalizedSQL, hash
}
return nil, "", ""
}
func getBindRecord(ctx sessionctx.Context, stmt ast.StmtNode) (*bindinfo.BindRecord, string) {
// When the domain is initializing, the bind will be nil.
if ctx.Value(bindinfo.SessionBindInfoKeyType) == nil {
return nil, ""
}
selectStmt, normalizedSQL, hash := extractSelectAndNormalizeDigest(stmt)
if selectStmt == nil {
return nil, ""
}
sessionHandle := ctx.Value(bindinfo.SessionBindInfoKeyType).(*bindinfo.SessionHandle)
bindRecord := sessionHandle.GetBindRecord(normalizedSQL, ctx.GetSessionVars().CurrentDB)
if bindRecord != nil {
if bindRecord.HasUsingBinding() {
return bindRecord, metrics.ScopeSession
}
return nil, ""
}
globalHandle := domain.GetDomain(ctx).BindHandle()
bindRecord = globalHandle.GetBindRecord(hash, normalizedSQL, ctx.GetSessionVars().CurrentDB)
if bindRecord == nil {
bindRecord = globalHandle.GetBindRecord(hash, normalizedSQL, "")
}
return bindRecord, metrics.ScopeGlobal
}
func handleInvalidBindRecord(ctx context.Context, sctx sessionctx.Context, level string, bindRecord bindinfo.BindRecord) {
sessionHandle := sctx.Value(bindinfo.SessionBindInfoKeyType).(*bindinfo.SessionHandle)
// The first two parameters are only used to generate hints, but since we already have the hints,
// we do not need to pass real values and the error won't happen too.
err := sessionHandle.DropBindRecord(nil, nil, &bindRecord)
if err != nil {
logutil.Logger(ctx).Info("drop session bindings failed")
}
if level == metrics.ScopeSession {
return
}
globalHandle := domain.GetDomain(sctx).BindHandle()
globalHandle.AddDropInvalidBindTask(&bindRecord)
}
func handleEvolveTasks(ctx context.Context, sctx sessionctx.Context, br *bindinfo.BindRecord, stmtNode ast.StmtNode, planHint string) {
// If would be nil for very simple cases such as point get, we do not need to evolve for them.
if planHint == "" {
return
}
paramChecker := &paramMarkerChecker{}
stmtNode.Accept(paramChecker)
// We need to evolve on current sql, but we cannot restore values for paramMarkers yet,
// so just ignore them now.
if paramChecker.hasParamMarker {
return
}
// We need to evolve plan based on the current sql, not the original sql which may have different parameters.
// So here we would remove the hint and inject the current best plan hint.
bindinfo.BindHint(stmtNode, &bindinfo.HintsSet{})
var sb strings.Builder
restoreCtx := format.NewRestoreCtx(format.DefaultRestoreFlags, &sb)
err := stmtNode.Restore(restoreCtx)
if err != nil {
logutil.Logger(ctx).Info("Restore SQL failed", zap.Error(err))
}
bindsql := strings.Replace(sb.String(), "SELECT", fmt.Sprintf("SELECT /*+ %s*/", planHint), 1)
globalHandle := domain.GetDomain(sctx).BindHandle()
charset, collation := sctx.GetSessionVars().GetCharsetInfo()
binding := bindinfo.Binding{BindSQL: bindsql, Status: bindinfo.PendingVerify, Charset: charset, Collation: collation}
globalHandle.AddEvolvePlanTask(br.OriginalSQL, br.Db, binding, planHint)
}
type paramMarkerChecker struct {
hasParamMarker bool
}
func (e *paramMarkerChecker) Enter(in ast.Node) (ast.Node, bool) {
if _, ok := in.(*driver.ParamMarkerExpr); ok {
e.hasParamMarker = true
return in, true
}
return in, false
}
func (e *paramMarkerChecker) Leave(in ast.Node) (ast.Node, bool) {
return in, true
}
// isPointGetWithoutDoubleRead returns true when meets following conditions:
// 1. ctx is auto commit tagged.
// 2. plan is point get by pk.
func isPointGetWithoutDoubleRead(ctx sessionctx.Context, p plannercore.Plan) bool {
if !ctx.GetSessionVars().IsAutocommit() {
return false
}
v, ok := p.(*plannercore.PointGetPlan)
return ok && v.IndexInfo == nil
}
// OptimizeExecStmt to optimize prepare statement protocol "execute" statement
// this is a short path ONLY does things filling prepare related params
// for point select like plan which does not need extra things
func OptimizeExecStmt(ctx context.Context, sctx sessionctx.Context,
execAst *ast.ExecuteStmt, is infoschema.InfoSchema) (plannercore.Plan, error) {
var err error
builder := plannercore.NewPlanBuilder(sctx, is, nil)
p, err := builder.Build(ctx, execAst)
if err != nil {
return nil, err
}
if execPlan, ok := p.(*plannercore.Execute); ok {
err = execPlan.OptimizePreparedPlan(ctx, sctx, is)
return execPlan.Plan, err
}
err = errors.Errorf("invalid result plan type, should be Execute")
return nil, err
}
// GenHintsFromSQL is used to generate hints from SQL and inject the hints into original SQL.
func GenHintsFromSQL(ctx context.Context, sctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (string, error) {
err := plannercore.Preprocess(sctx, node, is)
if err != nil {
return "", err
}
oldValue := sctx.GetSessionVars().UsePlanBaselines
// Disable baseline to avoid binding hints.
sctx.GetSessionVars().UsePlanBaselines = false
p, _, err := Optimize(ctx, sctx, node, is)
sctx.GetSessionVars().UsePlanBaselines = oldValue
if err != nil {
return "", err
}
return plannercore.GenHintsFromPhysicalPlan(p), nil
}
func init() {
plannercore.OptimizeAstNode = Optimize
bindinfo.GenHintsFromSQL = GenHintsFromSQL
}