前言

项目中发现有数据错误问题,经过排查发现是由于连表时使用字符串(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的表结构如下

# A表
create table A  
(  
    id   bigint auto_increment comment '主键',  
    c_id varchar(100) null comment 'c表id',  
    constraint A_pk  
        primary key (id)  
);

# B表
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'165456441072893952316545644107289395241654564410728939525被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'165456441072893952316545644107289395241654564410728939525是相等的,为啥会这样呢?

MySQL中字符串和数字比较时,会先进行隐式转换,然后再进行比较,这么看应该是隐式转换的时候出现了问题

经过查询MySQL的官方文档,MySQL在比较字符串和数字时,会使用双精度浮点比较,双精度浮点对于大数字会存在精度丢失问题,导致大数字相近值判定为相等

MySQL 5.7 文档中类型比较时的转换规则的描述,最后一条提到字符串和数字比较时会进行双精度浮点比较

MySQL5.7官方文档-表达式求值中的类型转换-1.webp

文档后面举了一个例子,字符串和数字比较,出现了近似值相等的情况,与我碰到的问题相同,还给出了解决方式,使用CAST('字符串' as UNSIGNED) = 数字进行判断

MySQL5.7官方文档-表达式求值中的类型转换-2.webp

参考文章

MySQL 5.7 文档 - 表达式求值中的类型转换

MyBatis-Plus 文档 - 自定义ID生成器