项目中发现有数据错误问题,经过排查发现是由于连表时使用字符串(varchar)和数字(bigint)的两个字段进行连表导致的,
MySQL文档
中有说明,字符串和数字作比较时会出现隐式转换,对于大数字的隐式转换,可能会出现精度丢失问题,导致大数字之间的近似值判定为相等的。
A表的varchar类型和B表的bigint类型连表查询时,出现了id不等但连表成功的情况,B表的bigint是由MyBatis-Plus的雪花算法生成的,一个19位长度的数字,由于数字过大,所以在MySQL隐式转换,字符串转数字时,出现了两个近似数相等问题,导致AB连表出现问题。
SQL中手动转换字符串
select cast('1654564410728939522' as unsigned) = 1654564410728939523
修改A表的varchar类型为bigint类型
alter table A modify c_id bigint null comment 'c表id';
将SQL拆成2条,在业务代码中手动转换类型(假如可以)
# SQL 1
select c_id from A where id = ?
# 业务代码中将c_id转为数字
# SQL 2
select * from B where c_id = ?
问题具体过程
有两张表,表A和表B的表结构如下
create table A
id bigint auto_increment comment '主键',
c_id varchar(100) null comment 'c表id',
constraint A_pk
primary key (id)
create table B
id bigint auto_increment comment '主键',
c_id bigint not null comment 'c表id',
d_id bigint not null comment 'd表id',
constraint B_pk
primary key (id)
向A表和B表中插入一些数据
# 向A表中插入一些数据
INSERT INTO A (id, c_id) VALUES (1, '1654564410728939522');
INSERT INTO A (id, c_id) VALUES (2, '1654564410728939523');
INSERT INTO A (id, c_id) VALUES (3, '1654564410728939524');
INSERT INTO A (id, c_id) VALUES (4, '1654564410728939525');
# 向B表中插入一些数据
INSERT INTO B (id, c_id, d_id) VALUES (1, 1654564410728939522, 1);
INSERT INTO B (id, c_id, d_id) VALUES (2, 1654564410728939523, 2);
INSERT INTO B (id, c_id, d_id) VALUES (3, 1654564410728939524, 3);
INSERT INTO B (id, c_id, d_id) VALUES (4, 1654564410728939525, 4);
有业务需求需要A表与B表通过c_id
字段关联查询,所以有如下SQL
select a.*, b.* from A a join B b on a.c_id = b.c_id
理想状态下,连表的结果应该是下面这样的
+----+---------------------+----+---------------------+------+
|a.id|a.c_id |b.id|b.c_id |b.d_id|
+----+---------------------+----+---------------------+------+
|1 |1654564410728939522 |1 |1654564410728939522 |1 |
|2 |1654564410728939523 |2 |1654564410728939523 |2 |
|3 |1654564410728939524 |3 |1654564410728939524 |3 |
|4 |1654564410728939525 |4 |1654564410728939525 |4 |
+----+---------------------+----+---------------------+------+
但实际查询后的结果是这样的
+----+---------------------+----+---------------------+------+
|a.id|a.c_id |b.id|b.c_id |b.d_id|
+----+---------------------+----+---------------------+------+
|1 |1654564410728939522 |1 |1654564410728939522 |1 |
|2 |1654564410728939523 |1 |1654564410728939522 |1 |
|3 |1654564410728939524 |1 |1654564410728939522 |1 |
|4 |1654564410728939525 |1 |1654564410728939522 |1 |
|1 |1654564410728939522 |2 |1654564410728939523 |2 |
|2 |1654564410728939523 |2 |1654564410728939523 |2 |
|3 |1654564410728939524 |2 |1654564410728939523 |2 |
|4 |1654564410728939525 |2 |1654564410728939523 |2 |
|1 |1654564410728939522 |3 |1654564410728939524 |3 |
|2 |1654564410728939523 |3 |1654564410728939524 |3 |
|3 |1654564410728939524 |3 |1654564410728939524 |3 |
|4 |1654564410728939525 |3 |1654564410728939524 |3 |
|1 |1654564410728939522 |4 |1654564410728939525 |4 |
|2 |1654564410728939523 |4 |1654564410728939525 |4 |
|3 |1654564410728939524 |4 |1654564410728939525 |4 |
|4 |1654564410728939525 |4 |1654564410728939525 |4 |
+----+---------------------+----+---------------------+------+
看了这个结果后不禁产生了怀疑,为啥等值连表连出这么多数据?仔细看了一下,以2-4行为例,'1654564410728939522'
和1654564410728939523
、1654564410728939524
、1654564410728939525
被MySQL判定为了相等,单独写了个SQL测试一下
select '1654564410728939522' = 1654564410728939523 as a,
'1654564410728939522' = 1654564410728939524 as b,
'1654564410728939522' = 1654564410728939525 as c;
执行结果为
+-+-+-+
|a|b|c|
+-+-+-+
|1|1|1|
+-+-+-+
在MySQL中1为true,所以MySQL认为'1654564410728939522'
和1654564410728939523
、1654564410728939524
、1654564410728939525
是相等的,为啥会这样呢?
MySQL中字符串和数字比较时,会先进行隐式转换,然后再进行比较,这么看应该是隐式转换的时候出现了问题
经过查询MySQL的官方文档,MySQL在比较字符串和数字时,会使用双精度浮点比较,双精度浮点对于大数字会存在精度丢失问题,导致大数字相近值判定为相等
MySQL 5.7 文档中类型比较时的转换规则的描述,最后一条提到字符串和数字比较时会进行双精度浮点比较
文档后面举了一个例子,字符串和数字比较,出现了近似值相等的情况,与我碰到的问题相同,还给出了解决方式,使用CAST('字符串' as UNSIGNED) = 数字
进行判断
MySQL 5.7 文档 - 表达式求值中的类型转换
MyBatis-Plus 文档 - 自定义ID生成器
MAX(id) 通过MAX()函数获取某个表的ID的最大值,手动自增后用于insert语句,代码如下: # 获取table_name表的当前最大id
select MAX(id) into @max_id from table_name;
# 手动自增id
set @max_id = @max_id