记一次SQL隐式转换导致精度丢失问题的排查 → 不规范就踩坑

开心一刻

刚毕业的侄子给我发消息

侄子:叔,人生太难了

我:怎么呢?

侄子:工作太难了,感情也太难了,怎么什么都这么难

我:你还小啊

侄子:大了就不难了?

我:大了你就习惯了

问题复现

先准备表:数据源( tbl_datasource )以及数据

sql 复制代码
DROP TABLE IF EXISTS `tbl_datasource`;
CREATE TABLE `tbl_datasource`  (
  `id` bigint(20) NOT NULL COMMENT 'ID',
  `name` varchar(100) NOT NULL COMMENT '数据库名称',
  `type` varchar(50) NOT NULL COMMENT '数据库类型',
  `data_supplier_id` varchar(32) NULL DEFAULT NULL COMMENT '数据商id',
  `data_supplier_name` varchar(200) NULL DEFAULT NULL COMMENT '数据商名称',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `database_un_idx1`(`name`) USING BTREE
) COMMENT = '数据源';

INSERT INTO `tbl_datasource`(`id`, `name`, `type`, `data_supplier_id`, `data_supplier_name`) VALUES (250521675081322496, 'MYSQL250521675081322496', 'MYSQL', '1909915075751776256', '供应商1');
INSERT INTO `tbl_datasource`(`id`, `name`, `type`, `data_supplier_id`, `data_supplier_name`) VALUES (250523399661686784, 'MYSQL250523399661686784', 'MYSQL', '1909915075751776256', '供应商1');
INSERT INTO `tbl_datasource`(`id`, `name`, `type`, `data_supplier_id`, `data_supplier_name`) VALUES (250894313117061120, 'HIVE250894313117061120', 'HIVE', '1909915075751776256', '供应商1');
INSERT INTO `tbl_datasource`(`id`, `name`, `type`, `data_supplier_id`, `data_supplier_name`) VALUES (1912678810919714817, 'FTP1912678810919714817', 'FTP', '1909915075751776300', '供应商1');
INSERT INTO `tbl_datasource`(`id`, `name`, `type`, `data_supplier_id`, `data_supplier_name`) VALUES (1912794318679678977, 'KAFKA1912794318679678977', 'KAFKA', '1909915075751776300', '供应商1');
INSERT INTO `tbl_datasource`(`id`, `name`, `type`, `data_supplier_id`, `data_supplier_name`) VALUES (1913070130303217665, 'FTP1913070130303217665', 'FTP', '1909915075751776300', '供应商1');
INSERT INTO `tbl_datasource`(`id`, `name`, `type`, `data_supplier_id`, `data_supplier_name`) VALUES (1913070213291716609, 'FTP1913070213291716609', 'FTP', '1909915075751776256', '供应商1');

有个字段我要特别说明下,data_supplier_id 对应数据商表(tbl_data_supplier)的 id

sql 复制代码
CREATE TABLE `tbl_data_supplier` (
  `id` bigint(20) NOT NULL,
  `supplier_name` varchar(50) DEFAULT NULL,
  `supplier_code` varchar(20) DEFAULT NULL,
  `state` int NOT NULL DEFAULT '1' COMMENT '状态(0-无效、1-有效)',
  `alias` varchar(20) DEFAULT NULL,
  `type` tinyint DEFAULT '1' COMMENT '数据商类型(1采购数据商、2意向数据商)',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) COMMENT='数据商表';

大家看仔细了,它俩类型不一致

data_supplier_id varchar(32) → id bigint(20)

铺垫已经完成,接下来我们看一个查询

sql 复制代码
SELECT * FROM tbl_datasource WHERE data_supplier_id = 1909915075751776256;

你们觉得会查出几条记录,是不是也觉得是 4 条?可实际上呢?

查出来 7 条,把 data_supplier_id = '1909915075751776256' 的 3 条记录也查出来了,这是为什么?

问题排查

如果是急着解决问题,我猜你们会这么处理

sql 复制代码
SELECT * FROM tbl_datasource WHERE data_supplier_id = '1909915075751776256';

查询结果如下

以字符串值去查询,不仅仅是你们的下意识,也是我的下意识,为什么会有这样的下意识,出于两点考虑

  1. WHERE data_supplier_id = 1909915075751776256 的查询结果上来看,19099150757517762561909915075751776300 只有最后 3 位不一致,是不是正好精度丢失忽略了最后 3 位,所以这两个数值比较是相等的?

    对于大数值,JavaScript 有精度丢失问题

    字符串的比较是不存在精度一说的

  2. data_supplier_id 类型不是 varchar(32) 吗,为什么要用整形值去查?

虽然问题能够快速解决,但用整形值去查,查询结果不对的原因还没找到,真的是精度丢失问题?需要我们去验证,如何验证了,可以去查官方文档,但我们要缩小查询范围,有针对性的去查;有 1 点是可以肯定的

data_supplier_id 是 varchar 类型的,用整型值去查,肯定存在类型转换

既然存在类型转换,那就看看 MySQL 对类型转换的说明:Type Conversion in Expression Evaluation

注意开头这段话

When an operator is used with operands of different types, type conversion occurs to make the operands compatible. Some conversions occur implicitly.

翻译过来就是

当运算符与不同类型的操作数一起使用时,会进行类型转换以使操作数兼容。有些转换是隐式发生的

很显然,我们案例中的转换就是隐式发生的,也就是我们平时说的 隐式转换;我们继续往下看,重点看转换规则

简单翻译一下

  • 如果一个或两个参数是 NULL,比较结果是 NULL

    关于 NULL,推荐大家看看:神奇的 SQL 之温柔的陷阱 → 为什么是 IS NULL 而非 = NULL ?

  • 如果比较操作中的两个参数都是字符串,则将它们作为字符串进行比较

  • 如果两个参数都是整数,则将它们作为整数进行比较

  • 如果不与数字进行比较,十六进制值将被视为二进制字符串

  • 时间类型的说明

  • 如果其中一个参数是十进制值,则比较取决于另一个参数。如果另一个参数是十进制或整数值,则将参数作为十进制值进行比较,如果另一参数是浮点值,则将其作为浮点值进行比较

  • 在所有其他情况下,参数都作为浮点(双精度)数字进行比较。例如,字符串和数字操作数的比较是作为浮点数的比较进行的

我们的案例是不是正好适用于最后那条规则?

字符串和数字操作数的比较是作为浮点数的比较进行的

继续往下看官方文档,关于浮点数有如下说明

浮点数与大整数值的比较,得到的是一个近似结果(而非一个精确结果),因为在比较之前,整数被转换为双精度浮点,这无法准确表示所有64位整数。有些整数值无法用浮点数精确表示,所以就存在四舍五入导致精度丢失,至于是入还是舍,跟平台(CPU、计算机体系结构、编译器版本等)有关系。

官方还给出了案例说明

sql 复制代码
mysql> SELECT '9223372036854775807' = 9223372036854775807;
        -> 1
mysql> SELECT '9223372036854775807' = 9223372036854775806;
        -> 1

类比我们的案例

字面值不相等的两个值,比较结果竟然是相等的,这就是因为隐式转换成浮点数比较,比较结果是一个近似值(而非精确值),而这个近似值可能是不准确的!问题的原因是不是就找到了?

问题解决

问题找到了,解决起来就简单了,方式有以下几种

  1. 用字符串值查,而非整数值查

    sql 复制代码
    SELECT * FROM tbl_datasource WHERE data_supplier_id = '1909915075751776256';
  2. 调整 data_supplier_id 类型成 bigint,与表 tbl_data_supplierid 类型保持一致,然后用整数值查

    sql 复制代码
    SELECT * FROM tbl_datasource WHERE data_supplier_id = 1909915075751776256;

    推荐这种方式,更规范,查询效率也更高

  3. 显示将整数值转成字符串

    利用 CASTCANCAT 等函数,将整数值转成字符串

    sql 复制代码
    SELECT * FROM tbl_datasource WHERE data_supplier_id = CAST(1909915075751776256 AS CHAR);
    SELECT * FROM tbl_datasource WHERE data_supplier_id = CONCAT(1909915075751776256);

    不要使用,相比于1、2,啥也不是

总结

  1. 类型转换可能会导致索引失效,还可能会导致精度丢失,一定要避免

  2. 不管是建表,还是查询,要规范起来,否则就隐藏着各种坑

    tbl_data_supplierid 字段是 bigint 类型,为什么 tbl_datasourcedata_supplier_id 字段要用 varchar(32) 类型;既然 data_supplier_idvarchar(32) 类型了,为什么代码中的查询又用整数值(WHERE data_supplier_id = 1909915075751776256 )?

  3. 后面我问了下 deepseek,感觉比搜索引擎搜的更精准,比盲翻官方文档更快速,推荐大家使用