

如何从头到脚彻底解决一个MySQL Bug
source link: https://juejin.cn/post/7073667235809607717
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

说明:本文中的MySQL,如果不做特殊说明,指的是开源社区版MySQL。
华为云数据库新版本在发布之前,会面临一系列严苛的测试规则,除了要求通过MySQL的所有测试用例之外,还需要通过由华为百万级更丰富、更贴近用户业务场景的测试用例构筑的测试防护网,以此充分验证新版本是否满足用户经典场景的稳定性。
正是在这样严苛的验证过程中,我们发现了MySQL的一个潜在Bug。
Bug描述
测试环境:
基于相同的测试用例、数据集,分别测试MySQL 8.0.22, MySQL 8.0.26,与华为云GaussDB(for MySQL)的返回结果。
测试语句:
select
subq_0.c2 as c0
from
(select
ref_6.C_STATE as c0,
case when ref_6.C_PHONE is not NULL then ref_5.C_ID else ref_5.C_ID end
as c1,
floor(
ref_3.c_id) as c2
from
sqltester.t0_hash_partition_p1_view as ref_0
right join sqltester.t4 as ref_1
on (EXISTS (
select
ref_1.c_middle as c0
from
sqltester.t1 as ref_2
where ((false)
and ((true)
or (true)))
or (false)
))
inner join sqltester.t0_range_key_subpartition_sub_view as ref_3
on (EXISTS (
select
ref_0.c_credit as c0,
ref_1.c_street_1 as c1,
ref_4.c_credit_lim as c2,
ref_3.c_credit as c3
from
sqltester.t0_hash_partition_p1 as ref_4
where true
))
left join sqltester.t10 as ref_5
inner join sqltester.t11 as ref_6
on (true)
on (((pi() is not NULL))
and (false))
where (((ref_5.C_D_ID is not NULL)
or (ref_3.c_middle is not NULL))
)) as subq_0
where (EXISTS (
select
subq_0.c0 as c0,
pi() as c1,
ref_11.c_street_1 as c2,
ref_11.c_discount as c3,
pi() as c4
from
sqltester.t0_partition_sub_view_mixed_001 as ref_11))
group by 1
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、首先精简查询,以方便后面分析。经过多次验证,将查询简化如下:
SELECT count(*)
FROM
(SELECT 1
FROM sqltester.t4 AS ref_1
INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS
(SELECT 1
FROM sqltester.t4 AS ref_4
WHERE TRUE ))
LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE)
WHERE (((ref_5.C_D_ID IS NOT NULL)
OR (ref_3.c_middle IS NOT NULL)))) AS subq_0
执行计划如下:
-> Aggregate: count(0) (cost=2.75 rows=0)
-> Filter: ((ref_5.C_D_ID is not null) or (ref_3.c_middle is null)) (cost=2.75 rows=0)
-> Inner hash join (no condition) (cost=2.75 rows=0)
-> Index scan on ref_3 using ndx_c_middle (cost=0.13 rows=50)
-> Hash
-> Inner hash join (no condition) (cost=1.50 rows=0)
-> Index scan on ref_1 using ndx_c_id (cost=6.25 rows=50)
-> Hash
-> Left hash join (no condition) (cost=0.25 rows=0)
-> Limit: 1 row(s) (cost=312.50 rows=1)
-> Index scan on ref_4 using ndx_c_id (cost=312.50 rows=50)
-> Hash
-> 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版本上执行问题查询,语句和执行结果如下:
SELECT count(*)
FROM
(SELECT 1
FROM sqltester.t4 AS ref_1
INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS
(SELECT 1
FROM sqltester.t4 AS ref_4
WHERE TRUE ))
LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE)
WHERE (((ref_5.C_D_ID IS NOT NULL) or(ref_3.c_middle IS NOT NULL))))AS subq_0;
+
+
| count(*) |
+
+
| 2500 |
+
+
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语句和执行结果如下:
SELECT count(*)
FROM
(SELECT 1
FROM sqltester.t4 AS ref_1
INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS
(SELECT 1
FROM sqltester.t4 AS ref_4
WHERE TRUE ))
LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE)
WHERE (((ref_5.C_D_ID IS NOT NULL))))assubq_0;+
+
| count(*) |
+
+
| 2500 |
+
+
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来查看设置是否正确:
Breakpoint 1, ZeroRowsIterator::SetNullRowFlag (this=0x7f92a413d510, is_null_row=false)
at /mywork/mysql-sql/sql/basic_row_iterators.h:398
398 assert(m_child_iterator != nullptr);
(gdb) n
399 m_child_iterator->SetNullRowFlag(is_null_row);
(gdb) s
std::unique_ptr<RowIterator, Destroy_only<RowIterator> >::operator-> (this=0x7f92a413d520)
at /opt/simon/taurus/mysql-root/src/tools/gcc-9.3.0/include/c++/9.3.0/bits/unique_ptr.h:355
355 return get();
(gdb) fin
Run till exit from #0 std::unique_ptr<RowIterator, Destroy_only<RowIterator> >::operator-> (
this=0x7f92a413d520)
at /opt/simon/taurus/mysql-root/src/tools/gcc-9.3.0/include/c++/9.3.0/bits/unique_ptr.h:355
ZeroRowsIterator::SetNullRowFlag (this=0x7f92a413d510,**is_null_row=false**)
at /home/simon/mywork/mysql-sql/sql/basic_row_iterators.h:399
399 m_child_iterator->SetNullRowFlag(is_null_row);
Value returned is $1 = (RowIterator *) 0x7f92a413d4d0
(gdb) s
TableRowIterator::SetNullRowFlag (this=0x7f92a413d4d0,**is_null_row=false**)
at /home/simon/mywork/mysql-sql/sql/records.cc:229
229 if (is_null_row) {
(gdb) n
232 m_table->reset_null_row();
(gdb)
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门槛抽奖!
Recommend
-
67
彻底解决 es 的 unassigned shards 症状 Original 孙彪彪...
-
95
【人体切成几千片后扫描的效果】人体可视化计划的作品,把人体封存在明胶里,保证固定后,就一毫米切一片,切完后照片扫描进电脑,就做出了人体从头到脚的细节模型~非常不得了的壮举~视频前面是男性,后面是女性~ (@变态学教授无铭)
-
70
前言 【 从头到脚 】会作为一个系列文章来发布,它包括但不限于 WebRTC 多人视频,预计会有: WebRTC 实战(一):也就是本期,主要是基础讲解以及一对一的本地对等连接,网络对等连接。 WebRTC 实战(二):主要讲解数据传输以及多端本地对等连接、
-
55
前言 笔者之前写过一篇 【从头到脚】撸一个多人视频聊天 — 前端 WebRTC 实战(一),主要讲 WebRTC 的一些基础知识以及单人通话的简单实现。原计划这篇写多人通话的,鉴于有同学留言说想看画板,所以把这篇文章提前了,希望可以给大家提供一些思路。 本期的
-
72
作者:江三疯,专注前端开发。欢迎关注公众号前端发动机,第一时间获得作者文章推送,还有各类前端优质文章,致力于成为推动前端成长的引擎。 前言 笔者之前写过一篇 【从头到脚】撸一个多人视频聊天 — 前端 WebRTC...
-
22
小贴士:本文较长,不适合碎片化阅读,建议用电脑观看。收藏+转发,幸福你我他 字符集转换概述 我们有必要说明一下, 字符 其实是面向人类的一个概念...
-
2
如何彻底解决拐卖妇女问题?需要加强哪些工作?已认证帐号国务院办公厅2021年印发《中国反对拐卖人口行动计划(2021-2030...
-
4
题图/ 受访者提供 正在举办的北京冬奥会为许多线下雪具店带去明显增多的咨询量。 2014年...
-
14
美团三面:一直追问我, MySQL 幻读被彻底解决了吗? 作者:小林coding 2022-09-21 09:00:10 MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。 大家好,我是小林。
-
5
8H智能电动床Pro Max发布:升级5电机支撑 从头到脚爽翻 2022-10-28 15:40 出处/作者:快科技 整合编辑:佚名 0
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK