ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TiDB 基于规则的索引选择优化:Always-Good 启发式与 Skyline Pruning 深度解析

TiDB 基于规则的索引选择优化:Always-Good 启发式与 Skyline Pruning 深度解析 TiDB 基于规则的索引选择优化Always-Good 启发式与 Skyline Pruning 深度解析【免费下载链接】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导读本文以 TiDB 官方设计文档 2021-07-07-rule-based-index-selection.md 为核心骨架系统讲解 TiDB 优化器在统计信息过期或不准确时如何通过「Always-Good 启发式 Skyline Pruning天际线剪枝 Prefer Range Scan」三条规则化机制提升索引选择的鲁棒性。读完本文你将掌握这几类规则的设计动机、判定逻辑、对应源码实现位置以及开关变量tidb_opt_prefer_range_scan的会话级/全局级用法。一、背景为什么统计信息之外还需要规则化索引选择TiDB 优化器为每个表选择访问路径Access Path时主要依赖统计信息来估算不同候选索引的代价Cost。但统计信息存在两个天然缺陷过期表数据变化后未及时ANALYZE与不准确采样误差、无统计信息时的伪统计 Pseudo Stats。此时基于代价的选路可能选中一个明显很差的索引导致 SQL 性能急剧退化。为降低这种可能性TiDB 在纯代价估算之外引入了一套基于规则Rule-Based的索引选择机制包括启发式Heuristics与 Skyline Pruning 两类可参考最初的 2019-01-25-skyline-pruning.md 设计。2021 年这份提案2021-07-07-rule-based-index-selection.md对应实现 PR #27223、跟踪 Issue #26020的目标是改进这套规则使其在如下两个典型场景中能兜底纠错唯一索引被等值条件覆盖、或可从单次扫描索引“精化refine”得到此时读取行数理论上可控应直接被选中候选索引都需回表、或物理属性排序匹配判定不准确时Skyline Pruning 应能剔除被严格支配strictly worse的索引。提案把改进后的机制拆成三部分Always-Good Heuristics、Skyline Pruning 增强、Maybe-Good Heuristics。二、Always-Good Heuristics总有好启发式2.1 四条核心规则「总有好」的含义是这些启发式结论不依赖统计信息也必然成立因此可以安全地直接决策。规则如下原文四条单扫唯一索引直接选若某唯一索引被查询条件覆盖且一次索引扫描Single Scan即足够例如覆盖了所有需要读取的列含主键列则直接选择它。需回表的唯一索引进候选池唯一索引虽被等值条件覆盖但需要“索引扫描 回表”两次扫描Double Scan则先放入数组uniqueIdxsWithDoubleScan收集完所有满足条件的唯一索引后选取预计读取行数最少的一个作为uniqueBest。由唯一索引“精化”出单扫索引若某个索引idx1只需单次扫描且存在一个属于uniqueIdxsWithDoubleScan的唯一索引idx2idx2的访问条件集合是idx1的子集则可推断idx1的读取行数同样有限——这相当于把idx2精化成了idx1。将这类索引放入数组singleScanIdxs最终选出读取行数最少的作为refinedBest。最终裁决若uniqueBest存在而refinedBest不存在选uniqueBest若两者都存在选读取行数更小的那一个。规则 1/2 中的“索引被条件覆盖covered by conditions”定义为索引的每一列都有对应的等值条件。2.2 示例验证CREATE TABLE t(a INT PRIMARY KEY, b INT, c INT, UNIQUE INDEX idx_b(b), UNIQUE INDEX idx_b_c(b, c)); -- 命中规则 1主键 a 被等值条件a2 OR a5即 IN 语义覆盖且为单扫直接被选中 SELECT * FROM t WHERE a 2 OR a 5; -- 命中规则 2/3idx_b 满足 b5 等值覆盖但需回表c 未覆盖→ 进 uniqueIdxsWithDoubleScan -- idx_b_c 仅需单扫覆盖 b、c且 idx_b 的访问条件 b 是 idx_b_c 访问条件 {b, c…} 的子集 -- 因此 idx_b_c 作为 refinedBest最终因读取行数更小胜出 SELECT b, c FROM t WHERE b 5 AND c 10;第二个查询的取舍要点uniqueBest idx_b只能过滤b5后回表逐行再判断c10而refinedBest idx_b_c可在索引内同时过滤b5与c10回表行数更少。虽然idx_b_c本身不是“每列都被等值覆盖”的唯一索引规则 3 依然能通过集合包含关系把它可靠地挑出来——这正是启发式的价值所在。2.3 源码实现位置该设计最终落在(*DataSource).DeriveStats阶段此时每个候选访问路径possibleAccessPaths的 AccessConds、TableFilters 等字段已填充完成因为 Always-Good 启发式与物理属性排序无关适合在逻辑属性推导期尽早收敛。实现于 pkg/planner/core/stats.go 附近关键结构完全对应提案uniqueIdxsWithDoubleScan : make([]*util.AccessPath, ...)与singleScanIdxs : make([]*util.AccessPath, ...)两个容器与提案同名候选变量selected, uniqueBest, refinedBest *util.AccessPath对应四种规则的输出uniqueBest的选择并非简单“最少行数”从源码看实际用范围数量len(uniqueIdx.Ranges)最少、其次TableFilters更少作为代理指标参见 stats.go 内注释Find the unique index with the minimal number of ranges as uniqueBest源码注释还专门复述了uniqueBest不一定最优的反例当uniqueBest是idx_b时idx_b_c可能更好因此要遍历singleScanIdxs检查其是否优于uniqueIdxsWithDoubleScan中的某个索引stats.go。三、Skyline Pruning 增强三维比较的精细化3.1 原有三维度回顾Skyline Pruning 用于剔除“严格劣于”另一索引的候选。比较两个索引时考虑三个维度单扫还是双扫Single Scan / Double Scan访问条件覆盖的列集合access condition 覆盖的列越多越优物理属性排序匹配是否满足ORDER BY/GROUP BY/ join key 等要求的属性。只有当候选 A 在所有维度不劣于B、且至少一个维度严格优于B 时B 才会被剪枝。其当前实现的核心是 pkg/planner/core/find_best_task.go 的compareCandidates以及外层遍历所有PossibleAccessPaths并逐步淘汰劣者的 skylinePruning 函数。3.2 改进点一双扫索引之间比较“回表行数”原实现里若两个索引都需要双扫第一维度视二者“相等”于是只靠第二维度访问条件列集合比较。但访问条件列集合相同、回表后还需过滤的列不同时二者优劣其实可分。提案给出示例CREATE TABLE t(a INT, b INT, c INT, INDEX idx_b(b), INDEX idx_b_c(b, c)); SELECT * FROM t WHERE b 5 AND c 5;idx_b与idx_b_c的访问条件都是b 5且都需要双扫但idx_b_c可以把c 5下沉为索引内过滤条件记录于AccessPath.IndexFilters而idx_b只能在回表后用TableFilters过滤c 5。因此idx_b_c的回表行数严格更少应当优于idx_b。在源码中该改进体现在candidatePath上维护的两张列集合映射accessCondsColMapAccessConds 涉及列与indexCondsColMapAccessConds IndexFilters 涉及列见 find_best_task.go。双扫比较函数 compareIndexBack 的逻辑正是func compareIndexBack(lhs, rhs *candidatePath) (int, bool) { result : compareBool(lhs.path.IsSingleScan, rhs.path.IsSingleScan) if result 0 !lhs.path.IsSingleScan { // 两者都需要回表时用 AccessConds 与 IndexFilters 涉及的列集合 // 比较回表行数多少IndexFilters 覆盖列越多回表行越少。 return util.CompareCol2Len(lhs.indexCondsColMap, rhs.indexCondsColMap) } return result, true }compareIndexBack的返回结果被compareCandidates汇总为totalSum : accessResult scanResult matchResult globalResult参与支配判定从而把“双扫但索引内过滤更多”的路径正确识别为更优。3.3 改进点二物理属性匹配判定的修正原实现对“索引是否匹配所需物理属性”的部分场景识别不正确。提案给出示例CREATE TABLE t(a INT, b INT, c INT, d INT, INDEX idx_a_b_c(a, b, c)); SELECT * FROM t WHERE b 4 ORDER BY a, c;索引idx_a_b_c的列序是 (a, b, c)看似无法匹配ORDER BY a, c中间隔了 b。但查询含b 4这一等值常量条件b 列取值已被钉死因此数据在 b 分组内天然按 (a, c) 有序idx_a_b_c实际可以匹配该排序要求。原实现的匹配判定未考虑此类“常量列不破坏前缀有序性”的情况属于需要修复的缺陷。对应的匹配能力在 pkg/planner/core/find_best_task.go 的matchProperty函数中实现候选路径会缓存matchPropResult property.PhysicalPropMatchResultfind_best_task.go随后compareCandidates用compareBool(lhs.matchPropResult.Matched(), rhs.matchPropResult.Matched())把“是否匹配物理属性”作为一维纳入剪枝比较。补充说明随着版本演进compareCandidates在原始三维之外又叠加了全局索引优先compareGlobalIndex、等值/IN 谓词覆盖数eqOrInCount、估算风险比MaxCountAfterAccess/CountAfterAccess的compareRiskRatio等维度并引入了“某侧仅有伪统计Pseudo Stats时允许其通过启发式存活”的特殊裁决逻辑详见 find_best_task.go 与comparePseudo/isFullIndexMatch。这说明 Skyline Pruning 已从最初的纯结构剪枝演化为“结构支配 风险感知 伪统计兜底”的混合机制。四、Maybe-Good Heuristics 与 tidb_opt_prefer_range_scan4.1 问题场景排序匹配与提前终止之间的两难CREATE TABLE t(a INT PRIMARY KEY, b INT, c INT, INDEX idx_b(b)); SELECT * FROM t WHERE b 2 ORDER BY a LIMIT 10;优化器可能选择TableFullScan表全扫尽管它明显比IndexRangeScan慢得多。原因在于两个候选“各有道理、难以比较”TableFullScan能匹配ORDER BY a主键天然有序并配合LIMIT 10提前终止取 10 行即停而IndexRangeScan过滤b2后还需额外排序。这类场景既不是“总有好”也不是 Skyline 可判定的支配关系故称Maybe-Good或许有好。TiDB 为此提供会话级开关tidb_opt_prefer_range_scan打开后优化器倾向于选择范围扫描Range Scan而非全表扫描。本提案在该开关上做了两项改进。4.2 改进一从会话级升级为“会话级 全局级”提案指出仅会话级开关不足以覆盖真实使用场景DBA 通常希望集群级统一开启、再按需在个别会话关闭。在现仓库源码中该变量已在系统变量注册表里声明为ScopeGlobal | ScopeSession// pkg/sessionctx/variable/sysvar.go L2391-L2400 { Scope: vardef.ScopeGlobal | vardef.ScopeSession, Name: vardef.TiDBOptPreferRangeScan, Value: BoolToOnOff(vardef.DefOptPreferRangeScan), Type: vardef.TypeBool, IsHintUpdatableVerified: true, SetSession: func(s *SessionVars, val string) error { s.SetAllowPreferRangeScan(TiDBOptOn(val)) return nil }},即当前版本中tidb_opt_prefer_range_scan是布尔型默认值见DefOptPreferRangeScan既可通过SET SESSION tidb_opt_prefer_range_scanON/OFF按会话设置也可通过SET GLOBAL tidb_opt_prefer_range_scanON/OFF做集群级配置。此外其IsHintUpdatableVerified: true与 pkg/sessionctx/variable/setvar_affect.go 中登记为SET_VAR提示可影响项意味着还可以用/* SET_VAR(tidb_opt_prefer_range_scanON) */把它做成单条语句级控制Hint 的具体生效需以对应版本行为为准。4.3 改进二无符号主键Unsigned Handle的全范围识别对AUTO_INCREMENT/无符号类型主键作为聚簇行句柄的表其理论取值全范围是[0, inf]但原实现没有把它识别为“全范围full range”导致 prefer-range-scan 逻辑无法正确判定该全扫路径。改进后每个候选路径会缓存isFullRange布尔字段见 candidatePath 定义注释为cached result of whether this path covers the full scan range供剪枝决策复用避免对无符号句柄表的误判。4.4 prefer-range 在实际剪枝中的落点skylinePruning函数把tidb_opt_prefer_range_scan称为“控制索引偏好的总开关”原文注释tidb_opt_prefer_range_scan is the master switch to control index preferencing见 find_best_task.go。其用法细节对理解行为边界很有帮助开关打开后并非无条件生效源码中做了收敛preferRange preferMerge || idxMissingStats || ds.TableStats.HistColl.Pseudo || ds.TableStats.RowCount 1find_best_task.go——即若本就倾向 IndexMerge、或存在索引缺统计、或表统计是伪统计、或表行数过少prefer-range 会被覆盖以让代价机制继续工作在保留候选阶段被强制的路径Forced、TiFlash 路径、MV 索引、全局索引会被直接保留其余路径只有在满足“单扫或有更多索引内过滤”且匹配所需物理属性时才被视作 range 候选若存在至少一个 range 扫描候选则全扫描候选被移除直接返回优选列表find_best_task.go。这与提案“开关打开时优化器总是倾向 range scan 胜过 full scan”的目标一致。五、设计动机与兼容性Rationale启发式与 Skyline Pruning 这类规则能阻止优化器选到“明显错误”的索引此类做法在众多数据库中均有实现属于业界共识它不追求在所有场景选出最优解而是以极低代价剔除劣解、抬高索引选择的下限。兼容性提案明确说明这些改动不改变 SQL 语义与对外行为仅影响物理计划的选择结果因此不影响兼容性tidb_opt_prefer_range_scan仅是行为开关的扩展作用域变宽默认值语义保持不变。六、实现位置总览与验证按提案的 Implementation 章节三类机制分别落位如下机制所在阶段仓库中的关键实现Always-Good Heuristics(*DataSource).DeriveStats在填充完possibleAccessPaths各字段后执行pkg/planner/core/stats.goSkyline Pruning 增强(*DataSource).findBestTask内的剪枝环节pkg/planner/core/find_best_task.go 的skylinePruning及其compareCandidatesL863Prefer Range Scan同上作为 skyline 剪枝的子逻辑与开关sysvar.go 注册变量 find_best_task.go 的候选保留逻辑测试侧TestSkylinePruning用例位于 pkg/planner/core/logical_plans_test.gocasetest目录如 pkg/planner/core/casetest/plan_test.go则用「SQL 输入 期望执行计划」的黄金文件方式回归验证包括 prefer-range 在内的计划选择行为读者可以在这些用例中看到规则对最终执行计划的直接影响。七、小结与使用建议回到本提案要解决的根因——统计信息不可尽信。TiDB 用三层结构为索引选择兜底Always-Good 启发式结构上必然正确的决策唯一索引等值覆盖、单扫精化在DeriveStats阶段尽早定案Skyline Pruning在三维度上剔除被支配的路径并对“双扫回表量”“物理属性匹配”做了精细化修复让剪枝结论更贴近真实代价差Prefer Range ScanMaybe-Good对“排序全扫 LIMIT 提前终止”这类难以估价的场景用显式开关引导优化器倾向范围扫描并支持SESSION/GLOBAL/SET_VAR多层作用域。实际运维中的典型用法是在确认某类点查/范围查因统计信息延迟而经常选错全表扫描时可先SET GLOBAL tidb_opt_prefer_range_scan ON全局兜底再对个别不适合的负载用SET SESSION或SET_VAR提示临时关闭与此同时配合定期的ANALYZE TABLE维护统计新鲜度让代价估算与规则化机制互为补充。【免费下载链接】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),仅供参考
返回列表