MySQL 索引优化实战:从原理到调优,一篇搞定

举报
Brian-Tony 发表于 2026/10/23 08:58:05 2026/10/23
【摘要】 MySQL 索引优化实战:从原理到调优,一篇搞定 前言数据库慢查询是后端开发最常遇到的性能问题之一,而索引是解决慢查询最直接的手段。但索引不是建得越多越好,建错了反而拖慢写入、浪费空间。本文从索引原理出发,梳理常见索引类型、失效场景、优化思路和实战案例,适合日常开发和面试查阅。 一、为什么需要索引?没有索引时,查询需要全表扫描(Full Table Scan),数据量一大就慢。索引的本质是...

MySQL 索引优化实战:从原理到调优,一篇搞定

前言

数据库慢查询是后端开发最常遇到的性能问题之一,而索引是解决慢查询最直接的手段。但索引不是建得越多越好,建错了反而拖慢写入、浪费空间。本文从索引原理出发,梳理常见索引类型、失效场景、优化思路和实战案例,适合日常开发和面试查阅。

一、为什么需要索引?

没有索引时,查询需要全表扫描(Full Table Scan),数据量一大就慢。索引的本质是用空间换时间,通过额外的数据结构(B+ 树)快速定位数据,把 O(n) 的扫描变成 O(log n) 的查找。

但索引也有代价:

  • 占用磁盘空间
  • 每次 INSERT/UPDATE/DELETE 都要维护索引树,影响写入性能
  • 索引过多会导致优化器选错索引,反而变慢

所以索引要建在刀刃上:高频查询字段、区分度高的字段、WHERE/JOIN/ORDER BY/GROUP BY 中常用的字段。

二、索引的数据结构:为什么是 B+ 树?

MySQL InnoDB 默认使用 B+ 树作为索引结构,而不是哈希、二叉树或 B 树,原因如下:

结构 问题
哈希表 不支持范围查询、排序,只适合等值查询
二叉搜索树 数据有序时会退化成链表,树太高
B 树 非叶子节点也存数据,单页能存的索引键少,树更高,IO 次数多
B+ 树 非叶子节点只存索引键,单页容纳更多键,树更矮胖;叶子节点用双向链表连接,适合范围查询和排序

B+ 树的关键特点:

  • 所有数据都存在叶子节点,非叶子节点只存索引
  • 叶子节点之间用双向链表连接,范围查询效率高
  • 树的高度通常为 3~4 层,一次查询只需 3~4 次磁盘 IO

三、聚簇索引 vs 非聚簇索引

InnoDB 中索引分为两类:

  • 聚簇索引(Clustered Index):叶子节点存储整行数据。InnoDB 按主键组织数据,主键索引就是聚簇索引。如果没有显式主键,InnoDB 会选唯一非空索引,都没有就自动生成一个隐藏的 row_id。
  • 非聚簇索引(Secondary Index / 二级索引):叶子节点存储的是索引列的值 + 主键值。查询时需要先查二级索引拿到主键,再回表查聚簇索引拿到完整行数据,这个过程叫回表。

覆盖索引

如果查询的列都在二级索引中,就不需要回表,这就是覆盖索引。例如:

-- idx_name_age (name, age) 是联合索引
SELECT name, age FROM users WHERE name = '张三';  -- 覆盖索引,不回表
SELECT name, age, email FROM users WHERE name = '张三';  -- 需要回表查 email

覆盖索引是优化的重要手段,能显著减少 IO。

四、常见索引类型

类型 说明 示例
主键索引 唯一 + 非空,一张表只能有一个 PRIMARY KEY (id)
唯一索引 列值唯一,允许 NULL UNIQUE INDEX uk_email (email)
普通索引 无限制,最常用的索引 INDEX idx_name (name)
联合索引 多列组合,遵循最左前缀原则 INDEX idx_name_age (name, age)
全文索引 用于全文搜索,MyISAM 和 InnoDB(5.6+)支持 FULLTEXT INDEX ft_content (content)
前缀索引 对长字符串取前缀建索引,节省空间 INDEX idx_email_prefix (email(20))

最左前缀原则

联合索引 (a, b, c) 相当于建立了 (a)、(a, b)、(a, b, c) 三个索引。查询条件必须从最左边开始匹配,否则索引失效:

--  走索引
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3

--  不走索引(跳过了 a)
WHERE b = 2
WHERE c = 3

--  部分走索引(a 走索引,b 不走)
WHERE a = 1 AND c = 3

五、索引失效的常见场景

1. 对索引列做函数运算

--  索引失效
SELECT * FROM users WHERE YEAR(create_time) = 2024;

--  走索引
SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

2. 隐式类型转换

-- phone 是 VARCHAR 类型
--  索引失效,字符串和数字比较会隐式转换
SELECT * FROM users WHERE phone = 13800138000;

--  走索引
SELECT * FROM users WHERE phone = '13800138000';

3. LIKE 左模糊

--  索引失效
SELECT * FROM users WHERE name LIKE '%张%';

--  走索引
SELECT * FROM users WHERE name LIKE '张%';

4. OR 连接非索引列

--  如果 age 没有索引,整个查询走全表扫描
SELECT * FROM users WHERE name = '张三' OR age = 25;

5. NOT IN / != / IS NOT NULL

-- 通常不走索引(优化器可能选择全表扫描)
SELECT * FROM users WHERE status != 1;
SELECT * FROM users WHERE status NOT IN (1, 2);

6. 联合索引不满足最左前缀

见上文"最左前缀原则"。

六、如何分析 SQL 是否走索引?

用 EXPLAIN 命令:

EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 25;

重点关注这几个字段:

字段 说明
type 访问类型,性能从好到坏:system > const > eq_ref > ref > range > index > ALL
key 实际使用的索引
key_len 使用的索引长度,联合索引中可以看出用了几列
rows 预估扫描行数
Extra 额外信息,Using index 表示覆盖索引,Using filesort 表示需要额外排序,Using temporary 表示用了临时表

优化目标:让 type 至少达到 ref 或 range,避免 ALL(全表扫描);Extra 中尽量避免 Using filesort 和 Using temporary。

七、索引优化实战案例

案例 1:分页优化

-- 深分页慢查询
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

-- 优化:利用覆盖索引 + 子查询
SELECT o.* FROM orders o
INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) t
ON o.id = t.id;

原理:子查询只查主键(覆盖索引),拿到 10 个 id 后再回表,避免大量回表操作。

案例 2:避免 SELECT *

--  可能回表
SELECT * FROM users WHERE name = '张三';

--  覆盖索引,不回表
SELECT id, name FROM users WHERE name = '张三';  -- idx_name(id, name)

案例 3:范围查询后索引失效

-- idx_name_age_status (name, age, status)
-- name 走索引,age 走范围,status 索引失效
SELECT * FROM users WHERE name = '张三' AND age > 20 AND status = 1;

联合索引中,范围查询后面的列无法使用索引。如果 status 也需要走索引,可以考虑调整索引列顺序或拆成多个索引。

案例 4:ORDER BY 优化

--  如果 idx_name_age (name, age),以下查询可以利用索引排序
SELECT * FROM users WHERE name = '张三' ORDER BY age;

--  以下无法利用索引排序,会 Using filesort
SELECT * FROM users WHERE name > '张三' ORDER BY age;
SELECT * FROM users WHERE name = '张三' ORDER BY age DESC, create_time ASC;

八、索引设计最佳实践

  1. 区分度高的列建索引:性别(只有男女)不适合建索引,用户 ID、手机号适合。
  2. 联合索引把区分度高的放左边:但不是绝对,要结合查询场景和最左前缀原则。
  3. 避免过多索引:单表索引建议不超过 5~8 个,写入频繁的表更要谨慎。
  4. 优先使用前缀索引:长字符串(如 URL、邮箱)可以取前缀建索引,节省空间。
  5. 删除不再使用的索引:用 pt-duplicate-key-checker 等工具定期清理冗余索引。
  6. **大表加索引用 ****ALTER TABLE ... ALGORITHM=INPLACE**:避免锁表时间过长(MySQL 5.6+ 支持 Online DDL)。

九、常见追问

1. 主键为什么建议用自增 ID?

InnoDB 聚簇索引按主键顺序存储,自增 ID 保证数据顺序写入,减少页分裂和碎片。用 UUID 做主键会导致随机 IO、页分裂频繁,插入性能差且占用空间大。

2. 什么情况下索引会失效但优化器还是选择走索引?

优化器基于成本估算选择执行计划。如果估算走索引的成本比全表扫描低,即使部分失效也可能走索引。可以用 FORCE INDEX 强制指定索引,但一般不建议,优先让优化器自己选择。

3. 怎么判断一个索引是否该建?

看慢查询日志,用 EXPLAIN 分析,结合业务查询频率。不要凭感觉建索引,要用数据说话。

总结

索引优化的核心思路:

  1. 用 EXPLAIN 定位慢查询
  2. 检查是否全表扫描(type = ALL)
  3. 确认索引是否失效(函数运算、类型转换、最左前缀等)
  4. 优先考虑覆盖索引,减少回表
  5. 定期清理冗余索引,避免过度索引

掌握这些,日常开发中 80% 的 SQL 性能问题都能搞定。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。