“给这个字段加个索引”几乎是性能优化讨论中出现频率最高的一句话。但索引不是性能魔法:有的索引能让查询从秒级降到毫秒级,有的索引建了却完全没被使用,还有的索引会拖慢写入并占用大量磁盘。

要真正用好 MySQL 索引,关键不是记住几个 DDL 语句,而是理解它在做什么:索引用额外的存储空间和写入维护成本,换取更少的数据访问次数。

本文以 MySQL 的 InnoDB 引擎为主线,结合常见业务表和 EXPLAIN,讲清楚索引为何更快、联合索引如何设计,以及如何定位“明明有索引却不生效”的问题。

一、没有索引时,MySQL 如何查数据?

先看一张简化的订单表:

1
2
3
4
5
6
7
8
CREATE TABLE `orders` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL,
`status` TINYINT NOT NULL,
`total_amount` DECIMAL(10, 2) NOT NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB;

现在要查询某位用户最近的已支付订单:

1
2
3
4
5
SELECT id, total_amount, created_at
FROM orders
WHERE user_id = 10086 AND status = 2
ORDER BY created_at DESC
LIMIT 20;

如果 user_idstatuscreated_at 都没有索引,MySQL 就只能逐行读取表数据,判断每一行是否满足条件。这种方式称为全表扫描(full table scan)。

当表只有几百行时,全表扫描往往没有问题;但订单表增长到千万级后,即使只有极少数行满足条件,也可能需要检查大量无关记录。真正慢的通常不是“比较条件”本身,而是读取和访问大量数据页的成本。

索引的意义,就是提前组织一份更适合查找的数据目录,让数据库先缩小范围,再读取需要的行。

二、索引的本质:一本按规则排序的目录

可以把数据表理解成一本很厚的书:

  • 没有目录时,找“Redis”这个词只能从第一页翻到最后一页;
  • 有了按拼音排序的目录,可以先定位到“R”附近,再找到页码;
  • 最后只翻到对应页面读取正文。

数据库索引也是类似思路。它会保存“索引列的值”和“如何定位数据行的信息”,并按照某种结构组织。查询条件如果符合这份目录的排序规则,就能快速跳过大批无关数据。

注意两个常被混淆的结论:

  1. 索引并不是复制一整张表,而是额外的数据结构;
  2. 索引不是越多越好。每一次 INSERTUPDATEDELETE 都可能需要同步维护相关索引。

三、为什么 InnoDB 大多使用 B+ 树索引?

MySQL 的 InnoDB 中,最常见的普通索引、唯一索引、主键索引,底层通常都是 B+ 树。不需要手写 B+ 树,但理解它能解释很多索引规则。

1. 先排除两个直觉方案

数组或有序链表:有序数组查找快,但插入中间代价高;链表插入容易,定位却慢。它们都不适合频繁读写的大表。

二叉搜索树:理论上查找复杂度是 O(log n),但树可能长得很高,且一个节点通常只放很少的值。数据库数据在磁盘或页缓存中,树高意味着更多随机 I/O。

2. B+ 树的关键优势

B+ 树的每个节点可以保存大量键值和子节点指针,因此分叉很多、树高很低。即使索引中有千万条记录,通常也只需要访问少量层级的数据页。

此外,B+ 树的叶子节点按顺序连接,非常适合范围查询:

1
2
3
4
SELECT *
FROM orders
WHERE created_at >= '2026-07-01'
AND created_at < '2026-08-01';

找到 7 月 1 日的起点后,数据库可以沿叶子节点顺序读取到 8 月 1 日为止,而不用为每一条记录重新从根节点查找。

B+ 树的重点不是“比较次数少”,而是“用很少的数据页访问定位到连续的数据范围”。这正适合磁盘和缓存页的工作方式。

四、聚簇索引与二级索引:InnoDB 最重要的结构

理解 InnoDB 索引,必须先区分两类索引。

1. 聚簇索引(Clustered Index)

InnoDB 的表数据本身就存放在主键索引的叶子节点中。因此主键索引被称为聚簇索引:叶子节点不仅有主键值,还存有整行数据。

1
SELECT * FROM orders WHERE id = 1001;

如果按主键查询,MySQL 可以直接沿着主键 B+ 树找到那一行完整数据,这类查询通常非常高效。

InnoDB 会优先选择以下内容作为聚簇索引:

  1. 显式声明的主键;
  2. 第一个非空唯一索引;
  3. 若两者都没有,则生成隐藏行 ID。

因此,业务表应显式设计稳定、尽量短的主键。常见的自增 BIGINT 简单且对写入友好;UUID 也能使用,但随机 UUID 可能让新记录分散插入 B+ 树的各个位置,带来更多页分裂和空间碎片。

2. 二级索引(Secondary Index)

除了主键以外的索引,通常称为二级索引。例如:

1
CREATE INDEX idx_orders_user_id ON orders(user_id);

二级索引的叶子节点保存的不是整行数据,而是索引列值 + 主键值。因此下面这条查询通常分两步:

1
2
3
SELECT total_amount
FROM orders
WHERE user_id = 10086;
  1. idx_orders_user_id 中找到满足 user_id = 10086 的主键;
  2. 再根据主键回到聚簇索引取出 total_amount

第二步叫回表。回表不是错误,只是当结果集很大时会增加随机访问成本。后文的覆盖索引,就是减少回表的一种手段。

五、索引的常见类型与适用场景

1. 主键索引

1
PRIMARY KEY (id)

主键唯一且不能为空,每张 InnoDB 表只有一个。它决定了聚簇索引的组织方式。

2. 普通索引

1
CREATE INDEX idx_orders_created_at ON orders(created_at);

用于加速筛选、排序和关联,允许重复值。

3. 唯一索引

1
CREATE UNIQUE INDEX uk_users_email ON users(email);

除了加速查询,它还把“邮箱不能重复”变成数据库层的约束。对于用户名、第三方平台用户 ID、业务幂等号等,唯一索引是正确性的重要保障。

4. 联合索引

1
2
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);

一个索引包含多个列,并且列顺序非常重要。它是业务查询优化中最常用、也最容易设计错的一类索引。

5. 全文索引与空间索引

全文检索可使用 FULLTEXT,空间地理数据可使用 SPATIAL。它们有独立的适用边界;常规的等值、范围、排序查询,讨论的仍主要是 B+ 树索引。

六、联合索引与最左前缀原则

假设有下面这个联合索引:

1
KEY idx_orders_user_status_created (user_id, status, created_at)

可以把它理解为先按 user_id 排序;同一 user_id 内再按 status 排序;两者都相同的记录再按 created_at 排序。

查询条件 能否有效利用该索引 原因
WHERE user_id = ? 可以 命中最左列
WHERE user_id = ? AND status = ? 可以 连续命中前两列
WHERE user_id = ? AND status = ? AND created_at >= ? 可以 连续命中三列
WHERE status = ? 通常不理想 缺少最左列 user_id
WHERE created_at >= ? 通常不理想 缺少前面的列

这就是常说的最左前缀原则:联合索引需要从最左边的列开始,按连续前缀使用。

一个常见误解:WHERE 条件的书写顺序不等于索引顺序

下面两条 SQL 的优化器通常会得到相同效果:

1
2
WHERE user_id = 10086 AND status = 2
WHERE status = 2 AND user_id = 10086

MySQL 优化器会重排可交换的条件。真正重要的是索引定义里的列顺序,而非你在 SQL 中把哪个条件写在前面。

联合索引如何确定列顺序?

没有放之四海皆准的口诀,应结合真实查询设计。一个实用起点是:

  1. 优先考虑高频查询的筛选条件;
  2. 等值匹配列通常放在范围条件前;
  3. 结合 ORDER BYGROUP BY 看能否避免额外排序;
  4. 让一个索引服务一组相似查询,而不是为每条 SQL 机械建一个索引;
  5. 通过 EXPLAIN 和真实数据分布验证,而非只凭“区分度高的列在前”。

例如“查询某用户某状态下的最近订单”常见写法是:

1
2
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at DESC);

在支持降序索引的 MySQL 版本中,这能更贴合倒序读取;即使不显式声明 DESC,也应实际验证执行计划是否消除了额外排序。

七、覆盖索引:为什么 SELECT * 往往不是好习惯

如果查询所需的列全部存在于某个二级索引中,MySQL 可以只扫描索引而无需回表,这叫覆盖索引

仍使用索引 (user_id, status, created_at)

1
2
3
SELECT id, created_at
FROM orders
WHERE user_id = 10086 AND status = 2;

id 是二级索引叶子节点中天然携带的主键,created_at 也在索引内,因此这条查询有机会只读二级索引。EXPLAINExtra 常会显示 Using index

反过来:

1
2
3
SELECT *
FROM orders
WHERE user_id = 10086 AND status = 2;

SELECT * 很可能要求读取索引中不存在的列,从而产生回表。它还会增大网络传输、让字段变更隐式影响接口返回。因此,生产代码应尽量显式选择真正需要的字段。

不要为了覆盖每一种查询而把大量列塞进联合索引。索引越宽,占用空间越大、写入越慢、缓存命中率越低。覆盖索引是有针对性的优化,而不是默认策略。

八、索引失效的常见原因

“索引失效”并不总是索引真的坏了。更准确地说,是当前 SQL 不能高效地利用索引顺序,或优化器估算全表扫描成本更低。

1. 对索引列做函数或表达式计算

1
2
3
4
5
6
-- 不理想:需要对每一行计算 YEAR
WHERE YEAR(created_at) = 2026

-- 推荐:保留列原样参与比较
WHERE created_at >= '2026-01-01'
AND created_at < '2027-01-01'

同理,WHERE DATE(created_at) = '2026-07-10'WHERE price * 100 > 999 都可能难以使用普通索引。可改写查询,或在合适版本使用函数索引/生成列后再建立索引。

2. 模糊查询以通配符开头

1
WHERE name LIKE '%张%'

B+ 树按前缀有序,%张 前面没有确定前缀,无法定位起点。LIKE '张%' 则通常可以利用索引。真正的全文搜索需求,应考虑全文索引或搜索引擎,而不是期望普通索引解决任意包含匹配。

3. 隐式类型转换

1
2
-- phone 是 VARCHAR,但传入未加引号的数字
WHERE phone = 13800138000

当列类型与参数类型不一致,MySQL 可能对列进行转换,进而影响索引使用,也可能带来意外匹配。应用层应让绑定参数与列类型一致:

1
WHERE phone = '13800138000'

4. OR!=NOT IN 不一定不能用索引,但要警惕

这类条件可能让选择性变差或让优化器选择其他计划。例如 status != 2 通常会匹配绝大多数行,走索引再回表未必比扫描表更快。遇到慢查询时应看实际执行计划,不要机械地认为“出现 OR 就一定不能用索引”。

5. 范围条件后面的列不一定还能用于精确定位

1
2
3
WHERE user_id = 10086
AND created_at >= '2026-07-01'
AND status = 2

对索引 (user_id, created_at, status) 来说,user_id 可等值定位,created_at 可做范围扫描;范围后的 status 是否还能继续参与索引过滤,取决于版本、索引条件下推等优化,但通常不能再像连续等值列那样缩小 B+ 树的定位范围。索引列顺序应围绕查询模式慎重安排。

九、用 EXPLAIN 看懂一条查询

优化索引前后都应该执行 EXPLAIN。例如:

1
2
3
4
5
6
EXPLAIN
SELECT id, total_amount, created_at
FROM orders
WHERE user_id = 10086 AND status = 2
ORDER BY created_at DESC
LIMIT 20;

常见字段可以这样理解:

字段 关注点
type 访问方式。consteq_refrefrange 通常优于 ALL;但不能只看这一列。
possible_keys 优化器认为理论上可选的索引。
key 实际选用的索引;为 NULL 不一定是错误,但值得分析。
key_len 实际使用的索引长度,可辅助判断联合索引利用到了哪些列。
rows 预估需要检查的行数,越少通常越好。
Extra Using index 表示可能覆盖索引;Using filesortUsing temporary 常提示排序或分组有优化空间。

一个健康的执行计划不等于必须“所有指标完美”。例如查询本来就需要返回全表大部分数据时,ALL 可能是合理选择;强行走低选择性索引反而更慢。优化的目标是降低整体成本,而不是追求某个字段看起来漂亮。

在 MySQL 8.0 中,必要时可进一步使用:

1
2
EXPLAIN ANALYZE
SELECT ...;

它会执行查询并给出实际耗时与行数,能帮助发现“估算与现实不一致”的情况。在线上使用前要注意:它不是纯分析命令,复杂查询本身会真实执行。

十、从慢查询到索引优化:一个完整例子

假设运营后台有一个订单列表:

1
2
3
4
5
6
7
SELECT id, user_id, total_amount, created_at
FROM orders
WHERE status = 2
AND created_at >= '2026-07-01'
AND created_at < '2026-08-01'
ORDER BY created_at DESC
LIMIT 50;

如果这是一条高频查询,可考虑:

1
2
CREATE INDEX idx_orders_status_created
ON orders(status, created_at DESC);

原因是:

  1. status 是等值条件,先缩小到已支付订单;
  2. created_at 用于时间范围,并承担排序;
  3. id 作为主键会包含在二级索引叶子节点中;
  4. 若还需 user_idtotal_amount,是否把它们加入索引,要根据数据量、写入频率和是否值得避免回表判断。

如果 status = 2 占表中 95% 的记录,单独使用 status 的选择性很差;但与时间范围组合后,联合索引仍可能有价值。反之,如果查询时间范围很宽、又需要返回大量行,优化器也可能认为扫描更便宜。结论必须由数据和 EXPLAIN 验证。

十一、索引设计的常见误区

1. “每个 WHERE 字段都建一个单列索引”

这会导致索引数量膨胀。MySQL 有时能做索引合并(Index Merge),但它通常不如一个合适的联合索引高效,还会增加写入负担。索引应围绕高频 SQL 的过滤、排序、分组与关联组合来设计。

2. “区分度越高的字段一定放最前面”

区分度很重要,但不是唯一标准。若查询固定为 WHERE tenant_id = ? AND created_at BETWEEN ...,把租户隔离列放在前面常常更符合访问模式,即使它的区分度不如其他列。等值、范围、排序、查询频率和数据分布都要综合考虑。

3. “加索引只影响读,不影响写”

新增索引后,每次插入都要写入索引记录,更新索引列时还可能删除旧索引项、插入新索引项。高写入表上的冗余索引,会直接影响吞吐、磁盘空间和备份恢复时间。

4. “有主键就不需要其他索引”

主键只擅长按主键查询。按邮箱查用户、按用户查订单、按时间筛选日志,都需要匹配业务访问路径的索引。

5. “强制索引总能解决问题”

FORCE INDEX 是诊断或特殊场景下的工具,不应成为常规修复方式。它把执行计划绑定到当前数据分布;数据量和统计信息变化后,今天的最优选择可能成为明天的负担。应先检查 SQL、索引设计、统计信息和返回行数。

十二、上线与维护建议

索引优化不仅是写一条 CREATE INDEX,还应考虑变更风险:

  • 在测试或预发环境用接近生产的数据验证执行计划和耗时;
  • 大表加索引可能占用较长时间和额外资源,评估 MySQL 版本的在线 DDL 能力与锁影响;
  • 保留慢查询日志,按执行次数、平均耗时、扫描行数排序处理;
  • 定期检查重复索引、冗余索引和长期未使用索引,但删除前必须确认没有隐藏查询依赖;
  • 对分页列表优先考虑“基于游标/最后一条记录”的翻页,避免深分页的 LIMIT 100000, 20

深分页之所以慢,是因为数据库即使最终只返回 20 行,也可能需要定位并跳过前面的十万行。对于按时间倒序的列表,更可控的方式是携带上一页最后一条记录的 (created_at, id) 作为游标:

1
2
3
4
5
6
SELECT id, user_id, created_at
FROM orders
WHERE user_id = 10086
AND (created_at, id) < ('2026-07-10 10:30:00', 9527)
ORDER BY created_at DESC, id DESC
LIMIT 20;

相应索引应与排序和筛选字段匹配,例如 (user_id, created_at, id)

结语

索引之所以快,不是因为它让数据库“更努力地计算”,而是因为它让数据库“不必看那么多无关数据”。B+ 树降低定位成本,联合索引匹配真实访问路径,覆盖索引减少回表;而 EXPLAIN 则帮助我们用证据而不是直觉验证优化结果。

最实用的工作方式是:先找出真实慢 SQL,再理解其筛选、排序和返回字段,然后设计尽量小且贴合查询的索引,最后用执行计划和真实数据复核。索引设计从来不是一次性的背诵题,而是持续围绕业务访问模式做出的工程权衡。