前言
索引是数据库查询性能优化重要的手段,但许多开发者都曾遇到这样的困惑:明明创建了索引,查询速度却依然很慢。这背后往往是由于索引失效导致的。本文将通过10个真实场景的SQL示例,带你了解索引失效的原因!
一、测试表设计
创建测试表结构的DDL语句
sql
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
`user_id` VARCHAR(20) NOT NULL COMMENT '用户ID(字符串类型)',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`age` TINYINT NOT NULL COMMENT '年龄',
`city` VARCHAR(20) NOT NULL COMMENT '城市',
`salary` INT NOT NULL COMMENT '薪资',
`phone` CHAR(11) COMMENT '手机号',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态',
`score` INT NOT NULL DEFAULT 0 COMMENT '分数',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- 索引配置
UNIQUE KEY `uniq_user_id` (`user_id`),
KEY `idx_username_age_city` (`username`,`age`,`city`),
KEY `idx_create_time` (`create_time`),
KEY `idx_score` (`score`),
KEY `idx_phone` (`phone`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
索引说明
索引名称 | 字段 | 索引类型 | 场景覆盖 |
---|---|---|---|
uniq_user_id | user_id | 唯一索引 | 隐式类型转换 |
idx_username_age_city | username+age+city | 联合索引 | 最左前缀原则 |
idx_create_time | create_time | 普通索引 | 函数操作 |
idx_score | score | 普通索引 | 范围查询/负向查询 |
idx_phone | phone | 普通索引 | NULL值查询 |
idx_status | status | 普通索引 | 低区分度索引 |
随机生成 100w 测试数据
二、十大索引失效场景详解
Tips :以下是 MySQL
EXPLAIN
中type
字段的常见类型及其说明的表格总结,按性能从优到劣排序。
类型 (type) | 说明 | 触发条件 | 示例 |
---|---|---|---|
system | 表中仅有一行数据(const 的特例)。 |
表仅有一行记录(如系统表或初始化后的 MyISAM /MEMORY 表)。 |
SELECT * FROM user; |
const | 通过主键或唯一索引直接定位单行记录。 | 主键或唯一索引的等值查询。 | SELECT * FROM user WHERE id = 1; |
eq_ref | 在 JOIN 中,使用主键或唯一索引关联另一张表,每行只匹配一条记录。 | 关联字段是另一表的主键或唯一索引。 | SELECT * FROM order JOIN user ON orders.user_id = user.id; |
ref | 使用非唯一索引的等值查询,可能返回多行。 | 普通索引的等值查询。 | SELECT * FROM user WHERE name = '张三'; (name 是普通索引) |
fulltext | 使用全文索引查询。 | 表有全文索引,且查询使用 MATCH ... AGAINST 。 |
SELECT * FROM articles WHERE MATCH(content) AGAINST('MySQL'); |
ref_or_null | 类似 ref ,但额外处理 NULL 值。 |
索引列的查询包含 NULL 条件(如 WHERE col = 'value' OR col IS NULL )。 |
SELECT * FROM user WHERE age = 25 OR age IS NULL; |
index_merge | 合并多个索引的结果(如 OR 或 AND 条件的联合)。 |
查询同时使用多个索引。 | SELECT * FROM user WHERE id = 1 OR name = '张三'; |
range | 在索引范围内扫描(如 BETWEEN 、IN 、> )。 |
索引的范围查询。 | SELECT * FROM user WHERE age BETWEEN 20 AND 30; |
index | 全索引扫描(遍历索引树),通常比全表扫描快。 | 查询仅需读取索引列。 | SELECT name FROM user; (name 是索引列) |
ALL | 全表扫描,性能最差。 | 无可用索引或索引失效。 | SELECT * FROM user WHERE address = 'Beijing'; (address 无索引) |
补充说明:
- 性能排序 :
system
>const
>eq_ref
>ref
>fulltext
>ref_or_null
>index_merge
>range
>index
>ALL
。- 优化目标 :尽量让查询的
type
达到const
、eq_ref
或ref
,避免出现ALL
。- 索引设计 :合理设计索引(如覆盖索引、联合索引)可显著优化
type
类型。
场景1:索引列参与运算
sql
-- ❌ 错误写法:对索引列使用函数
SELECT * FROM user WHERE YEAR(create_time) = 2025;
-- ✅ 优化方案:保持索引列原始值
SELECT * FROM user
WHERE create_time BETWEEN '2025-01-01' AND '2025-12-31';
SQL解释如下图 :
原理:索引存储的是原始值,经过函数处理后无法匹配索引结构。
场景2:隐式类型转换
sql
-- ❌ user_id是字符串,但用数字查询(触发类型转换)
SELECT * FROM user WHERE user_id = 10086;
-- ✅ 严格类型匹配
SELECT * FROM user WHERE user_id = '10086';
SQL解释如下图 :
原理 :MySQL会将所有user_id
转换为数字再比较,相当于全表扫描。
场景3:违反最左前缀原则
sql
-- ❌ 跳过username字段查询(联合索引失效)
SELECT * FROM user WHERE age = 25 AND city = '北京';
-- ✅ 正确使用联合索引
SELECT * FROM user
WHERE username = '张三' AND age = 25;
SQL解释如下图 :
场景4:OR连接非索引字段
sql
-- ❌ age字段无索引时(全表扫描)
SELECT * FROM user WHERE score > 90 OR age = 1;
-- ✅ 优化方案:拆分查询或为age添加索引
ALTER TABLE `user`
ADD INDEX `idx_age`(`age`);
SQL解释如下图 :
场景5:使用负向查询
sql
-- ❌ 不等号导致索引失效
SELECT * FROM user WHERE score != 90000;
-- ✅ 优化方案:限定范围或结合其他索引字段
SELECT * FROM user
WHERE score > 90000 AND city = '上海'; -- 利用city索引缩小范围
SQL解释如下图 :
场景6:模糊查询以%开头
sql
-- ❌ 前导通配符导致全表扫描
SELECT * FROM user WHERE username LIKE '%张%';
-- ✅ 允许索引的模糊查询
SELECT * FROM user WHERE username LIKE '张%';
SQL解释如下图 :
场景7:使用NOT IN
或者NOT EXISTS
sql
-- ❌ 子查询的集合过大
SELECT * FROM user
WHERE id NOT IN (SELECT id FROM table_b);
原理:子查询的集合过大(或者包含NULL值,会导致整个表达式结果为 UNKNOWN),优化器可能放弃使用索引。
场景8:数据量过小时
sql
-- 当表数据量<1000行时或者数据更少时,可能直接全表扫描
SELECT * FROM user WHERE phone = '13800138000';
原理 :数据量较小时,可能出现type=ALL
,因为维护索引的代价高于全表扫描,优化器会放弃使用索引。
场景9:低选择性索引
sql
-- ❌ 数据大量重复时,状态只有0和1两种
SELECT * FROM user WHERE status = 1; -- 可能全表扫描更优
建议:低区分度字段与其他字段组成联合索引(或者去掉索引,节省空间)。
场景10:隐式字符集转换
sql
-- ❌ 两个表的字符集不同时
SELECT * FROM table1 JOIN table2
ON table1.utf8_col = table2.latin1_col; -- 字符集转换导致索引失效
建议:连表条件的字段,要使用相同字符集和数据类型。
三、诊断工具:EXPLAIN执行计划
关键指标解析
sql
EXPLAIN SELECT * FROM user
WHERE username = '王五' AND age > 25;
字段 | 说明 | 优化目标 |
---|---|---|
type | 访问类型(至少达到range) | 避免ALL和index |
key_len | 使用的索引长度 | 值越大说明索引利用率越高 |
rows | 预估扫描行数 | 值越小越好 |
结语
索引失效的本质是查询逻辑与索引结构不匹配。
- 保持查询条件与索引结构的一致性:避免对索引列进行运算、类型转换或破坏最左前缀原则。
- 合理设计索引策略:优先使用覆盖索引,对低区分度字段谨慎建索引,定期清理冗余索引。
- 善用分析工具 :通过
EXPLAIN
和ANALYZE TABLE
持续监控索引使用效率。
最后:索引优化是一个动态过程。随着数据量增长和业务变化,曾经有效的索引可能会逐渐失效。 随着数据量大幅度的变化,要多次进行索引健康检查,结合慢查询日志分析,确保数据库始终保持最佳性能状态。