问题背景:索引都加了,查询还是慢
项目里有个订单查询,明明已经在相关字段上建好了索引,可接口还是慢。按惯性思维,大多数人都会觉得——「索引都加了,还能慢到哪去?」但现实偏偏就是这么打脸。
实在没招了,我把这条 SQL 单独捞出来,用 EXPLAIN 看了一遍执行计划,才发现问题压根不在「有没有索引」,而在 MySQL 根本没走我建的那个索引。它自己另挑了一条它认为「更划算」的路线,结果反而更慢。
于是就想到了用 force 强制指定索引,逼 MySQL 走对路。一试,查询速度立刻回到了正常水平。
这事让我真正开始认真研究两件事:为什么 MySQL 会选错索引,以及 ThinkPHP8 里怎么用 force 强制指定索引。
先搞清楚:MySQL 为什么会“犯傻”
很多人以为加了索引就万事大吉,其实不是。选不选你建的那个索引,是 MySQL 自己说了算的,不是我们说了算。
MySQL 会把你这条查询所有可能的执行方式都列出来,逐一估算“代价”,选最便宜的那个。代价包括从磁盘读数据要花多少功夫,以及在内存里过滤、排序要花多少计算量。
问题出在——它靠估算,而不是靠实际跑一遍。
统计信息不准,是最大的坑
MySQL 估算的核心依据是索引的“基数”,也就是这个索引上有多少个不重复的值。但这个值不是每次都精确统计的,InnoDB 默认是采样统计——随机选一些数据页看看不同值的分布,然后推算整个索引的情况。
采样就有偏差。表数据量变化大的时候,统计信息没及时更新,MySQL 拿的还是“老地图”,但数据早就变了。
我遇到的情况就是:订单表在促销期间数据量暴增,但 MySQL 手里那份统计信息还停留在几天前。它估算走某个索引需要扫描 120 万行,回表代价算下来要 500 万,而全表扫描才 127 万,于是果断选了全表扫描。实际上走索引只需要扫描几千行,比全表扫描快几十倍。
查询里带了 LIMIT 也会干扰判断
还有一种常见情况。查询带了 LIMIT 1,MySQL 会认为走主键索引能更快找到那一条数据,因为主键天生有序,不用额外排序。但它忽略了:如果过滤条件在另一个索引上,走那个索引可能扫几行就找到了,走主键反而要扫很多行才碰到匹配的。
说白了,MySQL 算的是一笔“理论账”,跟实际磁盘读写、缓存命中率、系统负载这些现实因素对不上。它不是坏了,就是在信息不完备的情况下做了个合理但错误的判断。
ThinkPHP8 里怎么用 force
知道了原因,解决方案就简单了。ThinkPHP8 提供了 force() 方法,用法很直接:
Db::table('think_order')
->force('idx_user_status')
->where('user_id', 123)
->where('status', 1)
->select();对查询强制使用 idx_user_status 索引,索引名必须是数据表里实际创建的索引名称。
模型里也一样:
Order::where('user_id', 123)
->force('idx_user_status')
->select();force() 方法在链式操作里的位置不挑,放 where 前面后面都行。
force 和 use index 有什么区别?
如果你用过原生 SQL 的 USE INDEX,可能会想这俩有什么区别。
区别挺关键的:
USE INDEX 是建议 MySQL 使用某个索引,但如果 MySQL 觉得全表扫描更快,它有权不听你的,还是走全表扫描。
FORCE INDEX 是强制——它假设表扫描的成本极其高昂,只有在实在没有办法用你指定的索引找到数据的时候,才会退化成表扫描。
说白了,USE INDEX 是商量,FORCE INDEX 是命令。ThinkPHP8 的 force() 底层就是生成 FORCE INDEX 这个 SQL 片段,比 USE INDEX 硬得多。
但 force 不是银弹,这几个坑得注意
索引名写错了直接报错
force() 里写的索引名必须跟数据库里建的一模一样。如果索引被删了或者改名了,SQL 会直接报错,不会降级为全表扫描。所以上线前一定要确认索引名。
数据分布变了,原来的索引可能不再是好选择
比如你强制用 idx_user_id,在数据量小的时候没问题。但数据量涨到几千万之后,如果 user_id 的分布严重倾斜(80% 都是测试账号),这个索引可能就不好使了,但 force 还是会死守着用它。
优先考虑更新统计信息
其实很多时候不需要一上来就 force。先试试 ANALYZE TABLE 手动刷新统计信息,让 MySQL 重新认识一下数据分布,可能问题就解决了。force 更适合那种“统计信息更新了但 MySQL 还是选错”的场景。
我的排查流程总结
分享一套我现在用的排查步骤:
第一步,先看真实 SQL。 ThinkPHP8 里开启 SQL 日志,或者用 fetchSql(true) 把生成的 SQL 打出来看看:
Db::table('think_order')->where(...)->fetchSql(true)->select();第二步,拿 SQL 去看执行计划。 重点看三列:实际用了哪个索引、预估扫描行数、是不是全表扫描。
第三步,确认真的是索引问题。 如果执行计划显示走了你期望的索引但还是很慢,那可能是回表太多,考虑加覆盖索引。如果不是你期望的索引,再考虑 force。
第四步,用 force 强制指定。 先在小范围测试,确认性能确实提升了再上线。
Thinkphp8编辑方法携带id参数出现方法参数错误:id的问题,主要是因为地址上没有携带id参数造成的,我们可以通过修改在 config/route.php 中添加配置项:// 操作方法参数绑定来源: route仅路由参数, param所有请求参数(GET+POST+路由), 默认仅GET+路由 // 设置为param以支持POST请求中的参数绑定到控制器方法参数(如: edit($id
authRule::destroy(['id'=>$id]) 和平时用的 delete 删除有什么区别