关于一个MYSQL异常问题的追查及优化

问题

用户工单疑问:相同的语句,只是最后的limit行数不同。奇怪的是,limit 10 的性能比limit 100的语句还慢约10倍。

隐藏用户表信息,语句及结果如下
SELECT f1 , SUM(`f2`) `CNT` FROM T WHERE f1 IS NOT NULL AND f3 = ’2014-05-12′ GROUP BY f1 ORDER BY `CNT` DESC LIMIT 10;
执行时间3 min 3.65 sec

SELECT f1 , SUM(`f2`) `CNT` FROM T WHERE f1 IS NOT NULL AND f3 = ’2014-05-12′ GROUP BY f1 ORDER BY `CNT` DESC LIMIT 100;
执行时间1.24Sec.

性能差距非常大!

分析
MySQL Tips:追查语句执行时最常用的方法,是通过explain来看语句的执行计划。 ​

更有冲击性的效果是通过缩小范围后,在这个数据下,limit 67和limit 68的执行计划相差很大。

两个执行计划:
LIMIT 67
id: 1
select_type: SIMPLE
table: a
type: range
possible_keys: A,B,C
key: B
key_len: 387
ref: NULL
rows: 2555192
Extra: Using where; Using temporary; Using filesort
1 row in set (0.00 sec)

LIMIT 68
id: 1
select_type: SIMPLE
table: a
type: ref
possible_keys: A,B,C
key: A
key_len: 3
ref: const
rows: 67586
Extra: Using where; Using temporary; Using filesort
1 row in set (0.00 sec)

可以看到,两个语句的执行计划不同:使用的索引不同。

MySQL Tips:explain的结果中,key表示最终使用的索引,rows表示使用这个索引需要扫描的行数,这是个估计值。

表中 索引A定义为 (f3, f4, f1, f2, f5); 索引B定义为(f1, f2, f3);

一个确认

虽然rows是估计值,但是指导索引使用的依据。既然limit 68能达到rows 67586,说明在第一个语句优化器可选结果中,也应该有此值,为什么不会选择索引A?
先确认一下我们上面的这个结论。

MySQL Tips:MySQL语法中能够用force index 来强行要求优化器使用某一个索引。

Explain SELECT f1 , SUM(f2) CNT FROM t force index(A) WHERE f1 IS NOT NULL AND f3 = ‘2014-05-12’ GROUP BY P ORDER BY CNT DESC LIMIT 67\G

id: 1
select_type: SIMPLE
table: a
type: ref
possible_keys:A
key: A
key_len: 3
ref: const
rows: 67586
Extra: Using where; Using temporary; Using filesort
1 row in set (0.00 sec)

顺便说明,由于我们指定了force index,因此优化器不会考虑其他索引,possible_keys里只会显示A。我们关注的是rows:67586。这说明在limit 67语句里,使用索引A也能够减少行扫描。

MySQL Tips:MySQL优化器会对possiable_key中的每个可能索引都计算查询代价,选择最小代价的查询计划。

至此我们大概可以猜测,这个应该是MySQL实现上的bug:没有选择合适的索引,导致使用了明显错误的执行计划。

MySQL Tips:MySQL的优化器执行期间需要依赖于表的统计信息,而统计信息是估算值,因此有可能导致得到的执行计划非最优。

但要说明的是,上述Tip是客观情况造成(可接受),但本例却是例外,因此优化器实际上可以拿到能够作出选择正确结果的数据(rows值),但是最终选择错误。

原因分析

MySQL优化器是按照查询代价的估算值,来确定要使用的索引。计算这个估算值的过程,基本是按照“估计需要扫描的行数”来确定的。

MySQL Tips:MySQL在目前集团主流使用的5.1和5.5版本中只能使用前缀索引。

因此,使用索引A只能用上字段f3,使用索引B只能用上字段f1。Rows即为使用了索引查到上下界,之后需要扫描的数据行数(估算值)。

上述的语句需要用到group和order by,因此执行计划中都有Using temporary; Using filesort。
流程上按顺序先计算使用索引A的查询代价。

之后依次计算其他possitabe_key的查询代价。由于过程中需要排序,在得到一个暂定结果后,需要判断是否有代价更低的排序方式(test_if_cheaper_ordering)。
与之前的大同小异,也是依靠估计扫描行数来计算代价。

在这个逻辑的实现过程中,存在一个bug:在估计当前索引的区分度的时候,没有考虑到前缀索引。

即:假设表中有50w行数据,索引B(f1,f2,f3),则计算索引区分度时,需要根据能够用上的前缀部分来确定。比如f1有1000个不同的值,则平均每个key值上的记录数为500.如(f1,f2)有10000个同的值,则平均每个组合key上的记录数为50,若(f1,f2,f3)有50w个不同的值,则平均每个组合key上的记录数为1。

MySQL Tips:每个key上的记录数越少,说明使用该索引查询时效率最高。对应于show index from tbl 输出结果中的Cardinality值越大。

在这个case下,索引B只能使用f1做前缀索引,但是在计算单key上的行平均值时用的是(f1,f2,f3),这就导致估算用索引B估算的时候,得到的代价偏小。导致误选。

回到问题本身

1、 为什么limit值大的时候反而选对了呢?
这是因为在计算B的查询代价时,查询需要返回的行数limit_rows也参与乘积,若limit值较大,则计算出来的B的代价就会更大,反而会由于代价。值超过A,而导致优化器最终选择A。

2、 这个表有50w行数就,为什么limit相差为就差别这么大?
这与语句本身有关。这个语句中有group by,这就意味着每多limit一个值,实际上需要扫描更多的行N。 这里N为“表的总行数”/“表中不同的f2值”。
也就是说这个语句使得这个bug有放大作用。

解决方案

分析清楚后解决方法就比较简单了,修改代码逻辑,在执行test_if_cheaper_ordering过程中,改用字段f1的区分度来计算即可。

 

时间: 2024-10-31 09:24:50

关于一个MYSQL异常问题的追查及优化的相关文章

mysql-求一个Mysql语句 查询出当前周的数据按照天分组

问题描述 求一个Mysql语句 查询出当前周的数据按照天分组 SELECT DATE_FORMAT(uploadTime_beg%Y-%m-%d"") as time SUM(field01) as sumStatus1 SUM(field02) as sumStatus2 SUM(field03) as sumStatus3 SUM(field04) as sumStatus4 SUM(field05) as sumStatus5 FROM health_realdata WHERE

mfc-MFC中一个关于异常的问题

问题描述 MFC中一个关于异常的问题 在VS中,为什么会让我这样写CATCH,中间的逗号是什么意思,没有的话就出错,说让我输入")" CATCH(CFileException,e) 解决方案 http://blog.chinaunix.net/uid-21411227-id-1826958.html 这里有详细一些的介绍,以及宏的定义. 注意区分作为C++语法的try catch和MFC的宏TRY CATCH. 解决方案二: CATCH是MFC中的一个宏,在MFC诞生的时候,C++还很

用sql语句遍历一个表里异常(时间段内很大或者很小)的数据,并将异常数据修改成相应时间段前的数据

问题描述 用sql语句遍历一个表里异常(时间段内很大或者很小)的数据,并将异常数据修改成相应时间段前的数据 我有一张表,里面3字段id.time.value. 由于value中的部分值异常的大,我想批量修改数据,比如说4.1日-4.20日出现中出现异常大的数据,我想把异常大的数据修改成和他时间段(时间段前或者后)差不多的数据,请问下这个sql应该怎么写啊.我用游标的方法怎么实现.谢谢啊! 解决方案 不知道你的数据库是什么,用MS SQL SERVER示意 建表SQL如下 CREATE TABLE

并发-请教一个mysql批量更新时的deadlock问题?

问题描述 请教一个mysql批量更新时的deadlock问题? 数据是mysql 5.6 表引擎是innoDB,DAO是mybatis3 有个表,比如是订单-物品表,里面保存每个订单的所有物品清单 字段有:record_id(记录id,自增长),order_id(订单id),good_id(物品id)等其他字段.. 有个数据同步的业务场景 需要在一个事务里 先根据order_id做删除操作,delete from xxx where order_id=xx 然后再批量增加订单-物品清单inser

Linux有问必答:如何通过命令行创建和设置一个MySQL用户

Linux有问必答:如何通过命令行创建和设置一个MySQL用户 问题:我想要在MySQL服务器上创建一个新的用户帐号,并且赋予他适当的权限和资源限制.如何通过命令行的方式来创建并且设置一个MySQL用户呢? 要访问一个MySQL服务器,你需要使用一个用户帐号登录其中方可进行.每个MySQL用户帐号都有许多与之相关连的属性,例如用户名.密码以及权限和资源限制."权限"定义了特定用户能够在MySQL服务器中做什么,而"资源限制"为用户设置了一系列服务器资源的使用许可.创

记录一个mysql连接慢的问题

问题现象是这样的: 我在一台机器上(61.183.23.23)启动了一个mysql,然后开通一个账号可以从127.0.0.1或者从61.183.23.23访问.但是遇到一个问题就是使用下面两个命令行访问的时候,速度差别非常大: mysql –h 127.0.0.1 –u user mysql –h 61.183.23.23 –u user   然后我使用ping,判断两个IP的速度差不多. 使用127.0.0.1的IP速度比另一个快太多了.虽然说这里的61.183.23.23需要去外网走一圈,但

mysql语法错误-请教一个mysql创建查询表问题

问题描述 请教一个mysql创建查询表问题 create table student(stuId int(10) not null auto_increment primary key comment '主键',stuNum varchar(50) default null comment '学号',stuName varchar(50) default null comment '名字',stuPwd varchar(25) default null comment '密码',stuSex v

Linux有问必答:如何在命令行创建一个MySQL数据库

Linux有问必答:如何在命令行创建一个MySQL数据库 问题:在一个某处运行的MySQL服务器上,我该怎样通过命令行创建和安装一个MySQL数据库呢? 为了能通过命令行创建一个MySQL数据库,你可以使用mysql命令行客户端.下面是通过mysql命令行客户端创建和安装MySQL的步骤. 第一步:安装MySQL客户端 当然你得确保MySQL客户端已经安装完毕.如果没有的话,可以按照下面的方法. 在Debian,Ubuntu 或者 Linux Mint上: $ sudo apt-get inst

写一个mysql数据库的sql 递归查询,我现在有个能查询三级的,谁能帮我改为查询四级或五级的

问题描述 select distinct * from (select * from news_types where news_types.id=1 union select n2.* from news_types n1,news_types n2 where n1.id=1 and n2.parentid=n1.id union select n3.* from news_types n3,( select n2.* from news_types n1,news_types n2 whe