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

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

2025-06-05 04:37:07 栏目:宝塔面板 87 阅读

前言

最近星球中有小伙伴问我:加了索引,SQL查询效率一定会提升吗?

答案是否定的。

让我想起了几年前查询订单的场景。

优化前下面这条SQL的查询耗时是0.5s。

SELECT * FROM orders WHERE user_id = 10086;

添加了下面的索引:

CREATE INDEX idx_user ON orders(user_id);

再次执行上面的查询SQL语句,此时的耗时却是5.2s。

出现了非常神奇的一幕:加了索引,SQL查询性能反而慢了10倍。

作为一个踩过无数数据库性能坑的老司机,今天跟大家一起聊聊那些“加了索引反而更慢”的诡异场景。

1. 索引失效

加了索引之后,你以为它在工作,其实它在摸鱼,因为它可能已经失效了。

1.1 最左前缀原则

如果查询条件的顺序不对,努力白费。

复合索引 (a, b, c) 生效的关键在于最左前缀匹配

用户表创建了名称、年龄和城市这三个字段的复合索引:

CREATE INDEX idx_user ON user(name, age, city);

✅ 有效:使用最左列 name

SELECT * FROM user WHERE name = '苏三';

✅ 有效:使用最左前缀 (name, age)

SELECT * FROM user WHERE name = '苏三' AND age = 30;

❌ 失效:跳过了最左列 name

SELECT * FROM user WHERE age = 30;

❌ 失效:未使用最左前缀

SELECT * FROM user WHERE city = '北京';

原理:复合索引的存储结构类似于电话簿(先按姓排序,再按名排序)。

如果跳过“姓”直接查“名”,索引就失效了。

1.2 函数操作

函数操作会让索引瞬间失忆。

对索引列做计算、函数转换或类型转换,会导致索引失效:

❌ 失效:对索引列使用函数:

SELECT * FROM user WHERE YEAR(create_time) = 2023;

❌ 失效:隐式类型转换 (phone 是 varchar)

SELECT * FROM user WHERE phone = 13800138000;

✅ 有效:避免函数操作

SELECT * FROM user WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31';

可以使用BETWEEN...AND查询时间范围。

原理:索引存储的是列的原始值。对值进行修改后,数据库无法在索引树中定位原始值。

1.3 范围查询

范围查询(><BETWEEN)会截断复合索引中后续列的匹配:

索引 (age, salary)

SELECT * FROM employee 
WHERE age > 25        -- ✅ age 范围查询
  AND salary = 10000; -- ❌ salary 无法使用索引

会导致salary的索引失效。

原理age>25 匹配到的是一系列值(非精确值),数据库无法高效地对 salary 进行索引过滤。

2. 索引维护成本

天下没有免费的午餐,索引是有额外的维护成本的。

2.1 DML 操作变慢

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

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

无索引表插入 100w 行:1.2 秒。

有 5 个索引的表插入 100w 行:15.8 秒 (实测差距 10 倍以上)。

场景:在写多读少的高并发场景下(如流水记录),索引反而会成为性能瓶颈。

2.2 索引占用空间

索引可能会给磁盘和内存带来双重压力。

每个索引都是一棵 B+ 树,存储完整的索引列值(或组合值)。大表的索引轻松占据几十GB空间。

MySQL中可以通过下面的SQL查看表索引大小:

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中我们可以通过下面的命令强制更新表统计信息:

ANALYZE TABLE user;

场景:当表中数据发生剧烈变化(如大批量删除/导入)后,统计信息未及时更新。

3.2 回表代价高昂

索引再好也怕“绕路”。

假如给user表给age创建了索引:

SELECT name, email FROM user 
WHERE age > 25;

即使 age 索引被使用,数据库仍需根据索引中的主键 ID 回表查询nameemail 字段。

当符合条件的数据量很大时,回表 I/O 可能远超索引扫描本身。

优化方案:使用覆盖索引(Covering Index),让索引包含查询所需的所有列。

创建覆盖索引 (包含 age, name, email):

CREATE INDEX idx_age_covering ON user(age, name, email);

查询可直接从索引获取数据,无需回表

SELECT name, email FROM user WHERE age > 25;

4. 索引过多

当表上存在多个索引时,优化器需要评估每个索引的成本,选择越多,决策时间越长

下面的查询可能使用索引 A 或索引 B

SELECT * FROM orders 
WHERE user_id = 1001 
  AND status = 'completed';

优化建议

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

5. 锁与并发

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

5.1 行锁升级

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

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

5.2 索引分裂的阻塞

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

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

总结

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

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

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

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

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

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

本文地址:https://www.yitenyun.com/266.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL Web 应用 异步数据库 数据同步 ACK 双主架构 循环复制 TIME_WAIT 运维 负载均衡 JumpServer SSL 堡垒机 跳板机 HTTPS HexHub Docker 服务器 管理口 服务器性能 JumpServer安装 堡垒机安装 Linux安装JumpServer Deepseek 宝塔面板 Linux宝塔 生命周期 esxi esxi6 root密码不对 无法登录 web无法登录 SQL 查询 序列 核心机制 Windows Windows server net3.5 .NET 安装出错 HTTPS加密 Windows宝塔 Mysql重置密码 开源 PostgreSQL 存储引擎 锁机制 宝塔面板打不开 宝塔面板无法访问 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 行业 趋势 Oracle 处理机制 无法访问宝塔面板 Undo Log 机制 Spring Redis 异步化 连接控制 InnoDB 数据库锁 监控 机器学习 优化 万能公式 动态查询 Serverless 无服务器 语言 ES 协同 响应模型 group by 索引 技术 分页查询 openHalo scp Linux的scp怎么用 scp上传 scp下载 scp命令 缓存方案 缓存架构 缓存穿透 Postgres OTel Iceberg 工具 存储 高可用 GreatSQL 连接数 数据 主库 SVM Embedding 日志文件 MIXED 3 R edis 线程 国产数据库 Linux 安全 R2DBC Netstat Linux 服务器 端口 SQLite-Web SQLite 数据库管理工具 加密 场景 启动故障 ​Redis 推荐模型 自定义序列化 防火墙 黑客 云原生 Recursive SQLark RocketMQ 长轮询 配置 共享锁 AI 助手 向量数据库 大模型 OB 单机版 Hash 字段 PG DBA Rsync Ftp 信息化 智能运维 不宕机 磁盘架构 电商 系统 架构 向量库 Milvus 数据分类 Python 修改DNS Centos7如何修改DNS 业务 IT运维 Canal filelock 流量 传统数据库 向量化 • 索引 • 数据库 线上 库存 预扣 分库 分表 sftp 服务器 参数 PostGIS mini-redis INCR指令 MVCC 人工智能 推荐系统 语句 MySQL 9.3 聚簇 非聚簇 频繁 Codis redo log 重做日志 同城 双活 Doris SeaTunnel MongoDB MCP 开放协议 失效 缓存 Redisson 锁芯 高效统计 今天这篇文章就跟大家 数据类型 虚拟服务器 虚拟机 内存 工具链 事务 Java 开发 INSERT COMPACT 主从复制 代理 prometheus Alert 数据备份 千万级 大表 窗口 函数 容器 数据结构 ZODB 发件箱模式 SSH 聚簇索引 非聚簇索引 网络架构 网络配置 EasyExcel MySQL8 引擎 性能 分布式架构 分布式锁​ QPS 高并发 崖山 新版本 Web B+Tree ID 字段 RDB AOF 数据脱敏 加密算法 分布式 集中式 分页 核心架构 订阅机制 Go 数据库迁移 Web 接口 OAuth2 Token Redis 8.0 速度 服务器中毒 播客 模型 数据页 数据集成工具 读写 自动重启 容器化 网络故障 DBMS 管理系统 StarRocks 数据仓库 SpringAI 排行榜 排序 池化技术 连接池 微软 SQL Server AI功能 Redka JOIN MGR 分布式集群 Caffeine CP 部署 原子性 Entity 网络 LRU 业务场景 事务隔离 Valkey Valkey8.0 Pottery Testcloud 云端自动化 dbt 数据转换工具 分页方案 排版 数据字典 兼容性 1 ReadView 优化器 事务同步 sqlmock 意向锁 记录锁 悲观锁 乐观锁 关系数据库 日志 Weaviate 对象 单点故障 单线程 UUIDv7 主键 仪表盘 InfluxDB Order AIOPS UUID ID 编程 RAG HelixDB Ansible Pump Crash 代码 IT 双引擎 分布式锁 Zookeeper 产业链 恢复数据 字典 订单 LLM 线程安全 List 类型 国产 用户 慢SQL优化 表空间 拦截器 动态代理 解锁 调优 Next-Key RR 互联网 GitHub Git 快照读 当前读 视图 矢量存储 数据库类型 AI代理 神经系统 查询规划 count(*) count(主键) 行数 CAS 算法 技巧 多线程 并发控制 恢复机制 闪回