数据库索引“我以为走了索引”(隐式转换、函数操作、不等号、OR条件导致索引失效)

6 阅读7分钟

9年Java开发,SQL优化做了无数轮。最崩溃的场景是:明明建了索引,EXPLAIN也显示走了,但就是慢。今天聊四个让索引“失效”的经典陷阱,每个都是我线上真实踩过的。


一、隐式转换:MySQL“悄悄”把类型转了,索引就废了

现象:明明给字段建了索引,查询还是全表扫描

sql

-- 表结构
CREATE TABLE `user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `phone` varchar(20) NOT NULL,  -- 手机号,varchar类型
    `age` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `idx_phone` (`phone`)
);

-- 错误查询:用数字去匹配varchar字段
EXPLAIN SELECT * FROM user WHERE phone = 13800138000;  -- 数字

EXPLAIN结果:

  • type: ALL(全表扫描)
  • Extra: Using where

为什么?——MySQL隐式转换规则

varchar字段和数字比较时,MySQL会把varchar转成数字再比较:

sql

-- 实际执行时变成了
SELECT * FROM user WHERE CAST(phone AS signed) = 13800138000;

函数操作导致索引失效。

解决方案

sql

-- ✅ 正确:用字符串匹配
SELECT * FROM user WHERE phone = '13800138000';

-- ✅ 反过来:如果字段是int,传字符串没问题(字符串会转数字)
SELECT * FROM user WHERE age = '18';  -- age是int,没问题

口诀:  字符串字段用字符串匹配,数字字段用数字匹配,类型要对齐。


二、函数操作:对索引列做计算,索引直接失效

场景1:对索引列使用函数

sql

-- 查询某一天的数据
EXPLAIN SELECT * FROM `order` WHERE DATE(create_time) = '2024-01-15';

问题:  DATE(create_time) 对create_time列做了函数计算,索引失效。

解决方案:

sql

-- ✅ 改为范围查询
SELECT * FROM `order` 
WHERE create_time >= '2024-01-15 00:00:00' 
  AND create_time < '2024-01-16 00:00:00';

场景2:对索引列做运算

sql

-- ❌ 错误:对索引列做运算
SELECT * FROM product WHERE price + 10 > 100;

-- ✅ 正确:把运算移到等号右边
SELECT * FROM product WHERE price > 90;

场景3:使用LENGTH等函数

sql

-- ❌ 错误
SELECT * FROM user WHERE LENGTH(name) > 5;

-- ✅ 正确:加一个冗余字段name_length,建索引
ALTER TABLE user ADD COLUMN name_length INT, ADD KEY idx_name_length(name_length);
-- 写入时计算好长度

常见的“索引杀手”函数:

函数替代方案
DATE(create_time)范围查询 >= 和 <
YEAR(create_time)范围查询或冗余字段
SUBSTRING(name,1,3)考虑ES或冗余字段
CONCAT(first_name, ' ', last_name)冗余字段
CAST(phone AS SIGNED)改参数类型

三、不等号(!=、<>):索引可以用,但基本是“全表扫描”效果

现象:加了索引,但查询还是很慢

sql

EXPLAIN SELECT * FROM `order` WHERE status != 'PAID';

EXPLAIN结果:

  • type: ALL 或 range
  • rows: 表中大部分数据

为什么?——不等号会扫描大量数据

如果status字段90%都是PAID,查询!= PAID意味着要返回10%的数据。MySQL优化器认为:

  • 走索引再回表,成本更高
  • 不如直接全表扫描

解决方案

sql

-- 方案1:改成IN或等值查询
SELECT * FROM `order` WHERE status IN ('PENDING', 'CANCELED', 'REFUND');

-- 方案2:分区表,把不等于的单独分区
-- 方案3:用两个索引合并(MySQL 5.6+ 索引合并)
-- 方案4:接受全表扫描,但加覆盖索引
CREATE INDEX idx_status_cover ON `order`(status, id, amount);  -- 覆盖索引,避免回表

记住:不等于查询基本等于索引失效,尽量改成等值或IN。


四、OR条件:左右两边都能用索引才行

现象:OR两边只有一个有索引,整个查询不走索引

sql

-- user_id有索引,name没有索引
EXPLAIN SELECT * FROM `order` WHERE user_id = 123 OR name = '张三';

EXPLAIN结果:

  • type: ALL(全表扫描)

为什么?——MySQL需要对两个条件的结果做OR合并

如果name没有索引,MySQL需要全表扫描来找到name='张三'的数据,然后和user_id=123的结果合并。既然已经全表扫描了,前面索引的优势就没了。

解决方案

sql

-- 方案1:给OR两边都建索引
CREATE INDEX idx_name ON `order`(name);  -- 两边都有索引

-- 方案2:用UNION代替OR
SELECT * FROM `order` WHERE user_id = 123
UNION
SELECT * FROM `order` WHERE name = '张三';

-- 方案3:改成IN(如果OR的值是固定的)
SELECT * FROM `order` WHERE user_id IN (123, 456);

五、最左前缀原则:索引顺序错了,白建

场景:复合索引 (a, b, c)

sql

CREATE INDEX idx_a_b_c ON table(a, b, c);

哪些查询能走索引?

查询条件是否能走索引说明
WHERE a = 1✅ 能用到了a
WHERE a = 1 AND b = 2✅ 能用到了a,b
WHERE a = 1 AND b = 2 AND c = 3✅ 能用到了全部
WHERE a = 1 AND c = 3⚠️ 部分只用到了a,c用不到
WHERE b = 2❌ 不能没用到最左列a
WHERE b = 2 AND c = 3❌ 不能没用到a
WHERE a = 1 ORDER BY b✅ 能索引排序

常见误区

sql

-- 误区1:范围查询右边的列用不到索引
WHERE a = 1 AND b > 10 AND c = 3  
-- b用了索引,c用不到(范围查询右边的列失效)

-- 误区2:跳过了中间的列
WHERE a = 1 AND c = 3  
-- 只有a用索引,c用不到

-- 误区3:顺序不对
WHERE b = 2 AND a = 1  
-- MySQL优化器会自动调整顺序,实际能走到a,b

设计原则

把区分度高的列放左边,范围查询的列放右边。

sql

-- 区分度:status(只有几种值)vs user_id(几百万种)
-- ✅ 正确:区分度高的放左边
INDEX idx_user_status (user_id, status)

-- ❌ 错误:区分度低的放左边
INDEX idx_status_user (status, user_id)

六、LIKE查询:%放前面,索引失效

场景

sql

-- ❌ 以%开头,索引失效
SELECT * FROM user WHERE name LIKE '%张三%';

-- ✅ 以%结尾,索引有效
SELECT * FROM user WHERE name LIKE '张三%';

-- ❌ 中间有%,索引失效
SELECT * FROM user WHERE name LIKE '张%三';

为什么?——B+Tree从左到右匹配

%开头,MySQL不知道从哪个节点开始找,只能全表扫描。

解决方案

sql

-- 方案1:改成后缀匹配
WHERE name LIKE '张三%'

-- 方案2:用ES(Elasticsearch)做全文检索
-- 方案3:用MySQL全文索引(不推荐,功能弱)
ALTER TABLE user ADD FULLTEXT(name);
SELECT * FROM user WHERE MATCH(name) AGAINST('张三');

七、NULL值:索引列允许NULL,查询可能不走索引

场景

sql

-- 表结构:age允许NULL
CREATE TABLE user (age INT, KEY idx_age(age));

-- 查询NOT NULL走索引
SELECT * FROM user WHERE age IS NOT NULL;  -- 可能不走

-- 查询NULL走索引
SELECT * FROM user WHERE age IS NULL;  -- 可能走

-- 但用!=或<>可能不走
SELECT * FROM user WHERE age != 18;  -- 可能全表扫描

最佳实践

sql

-- 设计时:索引列尽量设置NOT NULL DEFAULT
ALTER TABLE user MODIFY age INT NOT NULL DEFAULT 0;

-- 查询时:避免使用IS NULL / IS NOT NULL
-- 如果字段允许NULL,查询条件要考虑好

八、索引失效总结速查表

场景失效原因解决方案
phone = 13800138000隐式类型转换用字符串 '13800138000'
DATE(create_time) = '2024-01-01'对索引列用函数改为范围查询
price + 10 > 100对索引列做运算移到等号右边
status != 'PAID'不等号扫描大量数据IN或用覆盖索引
user_id = 123 OR name = '张三'OR右边无索引UNION或两边都建索引
LIKE '%张三%'%在开头改成 '张三%' 或用ES
age IS NOT NULLNULL值处理字段设NOT NULL
a=1 AND c=3(复合索引a,b,c)跳过中间列调整索引顺序
a=1 AND b>10 AND c=3范围查询右边列失效把范围放最后

九、EXPLAIN看懂这几个字段就够了

sql

EXPLAIN SELECT * FROM user WHERE phone = '13800138000';
字段说明
typeref/range/constALL/indexALL=全表扫描
key有索引名NULLNULL=没用索引
rows越小越好扫描的行数
ExtraUsing indexUsing filesort/Using temporaryUsing index=覆盖索引

十、一句话避坑口诀

text

隐式转换索引废,函数运算全表扫。
不等号、OR条件坑,LIKE百分放前面。
复合索引左到右,范围查询右边倒。
NULL值要设默认,EXPLAIN先看再跑。

十一、互动一下

你遇到过最离谱的索引失效是什么?

有没有一个SQL优化了一整天,最后发现是类型写错了?

评论区聊聊👇


下期预告:  避坑7——事务隔离级别“我以为读到了最新数据”(脏读、不可重复读、幻读、MVCC原理)


我是小李,9年Java,产假中持续输出。点个赞,收藏防丢❤️