一、先搞懂:索引的底层原理(精通的前提)
要优化慢查询,首先必须理解索引为何能提速、以及它的 "短板"------ 这是所有优化方案的底层逻辑。
1. 索引的本质:空间换时间的有序数据结构
MySQL 中默认的索引结构是 B + 树(哈希索引仅适用于 Memory 引擎,且不支持范围查询),其核心特性:
-
非叶子节点:仅存索引键和子节点指针,不存数据,能让树的高度极低(百万级数据的 B + 树高度通常≤3),实现 "快速定位";
-
叶子节点:
- 按索引键有序排列,且通过双向链表连接,支持范围查询;
- 聚集索引(主键索引)的叶子节点直接存整行数据,二级索引(非主键索引)的叶子节点仅存 "索引键 + 主键值"(回表的根源)。
2. 索引的核心代价
- 写性能损耗:增 / 删 / 改数据时,需同步维护索引的 B + 树结构(分裂、旋转等),索引越多,写损耗越大;
- 空间损耗:索引会占用额外磁盘空间;
- 回表代价:通过二级索引查到主键后,需再查聚集索引获取完整数据,多一次 IO。
二、慢 SQL 优化的完整流程(从定位到落地)
步骤 1:精准定位慢查询(先找问题,再谈优化)
首先要明确 "哪些 SQL 慢、慢在哪",而非盲目加索引。
-
开启慢查询日志(临时生效,重启失效):
ini# 开启慢查询日志 set global slow_query_log = ON; # 设置慢查询阈值(单位:秒,建议设0.1秒,捕捉潜在慢查询) set global long_query_time = 0.1; # 指定慢查询日志存储路径 set global slow_query_log_file = '/var/lib/mysql/slow.log'; # 记录未使用索引的查询(辅助定位) set global log_queries_not_using_indexes = ON; -
分析慢查询日志:
-
用
mysqldumpslow快速统计:bash# 按执行次数排序,显示前10条慢查询 mysqldumpslow -s c -t 10 /var/lib/mysql/slow.log # 按耗时排序,显示前10条 mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log -
用
explain分析 SQL 执行计划(核心):sql-- 分析目标SQL EXPLAIN SELECT * FROM order_info WHERE user_id = 123 AND create_time > '2026-01-01';重点关注
explain结果中的字段:字段 核心含义 type 访问类型(ALL/INDEX/RANGE/REF/eq_ref/CONST),ALL(全表扫描)是优化重点 key 实际使用的索引(NULL 表示未用索引) rows MySQL 预估要扫描的行数(越大越慢) Extra 额外信息(Using filesort/Using temporary 是性能杀手,需重点优化)
-
步骤 2:基于索引原理的优化方案(从基础到进阶)
方案 1:避免索引失效(基础但最易踩坑)
索引失效的本质是:SQL 条件破坏了 B + 树的 "有序性",导致 MySQL 无法通过索引快速定位,只能全表 / 全索引扫描。常见索引失效场景 + 原理 + 解决方案:
表格
| 失效场景 | 底层原理 | 解决方案 |
|---|---|---|
| 索引列做函数 / 运算 | 函数 / 运算会改变索引键的有序性,MySQL 无法匹配索引树 | 避免索引列运算:WHERE id+1=10 → WHERE id=9 |
| 使用模糊查询前缀 % | LIKE '%abc' 无法利用 B + 树的有序性(前缀无序) |
仅用后缀模糊:LIKE 'abc%';或用全文索引 |
| 用 OR 连接非索引列 | OR 两边有一个无索引,MySQL 会放弃索引(无法同时匹配两个树) | 给 OR 两边字段加联合索引;或拆分为两个查询 |
| 联合索引不满足最左前缀 | 联合索引 (a,b,c) 的 B + 树按 a→b→c 排序,跳过 a 查 b/c 会破坏有序性 | 遵循 "最左前缀原则",查询条件包含联合索引的左侧列 |
| 索引列用 NULL/NOT NULL | NULL 会破坏索引键的比较逻辑,MySQL 可能放弃索引 | 字段设为 NOT NULL,用默认值替代 NULL |
| 字符串不加引号 | WHERE phone=13800138000 会隐式转换,破坏索引有序性 |
字符串条件加引号:WHERE phone='13800138000' |
方案 2:设计高效的索引(从 "能用" 到 "好用")
核心原则:用最少的索引,覆盖最多的高频查询;减少回表和索引维护代价。
-
单值索引 vs 联合索引:
- 原理:高频多条件查询(如
user_id + create_time),单值索引(user_id、create_time)会触发 "索引合并"(效率低),而联合索引(user_id, create_time)可直接按有序性匹配。 - 实践:优先创建联合索引,而非多个单值索引;联合索引列顺序遵循 "区分度高的列在前"(如 user_id 区分度高于 create_time)。
- 原理:高频多条件查询(如
-
覆盖索引(减少回表) :
- 原理:如果查询的字段都包含在索引中,MySQL 无需回表,直接从索引叶子节点取数(Extra 显示
Using index)。 - 实践:原慢 SQL(回表):
SELECT id, order_no, amount FROM order_info WHERE user_id=123优化:创建联合索引idx_userid_orderno_amount (user_id, order_no, amount),实现覆盖索引,避免回表。
- 原理:如果查询的字段都包含在索引中,MySQL 无需回表,直接从索引叶子节点取数(Extra 显示
-
主键索引的优化:
- 原理:聚集索引的叶子节点存整行数据,主键设计不当会导致 B + 树频繁分裂。
- 实践:主键用自增整型(INT/BIGINT),而非 UUID(无序,插入时 B + 树频繁分裂);避免主键更新(会重构整个 B + 树)。
-
冗余索引清理:
- 原理:冗余索引(如联合索引 (a,b) 和单值索引 (a))会增加写维护代价,且 MySQL 优化器选择索引时会变慢。
- 实践:删除冗余索引,如已存在
idx_a_b (a,b),则删除idx_a (a)。
方案 3:优化 SQL 写法(配合索引发挥最大效能)
-
** 避免 SELECT ***:
- 原理:SELECT * 必然触发回表(除非覆盖索引包含所有字段),且传输多余数据。
- 实践:仅查询需要的字段,配合覆盖索引使用。
-
限制结果集大小:
-
原理:LIMIT 可让 MySQL 提前终止索引扫描,减少 IO。
-
实践:分页查询必须加 LIMIT,且避免
LIMIT 10000, 10(需扫描 10010 行),优化为:sql-- 优化前(慢) SELECT * FROM order_info LIMIT 10000, 10; -- 优化后(用主键定位,仅扫描10行) SELECT * FROM order_info WHERE id > 10000 LIMIT 10;
-
-
JOIN 查询优化:
-
原理:小表驱动大表(减少循环次数),且 JOIN 字段加索引(避免笛卡尔积)。
sql-- 优化前(大表驱动小表) SELECT * FROM big_table b JOIN small_table s ON b.id = s.big_id; -- 优化后(小表驱动大表) SELECT * FROM small_table s JOIN big_table b ON s.big_id = b.id;同时给
big_table.id和small_table.big_id加索引。
-
-
避免子查询:
- 原理:子查询会生成临时表(无索引),效率低;JOIN 的执行计划更优。
- 实践:将子查询改写为 JOIN。
方案 4:进阶优化(索引之外的补充)
-
分库分表:
- 原理:单表数据量超过千万级时,B + 树高度增加,索引效率下降;分库分表将数据拆分到多个表 / 库,降低单表数据量。
- 实践:按用户 ID 哈希分表(如
user_id % 100分 100 表),或按时间范围分表(如订单表按月份分表)。
-
查询缓存(按需使用) :
- 原理:MySQL8.0 已移除查询缓存,可通过 Redis 缓存高频只读查询结果(如商品详情、用户信息)。
- 实践:缓存热点数据,设置合理过期时间,避免缓存击穿 / 雪崩。
-
调整 MySQL 参数:
innodb_buffer_pool_size:设置为物理内存的 50%-70%,让索引和数据尽可能缓存在内存,减少磁盘 IO;query_cache_size:MySQL5.7 及以下可用,设置合理缓存大小(避免过大导致内存浪费)。
步骤 3:验证优化效果(闭环)
优化后需验证效果,避免 "越优化越慢":
- 对比优化前后的
explain结果:type是否从 ALL 变为 RANGE/REF,rows是否减少,Extra是否消除 Using filesort/Using temporary; - 对比执行耗时:用
SELECT SQL_NO_CACHE ...避免查询缓存干扰,统计执行时间; - 监控数据库状态:通过
show processlist查看慢查询是否减少,show engine innodb status查看 IO 和锁等待情况。
总结
- 索引精通的核心:理解 B + 树的有序性和回表代价,所有索引优化都是围绕 "利用有序性、减少回表、降低维护成本" 展开;
- 慢查询优化逻辑:先通过慢日志 + explain 定位问题→避免索引失效→设计高效索引(联合索引 / 覆盖索引)→优化 SQL 写法→进阶方案(分库分表 / 缓存)→验证效果;
- 关键原则:索引不是越多越好,需平衡读性能和写性能,遵循 "最左前缀、区分度优先、覆盖索引" 三大核心。