摘要:为了保障华为云GaussDB产品的可靠性,每一款产品发布前都要通过多轮严苛的测试用例。

说明:本文中的MySQL,如果不做特殊说明,指的是开源社区版MySQL。

华为云数据库新版本在发布之前,会面临一系列严苛的测试规则,除了要求通过MySQL的所有测试用例之外,还需要通过由华为百万级更丰富、更贴近用户业务场景的测试用例构筑的测试防护网,以此充分验证新版本是否满足用户经典场景的稳定性。

正是在这样严苛的验证过程中,我们发现了MySQL的一个潜在Bug。

Bug描述

测试环境:

基于相同的测试用例、数据集,分别测试MySQL 8.0.22, MySQL 8.0.26,与华为云GaussDB(for MySQL)的返回结果。

测试语句:

  1. select
  2. subq_0.c2 as c0
  3. from
  4. (select
  5. ref_6.C_STATE as c0,
  6. case when ref_6.C_PHONE is not NULL then ref_5.C_ID else ref_5.C_ID end
  7. as c1,
  8. floor(
  9. ref_3.c_id) as c2
  10. from
  11. sqltester.t0_hash_partition_p1_view as ref_0
  12. right join sqltester.t4 as ref_1
  13. on (EXISTS (
  14. select
  15. ref_1.c_middle as c0
  16. from
  17. sqltester.t1 as ref_2
  18. where ((false)
  19. and ((true)
  20. or (true)))
  21. or (false)
  22. ))
  23. inner join sqltester.t0_range_key_subpartition_sub_view as ref_3
  24. on (EXISTS (
  25. select
  26. ref_0.c_credit as c0,
  27. ref_1.c_street_1 as c1,
  28. ref_4.c_credit_lim as c2,
  29. ref_3.c_credit as c3
  30. from
  31. sqltester.t0_hash_partition_p1 as ref_4
  32. where true
  33. ))
  34. left join sqltester.t10 as ref_5
  35. inner join sqltester.t11 as ref_6
  36. on (true)
  37. on (((pi() is not NULL))
  38. and (false))
  39. where (((ref_5.C_D_ID is not NULL)
  40. or (ref_3.c_middle is not NULL))
  41. )) as subq_0
  42. where (EXISTS (
  43. select
  44. subq_0.c0 as c0,
  45. pi() as c1,
  46. ref_11.c_street_1 as c2,
  47. ref_11.c_discount as c3,
  48. pi() as c4
  49. from
  50. sqltester.t0_partition_sub_view_mixed_001 as ref_11))
  51. group by 1
  52. order by 1;

返回结果:

如下图所示,MySQL 8.0.22、MySQL 8.0.26与华为云GaussDB(for MySQL)的返回结果不一致,也就是说产生了Bug,如下图红色部分。

Bug分析

首先确定哪一个执行结果是正确的。当前这个语句执行的execution plan是Hash Join,而MySQL8.0里面引入了Hash Join,由此推论开源版本可能存在问题。接下来我们从MySQL成熟版本以及非MySQL数据库两个方面来进行验证。

验证过程:

  • 使用相对成熟的版本MySQL 5.6进行验证,返回结果与GaussDB(for MySQL)相同,但与MySQL 8.0不同。
  • 使用PostgreSQL进行验证,执行结果与MySQL 5.6、GaussDB(for MySQL)相同,但与MySQL 8.0及更高版本不同。

由此可以确定:MySQL 8.0以及更高版本存在问题。

那么,是什么原因引起了这一Bug呢?

1、首先精简查询,以方便后面分析。经过多次验证,将查询简化如下:

  1. SELECT count(*)
  2. FROM
  3. (SELECT 1
  4. FROM sqltester.t4 AS ref_1
  5. INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS
  6. (SELECT 1
  7. FROM sqltester.t4 AS ref_4
  8. WHERE TRUE ))
  9. LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE)
  10. WHERE (((ref_5.C_D_ID IS NOT NULL)
  11. OR (ref_3.c_middle IS NOT NULL)))) AS subq_0
  12.  
  13. 执行计划如下:
  14. -> Aggregate: count(0) (cost=2.75 rows=0)
  15. -> Filter: ((ref_5.C_D_ID is not null) or (ref_3.c_middle is null)) (cost=2.75 rows=0)
  16. -> Inner hash join (no condition) (cost=2.75 rows=0)
  17. -> Index scan on ref_3 using ndx_c_middle (cost=0.13 rows=50)
  18. -> Hash
  19. -> Inner hash join (no condition) (cost=1.50 rows=0)
  20. -> Index scan on ref_1 using ndx_c_id (cost=6.25 rows=50)
  21. -> Hash
  22. -> Left hash join (no condition) (cost=0.25 rows=0)
  23. -> Limit: 1 row(s) (cost=312.50 rows=1)
  24. -> Index scan on ref_4 using ndx_c_id (cost=312.50 rows=50)
  25. -> Hash
  26. -> Zero rows (Impossible filter) (cost=0.00..0.00 rows=0)

从上面的执行计划可以看出,ref_5被优化器进行了优化,转换成了Zero rows,而且ref_5是Left Hash Join的内表。作为Left Join的内表,如果内表没有匹配条件的记录(这里已经是Impossible条件了,也就是说连接条件始终是False),则需要内表生成NULL行来和外表进行外表连接。

2、在MySQL 8.0.22版本上执行问题查询,语句和执行结果如下:

  1. SELECT count(*)
  2. FROM
  3. (SELECT 1
  4. FROM sqltester.t4 AS ref_1
  5. INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS
  6. (SELECT 1
  7. FROM sqltester.t4 AS ref_4
  8. WHERE TRUE ))
  9. LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE)
  10. WHERE (((ref_5.C_D_ID IS NOT NULL) or(ref_3.c_middle IS NOT NULL))))AS subq_0;
  11. +
  12.  
  13. +
  14. | count(*) |
  15. +
  16.  
  17. +
  18. | 2500 |
  19. +
  20.  
  21. +
  22. 1 row in set (0.00 sec)

3、对问题查询进行修改:去掉Where条件里面的另外一个条件(ref_3.c_middle is NULL)。

现在Where条件只包含了(ref_5.C_D_ID IS NOT NULL)一个条件,要求当前查询过滤掉所有ref_5没有匹配的连接记录。

则SQL语句和执行结果如下:

  1. SELECT count(*)
  2. FROM
  3. (SELECT 1
  4. FROM sqltester.t4 AS ref_1
  5. INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS
  6. (SELECT 1
  7. FROM sqltester.t4 AS ref_4
  8. WHERE TRUE ))
  9. LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE)
  10. WHERE (((ref_5.C_D_ID IS NOT NULL))))assubq_0;+
  11.  
  12. +
  13. | count(*) |
  14. +
  15.  
  16. +
  17. | 2500 |
  18. +
  19.  
  20. +
  21. 1 row in set (0.01 sec)

对比修改前后的语句和执行结果可以看出:执行结果与条件(ref_3.c_middle is NULL)没有关系,只与(ref_5.C_D_ID IS NOT NULL)这个条件有关。正常情况下对ref_5表来说,因为是Impossible条件,所以ref_5被优化成了Zero rows。那么如果只剩(ref_5.C_D_ID IS NOT NULL)这个条件,正常的结果应该是空集(count返回0)。但现在开源版本的结果集却不是,这再次说明了开源版本出现了问题。

对于Left Join来说,如果Join条件不匹配,内表需要设置为NULL行来连接外表。而这里执行计划使用的是Zero rows,也就是说MySQL 8.0使用的是ZeroRowsIterator来执行的。执行器需要调用ZeroRowsIterator::SetNullRowFlag来设置Null flag。

4、通过gdb来查看设置是否正确:

  1. Breakpoint 1, ZeroRowsIterator::SetNullRowFlag (this=0x7f92a413d510, is_null_row=false)
  2. at /mywork/mysql-sql/sql/basic_row_iterators.h:398
  3. 398 assert(m_child_iterator != nullptr);
  4. (gdb) n
  5. 399 m_child_iterator->SetNullRowFlag(is_null_row);
  6. (gdb) s
  7. std::unique_ptr<RowIterator, Destroy_only<RowIterator> >::operator-> (this=0x7f92a413d520)
  8. at /opt/simon/taurus/mysql-root/src/tools/gcc-9.3.0/include/c++/9.3.0/bits/unique_ptr.h:355
  9. 355 return get();
  10. (gdb) fin
  11. Run till exit from #0 std::unique_ptr<RowIterator, Destroy_only<RowIterator> >::operator-> (
  12. this=0x7f92a413d520)
  13. at /opt/simon/taurus/mysql-root/src/tools/gcc-9.3.0/include/c++/9.3.0/bits/unique_ptr.h:355
  14. ZeroRowsIterator::SetNullRowFlag (this=0x7f92a413d510,is_null_row=false)
  15. at /home/simon/mywork/mysql-sql/sql/basic_row_iterators.h:399
  16. 399 m_child_iterator->SetNullRowFlag(is_null_row);
  17. Value returned is $1 = (RowIterator *) 0x7f92a413d4d0
  18. (gdb) s
  19. TableRowIterator::SetNullRowFlag (this=0x7f92a413d4d0,is_null_row=false)
  20. at /home/simon/mywork/mysql-sql/sql/records.cc:229
  21. 229 if (is_null_row) {
  22. (gdb) n
  23. 232 m_table->reset_null_row();
  24. (gdb)
  25. 234 }

从上面的gdb来看,断点处利用ZeroRowsIterator::SetNullRowFlag将表的Null flag设置为了False。后面的gdb信息也证明了这一点。

可以确定,导致此Bug的原因是:ZeroRowsIterator::SetNullRowFlag设置为False这里是不正确的。因为如果把ZeroRowsIterator::SetNullRowFlag设置为False,那就会导致内表为Zero Rows的Left Join生成内表非NULL的结果集。

如何解决

既然上面的Bug分析已经非常清楚了,那么修复起来也就比较简单了。只需要将ZeroRowsIterator::SetNullRowFlag始终设置为True就可以了。因为ZeroRowIterator只能产生两种结果,一种是空集,另一种就是作为外连接的内表产生NULL行。

对MySQL-8.0.26进行修复后,执行结果如下:

从返回的结果可以看出查询结果正确,也就是说问题得到了修复。

为了保障华为云GaussDB产品的可靠性,每一款产品发布前都要通过多轮严苛的测试用例。在发现问题后,华为云数据库团队以缜密的思路去逐步确定问题、分析问题,并第一时间修复Bug,解决问题,以确保客户的数据安全和业务结果的准确性。

华为云数据库团队荟聚了业内50%以上的数据库内核专家,以专业技术实时保障客户业务安全,助力企业业务安全上云!

华为云开年采购季盛大开幕!点击此处,快来0门槛抽奖!

点击关注,第一时间了解华为云新鲜技术~

如何从头到脚彻底解决一个MySQL Bug的更多相关文章

  1. 解决一个 MySQL 服务器进程 CPU 占用 100%解决一个 MySQL 服务器进程 CPU 占用 100%的技术笔记》[转]

    转载地址:http://bbs.chinaunix.net/archiver/tid-1823500.html 解决一个 MySQL 服务器进程 CPU 占用 100%解决一个 MySQL 服务器进程 ...

  2. iOS 解决一个复杂bug 之 计分卡

    由于该模块界面和业务逻辑都很复杂,并且整个界面设计和业务逻辑都在ViewController(下面简称为VC)里面完成.该VC共有3000多行,一个函数几百张的也有.所以,解决起来真是头疼. 1. 问 ...

  3. MySQL Bug导致异常宕机的分析流程

    原文链接:http://click.aliyun.com/m/42521/ 摘要: 本文主要通过一个bug来记录一下如何分析一个MySQL bug的崩溃信息. 版本:Percona 5.7.17-11 ...

  4. Linux 下一个 Mysql error 2002 错误解决

    Linux 下一个 Mysql error 2002 错误解决     首先查看 /etc/rc.d/init.d/mysqld status 查看mysql它已开始.     假设启动的的话,先将数 ...

  5. mysql的从头到脚优化之服务器参数的调优

    一. 说到mysql的调优,有许多的点可以让我们去做,因此梳理下,一些调优的策略,今天只是总结下服务器参数的调优  其实说到,参数的调优,我的理解就是无非两点: 如果是Innodb的数据库,innod ...

  6. 1.设计模式第一步-《设计模式从头到脚舔一遍-使用C#实现》

    更新记录: 完成第一次编辑:2022年4月23日20:29:33. 加入小黄人歌曲:2022年4月23日21:45:36. 1.1 设计模式(Design Pattern)是什么 设计模式是理论.是前 ...

  7. 记录一个mysql连接慢的问题

    问题现象是这样的: 我在一台机器上(61.183.23.23)启动了一个mysql,然后开通一个账号可以从127.0.0.1或者从61.183.23.23访问.但是遇到一个问题就是使用下面两个命令行访 ...

  8. paip.解决 数据库mysql增加列 字段很慢添加字段很慢

    paip.解决 数据库mysql增加列 字段很慢添加字段很慢 #环境如下: mysql5.6    数据仅仅3w alter table xxx add column yyy int default ...

  9. 【JMeter】JMeter完成一个MySql压力测试

    jmeter也可以用来做数据库的压力测试,并且兼容各种数据库类型,只需要更改对应的数据库驱动类和url.以下为整理到的数据库驱动类对应url.并且给出一个mysql数据库select的简单应用.如下: ...

随机推荐

  1. Spring源码-IOC部分-容器初始化过程【2】

    实验环境:spring-framework-5.0.2.jdk8.gradle4.3.1 Spring源码-IOC部分-容器简介[1] Spring源码-IOC部分-容器初始化过程[2] Spring ...

  2. 在终端或idea编译工具中的terminal中运行mvn install 失败

    原因是因为操作系统的差异导致,把所有参数加上引号即可. 如下所示: mvn install:install-file "-Dfile=cobra.jar" "-Dgrou ...

  3. 隐式参数arguments

    类数组对象中(长得像一个数组,本质上是一个对象):arguments 常见的对arguments的操作是三个 获取参数的长度  arguments.length 根据索引值获取某一个参数 argume ...

  4. linux下使用openssl生成 csr crt CA证书

    证书文件生成:一.服务器端1.生成服务器端    私钥(key文件);openssl genrsa -des3 -out server.key 1024运行时会提示输入密码,此密码用于加密key文件( ...

  5. Activity有多个启动图标

    (1)如果你想让你的Activity有多个启动图标 需要这样配置 <intent-filter> <action android:name="android.intent. ...

  6. 学习JDBC遇到的一些问题

    1. 数据库版本与驱动对应问题 参考官方文档:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-versions.html 具体详情还需 ...

  7. mysql导出csv格式命令

    mysql -h 127.0.0.1 -u user -p123456 -Bse "select name,age from user where age > 10;" | ...

  8. kubectl详解

    kubectl详解 目录 kubectl详解 一.陈述式管理 1. 陈述式资源管理方法 2. k8s相关信息查看 2.1 查看版本信息 2.2 查看资源对象简写 2.3 查看集群信息 2.4 配置ku ...

  9. Feign的异步调用或者MQ调用与Security的问题处理;

    两大踩坑点: 一:部分框架自带有查询当前登录人的信息工具,无需各种本地线程栈ThreadLocals取Request啥的折磨自己: 二:Security自带有uri匹配的工具,没事多翻翻源码,原创方法 ...

  10. Ubuntu20.04 PostgreSQL 14 安装配置记录

    PostgreSQL 名称来源 It was originally named POSTGRES, referring to its origins as a successor to the Ing ...