MySQL中的隐式转换造成的索引失效

隐式转换与索引失效
本文通过实验探讨了MySQL中隐式类型转换对索引性能的影响,特别是字符串到数字的转换如何导致索引失效,进而引发全表扫描的问题。

数据库优化是一个任重而道远的任务,想要做优化必须深入理解数据库的各种特性。在开发过程中我们经常会遇到一些原因很简单但造成的后果却很严重的疑难杂症,这类问题往往还不容易定位,排查费时费力最后发现是一个很小的疏忽造成的,又或者是因为不了解某个技术特性产生的。

于数据库层面,最常见的恐怕就是索引失效了,且一开始因为数据量小还不易被发现。但随着业务的拓展数据量的提升,性能问题慢慢的就体现出来了,处理不及时还很容易造成雪球效应,最终导致数据库卡死甚至瘫痪。造成索引失效的原因可能有很多种,相关技术博客已经有太多了,今天我要记录的是隐式转换造成的索引失效

数据准备

首先使用存储过程生成 1000 万条测试数据, 测试表一共建立了 7 个字段(包括主键),num1num2保存的是和ID一样的顺序数字,其中num2是字符串类型。 type1type2保存的都是主键对 5 的取模,目的是模拟实际应用中常用类似 type 类型的数据,但是type2是没有建立索引的。 str1str2都是保存了一个 20 位长度的随机字符串,str1不能为NULLstr2允许为NULL,相应的生成测试数据的时候我也会在str2字段生产少量NULL值(每 100 条数据产生一个NULL值)。

-- 创建测试数据表
DROP TABLE IF EXISTS test1;
CREATE TABLE `test1` (
    `id` int(11) NOT NULL,
    `num1` int(11) NOT NULL DEFAULT '0',
    `num2` varchar(11) NOT NULL DEFAULT '',
    `type1` int(4) NOT NULL DEFAULT '0',
    `type2` int(4) NOT NULL DEFAULT '0',
    `str1` varchar(100) NOT NULL DEFAULT '',
    `str2` varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `num1` (`num1`),
    KEY `num2` (`num2`),
    KEY `type1` (`type1`),
    KEY `str1` (`str1`),
    KEY `str2` (`str2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 创建存储过程
DROP PROCEDURE IF EXISTS pre_test1;
DELIMITER //
CREATE PROCEDURE `pre_test1`()
BEGIN
    DECLARE i INT DEFAULT 0;
    SET autocommit = 0;
    WHILE i < 10000000 DO
        SET i = i + 1;
        SET @str1 = SUBSTRING(MD5(RAND()),1,20);
        -- 每100条数据str2产生一个null值
        IF i % 100 = 0 THEN
            SET @str2 = NULL;
        ELSE
            SET @str2 = @str1;
        END IF;
        INSERT INTO test1 (`id`, `num1`, `num2`,
        `type1`, `type2`, `str1`, `str2`)
        VALUES (CONCAT('', i), CONCAT('', i),
        CONCAT('', i), i%5, i%5, @str1, @str2);
        -- 事务优化,每一万条数据提交一次事务
        IF i % 10000 = 0 THEN
            COMMIT;
        END IF;
    END WHILE;
END;
// DELIMITER ;
-- 执行存储过程
CALL pre_test1();

数据量比较大,还涉及使用MD5生成随机字符串,所以速度有点慢,稍安勿躁,耐心等待即可。

SQL 测试

先来看这组 SQL,一共四条,我们的测试数据表num1int类型,num2varchar类型,但是存储的数据都是跟主键id一样的顺序数字,两个字段都建立有索引。

SELECT * FROM `test1` WHERE num1 = 10000;
SELECT * FROM `test1` WHERE num1 = '10000';
SELECT * FROM `test1` WHERE num2 = 10000;
SELECT * FROM `test1` WHERE num2 = '10000';

这四条 SQL 都是有针对性写的,12 查询的字段是 int 类型,34 查询的字段是varchar类型。12 或 34 查询的字段虽然都相同,但是一个条件是数字,一个条件是用引号引起来的字符串。这样做有什么区别呢?先不看下边的测试结果你能猜出这四条 SQL 的效率顺序吗?

经测试这四条 SQL 最后的执行结果却相差很大,其中 124 三条 SQL 基本都是瞬间出结果,大概在 0.001~0.005 秒,在千万级的数据量下这样的结果可以判定这三条 SQL 性能基本没差别了。但是第三条 SQL,多次测试耗时基本在 4.5~4.8 秒之间。

为什么 34 两条 SQL 效率相差那么大,但是同样做对比的 12 两条 SQL 却没什么差别呢?查看一下执行计划,下边分别 1234 条 SQL 的执行计划数据:

 explain SELECT * FROM `test1` WHERE num1 = 10000;
 explain  SELECT * FROM `test1` WHERE num1 = '10000';
 explain  SELECT * FROM `test1` WHERE num2 = '10000';
 explain SELECT * FROM `test1` WHERE num2 = '10000';

可以看到,124 三条 SQL 都能使用到索引,连接类型都为ref,扫描行数都为 1,所以效率非常高。再看看第三条 SQL,没有用上索引,所以为全表扫描,rows直接到达 1000 万了,所以性能差别才那么大。

仔细观察你会发现,34 两条 SQL 查询的字段num2varchar类型的,查询条件等号右边加引号的第 4 条 SQL 是用到索引的,那么是查询的数据类型和字段数据类型不一致造成的吗?如果是这样那 12 两条 SQL 查询的字段num1int类型,但是第 2 条 SQL 查询条件右边加了引号为什么还能用上索引呢。

查阅 MySQL 相关文档发现是隐式转换造成的,看一下官方的描述:

     当操作符与不同类型的操作数一起使用时,会发生类型转换以使操作数兼容。某些转换是隐式发生的。例如,MySQL 会根据需要自动将字符串转换为数字,反之亦然。以下规则描述了比较操作的转换方式:

  1. 两个参数至少有一个是NULL时,比较的结果也是NULL,特殊的情况是使用<=>对两个NULL做比较时会返回1,这两种情况都不需要做类型转换
  2. 两个参数都是字符串,会按照字符串来比较,不做类型转换
  3. 两个参数都是整数,按照整数来比较,不做类型转换
  4. 十六进制的值和非数字做比较时,会被当做二进制串
  5. 有一个参数是TIMESTAMPDATETIME,并且另外一个参数是常量,常量会被转换为timestamp
  6. 有一个参数是decimal类型,如果另外一个参数是decimal或者整数,会将整数转换为decimal后进行比较,如果另外一个参数是浮点数,则会把decimal转换为浮点数进行比较
  7. 所有其他情况下,两个参数都会被转换为浮点数再进行比较

根据官方文档的描述,我们的第 23 两条 SQL 都发生了隐式转换,第 2 条 SQL 的查询条件num1 = '10000',左边是int类型右边是字符串,第 3 条 SQL 相反,那么根据官方转换规则第 7 条,左右两边都会转换为浮点数再进行比较。

先看第 2 条 SQL:SELECT * FROMtest1WHERE num1 = '10000'; 左边为 int 类型10000,转换为浮点数还是10000,右边字符串类型'10000',转换为浮点数也是10000。两边的转换结果都是唯一确定的,所以不影响使用索引。

第 3 条 SQL:SELECT * FROMtest1WHERE num2 = 10000; 左边是字符串类型'10000',转浮点数为 10000 是唯一的,右边int类型10000转换结果也是唯一的。但是,因为左边是检索条件,'10000'转到10000虽然是唯一,但是其他字符串也可以转换为10000,比如'10000a''010000''10000'等等都能转为浮点数10000,这样的情况下,是不能用到索引的。

关于这个隐式转换我们可以通过查询测试验证一下,先插入几条数据,其中num2='10000a''010000''10000'


INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000001', '10000', '10000a', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523');
INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000002', '10000', '010000', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523');
INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000003', '10000', ' 10000', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523');

从结果可以看到,后面插入的三条数据也都匹配上了。那么这个字符串隐式转换的规则是什么呢?为什么num2='10000a''010000''10000'这三种情形都能匹配上呢?查阅相关资料发现规则如下:

  1. 不以数字开头的字符串都将转换为0。如'abc''a123bc''abc123'都会转化为0
  2. 以数字开头的字符串转换时会进行截取,从第一个字符截取到第一个非数字内容为止。比如'123abc'会转换为123'012abc'会转换为012也就是12'5.3a66b78c'会转换为5.3,其他同理。

分析和总结

通过上面的测试我们发现 MySQL 使用操作符的一些特性:

  1. 当操作符左右两边的数据类型不一致时,会发生隐式转换
  2. 当 where 查询操作符左边为数值类型时发生了隐式转换,那么对效率影响不大,但还是不推荐这么做。
  3. 当 where 查询操作符左边为字符类型时发生了隐式转换,那么会导致索引失效,造成全表扫描效率极低。
  4. 字符串转换为数值类型时,非数字开头的字符串会转化为0,以数字开头的字符串会截取从第一个字符到第一个非数字内容为止的值为转化结果。

所以,我们在写 SQL 时一定要养成良好的习惯,查询的字段是什么类型,等号右边的条件就写成对应的类型。特别当查询的字段是字符串时,等号右边的条件一定要用引号引起来标明这是一个字符串,否则会造成索引失效触发全表扫描。

<think>嗯,用户想了解MySQL类型转换导致索引失效的原因。这个问题在数据库优化中很常见,需要从底层机制讲清楚。 用户提供的引用材料很有价值,特别是引用[1]详细描述了线上事故案例:8000万行表因为转换导致全表扫描超时30秒。引用[3]则解释了B+树索引的有序性被破坏的关键点。 需要重点解释三个核心机制:首先是MySQL的类型转换规则,当字符串与数字比较时,会把字符串转为浮点数;其次是索引的B+树结构依赖数据的严格有序性;最后是转换后的值破坏了这种有序性,比如"1"、" 1"、"1a"都会变成1。 用户可能还关心如何避免这种情况,所以应该给出具体解决方案,比如保持字段类型一致、使用CAST函数显转换等。另外引用[4]提到的EXPLAIN命令也很实用,可以教用户如何验证。 考虑到用户的技术水平,解释时要避免太理论化。用那个"1"、" 1"、"1a"的例子就很形象,说明为什么索引无法精确定位。最后生成的问题要覆盖进阶内容,比如如何检测和优化这类问题。</think>MySQL类型转换导致索引失效的核心原因在于**破坏了B+树索引的有序性**。以下是详细分析: ### 一、根本原因:有序性被破坏 1. **B+树索引依赖严格有序性** MySQL的索引(如InnoDB)基于B+树结构,要求索引列的值具有严格顺序(例如数字按大小、字符串按字典序)。 *示例*:对字符串列`phone`建立索引,其值在B+树中按`'123'`→`'456'`→`'789'`顺序存储。 2. **转换破坏有序性** 当查询条件与索引列类型不匹配时(如数字`= 123` vs 字符串索引`phone`),MySQL会将索引列**逐行转换**为查询条件的类型(此处转为数字)。 转换后出现以下问题: - 原有序值被转为无序数字(如`'123'`→`123`, `'456'`→`456`) - 不同字符串可能转为相同数字(如`'123'`、`'123 '`、`'0123'`均转为`123`) - 非法值转为`0`(如`'abc'`→`0`) ```sql -- 失效案例:phone是VARCHAR列,但条件用数字 SELECT * FROM users WHERE phone = 123; -- 索引失效 ``` ### 二、为何优化器放弃索引? 1. **转换后无法利用B+树的有序性** 优化器发现: - 需对**所有索引值**进行转换(相当于全索引扫描) - 转换结果无法直接定位到B+树的某个范围 > 此时全表扫描可能比“全索引扫描+回表”代价更低[^1]。 2. **索引值重复风险** 如`phone`列有值`'123'`、`'123 '`(末尾空格),转数字后均为`123`。若使用索引,可能遗漏部分数据,因此优化器选择更安全的全表扫描[^3]。 ### 三、典型案例分析 | 场景 | 是否失效 | 原因分析 | |---------------------|----------|------------------------------| | `WHERE int_col = '1'` | 否 | 字符串转数字(值唯一) | | `WHERE str_col = 1` | **是** | 索引列逐行转换(破坏有序性) | | `WHERE str_col = '1'` | 否 | 类型匹配,直接使用索引 | | `WHERE str_col = CAST(1 AS CHAR)` | 否 | 显转换保证类型一致 | ### 四、解决方案 1. **保持数据类型一致** ```sql -- 正确:phone为VARCHAR则用字符串查询 SELECT * FROM users WHERE phone = '123'; ``` 2. **显转换查询条件** ```sql SELECT * FROM logs WHERE event_time = CAST('2023-01-01' AS DATETIME); ``` 3. **使用`EXPLAIN`验证索引** ```sql EXPLAIN SELECT * FROM orders WHERE order_date = 20230101; -- 检查type列是否为ALL(全表扫描) ``` 4. **修改表结构对齐类型** 如将`ref_id VARCHAR(10)`改为`ref_id INT`,确保与关联表类型一致[^1]。 > **关键结论**:转换本质是**破坏了索引的有序性**,使B+树无法快速定位数据。避免此问题的核心在于**确保查询条件与索引列的存储类型严格一致**[^1][^3]。 --- ### 相关问题 1. 如何通过`EXPLAIN`结果判断MySQL是否使用了索引? 2. 除了转换,还有哪些常见场景会导致MySQL索引失效? 3. 当发现索引失效时,有哪些系统表(如`information_schema`)可以帮助分析原因? 4. 字符串类型的索引在什么情况下可能比数值型索引性能更差? [^1]: MySQL转换规则导致索引失效的机制分析 [^2]: 索引失效场景与MySQL版本的关联性 [^3]: B+树索引有序性与转换的冲突原理 [^4]: 使用EXPLAIN验证索引使用情况的实践方法
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值