Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

planner: support global binding fuzzy matching #50085

Merged
merged 25 commits into from
Jan 4, 2024
Merged
2 changes: 1 addition & 1 deletion pkg/bindinfo/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ go_test(
"bind_cache_test.go",
"binding_match_test.go",
"capture_test.go",
"fuzzy_binding_test.go",
"global_handle_test.go",
"main_test.go",
"optimize_test.go",
"session_handle_test.go",
"universal_binding_test.go",
],
embed = [":bindinfo"],
flaky = True,
Expand Down
13 changes: 11 additions & 2 deletions pkg/bindinfo/bind_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/pingcap/tidb/pkg/metrics"
"github.com/pingcap/tidb/pkg/parser"
"github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/sessionctx"
"github.com/pingcap/tidb/pkg/types"
"github.com/pingcap/tidb/pkg/util/hack"
Expand Down Expand Up @@ -85,6 +86,9 @@ type Binding struct {
PlanDigest string
// Type indicates the type of this binding, currently only 2 types: "" for normal and "u" for universal bindings.
Type string

// TableNames records all schema and table names in this binding statement, which are used for fuzzy matching.
TableNames []*ast.TableName `json:"-"`
}

func (b *Binding) isSame(rb *Binding) bool {
Expand Down Expand Up @@ -188,15 +192,20 @@ func (br *BindRecord) prepareHints(sctx sessionctx.Context) error {
continue
}
dbName := br.Db
if bind.Type == TypeUniversal {
bindingStmt, err := p.ParseOneStmt(bind.BindSQL, bind.Charset, bind.Collation)
if err != nil {
return err
}
isFuzzy := isFuzzyBinding(bindingStmt)
if isFuzzy {
dbName = "*" // ues '*' for universal bindings
}

hintsSet, stmt, warns, err := hint.ParseHintsSet(p, bind.BindSQL, bind.Charset, bind.Collation, dbName)
if err != nil {
return err
}
if sctx != nil && bind.Type == TypeNormal {
if sctx != nil && bind.Type == TypeNormal && !isFuzzy {
paramChecker := &paramMarkerChecker{}
stmt.Accept(paramChecker)
if !paramChecker.hasParamMarker {
Expand Down
45 changes: 41 additions & 4 deletions pkg/bindinfo/binding_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ func getBindRecord(ctx sessionctx.Context, stmt ast.StmtNode) (*BindRecord, stri
}
// the priority: session normal > session universal > global normal > global universal
sessionHandle := ctx.Value(SessionBindInfoKeyType).(SessionBindingHandle)
if bindRecord, err := sessionHandle.MatchSessionBinding(ctx.GetSessionVars().CurrentDB, stmt); err == nil && bindRecord != nil && bindRecord.HasEnabledBinding() {
if bindRecord, err := sessionHandle.MatchSessionBinding(ctx, stmt); err == nil && bindRecord != nil && bindRecord.HasEnabledBinding() {
return bindRecord, metrics.ScopeSession, nil
}
globalHandle := GetGlobalBindingHandle(ctx)
if globalHandle == nil {
return nil, "", nil
}
if bindRecord, err := globalHandle.MatchGlobalBinding(ctx.GetSessionVars().CurrentDB, stmt); err == nil && bindRecord != nil && bindRecord.HasEnabledBinding() {
if bindRecord, err := globalHandle.MatchGlobalBinding(ctx, stmt); err == nil && bindRecord != nil && bindRecord.HasEnabledBinding() {
return bindRecord, metrics.ScopeGlobal, nil
}
return nil, "", nil
Expand All @@ -77,11 +77,17 @@ func eraseLastSemicolon(stmt ast.StmtNode) {
// For normal bindings, DB name will be completed automatically:
//
// e.g. `select * from t where a in (1, 2, 3)` --> `select * from test.t where a in (...)`
func normalizeStmt(stmtNode ast.StmtNode, specifiedDB string) (stmt ast.StmtNode, normalizedStmt, sqlDigest string, err error) {
func normalizeStmt(stmtNode ast.StmtNode, specifiedDB string, fuzzy bool) (stmt ast.StmtNode, normalizedStmt, sqlDigest string, err error) {
normalize := func(n ast.StmtNode) (normalizedStmt, sqlDigest string) {
eraseLastSemicolon(n)
var digest *parser.Digest
normalizedStmt, digest = parser.NormalizeDigestForBinding(utilparser.RestoreWithDefaultDB(n, specifiedDB, n.Text()))
var normalizedSQL string
if !fuzzy {
normalizedSQL = utilparser.RestoreWithDefaultDB(n, specifiedDB, n.Text())
} else {
normalizedSQL = utilparser.RestoreWithoutDB(n)
}
normalizedStmt, digest = parser.NormalizeDigestForBinding(normalizedSQL)
return normalizedStmt, digest.String()
}

Expand Down Expand Up @@ -131,6 +137,37 @@ func normalizeStmt(stmtNode ast.StmtNode, specifiedDB string) (stmt ast.StmtNode
return nil, "", "", nil
}

func fuzzyMatchBindingTableName(currentDB string, stmtTableNames, bindingTableNames []*ast.TableName) (numWildcards int, matched bool) {
if len(stmtTableNames) != len(bindingTableNames) {
return 0, false
}
for i := range stmtTableNames {
if stmtTableNames[i].Name.L != bindingTableNames[i].Name.L {
return 0, false
}
if bindingTableNames[i].Schema.L == "*" {
numWildcards++
}
if bindingTableNames[i].Schema.L == stmtTableNames[i].Schema.L || // exactly same, or
(stmtTableNames[i].Schema.L == "" && bindingTableNames[i].Schema.L == currentDB) || // equal to the current DB, or
bindingTableNames[i].Schema.L == "*" { // fuzzy match successfully
continue
}
return 0, false
}
return numWildcards, true
}

// isFuzzyBinding checks whether the stmtNode is a fuzzy binding.
func isFuzzyBinding(stmt ast.Node) bool {
for _, t := range CollectTableNames(stmt) {
if t.Schema.L == "*" {
return true
}
}
return false
}

// CollectTableNames gets all table names from ast.Node.
// This function is mainly for binding fuzzy matching.
// ** the return is read-only.
Expand Down
4 changes: 2 additions & 2 deletions pkg/bindinfo/capture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func TestBindingSource(t *testing.T) {
tk.MustExec("create global binding for select * from t where a > 10 using select * from t ignore index(idx_a) where a > 10")
bindHandle := dom.BindHandle()
stmt, _, _ := internal.UtilNormalizeWithDefaultDB(t, "select * from t where a > ?")
bindData, err := bindHandle.MatchGlobalBinding("test", stmt)
bindData, err := bindHandle.MatchGlobalBinding(tk.Session(), stmt)
require.NoError(t, err)
require.NotNil(t, bindData)
require.Equal(t, "select * from `test` . `t` where `a` > ?", bindData.OriginalSQL)
Expand All @@ -350,7 +350,7 @@ func TestBindingSource(t *testing.T) {
tk.MustExec("admin capture bindings")
bindHandle.CaptureBaselines()
stmt, _, _ = internal.UtilNormalizeWithDefaultDB(t, "select * from t where a < ?")
bindData, err = bindHandle.MatchGlobalBinding("test", stmt)
bindData, err = bindHandle.MatchGlobalBinding(tk.Session(), stmt)
require.NoError(t, err)
require.NotNil(t, bindData)
require.Equal(t, "select * from `test` . `t` where `a` < ?", bindData.OriginalSQL)
Expand Down
Loading