TiDB SQL Plan Management从设计提案到 bind_info 落地实现的完整解析【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidbSQL Plan ManagementSQL 计划管理 / 执行计划绑定是 TiDB 为无法承受执行计划抖动的生产业务提供的关键能力它允许 DBA在不修改 SQL 文本的前提下强制优化器选用指定执行计划。本文以仓库中的设计文档 docs/design/2018-12-11-sql-plan-management.md 为主线对照其在pkg/bindinfo包中的真实实现从归一化、系统表存储、匹配到 hint 注入讲清一条绑定如何被创建、命中并发挥作用的完整链路帮助读者直接使用CREATE / DROP / SHOW BINDING系列语句并理解底层原理。一、背景优化器为什么会选错计划绑定要解决什么数据库优化器选择执行计划时依赖一组环境因素主要包括统计信息statistics行数估算变化会导致 join 顺序、索引选择整体改变优化器参数optimizer parameters如tidb_opt_insubq_to_join_and_agg等开关的不同取值会改变改写行为Schema 定义新增索引、修改列类型都会让优化器看到新的可能性。设计文档见 docs/design/2018-12-11-sql-plan-management.md指出了一个本质问题一旦环境发生变化我们无法保证新优化出的计划一定优于旧计划。对于不允许计划波动风险的应用例如大促核心链路、SLA 严格的报表任务就需要一种机制把某个已知良好的计划钉住。这也正是SQL Plan Management与普通 optimizer hint 的本质区别hint 需要修改业务 SQL 文本而绑定完全不需要。从会话变量角度绑定匹配整体受UsePlanBaselines控制见 pkg/bindinfo/binding.go 中的MatchSQLBindingWithCache判断该开关关闭时整条绑定链路直接短路避免额外开销。二、设计提案的核心思路归一化 用带 hint 的 SQL 的 AST表示计划原提案把问题拆成两部分如何绑定计划、用什么语法管理它。2.1 SQL 文本的归一化Normalization要让一条参数不同但结构相同的 SQL 命中间一条绑定必须先做归一化。设计文档给出的朴素方案是去掉所有空白字符、把参数替换为占位符、把其余部分转为小写。实现上这一思想被演进得更精细。当前 TiDB 统一入口是NormalizeStmtForBinding见 pkg/bindinfo/binding.go#L500-L558它实际做了自动跳过 EXPLAIN对EXPLAIN SELECT ...等语句直接取其内层 DML/查询语句做归一化且当 EXPLAIN 文本为空非用户输入时返回空串、不参与匹配去掉末尾分号eraseLastSemicolon避免;造成归一化结果不一致补齐或剥离库名noDBfalse时自动补全 schema 名select * from t→select * from db . tnoDBtrue时去掉 schema 名用于跨库匹配IN 列表字面量归一化为...例如select * from t where a in (1, 2, 3)归一化为select * from test.t where a in (...)。归一化后的文本会同时产出一个摘要哈希sql digest用于 O(1) 级别的快速比对这正是提案第 4 步先算 hash 再看是否命中的落地形态。2.2 计划表示为什么选 AST 而不是物理计划绑定需要把某个计划存下来、以后还能用。提案对比了两种表示法表示方式优点关键难点优化后的物理计划精确刻画算子树参数替换困难部分参数在逻辑/物理优化阶段已被改写进计划后续 SQL 无法做参数回填带 hint 的 SQL 的 AST后续只需遍历 AST、拷贝 hint语义上把计划压缩为提示集合依赖优化器在 hint 约束下重现计划提案最终选择AST of hinted SQLUSING子句里写一条带优化器 hint 的 SQL对后来的同构 SQL只需遍历其 AST 并把绑定的 hint 拷贝过去即可从而绕开参数替换难题。2.3 设计初稿的管理语法提案为绑定管理设计了一组带命名空间GLOBAL / SESSION的语句骨架CREATE [GLOBAL|SESSION] BINDING_NAME BINDING FOR SQL USING HINTED SQL DROP [GLOBAL|SESSION] BINDINGS DROP [GLOBAL|SESSION] BINDING BINDING_NAME SHOW [GLOBAL|SESSION] BINDINGS [SHOW_LIKE_OR_WHERE]需要注意BINDING_NAME属于初稿构想。在当前实现中一条绑定天然由归一化 SQL digest 所在 DB唯一标识因此落地语法演变为直接以语句本身为操作对象详见下文第三部分。2.4 Rationale为什么不像 Oracle 只存 hint提案专门解释了与 Oracle 的差异Oracle 只保存优化后查询的hintsTiDB 若要这么做需要先为子查询生成唯一标识、再把所有 hint 提升到最外层查询工程量较大。相比之下直接存整个带 hint 的 AST 是当前最简单可靠的路径——解析 SQL 与 AST 一一对应无需额外编码。三、语法的最终落地与实证原提案设想新增 MySQL 不支持的语法MySQL 本身没有 SQL plan management这一兼容性论断与现状一致TiDB 提供的是独立的BINDING语句族。以仓库测试 pkg/bindinfo/tests/bind_test.go 中的真实用例为例-- 对 DELETE 绑定强制走 idx_c 索引 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; -- 对多表 DELETE 绑定强制使用 index nested-loop join 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; -- 对 UPDATE 绑定 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; -- 对 INSERT ... SELECT 绑定注意INSERT/REPLACE 目前仅支持 SELECT 形态 create global binding for insert into t1 select * from t2 where t2.b 1 and t2.c 1 using insert /* use_index(t2,idx_c) */ into t1 select * from t2 where t2.b 1 and t2.c 1;从中可以归纳当前实际支持的操作形态CREATE [GLOBAL|SESSION] BINDING FOR 原SQL USING 带hint的SQL创建绑定。不写GLOBAL/SESSION时的默认行为与作用域相关请以实际文档为准DROP [GLOBAL|SESSION] BINDING FOR 原SQL删除绑定实际走 digestSHOW [GLOBAL|SESSION] BINDINGS查看绑定可配合过滤条件绑定还存在enabled / disabled状态切换见binding_operator.go的SetBindingStatus用于暂时停用某条绑定而不删除的场景。源码层面BindingOperator接口见 pkg/bindinfo/binding_operator.go#L31-L45把操作收敛为四类注释与原文档的四个实现步骤一一对应type BindingOperator interface { CreateBinding(sctx sessionctx.Context, bindings []*Binding) (err error) // 创建写入存储并刷新缓存 DropBinding(sqlDigests []string) (deletedRows uint64, err error) // 删除 SetBindingStatus(newStatus, sqlDigest string) (ok bool, err error) // 启用/禁用 GCBinding() (err error) // 物理清理已删除记录 }一个值得注意的工程细节由于 CREATE / DROP 之间需要保证顺序一致性多 TiDB 实例并发的写操作通过lockBindInfoTable实现互斥——它用一条针对内置伪行的悲观更新来模拟LOCK TABLE mysql.bind_info WRITE见 pkg/bindinfo/binding_handle.go#L36-L37UPDATE mysql.bind_info SET source builtin WHERE original_sql builtin_pseudo_sql_for_bind_lock四、存储层mysql.bind_info 系统表与状态机创建绑定时CreateBinding见 pkg/bindinfo/binding_operator.go#L64-L157会先在事务内把同一条归一化 SQL 的旧绑定标记为 deleted软删除保障多副本最终一致再插入新记录。插入语句完整给出了mysql.bind_info的列结构original_sql, bind_sql, default_db, status, create_time, update_time, charset, collation, source, sql_digest, plan_digest各列含义如下列含义original_sql归一化后的原始 SQL 文本绑定的匹配键之一bind_sql用户在USING子句中写的带 hint 的 SQL后续被重新解析为 ASTdefault_db创建绑定时所在库小写存储用于同名表的库内匹配status绑定状态见下方状态机create_time/update_time时间戳update_time同时承担并发版本控制职责见下文charset/collation字符集与排序规则保证解析bind_sql时上下文一致source来源标记manual手工create binding、history来自 statement summary 按 plan digest 生成另有自动演进来源等sql_digest/plan_digest归一化摘要与计划摘要供 O(1) 命中与后续自动演进状态常量定义在 pkg/bindinfo/binding.go#L41-L60enabled正常激活当前与未来推荐使用的唯一启用态using历史使用中状态仅为兼容保留语义等同 enableddisabled已停用匹配时会被跳过但记录仍保留deleted软删除标记不再参与匹配builtin内置记录即上文用于锁表的伪行。删除与状态切换都遵循update_time 本次时间戳的条件更新并让各实例时间戳严格递增见DropBinding中time.Now().Add(time.Microsecond)的累加处理确保删除/新建竞态下不会漏更或误伤新记录。GC 策略GCBinding只物理删除那些status deleted且update_time早于10 个 lease之前的记录见 pkg/bindinfo/binding_operator.go#L244-L259。之所以等待 10 个 lease是保证所有 TiDB 实例都已通过缓存刷新感知到删除避免旧实例把已删绑定又读回来。Lease 3 * time.Second定义在 pkg/bindinfo/binding_handle.go#L26。五、绑定匹配Session 优先、跨库通配与命中缓存新 SQL 进入优化流程后匹配入口是MatchSQLBinding→MatchSQLBindingWithCache→matchSQLBindingCore见 pkg/bindinfo/binding.go#L137-L253。整体决策顺序为提前负过滤mayHaveSQLBinding判断语句类型是否可能命中绑定——例如INSERT/REPLACE ... VALUES非 SELECT 形态直接判定不参与匹配见 pkg/bindinfo/binding.go#L168-L187计算 noDB 摘要与表名列表对 AST 做一次归一化得到noDBDigest同时用 AST 遍历收集[]*ast.TableNameCollectTableNames供跨库匹配使用Session 绑定优先先查当前会话的绑定MatchSessionBinding命中即返回Global 绑定兜底再查全局绑定globalHandle.MatchingBinding命中时若启用了 usage 统计还会更新last_used_at结果写入语句级临时缓存setMatchSQLBindingCache把本次匹配结果挂在StmtCtx上保证同一条语句如 prepared statement 场景不会重复归一化、重复匹配。跨库绑定是绑定体系的重要扩展设计文档讨论的是单库内同名表匹配而实现允许在USING的 SQL 中把 schema 写成*形成对所有库的同名表都生效的通配绑定。匹配逻辑crossDBMatchBindingTableName见 pkg/bindinfo/binding.go#L307-L326会比较语句与绑定的表名序列schema 相同、为空取当前库或为*均视为匹配且精确匹配优先于通配匹配通配符越多优先级越低通配绑定还需要会话变量tidb_enable_fuzzy_binding代码中为EnableFuzzyBinding放行才参与命中。六、命中之后hint 注入链路绑定匹配成功后真正让优化器听话的是 hint。核心函数prepareHints见 pkg/bindinfo/binding.go#L389-L435在创建绑定写入存储前就会完成 hint 的提取与校验用parser.ParseOneStmt解析binding.BindSQL即USING后的文本收集表名、判断是否为跨库绑定若是库名记为*调用hint.ParseHintsSet从带 hint 的 SQL 中解析出独立的*hint.HintsSet并通过HintsSet.Restore()得到规范化的 hint 字符串ID兜底校验对不含参数的绑定内部执行EXPLAIN FORMAThint临时关闭UsePlanBaselines验证该绑定 SQL 可成功生成计划避免把坏计划绑进去值得一提的是create binding ... using select * from thint 为空是被允许的占位绑定但若 hint 全部非法如不存在的 hint 名警告会被升级为错误直接拒绝创建。Binding结构体见 pkg/bindinfo/binding.go#L62-L89在内存中保存两类表示BindSQL可随时重新解析的原始文本Hint *hint.HintsSet已解析好的结构化 hintjson:-仅存内存不落库。这也印证了设计文档的选择——日常匹配只比对归一化 SQL/digest命中的瞬间把HintsSet附着到当前语句 AST 上不需要做任何参数替换参数问题被彻底绕开。七、缓存、后台刷新与多实例一致设计文档实现步骤中明确要求一个后台 goroutine 检查是否有新绑定并更新本地缓存每条 SQL 先比对 hash 再决定是否遍历 AST。落地时该职责由BindingHandle组合完成见 pkg/bindinfo/binding_handle.go#L59-L74它由三部分组成type bindingHandle struct { BindingCacheUpdater // 缓存刷新LoadFromStorageToCache BindingOperator // 创建/删除/状态/GC上文第四节 BindingPlanEvolution // 计划演进与自动绑定后文第八节 }关键机制与源码证据本地缓存 增量刷新每次CreateBinding/DropBinding/SetBindingStatus成功后都会在事务提交路径上调用cache.LoadFromStorageToCache(...)把变更拉回本实例缓存实例间通过 lease 周期与update_time水位last_plan_binding_update_time协作保证晚创建的记录覆盖早创建的旧记录多绑定取舍pickCachedBinding见 pkg/bindinfo/binding.go#L438-L486在候选绑定中依次过滤——只保留update_time最新的一批、再剔除 deleted最终收敛到唯一一条生效绑定。这正是同一归一化 SQL 只能有一份有效绑定、新建即覆盖旧绑定语义的实现来源实例间写串行化通过锁定内置伪行见第三节避免跨实例并发写互相覆盖域级 owner绑定刷新由 owner 机制驱动OwnerKey /tidb/bindinfo/owner避免多实例重复做全量刷新。八、从提案到现状实现比设计多走了多远对照 docs/design/2018-12-11-sql-plan-management.md可以清晰看到提案骨架归一化 → 语法 → 系统表 → 哈希匹配完整保留而后续演进补齐了生产级能力状态机取代硬删除deleted 软删除 10-lease GC让删除也能安全地跨实例传播来源标记与使用追踪sourcemanual/history 等区分人工与自动创建命中时更新last_used_time方便审计哪些绑定名存实亡跨库通配绑定*schema tidb_enable_fuzzy_binding一套绑定管理多套环境的同名表绑定自动演进与基线管理binding_plan_evolution.go、binding_auto.go等文件表明实现层还支持捕获历史计划生成基线sourcehistory以及在绑定上做计划验证/演进这超出了原设计纯手工钉计划的范畴摘要驱动的精确匹配sql_digest使匹配在多数场景退化为哈希比较会话变量控制总开关UsePlanBaselines最大化降低对无关 SQL 的性能影响——正是设计文档最后一步先比对 hash、避免逐条全文本比对影响无关 SQL的工程化解答。九、代码路径速查与延伸阅读关注点参考文件设计提案原文docs/design/2018-12-11-sql-plan-management.mdBinding 结构、状态常量、归一化与匹配pkg/bindinfo/binding.go创建/删除/状态切换/GC 与 bind_info 写库pkg/bindinfo/binding_operator.goBindingHandle 组装、Lease、锁表语句、owner keypkg/bindinfo/binding_handle.go会话级绑定句柄pkg/bindinfo/session_handle.go绑定自动演进pkg/bindinfo/binding_plan_evolution.go可执行 SQL 用例DELETE/UPDATE/INSERT 绑定pkg/bindinfo/tests/bind_test.go使用建议上线前用EXPLAIN FORMAThint提取目标 SQL 的 hint 并回填到USING子句先在 SESSION 作用域验证绑定对执行计划与性能的影响再提升为 GLOBAL 绑定对不再需要的绑定优先使用 disabled 状态灰度观察确认无回归后再 DROP让 GC 在 10 个 lease 后物理清理。【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考