这个 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]DDL操作导致备库复制中断
----------------- 在MySQL5.1及之前的版本中,如果有未提交的事务trx,当执行DROP/RENAME/ALTER TABLE RENAME操作时,不会被其他事务阻塞住.这会导致 ...
- 从MySQL Bug#67718浅谈B+树索引的分裂优化
从MySQL Bug#67718浅谈B+树索引的分裂优化 1月 6th, 2013 发表评论 | Trackback 问题背景 今天,看到Twitter的DBA团队发布了其最新的MySQL分支:Cha ...
- 项目纪实丨MySQL Bug引发客户现场升级失败 万里DBA 6小时攻克难关
上午10:00 某运营商核心报表平台升级前夕 作为万里数据库的战略合作伙伴,某运营商一直密切关注着国产数据库的发展.其系统中一套基于MySQL8.0.11版本的核心报表平台,近期由于存在安全扫描的漏洞 ...
- 这个 MySQL bug 99% 的人会踩坑!
这周收到一个 sentry 报警,如下 SQL 查询超时了. select * from order_info where uid = 5837661 order by id asc limit 1 ...
- 如何从头到脚彻底解决一个MySQL Bug
摘要:为了保障华为云GaussDB产品的可靠性,每一款产品发布前都要通过多轮严苛的测试用例. 说明:本文中的MySQL,如果不做特殊说明,指的是开源社区版MySQL. 华为云数据库新版本在发布之前,会 ...
- mysql索引如何分裂节点_从MySQL Bug#67718浅谈B+树索引的分裂优化(转)
原文链接:http://hedengcheng.com/?p=525 问题背景 今天,看到Twitter的DBA团队发布了其最新的MySQL分支:Changes in Twitter MySQL 5. ...
- MySQL Bug一例-----ibuf cursor restoration fails
产生原因: 1.开启change buffer(innodb_change_buffering) 2.对表进行大量delete 操作 3.对相同表进行truncate bug名称:ibuf curso ...
- mysql bug frash_MySQL Flush导致的等待问题
--MySQL Flush导致的等待问题 -------------------------------2014/07/13 前言 在实际生产环境中有时会发现大量的sql语句处于waiting for ...
- 一次生产慢响应问题排查:TRUNCATE TABLE (MySQL Bug 68184)
I.背景 生产环境观察到有时间规律的慢接口响应(每天固定时间点集中出现),需要解决接口有规律的慢响应问题. II .问题排查流程 1.观察skywalkiing定位具体接口响应慢的节点.(初步定位都是 ...
最新文章
- python统计特定类型文件数量_分享一些常见的Python编程面试题及答案
- synergy共享ubuntu和windows键鼠
- python小项目推荐项目-Python 的练手项目有哪些值得推荐?
- bootstrap table php,bootstrap table Tooltip
- airpods有时能连上有时连不上怎么办?
- 详解Linux 五种IO模型
- 英语语法---感叹词详解
- CSDN博文编辑技巧-如何去除上传的图片水印
- 测试项目:车牌检测,行人检测,红绿灯检测,人流检测,目标识别
- Wonderware配置-Intouch读取数据 6
- 【经验分享】突然我的SM.MS的图床没法访问了(内附解决方法)
- 【算法导论06】递归算法-perm算法
- html项目的致谢词,毕业论文致谢词范文200字(精选10篇)
- 7-1 电话聊天狂人 (25分) PTA 数据结构
- linux can总线接收数据串口打包上传_「干货」手把手教你用Zedboard学习Linux移植和驱动开发...
- Java工程师成神之路 | 2022正式版
- 用几何画板画七边形的方法
- C#上位机(编码/汉字转换)
- 236_自定义抽签器二
- 【论文笔记】MAGNN: Metapath Aggregated Graph Neural Network for Heterogeneous Graph Embedding
热门文章
- c的关于数组初始化 和 memset用法
- 计算机应用英语考什么,网考计算机应用基础(本)试卷10(国外英语资料).doc
- html元素移动时颜色逐渐变深,css实现随鼠标移动div渐变色效果
- oracle闪回保存多久,CSS_oracle 中关于flashback闪回的介绍, 1、必须设定undo保留时间足 - phpStudy...
- AI理论知识基础(22)-逻辑斯蒂映射-伪随机数
- 【Python】聊聊Pandas的前世今生
- 【机器学习基础】深入讨论机器学习 8 大回归模型的基本原理以及差异!
- 【机器学习基础】理解关联规则算法
- 【NLP】ACL 2010-2020研究趋势总结
- wuhan2020新型冠状病毒防疫信息收集平台社区版非正式发布