返回

MySQL VARCHAR 字段插入科学记数法字符串变 9 的原因及解决

mysql

MySQL 莫名其妙地把字符串当成 DECIMAL 插入 VARCHAR 字段

有个挺怪的问题,我们发现一些长得像科学记数法的字符串插到 VARCHAR 字段里的时候,有时候会变成一串 "9"。仔细一查,这情况还挺难重现。感觉像是 MySQL 把字符串当成了一个超级大的 DECIMAL 处理,然后溢出了。更怪的是,同一个字符串,大部分时候都能正常插入,偶尔又会变成 "99999999999999999999999999999999999999999999999999999999999999999"。

举几个出错的例子:

  • 4840e430eac9f22a5e8609a1c95faaeb8c921f66e24e55cf839a00eb35790c00
  • 2e540795-afe5-4644-a3d0-2aaae007c76e

表结构大概是这样的:

 CREATE TABLE `table1` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `UUID` CHAR(36) NOT NULL COLLATE 'utf8mb4_unicode_ci',
    `irrelevantColumn` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_unicode_ci',
    `columnName` VARCHAR(510) NOT NULL COLLATE 'utf8mb4_unicode_ci',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `UUID_irrelevantColumn_UNIQUE` (`UUID`, `irrelevantColumn`) USING BTREE,
    INDEX `table1_uuid_index` (`UUID`) USING BTREE,
    INDEX `table1_attributename_index` (`attributeName`) USING BTREE,
    CONSTRAINT `table1_uuid_foreign` FOREIGN KEY (`UUID`) REFERENCES `table2` (`uuid`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=123456
;

我们查了 Laravel 和 ProxySQL 的日志,发现发给 MySQL 的 INSERT 语句本身没问题,字符串还好好的:

insert into `table1` (`irrelevantColumn`, `columnName`, `UUID`) 
values (
 'someString', 
 '4840e430eac9f22a5e8609a1c95faaeb8c921f66e24e55cf839a00eb35790c00', 
 '2e540795-afe5-4644-a3d0-2aaae007c76e'
) 

所以基本确定是 MySQL 这边出了岔子。我们想知道是啥原因导致了这个情况,以及怎么避免。

用到的技术栈:

  • Laravel 11 + Octane (开启了 strict mode)
  • ProxySQL 2.7.1
  • AWS RDS MySQL 8.0.35 (InnoDB 引擎)

Laravel 启动连接的时候会设置这些参数:
SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci', time_zone='+00:00', SESSION sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'

一、 问题原因分析

这个问题的根源,很可能是 MySQL 对包含 "e" 或 "E" 的字符串,在特定情况下进行了隐式类型转换,试图将其解析为科学记数法的数字。虽然 columnName 字段是 VARCHAR 类型,但如果字符串的格式符合科学记数法,MySQL 有可能会“自作主张”地进行转换。

而且这种行为不稳定,可能和以下几个因素有关:

  1. MySQL 版本和配置: 不同的 MySQL 版本,甚至相同版本但配置不同(比如 sql_mode),都可能导致不同的行为。我们用的是 8.0.35,不排除是某个小版本的 bug。

  2. 数据内容: 虽然是 VARCHAR,但如果字符串长得很像科学记数法的数字,更容易触发这个问题。比如 "1e2" 这种肯定会被当成数字,但像 "4840e430eac9f22a..." 这种,就不一定了。

  3. 隐式类型转换的“潜规则”: MySQL 有一套自己的隐式类型转换规则。在一些边界条件下,这些规则可能会产生意料之外的结果。

  4. ProxySQL的角色(较小可能): 虽然说查询日志显示发送给MySQL的内容正确, 但ProxySQL本身是支持查询重写的, 一些特殊的配置(比较罕见)有可能改变发送给后端MySQL的语句。但从你给出的信息来看,直接操作MySQL很可能也一样会出现问题,因此这个可能性较小。

二、 解决方案

既然找到了可能的原因,我们可以尝试从以下几个方面解决:

1. 明确禁用隐式类型转换 (强烈推荐)

通过修改 sql_mode, 可以禁用这种隐式转换, 让MySQL 严格按照字段类型处理数据。

  • 原理: sql_mode 是 MySQL 的一个重要配置项,控制着 SQL 语法的严格程度和数据校验规则。STRICT_TRANS_TABLES 模式会启用严格模式,对于类型不匹配的插入操作,会直接报错,而不是尝试转换。

  • 操作:

    • 临时修改 (仅对当前会话有效):

      SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
      
    • 全局修改 (对所有新连接生效,需要 SUPER 权限):

      SET GLOBAL sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
      
    • 永久配置:

      修改MySQL 配置文件 (通常是 /etc/mysql/my.cnf/etc/my.cnf),在 [mysqld] 部分添加:

      sql_mode = ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
      

    然后重启 MySQL 服务。对于AWS RDS,在 Parameter Group 中修改。

  • 安全建议: 强烈建议开启 STRICT_TRANS_TABLES。 这样做不仅能解决这个问题, 还能避免其他潜在的数据类型错误, 提高数据完整性。

  • 进阶使用:
    可以考虑同时设置 ERROR_FOR_DIVISION_BY_ZERO,杜绝除以0操作。
    结合你的场景,目前的sql_mode 设置已经较为严格。如果还有特殊需求可以增加ANSI_QUOTES等其他mode。

2. 修改字段类型 (如果可以)

如果 columnName 字段存储的确实是字符串, 且长度不会变化,,可以考虑改成 CHAR 类型。

  • 原理: CHAR 是固定长度的字符串类型。相比 VARCHAR,它不太容易被 MySQL “误判”为数字。

  • 操作:

    ALTER TABLE table1 MODIFY columnName CHAR(510) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
    
  • 安全建议 : 选用 CHAR 需确保字段长度足够。如果长度超过设置,一样会被截断,丢失信息。由于已知例子中包括 UUID 和 很长的字符串,VARCHAR 可能是更适合的选择。

3. 在应用程序中进行预处理

在把字符串插入数据库之前, 先在应用程序中做一下处理。

  • 原理: 如果字符串里有 "e" 或 "E",可以在前面加个转义符,或者用其他方式处理,确保 MySQL 不会把它当成科学记数法。

  • 代码示例 (PHP/Laravel):

    $string = '4840e430eac9f22a5e8609a1c95faaeb8c921f66e24e55cf839a00eb35790c00';
    
    // 方法一: 在 "e""E" 前面加反斜杠 (简单粗暴, 可能影响其他逻辑)
    $string = str_replace(['e', 'E'], ['\e', '\E'], $string);
    
    // 方法二: 使用 BINARY  (推荐,更安全)
       $query= "INSERT INTO `table1` (`irrelevantColumn`, `columnName`, `UUID`) VALUES(?, BINARY ?,?)";
    
     //推荐这种绑定参数方式。避免SQL 注入风险。  
    DB::insert($query,['somestring',$string, $uuid]);
    
    
    
  • 安全建议: 使用参数化查询(如上面示例所示), 绝对不要直接把变量拼接到 SQL 语句里!

4. 检查和调整ProxySQL的设置 (作为预防措施)

虽然主要问题可能不在ProxySQL, 还是确认一下比较好。

  • 原理: ProxySQL 的查询重写规则、或者某些缓存机制,有可能在极端情况下影响到数据。
  • 操作步骤:
    1. 查看ProxySQL的错误日志,寻找与这个问题相关的可疑信息。
    2. 检查ProxySQL的配置中,是否有与查询重写相关的规则 (mysql_query_rules)。
    3. 检查ProxySQL 的缓存设置,是否启用了某些可能导致数据被错误处理的缓存。如果开启了,可以试着清空或禁用缓存。
    4. 简化ProxySQL 的配置,排除可能的干扰因素。 比如,可以暂时绕过 ProxySQL, 直接连接MySQL 进行测试.
  • 安全建议:
    对于不确定的ProxySQL设置,建议查看官方文档或者咨询ProxySQL的专家。

5. 升级 MySQL (作为备选方案)

如果上面的方法都不好使, 不排除真的是 MySQL 的 bug. 升级到更新的版本可能会解决.

  • 原理:
    新版本通常会修复已知的 bug,并改进性能和稳定性.

  • 操作步骤

    1. 在测试环境充分测试新版本的兼容性和稳定性!
    2. 做好数据备份!
    3. 按照MySQL 或 AWS RDS的官方文档进行升级操作.

    如果当前版本已经很新, 可以考虑先观察一段时间,看看官方有没有发布补丁。

通过上面这些方法, 大概率能解决或者绕过 MySQL 把字符串当成数字插入的问题. 强烈建议先用方法1 , 如果有困难, 再考虑其他几种。