加了个索引,SQL性能竟然慢了10倍!

大家好,我是苏三,又跟大家见面了。

前言

最近星球中有小伙伴问我:加了索引,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 操作变慢

每一次写入都可能在负重前行。

每次 INSERTUPDATEDELETE 操作,数据库不仅要修改数据,还要维护相关索引。

无索引表插入 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 回表查询 nameemail 字段。

当符合条件的数据量很大时,回表 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';

优化建议

  1. 删除重复或冗余索引
  2. 合并可组合的索引
  3. 使用工具分析索引使用率(如 sys.schema_unused_indexes

5. 锁与并发

锁的竞争是一个看不见的战场。

5.1 行锁升级

在事务中通过索引检索并锁定行时,如果锁数量过多 (超过阈值),数据库可能将锁升级为表锁,严重降低并发性能。

场景:全表更新或删除大量数据时,索引的存在可能导致锁升级。

5.2 索引分裂的阻塞

B+树索引在插入数据时可能发生页分裂

这个过程需要加锁,在高并发写入场景下可能导致短暂阻塞。

总结

索引是把双刃剑,用对场景是关键。

  1. 理解原理:掌握最左前缀、索引失效条件、覆盖索引等核心机制
  2. 权衡成本:在写密集场景谨慎添加索引,评估维护代价
  3. 精准设计:按实际查询模式设计复合索引,避免冗余
  4. 关注统计信息:定期更新统计信息,确保优化器决策准确
  5. 监控分析 :使用 EXPLAIN、慢查询日志等工具持续跟踪索引效果

某电商平台曾因在流水表上盲目添加索引,导致高峰时段写入延迟飙升。

后经分析,移除两个非核心索引,写入速度提升8倍,而相关查询仅增加20毫秒------这正是索引取舍的艺术。

记住:索引不是越多越好,而是越准越好

真正的高手,懂得在索引的利刃上优雅行走。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

相关推荐
颜酱几秒前
# 02 | 搭骨架:用 LangGraph 编排 12 步工作流(思路)
前端·人工智能·后端
颜酱2 分钟前
02 | 搭骨架:用 LangGraph 编排 12 步工作流
前端·人工智能·后端
嘉伟桑1 小时前
Python调用电价API返回JSON示例:分时电价、现货电价和字段解析
后端
武子康1 小时前
Shippy:确定性工具、会话级 Sandbox 与 Live-Data Eval(4 类收敛 + 7 步实现方案 + 6 类评测指标)
前端·人工智能·后端
我叫黑大帅2 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试
AskHarries2 小时前
文件上传系统
后端
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端