这个 MySQL bug 让我大开眼界
这周收到一个 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 都还没解决,所以在官方修复前我们要尽量避免这种写法,那么怎么避免呢,主要有两种方案
- 使用 force index 来强制使用指定的索引,如下:
select * from order_info force index(idx_uid_stat) where uid = 5837661 order by id asc limit 1
这种写法虽然可以,但不够优雅,如果这个索引被废弃了咋办?于是有了第二种比较优雅的方案
- 使用 order by (id+0) 方案,如下
select * from order_info where uid = 5837661 order by (id+0) asc limit 1
这种方案也可以让优化器选择正确的索引,更推荐!
巨人的肩膀
- mysql 优化器 bug http://4zsw5.cn/L1zEi
最后,求关注,原创不易,需要一些正反馈,欢迎大家关注我的公众号「码海」,一起进阶,一起牛逼

这个 MySQL bug 让我大开眼界的更多相关文章
- MySQL Bug导致异常宕机的分析流程
原文链接:http://click.aliyun.com/m/42521/ 摘要: 本文主要通过一个bug来记录一下如何分析一个MySQL bug的崩溃信息. 版本:Percona 5.7.17-11 ...
- MySQL bug:server-id默认被自己主动置为1
昨天同事在做主从时,从库报例如以下错误: Got fatal error 1236 from master when reading data from binary log: 'Misconfigu ...
- 从MySQL Bug#67718浅谈B+树索引的分裂优化(转)
原文链接:http://hedengcheng.com/?p=525 问题背景 今天,看到Twitter的DBA团队发布了其最新的MySQL分支:Changes in Twitter MySQL 5. ...
- MySQL Bug剖析之Slave节点并行复制死锁
此文已由作者温正湖授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 有天一早,DBA同学就找上来了,说有个DDB集群下的RDS实例Slave节点(从库)死锁了,请求支援.说实话 ...
- MySQL Bug#67718 浅谈B+树索引的分裂优化
原文链接:http://hedengcheng.com/?p=525 问题背景 今天,看到Twitter的DBA团队发布了其最新的MySQL分支:Changes in Twitter MySQL 5. ...
- 如何从头到脚彻底解决一个MySQL Bug
摘要:为了保障华为云GaussDB产品的可靠性,每一款产品发布前都要通过多轮严苛的测试用例. 说明:本文中的MySQL,如果不做特殊说明,指的是开源社区版MySQL. 华为云数据库新版本在发布之前,会 ...
- MySQL · BUG分析 · Rename table 死锁分析
http://mysql.taobao.org/monthly/2016/03/06/ 背景 InnoDB buffer pool中的page管理牵涉到两个链表,一个是lru链表,一个是flush 脏 ...
- Mysql bug: The server time zone value '�й���ʱ��' is unrecognized or represents more than one time zone.
在 MySQL 中执行命令试下: set global time_zone='+8:00': 解释:在访问数据库时出现时区无法识别问题,在通过在数据库连接URL后,加上?serverTimezone= ...
- MySQL 5.6查询优化器新特性的“BUG” eq_range_index_dive_limit
本文转自 http://www.imysql.cn 最近碰到一个慢SQL问题,解决过程有点小曲折,和大家分享下. SQL本身不复杂,表结构.索引也比较简单,不过个别字段存在于多个索引中. CREATE ...
随机推荐
- Django 模版语法 测试环境 ORM单表查询
模版语法 传值 视图函数向前端html页面传值,基本上所有的数据类型都可以渲染在前端页面上. views.py from django.shortcuts import render, redirec ...
- Linux修改Ip简单知识了解
1. 在终端输入:vim /etc/sysconfig/network-scripts/ifcfg-etho(etho是指的安装centos的产生的网卡) 2.按i开始编辑,填写ip地址.子网掩码.网 ...
- C#:[StructLayout(LayoutKind.Sequential)]
参考网址: https://blog.csdn.net/hongkonglife/article/details/23422857 结构体是由若干成员组成的.布局有两种1.Sequential,顺序布 ...
- 复习git
git 常用点,详解 from my typora 文章目录 git 常用点,详解 git 模式解析 删除文件 方式一: 方式二: 远程库 配置忽略文件 查看版本库日志,以及版本回退 解决冲突 替换我 ...
- Struts2之Json插件的使用
时间:2017-1-15 02:27 --普通方式处理异步请求: ServletActionContext.getResponse().getWriter().print("aa&qu ...
- 聊聊 PC 端自动化最佳方案 - Pywinauto
1. 前言 大家好,我是安果! 上一篇文章,聊到 PC 端的一种自动化方案:WinAppDriver 聊聊 PC 端自动化最佳方案 - WinAppDriver 有小伙伴后台给我留言,说「 pywin ...
- 初探Spring Security
Spring Security 简介 Spring Security是Spring家族中的一个组成框架,具有强大且高度可定制的身份验证和访问控制功能,致力于为Java应用程序提供身份的验证和授权 (先 ...
- linux shell 删除满足正则表达式的文件
用find配合xargs rm find . -type f -name "to_delete_file_[a-z]_*_[0-9].jpg" | xargs rm
- centos7 ftp 拒绝连接
2021-09-03 1. 问题描述 刚才在重新搭建 ftp 服务器时,发现 ftp 拒绝连接,想起来我还没启动 vsftpd 服务,尝试启动却无法启动 vsftpd 服务 2. 解决方法 使用命令 ...
- linux多次登录失败锁定账户
2021-07-22 1.配置对系统进行失败的ssh登录尝试后锁定用户帐户 # 配置登录访问的限制 vi /etc/pam.d/system-auth 或者 vi etc/pam.d/password ...