Mysql 各种索引详解和使用指南
索引是一种数据结构,用于帮助我们在大量数据中快速定位到我们想要查找的数据,类似于书籍的目录
索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录
以下以 MySQL 8 为例说明:
1 | -- 查看表索引 |
索引详解
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
不同的存储引擎索引的结构也有区别:
-
InnoDB 存储引擎只有一个 .ibd 文件,涉及到事务的细粒度控制和并发安全考虑。
-
MyISAM 存储引擎存在 .MYI 文件中。所以 B+ 树索引的叶子节点并不存储数据,而是存储.MYD地址。
添加索引后 .MYI 文件增大,可见索引是单独存储的
null 值索引可以重复,在mysql中,不算为值,跟空值有区别
1 | # 查看mysql中索引使用的统计情况 |
优点:大大提高了查询速度。
缺点:降低更新表的速度,如对表进行INSERT、UPDATE和DELETE时,MySQL不仅要保存数据,还要保存一下索引文件。且建立索引会占用磁盘空间。
以1000条数据为例,占用空间比对图:
![]()
添加一个组合索引后,磁盘占用多了越160 KB
![]()
在组合索引中减少字段,磁盘占用页相应减少,字段值越大占用空间越大。
![]()
提示:在删除索引后,空间不会立刻被回收,需要执行optimize、repair或recreate后才会立刻回收
索引类型
-
B+ 树索引:是一种平衡多路查找树,所有数据记录都存储在叶子节点,非叶子节点只存索引键值和指向下一级节点的指针。
-
Hash 索引 :基于哈希表实现,对索引列计算哈希值,直接定位到数据位置。
-
单列索引:即一个索引只包含单个列,一个表可以有多个单列索引
-
组合索引:即一个索引包含多个列
-
聚簇索引(聚集索引、主键索引):以 InnoDB 作为存储引擎的表,表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。
-
非聚簇索引(辅助索引或二级索引):以主键以外的列值作为键值构建的 B+ 树索引
B+ 树索引
B+Tree 是一种平衡多路查找树,所有数据记录都存储在叶子节点,非叶子节点只保存索引键值和指向下一级节点的指针。
叶子节点之间通过指针连接,形成有序链表,支持高效的范围查询(如 BETWEEN、>、<)和排序(ORDER BY)。
支持等值查询(
=)、范围查询、排序。查询复杂度为 O(log n)。
InnoDB 的主键索引(聚簇索引)就是 B+Tree。
B树特征
每个节点都存储key和data。
所有节点带有指向记录的指针,在内部节点出现的索引项不会再出现在叶子节点中。
所有叶子节点指针为null,都是没有通过指针连接的。
对于在内部节点的数据,可直接得到,不必根据叶子节点来定位。
B+树特征
1.单一节点存储更多的元素,使得查询的IO次数更少。(树的高度较低)
2.所有查询都要查找到叶子节点,查询性能稳定。(非叶子节点存储索引,叶子节点存储数据)
3.所有叶子节点形成有序链表,便于范围查询。(叶子节点是链表结构(顺序连接存储数据))
B+树优势
非叶子节点只存储索引数据。所以每层能存更多索引数据(默认16K,通过
show variables like 'innodb_page_size'命令查询),每次IO加载一层16K数据做索引筛选,筛选完成继续IO第二层叶子节点为有序的双向链表。当IO定位到叶子节点数据时,每次加载一页数据,当需要继续加载下一页数据时,无序再IO。
三层能存储的数据量(int型的id为例,占8bit,指针占6bit,默认每页16k):
16 * 1024 / (6 + 8) = 1170, 表示每页存的索引数量
1170 * 1170 * 16 = 438400512 表示三层树可以存储的数据量(两层索引+一层数据,叶子节点的每个节点存储16k)一个千万量级,且存储引擎是MyISAM或者InnoDB的表,其索引树的高度在3~5之间。取决于索引占的大小
Hash 索引
基于哈希表实现,对索引列计算哈希值,直接定位到数据位置。
InnoDB中采用除法散列函数,冲突机制采用链接法(类似于Map集合,只是没有红黑树)。全表扫描方式
Hash索引是将索引键通过Hash运算之后,将 Hash运算结果的Hash值和所对应的行指针信息存放于一个Hash表中,由于不同索引键存在相同Hash值,所以即使取满足某个Hash键值的数据的记录条数,也无法从Hash索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。
只支持等值查询(
=、IN)。不支持范围查询、排序、前缀匹配(如
LIKE 'abc%')。优化器不能使用 HASH 索引来加速 ORDER BY 操作。
查询速度极快(O(1)),但冲突处理和哈希函数可能带来开销,所以不适合高重复值的列(如性别、状态等)。
和 BTree 的区别
-
Hash索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以Hash索引的查询效率要远高于B-Tree索引。
-
哈希索引只支持等值比较查询(=、 IN 、<=>),不支持任何范围查询(因为经过Hash算法处理之后的Hash值大小关系,并不能保证和Hash运算前完全一样);
-
Hash索引无法避免数据排序操作;
-
Hash索引不能利用部分索引键查询(不支持多列联合索引的最左匹配规则 )
-
Hash索引需要全表扫描查询
-
如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题
全文索引
全文索引(FULLTEXT),是基于倒排索引(Inverted Index),将文档中每个词映射到包含该词的文档列表。
用于对大文本字段(如 TEXT、VARCHAR) 进行关键词搜索。适用 InnoDB(MySQL 5.6+)、MyISAM 存储引擎
支持自然语言搜索、布尔搜索。
不支持普通
WHERE条件(如LIKE)。
使用方式
1 | # 为 users 的 remarks 字段场景 FULLTEXT 索引 |
会检索到如下数据,如果将检索换为 ‘备注’,则不会被检索到:
![]()
MySQL 原生全文索引对中文支持极差,因为中文没有天然的空格分隔,且每个汉字都是“字母数字字符”。
MySQL 不会按语义分词,如果使用
utf8mb4字符集,每个汉字被视为一个独立的“词”。但由于 最小词长限制(默认 ≥3),单个汉字长度为 1,会被直接丢弃。所以最终就根据符号分隔了,没有语义检索效果。
分词原理和规则:
在建立和查询时会进行分词,但其分词机制相对简单,不依赖外部分词器(如中文常用的 jieba、IK 等),而是基于预定义的分隔符和停用词规则进行处理。
MySQL 全文索引的分词主要依据以下规则:
-
以“非字母数字字符”作为分隔符
-
默认将字母、数字、下划线(_)和美元符号($) 视为“词字符”
-
其他字符(如空格、标点、中文标点、换行符等)被当作分隔符,用于切分词语
Hello, world! How are you? 会被分为:
['Hello', 'world', 'How', 'are', 'you']
-
-
最小词长限制
-
MyISAM 引擎:由系统变量
ft_min_word_len控制(默认为 4)。 -
InnoDB 引擎:由
innodb_ft_min_token_size控制(默认为 3)。小于该长度的词会被直接忽略,不会被索引。如果
ft_min_word_len=4,则 “the”、“a”、“is” 等短词不会被索引。修改该参数后,必须重建全文索引才能生效。
-
-
停用词过滤
- MySQL 内置一个停用词列表(如 “the”, “and”, “or”, “is” 等常见无意义词),这些词即使长度达标,也不会被索引。
- 默认停用词表包含约 36 个英文单词(InnoDB)或 700+ 个(MyISAM)。可通过自定义停用词表覆盖默认行为。
如何让 MySQL 全文索引支持中文?
预处理文本,人工插入分隔符:在应用层对中文进行分词(如用 jieba),然后用空格或特殊符号连接
降低最小词长 + 接受单字匹配:设置
innodb_ft_min_token_size = 1,这样就能搜单字,但无法理解词组语义
空间索引
空间索引(SPATIAL)基于 R-Tree(一种多维空间索引结构)。用于地理空间数据类型(如 POINT、GEOMETRY)。
适用 MyISAM、InnoDB(MySQL 5.7+)存储引擎。要求列必须为 NOT NULL,且使用空间数据类型。
聚簇/主键索引
每张表只能拥有一个聚簇索引。可以存在多个 null
聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分;
聚集规则:有主键则定义主键索引为聚集索引;没有主键则选第一个不允许为NULL的唯一索引;还没有就使用innodb的内置rowid为聚集索引。 主键如果更改代价较高,故建表时要考虑自增ID不能频繁update这点,且最好有顺序(数字)。
优点:
- 数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,从聚簇索引中获取数据更快
- 对主键的排序查找和范围查找非常快
- 叶子节点存储全部数据(以)
缺点:
插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键
更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,一般定义主键为不可更改
二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。
查找原理:
1 | select * from user where id >= 18 and id < 40 # 以主键id范围查找 |
(注:一般数据库默认都会为主键生成聚簇索引,一般根节点都是常驻内存的,也就是说页 1 已经在内存中)
首先 找到 id=18 的键值,对应指针为p2,定位到页3。
将页3数据读入内存,找到索引键值18,拿到对应的指针,定位到页8(第一次IO)。
将页8数据读入内存,找到索引键值18,拿到符合的数据。(第二次IO,叶子节点数据都是链表顺序连接的)
将页8中数据遍历查找符合条件的。
根据页8的指针,读取页9数据到内存,继续遍历符合的数据,以此类推(第三次IO)。
最终我们找到满足条件的所有数据,总共 12 条记录:
(18,kl), (19,kl), (22,hj), (24,io), (25,vg) , (29,jk), (31,jk) , (33,rt) , (34,ty) , (35,yu) , (37,rt) , (39,rt) 。
非聚簇/非主键索引
又称辅助索引、二级索引、非聚集,是除主键以外的其他索引。
在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找(回表)。辅助索引叶子节点存储的不再是行的物理位置,而是主键值。
Innodb辅助索引的叶子节点并不包含行记录的全部数据,叶子节点除了包含键值外,还包含了相应行数据的聚簇索引键。以便于拿到该键值二次查询聚簇索引。
查找原理
通过辅助索引首先找到的是主键值,再通过主键值找到数据行的数据页,再通过数据页中的Page Directory找到数据行。
在叶子节点中,不再存储所有的数据了,存储的是键值和主键,如,叶子节点中的 x-y(索引键值 - 主键值)。
查找的流程跟聚集索引一样,最终找到主键后,需要再到聚集索引中查找具体对应的数据信息,此时又回到了聚集索引的查找流程。
组合索引
使用组合索引,需要遵守最左前缀原则:查询条件必须从最左边的列开始,才能有效利用索引。
例如:索引 (a, b, c) 可用于 WHERE a=1 AND b=2,但不能用于 WHERE b=2 或 WHERE c=3。
但是在 Mysql 8中,在优化器做了大量优化,在符合场景的数据也可能会用到索引,具体参考:MySQL 8.0 新增功能函数和使用案例
where子句中使用最频繁的一列放在最左边,因为MySQL索引查询会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
将离散度高的列放前面,可以过滤更多的数据,使得扫描行数更少,过滤掉的数据更多。
覆盖索引
如果查询的所有列都在索引中,无需回表查询数据行,性能极高(从索引中就能够取得所需数据,不必从数据表中读取)。
SELECT id from orders WHERE status = 'PAID' – id 在索引中,无需再到叶子结点查询数据节点,称之为实现了索引覆盖。
避免了回表操作,减少I/O提高效率。MySQL 只能使用B+Tree索引做覆盖索引,因为只有B+树能存储索引列值。
辅助索引不包含行数据的所有信息,故其大小远小于聚簇索引,因此可以减少大量的IO操作。
索引覆盖只需要扫描一次索引树,不需要回表扫描聚簇索引树,所以性能比回表查询要高。
索引中列值是按顺序存储的,索引覆盖能避免范围查询回表带来的大量随机IO操作。
判断一条语句是否用到索引覆盖: explain 命令中的 Extra列中包含 Using index 就表示使用到了索引覆盖
索引下推
处理包含过滤条件的查询语句。是MySQL5.6推出来的一个查询优化方案,主要是减少数据库中不必要的数据读取和计算。
作用:本该在 server 层进行筛选的条件,下推到存储引擎层进行筛选判断,能有效减少回表(下推的前提是查询条件都是索引列)。
原理:在使用索引进行查询时,将查询的过滤条件也应用到索引查找过程中,以减少需要读取和处理的数据量,从而提高查询性能。
1 | # 默认情况下,索引下推处于启用状态。 |
所以,应该尽可能的建立联合索引,而不是独立的索引。既可以节省空间,又可以提高查询效率(索引覆盖、索引下推)。
示例:
select * from t_user where name like 'L%' and age = 17;其中name 和age为一个联合索引不用索引下推的执行过程:
第一步:利用索引找出name带’L’的数据行:LiLei、Lili、Lisa、Lucy 这四条索引数据第二步:再根据这四条索引数据 id 值,逐一进行回表扫描,从聚簇索引中找到相应的行数据,将找到的行数据返回给 server 层。第三步:在server层判断age = 17,进行筛选,最终只留下 Lucy 用户的数据信息。第四步:将符合条件的索引对应的 id 进行回表扫描,最终将找到的行数据返回给 server 层。
用索引下推的执行过程:
第一步:利用索引找出name带’L’的数据行:LiLei、Lili、Lisa、Lucy 这四条索引数据第二步:根据 age = 17 这个条件,对四条索引数据进行判断筛选,最终只留下 Lucy 用户的数据信息。(注意:这一步不是直接进行回表操作,而是根据 age = 17 这个条件,对四条索引数据进行判断筛选)第三步:将符合条件的索引对应的 id 进行回表扫描,最终将找到的行数据返回给 server 层。
8.0 索引优化
mysql 8 对索引做了大量优化,具体参考:MySQL 8.0 新增功能函数和使用案例
-
索引跳跃扫描( index skip scan,针对最左匹配原则的优化)
-
隐藏索引(新增)
-
降序索引(新增)
-
函数索引(新增)
规则与失效
失效场景
什么情况下索引不会被命中
查询条件没有使用索引列
where条件带有 or ,即使其中有条件带索引也不会使用(想让索引生效,只能将or条件中的每个列都加上索引,如果OR的其中一个条件没有建立索引,则都不会使用索引,建议使用 union ,拼接多个查询语句)。
like 以%开头,索引不会命中(除非只查询索引列),如:“%name”。
范围查询条件右边的列索引失效,如:age > 20 and name = ‘test’
不等于(!= 或者<>)索引失效(8.0 版本以后支持)
类型转换:如果列类型是字符串,一定要在条件中将数据使用引号引用起来,否则不使用索引。
函数计算:查询条件中,在索引列上使用函数(+, - ,*,/), 这种情况下需建立函数索引。
采用 not in, not exist (8.0 版本以后支持,以前不会命中,采用的也是当前字段的索引)。
采用 is not null (8.0 版本以后支持,以前不会命中,采用的也是当前字段的索引)。
散列小被优化:索引列的散列较小,如,tinyint,bool等,选择和查询的筛选后的数据量过大,经过优化器后可能会全表扫描。
选择与规则
是否需要建立索引、值得建立索引?哪些字段适合建立索引?
MySQL是没有计算索引选择性的,只是预测逻辑IO操作的数量,因为索引不止消耗磁盘空间,也会耗费性能,每次的DDL操作也会同时更新索引。因此对于MySQL索引要慎重选择。
索引选择性占比:结果个数与总个数的比值,(x / sum) > 20。
一般来说,如果选择性超过 20% 那么全表扫描比使用索引性能更优。
-
选择高区分度的列建索引(如用户ID,而非性别)。
-
避免在索引列上使用函数或表达式,如
WHERE YEAR(create_time) = 2024会导致索引失效。 -
合理使用组合索引,遵循最左前缀原则。
-
监控索引使用情况:通过
EXPLAIN分析执行计划。 -
避免过度索引:索引会占用空间,并降低
INSERT/UPDATE/DELETE性能。
1 | 计算公式 |
不适合建立索引的列:
tinyint 或 用以标致类型的int列。比如用以保存性别,选择性过大,最小都是超过25%了,因此没有设置索引的必要。
更新频繁的字段。
不会出现在where子句中的字段。
数据可能为null或空或0的字段
存储占用过大的,如:text,json
适合建立索引的列:
字段离散度越高,越适合选作索引的关键字。如主键和唯一字段(主键索引自动创建)。
很少更新的字段(因为也会牵连更新索引)。
占用存储空间少的字段。
存储空间固定的字段。
Where子句中经常使用的字段,分组字段、排序字段和多表连接字段(外键索引自动创建)。
尽量使用联合索引(前缀)
最左匹配规则(联合索引)
最左优先,以最左边的为起点任何连续的索引都能匹配上。右边索引需要依赖左表索引的执行结果,否则数据无法正常查询。因为索引结构存储是顺序存储的,先根据最左边索引排序,再依次根据后面排序。
同时遇到范围查询(>、<、between、like)就会停止匹配。
所以,将范围性和模糊等不确定性查询条件,放置靠后位置。 如: where a = 1 and d in (2,3) and c > 4 and b like 5
1 | # 8.0之前版本: |
EXPLAIN 详解
EXPLAIN 是分析和优化 SQL 查询性能的重要工具,以下为EXPLAIN 输出中各列的含义,带***最重要,**次之:
type(访问类型 ***)
按性能从优到劣排序:
-
最优级别
-
system:表只有一行记录(系统表)
-
const:通过主键或唯一索引一次就找到
SELECT * FROM users WHERE id = 1;
-
-
优秀级别
-
eq_ref:唯一索引扫描,通常用于多表 JOIN 之间的外键索引
SELECT * FROM users u JOIN orders o ON u.id = o.user_id;
-
-
良好级别
-
ref:非唯一索引扫描,返回匹配某个值的所有行
EXPLAIN SELECT * FROM users WHERE phone_number = '14524043463' -
fulltext:全文索引扫描
-
ref_or_null:类似 ref,但包含 NULL 值搜索
-
-
一般级别
-
range:索引范围扫描(BETWEEN、IN、>、< 等)
EXPLAIN SELECT * FROM users WHERE id BETWEEN 1997867665955237888 AND 1997869822548984078;
-
-
需要优化
-
index:全索引扫描(读取整个索引)
1
2
3EXPLAIN SELECT id FROM users WHERE MOD(id,5)=0 or MOD(id,6)=0 limit 100, 1000
# 可以优化,从 index -> range:
EXPLAIN SELECT id FROM users WHERE id > 1997869822548984078 AND (MOD(id,5)=0 or MOD(id,6)=0) limit 100, 1000 -
ALL:全表扫描(不使用索引,性能最差,需要优化)
1
2
3
4# 改为这样,从 index -> ALL
EXPLAIN SELECT * FROM users WHERE MOD(id,5)=0 or MOD(id,6)=0 limit 100, 1000
# 可以优化,从 index -> range:
EXPLAIN SELECT * FROM users WHERE id > 1997869 AND (MOD(id,5)=0 or MOD(id,6)=0) limit 100, 1000
-
Extra(额外信息 ***)
提供额外的执行信息:
-
Using index:覆盖索引直接返回结果id,无需回表其他非索引字段。
-
Using index condition:索引条件下推(ICP), 将 WHERE 条件中索引列的过滤下推到存储引擎层执行。可以减少回表次数,提高查询性能
需要关注和优化的问题:
-
Using where:在存储引擎检索后再过滤相关数据
-
Using temporary:使用临时表(可能需要优化)
优化排序后
-
Using filesort:使用文件排序(需要优化)
-
Using join buffer:使用连接缓冲区
多表连接查询,如果type = ALL ,这是因为相关表缺少外键连接索引,则使用连接缓冲区。
有外键索引的 type = ref,并显示具体的索引名信息。
ref(索引引用 **)
显示索引的哪一列被使用
-
const:常量
-
列名:使用的列
-
func:函数
select_type(查询类型 **)
-
SIMPLE:简单 SELECT,不使用 UNION 或子查询
-
PRIMARY:查询中最外层的 SELECT
-
SUBQUERY:子查询中的第一个 SELECT
-
DERIVED:派生表(FROM 子句中的子查询)
-
UNION:UNION 中的第二个或后续 SELECT
-
UNION RESULT:UNION 的结果
-
DEPENDENT SUBQUERY:依赖外部查询的子查询
rows(预估扫描行数 *)
MySQL 预估需要扫描的行数(值越小越好)
filtered(过滤比例 *)
存储引擎层返回的数据在服务层过滤后,剩余的比例(百分比值,越大越好)
id(查询标识符)
SELECT 查询的序列号
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders); 子查询的 id 会不同
-
id 相同:按顺序执行
-
id 不同:id 值越大,优先级越高,越先执行
-
id 为 NULL:表示 UNION 结果或子查询
key(实际用的索引)
查询实际使用的索引。如果为 NULL,表示没有使用索引
key_len(索引长度)
使用的索引字节数。可判断是否使用了索引的全部部分
复合索引 (name, age), 如果只用到 name,key_len 会较短
possible_keys(可能用的索引)
查询可能使用的索引列表,如果为 NULL,考虑添加合适的索引
table(访问的表)
显示查询涉及的表名。可能是实际表名、别名或 <derivedN>、<unionM,N> 等