Skip to content

Commit 70c7d50

Browse files
authored
bindinfo: add last_used_date to track bindinfo usage frequency (#63409)
close #63407
1 parent 0078610 commit 70c7d50

File tree

18 files changed

+380
-11
lines changed

18 files changed

+380
-11
lines changed

br/pkg/restore/snap_client/systable_restore_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func TestCheckSysTableCompatibility(t *testing.T) {
121121
//
122122
// The above variables are in the file br/pkg/restore/systable_restore.go
123123
func TestMonitorTheSystemTableIncremental(t *testing.T) {
124-
require.Equal(t, int64(252), session.CurrentBootstrapVersion)
124+
require.Equal(t, int64(253), session.CurrentBootstrapVersion)
125125
}
126126

127127
func TestIsStatsTemporaryTable(t *testing.T) {

pkg/bindinfo/binding.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"fmt"
2020
"strings"
2121
"sync"
22+
"sync/atomic"
23+
"time"
2224
"unsafe"
2325

2426
"github.com/pingcap/tidb/pkg/metrics"
@@ -77,6 +79,10 @@ type Binding struct {
7779

7880
// TableNames records all schema and table names in this binding statement, which are used for cross-db matching.
7981
TableNames []*ast.TableName `json:"-"`
82+
83+
// UsageInfo is to track the usage information `last_used_time` of this binding
84+
// and it will be updated when this binding is used.
85+
UsageInfo bindingInfoUsageInfo
8086
}
8187

8288
// IsBindingEnabled returns whether the binding is enabled.
@@ -90,6 +96,27 @@ func (b *Binding) size() float64 {
9096
return float64(res)
9197
}
9298

99+
// UpdateLastUsedAt is to update binding usage info when this binding is used.
100+
func (b *Binding) UpdateLastUsedAt() {
101+
now := time.Now()
102+
b.UsageInfo.LastUsedAt.Store(&now)
103+
}
104+
105+
// UpdateLastSavedAt is to update the last saved time
106+
func (b *Binding) UpdateLastSavedAt(ts *time.Time) {
107+
b.UsageInfo.LastSavedAt.Store(ts)
108+
}
109+
110+
type bindingInfoUsageInfo struct {
111+
// LastUsedAt records the last time when this binding is used.
112+
// It is nil if this binding has never been used.
113+
// It is updated when this binding is used.
114+
// It is used to update the `last_used_time` field in mysql.bind_info table.
115+
LastUsedAt atomic.Pointer[time.Time]
116+
// LastSavedAt records the last time when this binding is saved into storage.
117+
LastSavedAt atomic.Pointer[time.Time]
118+
}
119+
93120
var (
94121
// GetBindingHandle is a function to get the global binding handle.
95122
// It is mainly used to resolve cycle import issue.
@@ -152,6 +179,8 @@ func matchSQLBinding(sctx sessionctx.Context, stmtNode ast.StmtNode, info *Bindi
152179
}
153180
binding, matched = globalHandle.MatchingBinding(sctx, noDBDigest, tableNames)
154181
if matched {
182+
// After hitting the cache, update the usage time of the bind.
183+
binding.UpdateLastUsedAt()
155184
return binding, matched, metrics.ScopeGlobal
156185
}
157186

pkg/bindinfo/binding_cache.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ type BindingCacheUpdater interface {
3838
// LoadFromStorageToCache loads global bindings from storage to the memory cache.
3939
LoadFromStorageToCache(fullLoad bool) (err error)
4040

41+
// UpdateBindingUsageInfoToStorage is to update the binding usage info into storage
42+
UpdateBindingUsageInfoToStorage() error
43+
4144
// LastUpdateTime returns the last update time.
4245
LastUpdateTime() types.Time
4346
}
@@ -96,6 +99,17 @@ func (u *bindingCacheUpdater) LoadFromStorageToCache(fullLoad bool) (err error)
9699
return nil
97100
}
98101

102+
// UpdateBindingUsageInfoToStorage is to update the binding usage info into storage
103+
func (u *bindingCacheUpdater) UpdateBindingUsageInfoToStorage() error {
104+
defer func() {
105+
if r := recover(); r != nil {
106+
bindingLogger().Warn("panic when update usage info for binding", zap.Any("recover", r))
107+
}
108+
}()
109+
bindings := u.GetAllBindings()
110+
return updateBindingUsageInfoToStorage(u.sPool, bindings)
111+
}
112+
99113
// LastUpdateTime returns the last update time.
100114
func (u *bindingCacheUpdater) LastUpdateTime() types.Time {
101115
return u.lastUpdateTime.Load().(types.Time)

pkg/bindinfo/binding_operator.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ func (op *bindingOperator) CreateBinding(sctx sessionctx.Context, bindings []*Bi
106106
}
107107
_, err = exec(
108108
sctx,
109-
`INSERT INTO mysql.bind_info VALUES (%?,%?, %?, %?, %?, %?, %?, %?, %?, %?, %?)`,
109+
`INSERT INTO mysql.bind_info(
110+
original_sql, bind_sql, default_db, status, create_time, update_time, charset, collation, source, sql_digest, plan_digest
111+
) VALUES (%?,%?, %?, %?, %?, %?, %?, %?, %?, %?, %?)`,
110112
binding.OriginalSQL,
111113
binding.BindSQL,
112114
strings.ToLower(binding.Db),

pkg/bindinfo/binding_operator_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func TestBindingLastUpdateTimeWithInvalidBind(t *testing.T) {
9696
updateTime0 := rows0[0][1]
9797
require.Equal(t, updateTime0, "0000-00-00 00:00:00")
9898

99-
tk.MustExec("insert into mysql.bind_info values('select * from `test` . `t`', 'invalid_binding', 'test', 'enabled', '2000-01-01 09:00:00', '2000-01-01 09:00:00', '', '','" +
99+
tk.MustExec("insert into mysql.bind_info (original_sql, bind_sql, default_db, status, create_time, update_time, charset, collation, source, sql_digest, plan_digest) values('select * from `test` . `t`', 'invalid_binding', 'test', 'enabled', '2000-01-01 09:00:00', '2000-01-01 09:00:00', '', '','" +
100100
bindinfo.SourceManual + "', '', '')")
101101
tk.MustExec("use test")
102102
tk.MustExec("drop table if exists t")
@@ -260,7 +260,7 @@ func TestSetBindingStatusWithoutBindingInCache(t *testing.T) {
260260

261261
// Simulate creating bindings on other machines
262262
_, sqlDigest := parser.NormalizeDigestForBinding("select * from `test` . `t` where `a` > ?")
263-
tk.MustExec("insert into mysql.bind_info values('select * from `test` . `t` where `a` > ?', 'SELECT /*+ USE_INDEX(`t` `idx_a`)*/ * FROM `test`.`t` WHERE `a` > 10', 'test', 'enabled', '2000-01-02 09:00:00', '2000-01-02 09:00:00', '', '','" +
263+
tk.MustExec("insert into mysql.bind_info (original_sql, bind_sql, default_db, status, create_time, update_time, charset, collation, source, sql_digest, plan_digest) values('select * from `test` . `t` where `a` > ?', 'SELECT /*+ USE_INDEX(`t` `idx_a`)*/ * FROM `test`.`t` WHERE `a` > 10', 'test', 'enabled', '2000-01-02 09:00:00', '2000-01-02 09:00:00', '', '','" +
264264
bindinfo.SourceManual + "', '" + sqlDigest.String() + "', '')")
265265
tk.MustExec("set binding disabled for select * from t where a > 10")
266266
tk.MustExec("admin reload bindings")
@@ -272,7 +272,7 @@ func TestSetBindingStatusWithoutBindingInCache(t *testing.T) {
272272
utilCleanBindingEnv(tk)
273273

274274
// Simulate creating bindings on other machines
275-
tk.MustExec("insert into mysql.bind_info values('select * from `test` . `t` where `a` > ?', 'SELECT * FROM `test`.`t` WHERE `a` > 10', 'test', 'disabled', '2000-01-02 09:00:00', '2000-01-02 09:00:00', '', '','" +
275+
tk.MustExec("insert into mysql.bind_info (original_sql, bind_sql, default_db, status, create_time, update_time, charset, collation, source, sql_digest, plan_digest) values('select * from `test` . `t` where `a` > ?', 'SELECT * FROM `test`.`t` WHERE `a` > 10', 'test', 'disabled', '2000-01-02 09:00:00', '2000-01-02 09:00:00', '', '','" +
276276
bindinfo.SourceManual + "', '" + sqlDigest.String() + "', '')")
277277
tk.MustExec("set binding enabled for select * from t where a > 10")
278278
tk.MustExec("admin reload bindings")

pkg/bindinfo/tests/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ go_test(
55
timeout = "moderate",
66
srcs = [
77
"bind_test.go",
8+
"bind_usage_info_test.go",
89
"cross_db_binding_test.go",
910
"main_test.go",
1011
],
1112
flaky = True,
1213
race = "on",
13-
shard_count = 22,
14+
shard_count = 23,
1415
deps = [
1516
"//pkg/bindinfo",
1617
"//pkg/domain",

pkg/bindinfo/tests/bind_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ func utilCleanBindingEnv(tk *testkit.TestKit) {
4444
func TestPrepareCacheWithBinding(t *testing.T) {
4545
store := testkit.CreateMockStore(t)
4646
tk := testkit.NewTestKit(t, store)
47-
tk.MustExec(`set tidb_enable_prepared_plan_cache=1`)
4847
tk.MustExec("use test")
4948
tk.MustExec("drop table if exists t1, t2")
5049
tk.MustExec("create table t1(a int, b int, c int, key idx_b(b), key idx_c(c))")
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 tests
16+
17+
import (
18+
"fmt"
19+
"testing"
20+
"time"
21+
22+
"github.com/pingcap/tidb/pkg/bindinfo"
23+
"github.com/pingcap/tidb/pkg/testkit"
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
func TestBindUsageInfo(t *testing.T) {
28+
checklist := []string{
29+
"5ce1df6eadf8b24222668b1bd2e995b72d4c88e6fe9340d8b13e834703e28c32",
30+
"5d3975ef2160c1e0517353798dac90a9914095d82c025e7cd97bd55aeb804798",
31+
"9d3995845aef70ba086d347f38a4e14c9705e966f7c5793b9fa92194bca2bbef",
32+
"aa3c510b94b9d680f729252ca88415794c8a4f52172c5f9e06c27bee57d08329",
33+
}
34+
bindinfo.UpdateBindingUsageInfoBatchSize = 2
35+
bindinfo.MaxWriteInterval = 100 * time.Microsecond
36+
store, dom := testkit.CreateMockStoreAndDomain(t)
37+
bindingHandle := dom.BindingHandle()
38+
tk := testkit.NewTestKit(t, store)
39+
40+
tk.MustExec(`use test`)
41+
tk.MustExec(`set @@tidb_opt_enable_fuzzy_binding=1`)
42+
tk.MustExec("create table t1(a int, b int, c int, key idx_b(b), key idx_c(c))")
43+
tk.MustExec("create table t2(a int, b int, c int, key idx_b(b), key idx_c(c))")
44+
tk.MustExec("create table t3(a int, b int, c int, key idx_b(b), key idx_c(c))")
45+
tk.MustExec("create table t4(a int, b int, c int, key idx_b(b), key idx_c(c))")
46+
tk.MustExec("create table t5(a int, b int, c int, key idx_b(b), key idx_c(c))")
47+
48+
tk.MustExec("prepare stmt1 from 'delete from t1 where b = 1 and c > 1';")
49+
tk.MustExec("prepare stmt2 from 'delete t1, t2 from t1 inner join t2 on t1.b = t2.b';")
50+
tk.MustExec("prepare stmt3 from 'update t1 set a = 1 where b = 1 and c > 1';")
51+
tk.MustExec("execute stmt1;")
52+
tk.MustExec("create global binding for delete from t1 where b = 1 and c > 1 using delete /*+ use_index(t1,idx_c) */ from t1 where b = 1 and c > 1")
53+
tk.MustExec("create global binding for delete t1, t2 from t1 inner join t2 on t1.b = t2.b using delete /*+ inl_join(t1) */ t1, t2 from t1 inner join t2 on t1.b = t2.b")
54+
tk.MustExec("create global binding for update t1 set a = 1 where b = 1 and c > 1 using update /*+ use_index(t1,idx_c) */ t1 set a = 1 where b = 1 and c > 1")
55+
// cross database binding
56+
tk.MustExec(`create global binding using select /*+ leading(t1, t2, t3, t4, t5) */ * from *.t1, *.t2, *.t3, *.t4, *.t5`)
57+
tk.MustExec("select * from t1, t2, t3, t4, t5")
58+
tk.MustExec("execute stmt1;")
59+
origin := tk.MustQuery(fmt.Sprintf(`select sql_digest,last_used_date from mysql.bind_info where original_sql != '%s' order by sql_digest`, bindinfo.BuiltinPseudoSQL4BindLock))
60+
origin.Check(testkit.Rows(
61+
"5ce1df6eadf8b24222668b1bd2e995b72d4c88e6fe9340d8b13e834703e28c32 <nil>",
62+
"5d3975ef2160c1e0517353798dac90a9914095d82c025e7cd97bd55aeb804798 <nil>",
63+
"9d3995845aef70ba086d347f38a4e14c9705e966f7c5793b9fa92194bca2bbef <nil>",
64+
"aa3c510b94b9d680f729252ca88415794c8a4f52172c5f9e06c27bee57d08329 <nil>"))
65+
time.Sleep(50 * time.Microsecond)
66+
require.NoError(t, bindingHandle.UpdateBindingUsageInfoToStorage())
67+
result := tk.MustQuery(fmt.Sprintf(`select sql_digest,last_used_date from mysql.bind_info where original_sql != '%s' order by sql_digest`, bindinfo.BuiltinPseudoSQL4BindLock))
68+
t.Log("result:", result.Rows())
69+
// The last_used_date should be updated.
70+
require.True(t, !origin.Equal(result.Rows()))
71+
var first *testkit.Result
72+
for range 5 {
73+
tk.MustExec("execute stmt1;")
74+
tk.MustExec("execute stmt2;")
75+
tk.MustExec("execute stmt3;")
76+
tk.MustExec("select * from t1, t2, t3, t4, t5")
77+
time.Sleep(1 * time.Second)
78+
// Set all last_used_date to null to simulate that the bindinfo in storage is not updated.
79+
resetAllLastUsedData(tk)
80+
require.NoError(t, bindingHandle.UpdateBindingUsageInfoToStorage())
81+
checkBindinfoInMemory(t, bindingHandle, checklist)
82+
tk.MustQuery(fmt.Sprintf(`select last_used_date from mysql.bind_info where original_sql != '%s' and last_used_date is null`, bindinfo.BuiltinPseudoSQL4BindLock)).Check(testkit.Rows())
83+
result := tk.MustQuery(fmt.Sprintf(`select sql_digest,last_used_date from mysql.bind_info where original_sql != '%s' order by sql_digest`, bindinfo.BuiltinPseudoSQL4BindLock))
84+
t.Log("result:", result.Rows())
85+
if first == nil {
86+
first = result
87+
} else {
88+
// in fact, The result of each for-loop should be the same.
89+
require.True(t, first.Equal(result.Rows()))
90+
}
91+
}
92+
// Set all last_used_date to null to simulate that the bindinfo in storage is not updated.
93+
resetAllLastUsedData(tk)
94+
for range 5 {
95+
time.Sleep(1 * time.Second)
96+
// No used, so last_used_date should not be updated.
97+
require.NoError(t, bindingHandle.UpdateBindingUsageInfoToStorage())
98+
tk.MustQuery(`select last_used_date from mysql.bind_info where last_used_date is not null`).Check(testkit.Rows())
99+
}
100+
tk.MustExec("execute stmt1;")
101+
tk.MustExec("execute stmt2;")
102+
tk.MustExec("execute stmt3;")
103+
time.Sleep(1 * time.Second)
104+
require.NoError(t, bindingHandle.UpdateBindingUsageInfoToStorage())
105+
// it has been updated again.
106+
rows := tk.MustQuery(
107+
fmt.Sprintf(`select * from mysql.bind_info where original_sql != '%s' and last_used_date is not null`, bindinfo.BuiltinPseudoSQL4BindLock)).Rows()
108+
require.Len(t, rows, 3)
109+
}
110+
111+
func resetAllLastUsedData(tk *testkit.TestKit) {
112+
tk.MustExec(fmt.Sprintf(`update mysql.bind_info set last_used_date = null where original_sql != '%s'`, bindinfo.BuiltinPseudoSQL4BindLock))
113+
}
114+
115+
func checkBindinfoInMemory(t *testing.T, bindingHandle bindinfo.BindingHandle, checklist []string) {
116+
for _, digest := range checklist {
117+
binding := bindingHandle.GetBinding(digest)
118+
require.NotNil(t, binding)
119+
lastSaved := binding.UsageInfo.LastSavedAt.Load()
120+
if lastSaved != nil {
121+
require.GreaterOrEqual(t, *binding.UsageInfo.LastSavedAt.Load(), *binding.UsageInfo.LastUsedAt.Load())
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)