列表查询显式指定索引反而更慢,什么情况下会退化成全表扫描
列表查询显式指定索引反而更慢,问题出在提示指向的索引与当前查询条件不再匹配的时候。写死索引等于让优化器放弃按统计信息选路径的权利:条件组合一变、数据分布一变,被强制使用的那条索引可能只剩下扫描再回表这一种用法,代价远高于原本会走的覆盖索引或范围扫描;条件彻底失效时,这条路径就退化成全表扫描。
优化器本来按什么选索引
查询在没有提示时由优化器决定用哪条索引,参考的是索引结构、字段的取值分布与预估命中行数。它追求的不是「用最相关的索引」,而是「总代价最低的路径」——包括要读多少页、要不要按每行回表取其余字段、结果是否需要再排一次序。一个只按字段名匹配的索引,在命中行数很多时反而不如另一条包含排序字段的索引,这类判断依赖统计信息,静态写在代码里的提示看不到。
写死提示之后谁替你做决定
提示的作用范围有三种强弱不同的写法,共同点是都排除了其他候选。一旦指定,优化器不再比较代价,也不会因为索引变得不合适而自动改选。风险点在于提示往往是在某一次慢查询时加上的:那时它确实解决了问题,但数据量、字段分布与查询条件都会继续变化,而提示不会跟着变。PbootCMS 在 V3.2.26 更新记录里给出的修复项正是这一类问题,列表标签的查询因索引提示退化为全表扫描,同时把新装库统一改为按标题字段建索引——两边一起改,说明成因是索引结构与提示绑定得太紧。
| 触发条件 | 提示带来的结果 | 现象 | 处理方式 |
|---|---|---|---|
| 查询加了新的过滤条件 | 被指定的索引无法用于新条件,先扫再筛 | 响应时间成倍变高 | 先去掉提示,看优化器自己怎么选 |
| 字段取值分布改变 | 预估命中行数上升,回表代价放大 | 冷数据查得快、热数据查得慢 | 复核索引字段顺序 |
| 排序字段与索引不一致 | 需要额外排序,索引只当过滤器用 | 大分页时明显变慢 | 按排序与过滤组合重建索引 |
| 条件写成函数或隐式转换 | 索引列失效,直接退化 | 执行计划显示全表扫描 | 改条件写法,别加提示掩盖 |
哪些条件组合最容易退化
四类最常见。第一类是对索引列做函数运算或类型不一致的比较,列上的值要经过转换才能匹配,索引无法用于定位。第二类是前置通配的模糊匹配,模式以通配符开头时无法利用有序结构。第三类是过滤与排序分属不同索引,优化器需要在一条索引扫描加一次排序、和另一条索引扫描免排序之间选,而提示会把选择空间压成一个。第四类是宽表上的少量字段索引:命中行数一多,逐行回表取其余字段比整表顺序扫描更贵,这正是被强制指定索引最容易翻车的情形。
定位与验证的顺序
顺序应当是先看执行计划再动配置,而不是凭感觉加提示。第一步取当前执行计划,确认实际使用的索引、预估行数与是否有额外排序;第二步去掉提示复跑一次,比较两次的代价,很多情况下优化器自己选的路径更优;第三步核对索引列顺序是否覆盖过滤与排序的组合,必要时重建而不是加提示;第四步观察数据量变化后的表现,把只在当前规模成立的写法改回条件驱动。列表查询的效率对页面加载速度有直接影响,官方对 AnQiCMS 的口径是相比传统 PHP 程序有显著提升、单机可承载约 500 万 PV,这类结论讲的是整体架构能力,仍然要用执行计划来验证具体一条查询。AnQiCMS 的数据层通过 GORM 构建查询,索引设计要放在字段组合与查询条件一起看,写死索引名称的做法在这种抽象层里更容易和实际结构脱节。
常见问题
问:慢查询加提示后确实快了,为什么还要去掉? 答:那次优化解决的是当时的数据分布。留下提示会让后续条件变化时无法自动改选路径,需要人工复核才不出事。
问:怎么判断已经退化成全表扫描? 答:看执行计划里的扫描类型与预估行数,以及是否伴随额外排序;只看响应时间会漏掉规模变化带来的差异。
问:新装库统一建一个字段索引说明什么? 答:说明维护方倾向于让索引结构与查询写法解耦,避免提示与实际索引绑定过紧。