这周收到一个 sentry 报警,如下 SQL 查询超时了。

select * from order_info where uid = 5837661 order by id asc limit 1

执行show create table order_info 发现这个表其实是有加索引的

CREATE TABLE `order_info` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,`uid` int(11) unsigned,`order_status` tinyint(3) DEFAULT NULL,... 省略其它字段和索引PRIMARY KEY (`id`),KEY `idx_uid_stat` (`uid`,`order_status`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8

理论上执行上述 SQL 会命中 idx_uid_stat 这个索引,但实际执行 explain 查看

explain select * from order_info where uid = 5837661 order by id asc limit 1

可以看到它的 possible_keys(此 SQL 可能涉及到的索引) 是 idx_uid_stat,但实际上(key)用的却是全表扫描

我们知道 MySQL 是基于成本来选择是基于全表扫描还是选择某个索引来执行最终的执行计划的,所以看起来是全表扫描的成本小于基于 idx_uid_stat 索引执行的成本,不过我的第一感觉很奇怪,这条 SQL 虽然是回表,但它的 limit 是 1,也就是说只选择了满足 uid = 5837661 中的其中一条语句,就算回表也只回一条记录,这种成本几乎可以忽略不计,优化器怎么会选择全表扫描呢。

为了查看 MySQL 优化器为啥选择了全表扫描,我打开了 optimizer_trace 来一探究竟

画外音:在MySQL 5.6 及之后的版本中,我们可以使用 optimizer trace 功能查看优化器生成执行计划的整个过程

使用 optimizer_trace 的具体过程如下

SET optimizer_trace="enabled=on";        // 打开 optimizer_trace
SELECT * FROM order_info where uid = 5837661 order by id asc limit 1
SELECT * FROM information_schema.OPTIMIZER_TRACE;    // 查看执行计划表
SET optimizer_trace="enabled=off"; // 关闭 optimizer_trace

MySQL 优化器首先会计算出全表扫描的成本,然后选出该 SQL 可能涉及到的所有索引并且计算索引的成本,然后选出所有成本最小的那个来执行,来看下 optimizer trace 给出的关键信息

{"rows_estimation": [{"table": "`rebate_order_info`","range_analysis": {"table_scan": {"rows": 21155996,"cost": 4.45e6    // 全表扫描成本}},..."analyzing_range_alternatives": {"range_scan_alternatives": [{"index": "idx_uid_stat","ranges": ["5837661 <= uid <= 5837661"],"index_dives_for_eq_ranges": true,"rowid_ordered": false,"using_mrr": false,"index_only": false,"rows": 255918,"cost": 307103,            // 使用idx_uid_stat索引的成本"chosen": true}],"chosen_range_access_summary": {    // 经过上面的各个成本比较后选择的最终结果"range_access_plan": {"type": "range_scan","index": "idx_uid_stat",  // 可以看到最终选择了idx_uid_stat这个索引来执行"rows": 255918,"ranges": ["58376617 <= uid <= 58376617"]},"rows_for_plan": 255918,"cost_for_plan": 307103,"chosen": true}}  ...

可以看到全表扫描的成本是 4.45e6,而选择索引 idx_uid_stat 的成本是 307103,远小于全表扫描的成本,而且从最终的选择结果(chosen_range_access_summary)来看,确实也是选择了 idx_uid_stat 这个索引,但为啥从 explain 看到的选择是执行 PRIMARY 也就是全表扫描呢,难道这个执行计划有误?

仔细再看了一下这个执行计划,果然发现了猫腻,执行计划中有一个 reconsidering_access_paths_for_index_ordering 选择引起了我的注意

{"reconsidering_access_paths_for_index_ordering": {"clause": "ORDER BY","index_order_summary": {"table": "`rebate_order_info`","index_provides_order": true,"order_direction": "asc","index": "PRIMARY",    // 可以看到选择了主键索引"plan_changed": true,"access_type": "index_scan"}}
}

这个选择表示由于排序的原因再进行了一次索引选择优化,由于我们的 SQL 使用了 id 排序(order by id asc limit 1),优化器最终选择了 PRIMARY 也就是全表扫描来执行,也就是说这个选择会无视之前的基于索引成本的选择,为什么会有这样的一个选项呢,主要原因如下:

The short explanation is that the optimizer thinks — or should I say hopes — that scanning the whole table (which is already sorted by the id field) will find the limited rows quick enough, and that this will avoid a sort operation. So by trying to avoid a sort, the optimizer ends-up losing time scanning the table.

从这段解释可以看出主要原因是由于我们使用了 order by id asc 这种基于 id 的排序写法,优化器认为排序是个昂贵的操作,所以为了避免排序,并且它认为 limit n 的 n 如果很小的话即使使用全表扫描也能很快执行完,所以它选择了全表扫描,也就避免了 id 的排序(全表扫描其实就是基于 id 主键的聚簇索引的扫描,本身就是基于 id 排好序的)

如果这个选择是对的那也罢了,然而实际上这个优化却是有 bug 的!实际选择 idx_uid_stat 执行会快得多(只要 28 ms)!网上有不少人反馈这个问题,而且出现这个问题基本只与 SQL 中出现 order by id asc limit n这种写法有关,如果 n 比较小很大概率会走全表扫描,如果 n 比较大则会选择正确的索引。

这个 bug 最早追溯到 2014 年,不少人都呼吁官方及时修正这个bug,可能是实现比较困难,直到 MySQL 5.7,8.0 都还没解决,所以在官方修复前我们要尽量避免这种写法,如果一定要用这种写法,怎么办呢,主要有两种方案

  1. 使用 force index 来强制使用指定的索引,如下:

select * from order_info force index(idx_uid_stat) where uid = 5837661 order by id asc limit 1

这种写法虽然可以,但不够优雅,如果这个索引被废弃了咋办?于是有了第二种比较优雅的方案

  1. 使用 order by (id+0) 方案,如下

select * from order_info where uid = 5837661 order by (id+0) asc limit 1

这种方案也可以让优化器选择正确的索引,更推荐!为什么这个 trick 可以呢,因为此 SQL 虽然是按 id 排序的,但在 id 上作了加法这样耗时的操作(虽然只是加个无用的 0,但足以骗过优化器),优化器认为此时基于全表扫描会更耗性能,于是会选择基于成本大小的方式来选择索引

巨人的肩膀

  • mysql 优化器 bug http://4zsw5.cn/L1zEi

这个 MySQL bug 99% 的人会踩坑!相关推荐

  1. 警惕参数变化,MySQL 8.0 升级避免再次踩坑

    MySQL 8.0新特性专栏目录 <MySQL开发规范>过时了,视图查询性能提升了一万倍 你真的会用EXPLAIN么,SQL性能优化王者晋级之路 索引三剑客之降序索引和不可见索引 千呼万唤 ...

  2. Mysql数据类型TINYINT(1)与BOOLEAN踩坑记

    熟悉Mysql的同学应该都知道,Mysql查询的boolean结果将输出为0或者1. 比如: select 1=1; 其输出结果为1. 查阅mysql官方文档仅找到如下描述: 11.10 Using ...

  3. Logstash同步mysql一对多数据到ES(踩坑日记系列)

    场景: Logstash .Kibana.ES版本:6.3.1. 使用Logstash从mysql同步用户和用户所有的宠物到ES中. 希望的格式: "register_name": ...

  4. 4、Mysql 主从复制报错[ERROR] [MY-013117] 踩坑

    2020-07-01T14:34:42.709318Z 12 [ERROR] [MY-013117] [Repl] Slave I/O for channel '': Fatal error: The ...

  5. mysql 结束符报错_【踩坑记录】MySQL 实现自定义递归函数

    因项目需要,需根据某个商品类别path,查询该类别下的所有子类别 表  goods-categories(path,parent_id,id) 该处使用的表为临时创建的表 t1(id,parent_i ...

  6. 阿里云ECS云服务器(linux系统)安装mysql后远程连接不了(踩坑)

    转载:https://www.jb51.net/article/159514.htm 用navcat远程连阿里云的数据库死活连接不上.始终报:2003 - Can't connect to MySQL ...

  7. MySQL导入官方数据库world防踩坑完全操作

    1.在MySQL官方网站下载官方数据集world,链接.下载zip格式文件然后解压即可,是一个名叫world.sql的文件 2.打开电脑中mysql安装目录下的my.ini文件,找到secure_fi ...

  8. storm mysql spout_storm kafkaSpout 踩坑问题记录! offset问题!

    整合kafka和storm例子网上很多,自行查找 问题描述: kafka是之前早就搭建好的,新建的storm集群要消费kafka的主题,由于kafka中已经记录了很多消息,storm消费时从最开始消费 ...

  9. excel删除空行_Excel里99.9%的人都踩过的坑,早看早避开!

    本文作者丨可可(小 E 背后的小仙女) 本文由「秋叶 Excel」原创发布 如需转载,请在公众号发送关键词「转载」查看说明 2019 年上班第一天感觉怎么样呢? 望着满屏幕铺天盖地的表格,我只能摸摸自 ...

最新文章

  1. uhttpd 架构调用细节之lua
  2. JavaScript跨域总结与解决办法(转)
  3. MST(最小生成树)上的确定性和存在性问题
  4. JBOSS内存溢出处理
  5. 【Python】os库介绍
  6. 【FLink】Flink 1.9 升级到 1.12.4 无配置页面 无日志
  7. 落后产能的实现路径 | 凌云时刻
  8. 利用kd树实现最近邻搜索
  9. 仓储rfid文件_物联网RFID标签的四大主流应用场景
  10. Verilog实现之任意分频电路
  11. java后台将数据导出到Excel表格
  12. img: SVG格式在vue中的使用
  13. 部门来了一位前阿里的大神…
  14. 真正意义上能够全部抓取昵图网全站图片
  15. redis list操作leftpop
  16. 第一个panda3d C++程序
  17. ShareSDK移动APP社会化分享组件
  18. 售后回访:汽车行业电话回访满意度及售后服务流程
  19. python的优点和缺点以及用途
  20. tecplot 脚本文件

热门文章

  1. flutter 刷脸_GitHub - nnnggel/baidu_face_plugin: 百度人脸识别和活体检测 Flutter 插件(目前版本仅支持 Android)...
  2. 解析postgresql 删除重复数据案例
  3. 人工智能改善客户服务体验
  4. JavaScript初学者编程题(5)
  5. java 数字三角形_数字三角形 Number Triangles(java的MLE解决办法)
  6. 【数据结构】HashMap 面试题8问
  7. Kruskal算法模版
  8. poj3007(set的应用)
  9. python怎么控制while循环_Python流程控制之while循环怎么学呢?老男孩Python
  10. 一致 先验分布 后验分布_遇到分布式一致性问题,咋整?