大家好,我是苏三,又跟大家见面了。
前言
最近星球中有小伙伴问我:加了索引,SQL查询效率一定会提升吗?
答案是否定的。
让我想起了几年前查询订单的场景。
优化前下面这条SQL的查询耗时是0.5s。
ini
SELECT * FROM orders WHERE user_id = 10086;
添加了下面的索引:
scss
CREATE INDEX idx_user ON orders(user_id);
再次执行上面的查询SQL语句,此时的耗时却是5.2s。
出现了非常神奇的一幕:加了索引,SQL查询性能反而慢了10倍。
作为一个踩过无数数据库性能坑的老司机,今天跟大家一起聊聊那些"加了索引反而更慢"的诡异场景。
最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、场景题、面试真题、项目实战、工作内推什么都有。
1. 索引失效
加了索引之后,你以为它在工作,其实它在摸鱼,因为它可能已经失效了。
1.1 最左前缀原则
如果查询条件的顺序不对,努力白费。
复合索引 (a, b, c)
生效的关键在于最左前缀匹配。
用户表创建了名称、年龄和城市这三个字段的复合索引:
scss
CREATE INDEX idx_user ON user(name, age, city);
✅ 有效:使用最左列 name
sql
SELECT * FROM user WHERE name = '苏三';
✅ 有效:使用最左前缀 (name, age)
sql
SELECT * FROM user WHERE name = '苏三' AND age = 30;
❌ 失效:跳过了最左列 name
sql
SELECT * FROM user WHERE age = 30;
❌ 失效:未使用最左前缀
sql
SELECT * FROM user WHERE city = '北京';
原理:复合索引的存储结构类似于电话簿(先按姓排序,再按名排序)。
如果跳过"姓"直接查"名",索引就失效了。
1.2 函数操作
函数操作会让索引瞬间失忆。
对索引列做计算、函数转换或类型转换,会导致索引失效:
❌ 失效:对索引列使用函数:
sql
SELECT * FROM user WHERE YEAR(create_time) = 2023;
❌ 失效:隐式类型转换 (phone 是 varchar)
sql
SELECT * FROM user WHERE phone = 13800138000;
✅ 有效:避免函数操作
sql
SELECT * FROM user WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31';
可以使用BETWEEN...AND查询时间范围。
原理:索引存储的是列的原始值。对值进行修改后,数据库无法在索引树中定位原始值。
1.3 范围查询
范围查询(>
、<
、BETWEEN
)会截断复合索引中后续列的匹配:
索引 (age, salary)
sql
SELECT * FROM employee
WHERE age > 25 -- ✅ age 范围查询
AND salary = 10000; -- ❌ salary 无法使用索引
会导致salary的索引失效。
原理 :age>25
匹配到的是一系列值(非精确值),数据库无法高效地对 salary
进行索引过滤。
最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:所在城市,即可进群。
2. 索引维护成本
天下没有免费的午餐,索引是有额外的维护成本的。
2.1 DML 操作变慢
每一次写入都可能在负重前行。
每次 INSERT
、UPDATE
、DELETE
操作,数据库不仅要修改数据,还要维护相关索引。
无索引表插入 100w 行:1.2 秒。
有 5 个索引的表插入 100w 行:15.8 秒 (实测差距 10 倍以上)。
场景 :在写多读少的高并发场景下(如流水记录),索引反而会成为性能瓶颈。
2.2 索引占用空间
索引可能会给磁盘和内存带来双重压力。
每个索引都是一棵 B+ 树,存储完整的索引列值(或组合值)。大表的索引轻松占据几十GB空间。
MySQL中可以通过下面的SQL查看表索引大小:
less
SELECT
table_name AS `Table`,
index_name AS `Index`,
ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) `Size(MB)`
FROM mysql.innodb_index_stats
WHERE table_name = 'your_table';
后果:索引过大导致内存中缓存命中率降低,物理 I/O 增加。
3. 优化器的"错误"选择
有时候,可能会出现聪明反被聪明误,加了索引可能会导致性能变慢。
3.1 统计信息过时
导航用了旧地图。
优化器依赖统计信息(如索引区分度、数据分布)来选择索引。
如果统计信息过期,优化器可能选择性能更差的索引。
MySQL中我们可以通过下面的命令强制更新表统计信息:
sql
ANALYZE TABLE user;
场景:当表中数据发生剧烈变化(如大批量删除/导入)后,统计信息未及时更新。
3.2 回表代价高昂
索引再好也怕"绕路"。
假如给user表给age创建了索引:
sql
SELECT name, email FROM user
WHERE age > 25;
即使 age
索引被使用,数据库仍需根据索引中的主键 ID 回表查询 name
、email
字段。
当符合条件的数据量很大时,回表 I/O 可能远超索引扫描本身。
优化方案 :使用覆盖索引(Covering Index),让索引包含查询所需的所有列。
创建覆盖索引 (包含 age, name, email):
scss
CREATE INDEX idx_age_covering ON user(age, name, email);
查询可直接从索引获取数据,无需回表
sql
SELECT name, email FROM user WHERE age > 25;
4. 索引过多
当表上存在多个索引时,优化器需要评估每个索引的成本,选择越多,决策时间越长。
下面的查询可能使用索引 A 或索引 B
ini
SELECT * FROM orders
WHERE user_id = 1001
AND status = 'completed';
优化建议:
- 删除重复或冗余索引
- 合并可组合的索引
- 使用工具分析索引使用率(如
sys.schema_unused_indexes
)
5. 锁与并发
锁的竞争是一个看不见的战场。
5.1 行锁升级
在事务中通过索引检索并锁定行时,如果锁数量过多 (超过阈值),数据库可能将锁升级为表锁,严重降低并发性能。
场景:全表更新或删除大量数据时,索引的存在可能导致锁升级。
5.2 索引分裂的阻塞
B+树索引在插入数据时可能发生页分裂。
这个过程需要加锁,在高并发写入场景下可能导致短暂阻塞。
总结
索引是把双刃剑,用对场景是关键。
- 理解原理:掌握最左前缀、索引失效条件、覆盖索引等核心机制
- 权衡成本:在写密集场景谨慎添加索引,评估维护代价
- 精准设计:按实际查询模式设计复合索引,避免冗余
- 关注统计信息:定期更新统计信息,确保优化器决策准确
- 监控分析 :使用
EXPLAIN
、慢查询日志等工具持续跟踪索引效果
某电商平台曾因在流水表上盲目添加索引,导致高峰时段写入延迟飙升。
后经分析,移除两个非核心索引,写入速度提升8倍,而相关查询仅增加20毫秒------这正是索引取舍的艺术。
记住:索引不是越多越好,而是越准越好。
真正的高手,懂得在索引的利刃上优雅行走。
最后说一句(求关注,别白嫖我)
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。