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
6 changes: 5 additions & 1 deletion 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
}

func (b *Binding) isSame(rb *Binding) bool {
Expand Down Expand Up @@ -196,7 +200,7 @@ func (br *BindRecord) prepareHints(sctx sessionctx.Context) error {
if err != nil {
return err
}
if sctx != nil && bind.Type == TypeNormal {
if sctx != nil && bind.Type == TypeNormal && !isFuzzyBinding(stmt) {
paramChecker := &paramMarkerChecker{}
stmt.Accept(paramChecker)
if !paramChecker.hasParamMarker {
Expand Down
43 changes: 40 additions & 3 deletions pkg/bindinfo/binding_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func getBindRecord(ctx sessionctx.Context, stmt ast.StmtNode) (*BindRecord, stri
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
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ func removeAllBindings(tk *testkit.TestKit, global bool) {
tk.MustQuery(fmt.Sprintf("show %v bindings", scope)).Check(testkit.Rows()) // empty
}

func TestUniversalBindingBasic(t *testing.T) {
t.Skip("skip it temporarily")
func TestFuzzyBindingBasic(t *testing.T) {
store := testkit.CreateMockStore(t)
tk1 := testkit.NewTestKit(t, store)

Expand All @@ -66,16 +65,11 @@ func TestUniversalBindingBasic(t *testing.T) {
tk1.MustExec(`use test2`)
tk1.MustExec(`create table t (a int, b int, c int, d int, e int, key(a), key(b), key(c), key(d), key(e))`)

for _, hasForStmt := range []bool{true, false} {
for _, scope := range []string{"", "session", "global"} {
for _, scope := range []string{ /*"", "session",*/ "global"} {
tk := testkit.NewTestKit(t, store)
for _, idx := range []string{"a", "b", "c", "d", "e"} {
tk.MustExec("use test")
forStmt := "for select * from t"
if !hasForStmt {
forStmt = ""
}
tk.MustExec(fmt.Sprintf(`create %v universal binding %v using select /*+ use_index(t, %v) */ * from t`, scope, forStmt, idx))
tk.MustExec(fmt.Sprintf(`create %v binding using select /*+ use_index(t, %v) */ * from *.t`, scope, idx))
for _, useDB := range []string{"test", "test1", "test2"} {
tk.MustExec("use " + useDB)
for _, testDB := range []string{"", "test.", "test1.", "test2."} {
Expand All @@ -88,7 +82,6 @@ func TestUniversalBindingBasic(t *testing.T) {
tk.MustQuery(fmt.Sprintf("select * from %vt", testDB))
tk.MustQuery(`select @@last_plan_from_binding`).Check(testkit.Rows("0"))
}
}
}
}
}
Expand Down
85 changes: 77 additions & 8 deletions pkg/bindinfo/global_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type GlobalBindingHandle interface {
// Methods for create, get, drop global sql bindings.

// MatchGlobalBinding returns the matched binding for this statement.
MatchGlobalBinding(currentDB string, stmt ast.StmtNode) (*BindRecord, error)
MatchGlobalBinding(sctx sessionctx.Context, stmt ast.StmtNode) (*BindRecord, error)

// GetAllGlobalBindings returns all bind records in cache.
GetAllGlobalBindings() (bindRecords []*BindRecord)
Expand Down Expand Up @@ -112,6 +112,11 @@ type globalBindingHandle struct {

bindingCache atomic.Pointer[bindCache]

// fuzzyDigestMap is used to support fuzzy matching.
// fuzzyDigest is the digest calculated after eliminating all DB names, e.g. `select * from test.t` -> `select * from t` -> fuzzyDigest.
// exactDigest is the digest where all DB names are kept, e.g. `select * from test.t` -> exactDigest.
fuzzyDigestMap atomic.Value // map[string][]string fuzzyDigest --> exactDigests

// lastTaskTime records the last update time for the global sql bind cache.
// This value is used to avoid reload duplicated bindings from storage.
lastUpdateTime atomic.Value
Expand Down Expand Up @@ -165,6 +170,33 @@ func (h *globalBindingHandle) setCache(c *bindCache) {
h.bindingCache.Store(c)
}

func (h *globalBindingHandle) getFuzzyDigestMap() map[string][]string {
return h.fuzzyDigestMap.Load().(map[string][]string)
}

func (h *globalBindingHandle) setFuzzyDigestMap(m map[string][]string) {
h.fuzzyDigestMap.Store(m)
}

func buildFuzzyDigestMap(bindRecords []*BindRecord) map[string][]string {
m := make(map[string][]string)
p := parser.New()
for _, bindRecord := range bindRecords {
for _, binding := range bindRecord.Bindings {
stmt, err := p.ParseOneStmt(binding.BindSQL, binding.Charset, binding.Collation)
if err != nil {
logutil.BgLogger().Warn("parse bindSQL failed", zap.String("bindSQL", binding.BindSQL), zap.Error(err))
p = parser.New()
qw4990 marked this conversation as resolved.
Show resolved Hide resolved
continue
}
sqlWithoutDB := utilparser.RestoreWithoutDB(stmt)
_, fuzzyDigest := parser.NormalizeDigestForBinding(sqlWithoutDB)
m[fuzzyDigest.String()] = append(m[fuzzyDigest.String()], binding.SQLDigest)
}
}
return m
}

// Reset is to reset the BindHandle and clean old info.
func (h *globalBindingHandle) Reset() {
h.lastUpdateTime.Store(types.ZeroTimestamp)
Expand Down Expand Up @@ -220,6 +252,7 @@ func (h *globalBindingHandle) LoadFromStorageToCache(fullLoad bool) (err error)
defer func() {
h.setLastUpdateTime(lastUpdateTime)
h.setCache(newCache) // TODO: update it in place
h.setFuzzyDigestMap(buildFuzzyDigestMap(newCache.GetAllBindings()))
}()

for _, row := range rows {
Expand Down Expand Up @@ -488,17 +521,43 @@ func (h *globalBindingHandle) Size() int {
}

// MatchGlobalBinding returns the matched binding for this statement.
func (h *globalBindingHandle) MatchGlobalBinding(currentDB string, stmt ast.StmtNode) (*BindRecord, error) {
func (h *globalBindingHandle) MatchGlobalBinding(sctx sessionctx.Context, stmt ast.StmtNode) (*BindRecord, error) {
bindingCache := h.getCache()
if bindingCache.Size() == 0 {
return nil, nil
}
fuzzyDigestMap := h.getFuzzyDigestMap()
if len(fuzzyDigestMap) == 0 {
return nil, nil
}

// TODO: support fuzzy matching.
_, _, sqlDigest, err := normalizeStmt(stmt, currentDB)
_, _, fuzzDigest, err := normalizeStmt(stmt, sctx.GetSessionVars().CurrentDB, true)
if err != nil {
return nil, err
}
return bindingCache.GetBinding(sqlDigest), nil

tableNames := CollectTableNames(stmt)
var bestBinding *BindRecord
leastWildcards := len(tableNames) + 1
for _, exactDigest := range fuzzyDigestMap[fuzzDigest] {
sqlDigest := exactDigest
if bindRecord := bindingCache.GetBinding(sqlDigest); bindRecord != nil {
for _, binding := range bindRecord.Bindings {
numWildcards, matched := fuzzyMatchBindingTableName(sctx.GetSessionVars().CurrentDB, tableNames, binding.TableNames)
if matched && numWildcards > 0 && sctx != nil && !sctx.GetSessionVars().EnableFuzzyBinding {
continue // fuzzy binding is disabled, skip this binding
}
if matched && numWildcards < leastWildcards {
bestBinding = bindRecord
leastWildcards = numWildcards
break
}
}
}
}

return bestBinding, nil
}

// GetAllGlobalBindings returns all bind records in cache.
Expand Down Expand Up @@ -534,17 +593,27 @@ func newBindRecord(sctx sessionctx.Context, row chunk.Row) (string, *BindRecord,
if defaultDB == "" {
bindingType = TypeUniversal
}

bindSQL := row.GetString(1)
charset, collation := row.GetString(6), row.GetString(7)
stmt, err := parser.New().ParseOneStmt(bindSQL, charset, collation)
if err != nil {
return "", nil, err
}
tableNames := CollectTableNames(stmt)

binding := Binding{
BindSQL: row.GetString(1),
BindSQL: bindSQL,
Status: status,
CreateTime: row.GetTime(4),
UpdateTime: row.GetTime(5),
Charset: row.GetString(6),
Collation: row.GetString(7),
Charset: charset,
Collation: collation,
Source: row.GetString(8),
SQLDigest: row.GetString(9),
PlanDigest: row.GetString(10),
Type: bindingType,
TableNames: tableNames,
}
bindRecord := &BindRecord{
OriginalSQL: row.GetString(0),
Expand All @@ -553,7 +622,7 @@ func newBindRecord(sctx sessionctx.Context, row chunk.Row) (string, *BindRecord,
}
sqlDigest := parser.DigestNormalized(bindRecord.OriginalSQL)
sctx.GetSessionVars().CurrentDB = bindRecord.Db
err := bindRecord.prepareHints(sctx)
err = bindRecord.prepareHints(sctx)
return sqlDigest.String(), bindRecord, err
}

Expand Down
15 changes: 8 additions & 7 deletions pkg/bindinfo/global_handle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestBindingLastUpdateTime(t *testing.T) {
require.NoError(t, err)
stmt, err := parser.New().ParseOneStmt("select * from test . t0", "", "")
require.NoError(t, err)
bindData, err := bindHandle.MatchGlobalBinding("test", stmt)
bindData, err := bindHandle.MatchGlobalBinding(tk.Session(), stmt)
require.NoError(t, err)
require.Equal(t, 1, len(bindData.Bindings))
bind := bindData.Bindings[0]
Expand Down Expand Up @@ -125,7 +125,8 @@ func TestBindParse(t *testing.T) {
charset := "utf8mb4"
collation := "utf8mb4_bin"
source := bindinfo.Manual
mockDigest := "0f644e22c38ecc71d4592c52df127df7f86b6ca7f7c0ee899113b794578f9396"
_, digest := parser.NormalizeDigestForBinding(originSQL)
mockDigest := digest.String()
sql := fmt.Sprintf(`INSERT INTO mysql.bind_info(original_sql,bind_sql,default_db,status,create_time,update_time,charset,collation,source, sql_digest, plan_digest) VALUES ('%s', '%s', '%s', '%s', NOW(), NOW(),'%s', '%s', '%s', '%s', '%s')`,
originSQL, bindSQL, defaultDb, status, charset, collation, source, mockDigest, mockDigest)
tk.MustExec(sql)
Expand All @@ -136,7 +137,7 @@ func TestBindParse(t *testing.T) {

stmt, err := parser.New().ParseOneStmt("select * from test . t", "", "")
require.NoError(t, err)
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`", bindData.OriginalSQL)
Expand Down Expand Up @@ -437,7 +438,7 @@ func TestGlobalBinding(t *testing.T) {

stmt, _, _ := internal.UtilNormalizeWithDefaultDB(t, testSQL.querySQL)

bindData, err := dom.BindHandle().MatchGlobalBinding("test", stmt)
bindData, err := dom.BindHandle().MatchGlobalBinding(tk.Session(), stmt)
require.NoError(t, err)
require.NotNil(t, bindData)
require.Equal(t, testSQL.originSQL, bindData.OriginalSQL)
Expand Down Expand Up @@ -471,7 +472,7 @@ func TestGlobalBinding(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 1, bindHandle.Size())

bindData, err = dom.BindHandle().MatchGlobalBinding("test", stmt)
bindData, err = dom.BindHandle().MatchGlobalBinding(tk.Session(), stmt)
require.NoError(t, err)
require.NotNil(t, bindData)
require.Equal(t, testSQL.originSQL, bindData.OriginalSQL)
Expand All @@ -487,7 +488,7 @@ func TestGlobalBinding(t *testing.T) {
_, err = tk.Exec("drop global " + testSQL.dropSQL)
require.Equal(t, uint64(1), tk.Session().AffectedRows())
require.NoError(t, err)
bindData, err = dom.BindHandle().MatchGlobalBinding("test", stmt)
bindData, err = dom.BindHandle().MatchGlobalBinding(tk.Session(), stmt)
require.NoError(t, err)
require.Nil(t, bindData)

Expand All @@ -496,7 +497,7 @@ func TestGlobalBinding(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 0, bindHandle.Size())

bindData, err = dom.BindHandle().MatchGlobalBinding("test", stmt)
bindData, err = dom.BindHandle().MatchGlobalBinding(tk.Session(), stmt)
require.NoError(t, err)
require.Nil(t, bindData)

Expand Down
2 changes: 1 addition & 1 deletion pkg/bindinfo/session_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func (h *sessionBindingHandle) MatchSessionBinding(currentDB string, stmt ast.St
return nil, nil
}
// TODO: support fuzzy matching.
_, _, sqlDigest, err := normalizeStmt(stmt, currentDB)
_, _, sqlDigest, err := normalizeStmt(stmt, currentDB, false)
if err != nil {
return nil, err
}
Expand Down
Loading