☰
MySQL索引类型详解:从B+Tree到慢查询优化的实战指南
2026/10/2 14:39:04 网站建设 项目流程

1. 索引类型全景图:先分清分类维度再谈选型

聊 MySQL 索引以前,先要摆正一个观念:网上能搜到“MySQL 索引有哪几种类型”,答案五花八门,普通索引、唯一索引、主键索引、全文索引、哈希索引、联合索引、覆盖索引全混在一起。其实这些名词是从不同维度在描述索引,不是同一级的分类。

从数据结构维度看,MySQL 常见索引有 B+Tree 索引、哈希索引、全文索引(倒排索引)、R-Tree 空间索引;从逻辑功能维度看,有普通索引、唯一索引、主键索引、全文索引、空间索引;从字段个数维度看,有单列索引和联合索引(复合索引);从物理存储维度看,InnoDB 里又分成聚簇索引(主键索引)和二级索引(辅助索引)。

很多人一上来就背“有哪些索引”,结果面试一问“为什么主键索引快、普通索引慢”就卡壳,原因就是没理解这些分类是交叉的。比如主键索引既是逻辑上的唯一索引,又是物理上的聚簇索引,底层结构还是 B+Tree。文章后面我会把所有维度串起来讲清楚,你就不会再懵了。

这篇内容适合三类人:刚学数据库的初学者想建立完整索引知识框架,工作中要被 SQL 慢查询折腾的开发运维,准备面试需要系统性复盘索引原理的候选人。我会结合真实建表场景、Explain 实际结果、以及我这些年踩过的坑来讲,保证你能直接照着用。

2. 索引底层数据结构:为什么偏偏是 B+Tree

2.1 B+Tree 比二叉树、哈希强在哪

数据存储以页为单位。MySQL InnoDB 默认一页是 16KB,磁盘读写的最小单位和内存交换单位都是页。B+Tree 的设计是让非叶子节点只存索引键值和子节点指针,一个 16KB 的页可以放下大量键值。假设一个索引键是 8 字节,页指针是 6 字节,那么一个页大约能存 16 * 1024 / 14 ≈ 1170 个键值对,树的高度通常就 2 到 4 层。换句话说,查找 1000 万行数据,可能只需要 3 到 4 次磁盘 I/O。

对比一下其他结构:

  • 二叉搜索树:数据有序插入时会退化成链表,树高变成 N,查找等于全表扫描。
  • 红黑树:虽然能自平衡,但树高在数据量大时依然有 logN 级别,同样数据量下比 B+Tree 高得多,磁盘 I/O 次数多。
  • 哈希索引:等值查询 O(1),但底层是哈希表,无法范围查询,也无法排序。

B+Tree 还有一个关键特性:叶子节点之间通过双向链表相连。这意味着范围查询比如WHERE age BETWEEN 20 AND 30,找到第一个满足条件的叶子页之后,可以顺着链表往后扫,不需要回溯到父节点重新遍历。这正是数据库里范围查询最频繁的底层支撑。

2.2 聚簇索引和二级索引:回表问题的根源

InnoDB 中主键索引就是聚簇索引,它的叶子节点直接存放整行数据。而二级索引(普通索引、联合索引等)的叶子节点存放的是索引键值 + 主键值。

所以当你用一个普通索引查数据时,过程通常是两步:

  1. 在二级索引的 B+Tree 里定位到对应键值,拿到主键 ID。
  2. 拿这个主键 ID 再去聚簇索引里查一次,取出完整行数据。

这个第二步就叫回表。回表不是必然发生的,如果查询需要的列恰好都包含在索引里,就不需要回表,这叫覆盖索引。理解聚簇索引和二级索引的区别,就能解释很多索引优化技巧的来源。

注意:InnoDB 表没有显式主键时,会找第一个非空的 unique 键作为聚簇索引,都没有就生成一个隐藏的 row_id 作为聚簇索引。所以“没有主键”并不是没有聚簇索引,只是你没看到它而已。

3. 按逻辑功能分类:六种索引实战拆解

3.1 普通索引、唯一索引、主键索引的取舍

普通索引(KEY/INDEX):最基础的索引,不限制键值是否重复,目的就是为了加速查询。建表时写INDEX idx_name (name)就是普通索引。

唯一索引(UNIQUE KEY):在普通索引基础上加了唯一约束,键值不能重复,但允许 NULL,并且 NULL 可以有多个。唯一索引有两个作用:一是保证业务数据的唯一性,比如用户手机号、订单号;二是能帮优化器做更多优化,因为它知道最多只有一条匹配记录,查到就能停。

主键索引(PRIMARY KEY):唯一索引的特殊形式,且不允许 NULL。在 InnoDB 里它还承担聚簇索引的角色。选主键时我非常推荐用自增 ID 或者有序的雪花 ID,因为聚簇索引在插入时是顺序追加的,页分裂概率小,写性能稳定。如果用了 UUID 这种随机字符串当主键,每次插入都可能在不同位置触发页分裂,会产生碎片,写入性能明显下降。

实战建议:普通索引和唯一索引选择上,只要业务上字段需要唯一,就直接建唯一索引,别为了图省事只建普通索引然后在应用层做判断。数据库层的唯一约束是最可靠的,应用层并发判断永远有漏洞。

3.2 联合索引与最左前缀原则

联合索引也叫复合索引,是多个字段构成的索引,比如INDEX idx_name_age (name, age)。很多人以为建了联合索引,等于是给每个字段都建了索引,这个理解是错的。联合索引底层是一棵 B+Tree,排序规则是先比第一个字段,相同的再比第二个字段,依此类推。

所以联合索引能命中哪些查询,取决于最左前缀原则:

  • WHERE name = '张三':命中,使用 name 部分。
  • WHERE name = '张三' AND age = 25:命中,两个字段都用上。
  • WHERE age = 25:无法命中联合索引,因为跳过了第一个字段 name。
  • WHERE name = '张三' AND age > 20 AND city = '北京':name 等值、age 范围能用到,但 city 用不到,因为 age 的范围条件让 name_age 索引在 age 之后无法继续有序定位。

最左前缀原则背后的原因是 B+Tree 本身就是按字段顺序排序的,跳过第一个字段,整棵树找不到一个稳定的入口。

建联合索引的时候,统计上有一个口诀:区分度高的字段放前面,查询频率均等的时候才考虑等值优先。实际上更准确的做法是:经常作为等值查询条件的字段放前面,经常做范围查询的字段放后面,因为范围查询会让后续字段失效。

3.3 覆盖索引:少一次回表就多一份性能

覆盖索引不是一种独立的索引类型,而是一种查询优化的状态:查询语句需要的所有列,都包含在某个二级索引中。此时 MySQL 不需要回表,直接遍历索引就能拿到数据,因为二级索引的 B+Tree 比聚簇索引小,一次查询的 I/O 和 CPU 消耗都更低。

举个例子:

CREATE TABLE user ( id INT PRIMARY KEY, name VARCHAR(50), age INT, email VARCHAR(100), INDEX idx_name_age (name, age) );

如果执行:

SELECT name, age FROM user WHERE name = '张三';

MySQL 在idx_name_age索引里就能直接拿到 name 和 age,不需要回表查聚簇索引。但如果是:

SELECT name, email FROM user WHERE name = '张三';

email 不在idx_name_age中,就需要回表。

这也是为什么很多高性能表设计会用“索引冗余字段”的方式来覆盖高频查询:把查询涉及的列尽量塞进联合索引,让查询变成覆盖索引查询。当然,这要权衡写入代价,索引字段越多,写入越慢,空间越大,不是无限往里塞。

3.4 全文索引与哈希索引适用边界

全文索引用在LIKE '%keyword%'这类模糊匹配场景。InnoDB 的全文索引底层是倒排索引结构,支持自然语言检索和布尔检索。要注意 MySQL 5.7 以后 InnoDB 才默认支持中文全文检索能力,而且必须使用 ngram 分词插件。建法:

CREATE FULLTEXT INDEX ft_idx_content ON article (content) WITH PARSER ngram;

不过说实话,业务里对中文全文搜索,我一般更推荐引入 Elasticsearch 这类专用搜索引擎,MySQL 全文索引适合轻量级、数据量不大、不想额外维护一套搜索组件的场景。

哈希索引在 InnoDB 中默认是不可人工创建的,但 InnoDB 内部有一个自适应哈希索引(Adaptive Hash Index),当某些等值查询被频繁命中,且 B+Tree 访问模式足够规律,InnoDB 会自动为这部分热点页建立哈希索引,加速等值查找。你没法控制它,也不需要控制。MySQL 的 Memory 引擎支持显式创建哈希索引,但它不支持范围查询,加上 Memory 引擎本身有崩溃丢数据风险,生产环境用得很少,了解即可。

3.5 MySQL 8 中创建索引的标准语法

我直接列一下最常用的语法,方便你抄:

-- 普通索引 CREATE INDEX idx_name ON user(name); -- 唯一索引 CREATE UNIQUE INDEX uk_email ON user(email); -- 联合索引 CREATE INDEX idx_name_age ON user(name, age); -- 前缀索引(对字段前N个字符建立索引) CREATE INDEX idx_name_prefix ON user(name(10)); -- 全文索引 CREATE FULLTEXT INDEX ft_content ON article(content) WITH PARSER ngram; -- 删除索引 DROP INDEX idx_name ON user;

前缀索引在字段超长、比如VARCHAR(500)的 URL 或文本场景很有用,能显著减少索引体积,代价是选择性可能下降,需要提前算一下前缀长度对区分度的影响。

4. 使用场景与优缺点:什么情况该建,什么情况别乱建

4.1 各类型索引的场景速查表

索引类型最佳场景优点缺点
主键索引每张表必须有的聚簇索引查询快,数据物理有序随机主键插入会页分裂
唯一索引业务唯一字段,如手机号保证一致性,优化器等值查询写入需唯一校验,稍慢
普通索引高频等值/范围查询字段灵活,成本低无约束能力
联合索引多条件组合查询一次索引服务多个查询字段顺序设计不当容易失效
前缀索引超长字符串字段省空间,减小索引体积可能牺牲区分度,不支持覆盖索引优化
全文索引轻量级全文检索原生SQL即可实现搜索中文分词弱,维护成本高
哈希索引等值查询集中场景极速等值查找范围查找无效,InnoDB中不可控

4.2 索引的隐性代价:写入放大与空间成本

很多新手只知道“查询慢就建索引”,不知道索引是有代价的。每次 INSERT 或 UPDATE 时,不仅要更新聚簇索引,还要同步维护这张表上的所有二级索引。索引越多,写入放大越严重。一个极端例子:一张表只有 3 列,你却建了 5 个索引,每次插入一条数据,MySQL 可能要做多次随机 I/O,性能反而比没索引还差。

所以我的经验是:单表二级索引数量控制在 5 个以内,除非业务非常明确。新项目上线前,用慢查询日志和数据字典分析找出真正被频繁查询的路径,再针对性建索引,而不是每个字段都先加上。

还有一个容易忽视的点:索引也占用磁盘和内存缓冲池空间。InnoDB 的缓存池(innodb_buffer_pool_size)既要缓存数据页也要缓存索引页,索引多了,能缓存的数据页就少了,极端情况下会放大磁盘读。这也是连接池、配置调优时经常被忽略的一环。

4.3 为什么有时候明明有索引,优化器还是不走索引

这是我在实际排查里遇到最多的现象。SQL 里有索引,Explain 却显示type = ALL或者key = NULL。原因基本是这几种:

  1. 查询返回的数据量过大。优化器估算走索引需要大量回表,比如你要查表中 30% 以上的数据,直接扫聚簇索引可能比走二级索引再回表更快,优化器会放弃索引。
  2. 经典索引失效条件:对索引列做函数运算、隐式类型转换、左模糊匹配、OR 条件包含非索引列、联合索引不满足最左前缀等。
  3. 统计信息不准确。MySQL 的基数统计是采样估算的,如果长时间没跑 ANALYZE TABLE,统计可能偏差很大,导致优化器选错执行计划。

基于常见的失效场景,我做了一个速查表,很多 case 我都在真实服务器上验证过:

失效场景示例原因
函数运算WHERE DATE(created_at) = '2024-01-01'对索引列做函数处理后,无法使用原B+Tree有序性
隐式类型转换WHERE phone = 13800138000字符串列与数字比较时,MySQL隐式转为数字,索引失效
前模糊查询WHERE name LIKE '%张%'B+Tree必须从最左侧前缀开始定位
OR 条件WHERE age = 25 OR name = '张三'OR 两边必须都有索引,否则全扫
联合索引跳过前导列WHERE age = 25(索引为 name,age)不满足最左前缀原则
范围查询右侧列WHERE name = '张三' AND age > 20 AND score = 90(索引 name,age,score)范围条件阻断后续字段有序查找

注意:WHERE name = '张三' AND age > 20 AND score = 90其实可以在 MySQL 8.0 的某些场景下利用下推优化继续过滤,但那是索引条件下推(ICP)的机制,跟索引能否定位到点是两码事。ICP 能减少回表次数,但 score 依然不参与索引的键值定位。

5. 实战复盘:从慢 SQL 到一个合理索引设计的完整过程

5.1 用 EXPLAIN 输出的核心列判断索引执行情况

先说怎么验证索引到底走没走对。执行EXPLAIN SELECT ...之后,重点看这几列:

  • type:从好到差依次是 system、const、eq_ref、ref、range、index、ALL。至少要达到 range 或 ref 级别,最差是 ALL 全表扫描。
  • key:实际选择的索引名,NULL 表示没走索引。
  • rows:预估扫描行数,数字越小通常越好,但只是估算。
  • Extra:出现Using filesort说明排序没用上索引,出现Using temporary说明用了临时表,这俩都是性能危险信号。出现Using index则是覆盖索引,表现优秀。

举例说明,我之前优化过一个订单查询接口:

SELECT order_no, user_id, amount, status FROM orders WHERE user_id = 12345 ORDER BY created_at DESC LIMIT 20;

原表只有一个主键索引,Explain 结果是 type=ALL、rows=240万、Extra 里有 Using filesort。这个查询是所有用户都能触发的,频率极高,240 万行扫描加文件排序,接口必然慢。

5.2 联合索引排序优化:减少 filesort 的实操思路

遇到排序配合查询条件,核心优化思路是让 B+Tree 的有序性同时满足 WHERE 的定位和 ORDER BY 的排序。

上面那个订单查询,我建了一个联合索引:

CREATE INDEX idx_user_created ON orders(user_id, created_at);

为什么这么建?因为user_id是等值条件,created_at是排序字段。索引键先按 user_id 排序,再按 created_at 排序,所以定位到 user_id=12345 的记录时,created_at 天然就是按时间排好的,MySQL 直接顺序读前 20 条即可,不再需要 filesort。

再配合把查询改成覆盖索引:

CREATE INDEX idx_user_created_cover ON orders(user_id, created_at, order_no, amount, status);

这样查询需要的所有列都能在二级索引里拿到,连回表都省了。实测优化后,该查询从 280ms 降到 3ms 左右,效果非常明显。

这里有一个取舍:我用了一个包含 5 个字段的联合索引,读写放大是有的,但 orders 表常用的查询就是这套字段组合,写入频率也不算极高,所以性价比很划算。索引设计始终是读写平衡的艺术,没有绝对最优,只有对具体业务最优。

5.3 索引设计前先做这几步排查

我给团队定的索引设计流程是这样,很枯燥但是很有效:

  1. 打开慢查询日志(slow_query_log=ON,long_query_time=1),抓一周的真实慢 SQL。
  2. 把慢 SQL 集合起来,去掉重复,统计每类 SQL 的执行次数和平均耗时。
  3. 对每条核心慢 SQL,先解释 explain 看执行计划,再决定加索引还是改写 SQL。
  4. 加完索引后,在测试环境用生产数据量级验证,对比前后执行计划。
  5. 上线后持续观察慢查询日志,谨防统计信息偏差导致执行计划回退。

6. 常见问题与排查技巧实录

6.1 创建索引很慢,甚至锁表,怎么办

大表加索引时,MySQL 8.0 之前 InnoDB 加索引虽然是 online DDL,但不同阶段仍可能短暂锁表。我的经验是:

  1. 低峰期执行,比如凌晨流量低的时候。
  2. 先用ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE试着指定,允许以可并发读写方式执行。
  3. 如果表非常大,几十亿行那种,一次性建索引可能要跑几个小时,甚至产生主从延迟。更稳妥的方式是先在备库建索引,再切换主从,或者使用 gh-ost、pt-online-schema-change 这类在线变更工具,能极大减少对线上读写的影响。

6.2 为什么删了索引后查询反而变快了

遇到过几次。某张报表查询每次要查大量数据,比如按天统计汇总,走了二级索引后需要回表读取成千上万行,还不如直接扫描聚簇索引顺序读。优化器可能在某个时间点选择了索引,但数据分布变化后,索引路径并不总是最优。

这种情况不能只看单条 SQL,要结合查询返回的数据量占全表的比例来看。如果比例超过 10% 到 20%,直接全表扫描的顺序读取往往更快,这是机械硬盘时代就总结出来的经验,SSD 时代也大致成立,因为顺序 I/O 比随机回表 I/O 永远有优势。

6.3 我的几条避坑总结

  • 不要在区分度低的字段上建单列索引,比如性别、状态。用SELECT COUNT(DISTINCT col) / COUNT(*)算一下区分度,太低就别建。
  • 不要在高频写的表上无限叠加索引,5 个以内差不多是普通业务的安全线。
  • 联合索引字段顺序不要凭感觉,先算区分度,再结合查询条件里是等值还是范围来排。
  • 字符串索引能建前缀索引就建前缀索引,省的空间远超你想象。
  • 每次改完索引后,用ANALYZE TABLE更新统计信息,别等优化器给你惊喜。

我个人在实际排查里最大的体会是:索引优化的瓶颈往往不在索引本身,而在 SQL 写法。很多人写 SQL 习惯对索引列加函数,日期用DATE_FORMAT,字符串用CONCAT,这些写法再好的索引也发挥不出来。所以我遇到慢查询,第一步永远是看 SQL 能不能改写成“对索引友好”的形式,第二步才考虑加索引。索引设计没有银弹,但只要你理解 B+Tree 的有序性、回表机制和最左前缀原则,80% 的慢查询问题都能靠这几个点直接解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询