目录
问题现象:
问题分析:
结论:
解决方法:
拓展:
问题现象:
今天在项目开发中发现了一个非常奇怪的bug:
如图,我在数据库中以“dept_id = 1712651046956421123”为条件,执行了sql查询,但是查询出来的结果显示:dept_id 是 1712651046956421122。
当时是在查看项目的服务日志,排查问题的时候才看出来这个奇怪的现象,还以为是bug,查了一下资料终于知道了具体原因,确实是个大坑,特此记录。
问题分析:
一开始还以为是mysql8+的bug,但是想了想事情可能没有这么简单,于是就开始我的实验测试:
首先、我用navicat工具直接去这个表里面进行可视化筛选,发现:以“dept_id = 1712651046956421123”为条件查询不出来数据,以以“dept_id = 1712651046956421122”则可以查询出数据:
由此可见navicat工具的筛选功能是没有问题的,于是我分别尝试用这两个dept_id值(即1712651046956421123和1712651046956421122)去执行sql来查询,发现:还是可以查询出相同的结果:
这就让我很纳闷了,一时间毫无头绪,只好上网查一下资料,终于在MySQL官网的一篇讲解中,我知道了具体原因,重点在于我红框标注的这个地方:
感兴趣的小伙伴可以看看,这里我贴上链接:
MySQL :: MySQL 5.7 Reference Manual :: 12.3 Type Conversion in Expression Evaluation
根据讲解中提到的例子,我直接去数据库里执行sql,这里就不一一贴出结果来了,直接贴sql代码和结果,感兴趣的小伙伴可以自己去验证一下:
select 1712651046956421123 = 1712651046956421123;-- 结果:1
select 1712651046956421122 = 1712651046956421122;-- 结果:1
select 1712651046956421123 = 1712651046956421122;-- 结果:0select '1712651046956421123' = '1712651046956421123';-- 结果:1
select '1712651046956421123' = 1712651046956421123;-- 结果:1
select '1712651046956421123' = 1712651046956421122;-- 结果:1select '1712651046956421122' = '1712651046956421122';-- 结果:1
select '1712651046956421122' = 1712651046956421122;-- 结果:1
select '1712651046956421122' = 1712651046956421123;-- 结果:1
相信有部分小伙伴已经看出原因来了,下面我再贴个图,估计大家就完全懂了:
建表sql如下:
CREATE TABLE `xxxxxxxxxxxxxx_dept` (`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '名称',`dept_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '部门id',PRIMARY KEY (`dept_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1712757077034344450 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='部门信息表';
其实就是因为这个dept_id的字段类型设置成了varchar类型,也就是字符串类型,但是我执行sql 时传入的是整型数值,而不是字符串类型的数值,这就会触发mysql的隐藏机制,数据类型转换,然后就可能会触发意想不到的问题,从文章的解释文字可以知道:
翻译后的意思大概就是:
当发生从字符串到浮点和从整数到浮点的转换时,它们不一定以相同的方式发生。整数可以由CPU转换为浮点,而字符串则在涉及浮点乘法的操作中逐位转换。此外,结果可能受到计算机体系结构或编译器版本或优化级别等因素的影响。避免此类问题的一种方法是使用CAST(),这样值就不会隐式转换为浮点数。
由于可见,其实文章开头提到的问题,发生的具体原因就是:
当对字符串类型的字段赋值为整数类型时,会发生类型转换,整数转为浮点数,而浮点数是带有精度的,当浮点数超过一定的精度值时,在精度值前的数据如果是一致的,则会被认为是相等的值。
通过肉眼观察1712651046956421123和1712651046956421122,就可以发现,这两数值其实就只是最后一位数字不同,前面的数据是完全相同的,因此符合我的推论。
接下来我们在继续测试并验证一下我的推论:
select '1234567890' = 1234567891;-- 结果:0
select '123456789012345' = 123456789012341;-- 结果:0
select '123456789012345678' = 123456789012345671;-- 结果:0
select '1234567890123456789' = 1234567890123456781;-- 结果:1
select '12345678901234567890' = 12345678901234567891;-- 结果:1
select '1234567890123456789012345678901234567890' = 1234567890123456789012345678901234567891111;-- 结果:1
select '12345678901234567899999' = 12345678901234567811111;-- 结果:1
select '1234567890123456789012345678901234567890' = 1234567890123456789012345678901234567891111111111111111;-- 结果:0
select 1234567890123456789 = 1234567890123456781;-- 结果:1
亲测发现,这个精度的临界值就是18位数,也就是说如果 = 左右两边的数值长度一样,且数值的长度>18位数时,如果数据类型不同触发了类型转换,则会直接判断 = 左右两边的前面18位数的数值;如果完全一致,则不管后面的数据有多大差异都会认为是相等的。
结论:
出现开头提到的问题现象的具体原因如下:
1、当where条件中的字段和赋值数据的类型不同(主要指字符串、整数、浮点数,其他的数据类型感兴趣的小伙伴可以自己去测试一下)时,会触发类型转换机制【字符串->整数->浮点数】。
2、触发类型转换机制时,如果满足以下条件会被判断为 = 左右两边的数据相等:
2.1、两边的数据长度相同;
2.2、两边的数据长度都>18位数;
2.3、两边的数据前18位数的数值完全一致;
解决方法:
为了避免这个因为触发了MySQL的类型转换的隐藏机制,而导致类似于文章开头举例的这种查询出错误结果集的问题,这里提供一些我能想到的解决方法,还原补充:
1、就如文章中提到的MySQL官网,其实已经给了一种解决方法,那就是使用CAST函数,将数据做一下类型转换,这样就不会触发类型转换的机制,自然也就能避免问题发生。
select '1234567890123456789' = 1234567890123456781;-- 结果:1
select CAST('1234567890123456789' as UNSIGNED) = 1234567890123456781;-- 结果:0
不过这里并不建议采用这种方法,因为它具有sql代码侵入性,代码修改的维护成本比较高,而且治标不治本。
2、修改表字段,在定义表字段类型的时候,应该想清楚这个字段要存储什么类型的数据,然后在代码中要保证类型是对应的(如varchar类型字段对应JAVA中的String类型),这样执行sql的时候就能保证数据类型一致,防止触发类型转换的机制,自然也就能避免问题发生。
强烈建议采用这种方法,因为只要大家都遵守规范自然就能避免问题。
拓展:
当然,也许有人会问为啥MySQL不直接去掉这个类型转换的隐藏机制呢?
要知道,事情都是由两面性的,类型转换的隐藏机制其实也有很多好处,不去掉自然是因为利大于弊;这里就不仔细举例了,因为不在本文的谈论范围,后续可能会出一篇新的文章详细说明一下。