前言
本章内容章节顺序快速浏览:

索引的初步认识

在了解什么是索引之前,我们先来举个例子:当我们想要查找一本书中的内容时,我们会选择一页页全部翻看;还是选择先通过目录来大致缩小查找范围呢? 毫无疑问:我们的时间是宝贵的,当然选择去查找目录。理解了这个,我们就理解了索引。我们把目录比作索引,当数据库查找具体内容时,就会先通过索引来快速查找到内容。这种核心思想就是:用空间来换取时间,刷力扣的小伙伴肯定不陌生。
通过上一篇我们理解到了MySQL分为Service服务层与存储引擎层。 图解MySQL 执行全链路讲解 我们在这里提到的索引与数据内容,便是存储在这存储引擎中。提到了存储引擎,我们需要对他的认识有:如何存储一你请数据,如何为存储的数据建立索引和如何更新,查询数据等技术的实现方法。MySQL的存储引擎有:'MyISAM,InnoDB、Memory,其中 InnoDB 是在 MySQL 5.5 之后成为默认的存储引擎。
索引的分类
具体的分类细节已经如下分类好了,我们在这里对他们的特点进行讲解:
按数据结构分类
常见的有:B+tree索引、Hash索引、Full-text索引
每种存储引擎支持的索引类型不同,具体见下图:
InnoDB在MySQL 5.5版本后成为了MySQL的默认存储引擎,其中的B+Tree是被采用最多的索引类型。
InnoDB存储引擎创建表规则:
- 如果有主键,就用主键作为key
- 没有主键,用第一个不含NULL值的唯一列作为key
- 如果都没有,InnoDB 将自动生成一个隐式自增 id 列作为ley
我们通过一个例子,来了解一下B+Tree 索引的存储和查询的过程: 先创建一张商品表,id为主键
sql
CREATE TABLE `product` (
`id` int(11) NOT NULL,
`product_no` varchar(20) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`price` decimal(10, 2) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
商品表中,有这些数据:
这些数据,存储在B+Tree索引时是这样的:
B+Tree是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,节点中的数据是按照主键顺序存放的。 每一层父节点的索引值都会出现在下层子节点的索引值中,也就是说,在叶子节点中包括了所有的索引值信息。 每个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点(图中只画出了一个节点)
通过主键查询数据的过程

sql
select * from product where id= 5;
查询过程如下,B+Tree 会自顶向下逐层进行查找:
- 将 5 与根节点的索引数据 (1, 10, 20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1, 4, 7);
- 在第二层的索引数据 (1, 4, 7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4, 5, 6);
- 在叶子节点的索引数据(4, 5, 6)中进行查找,然后我们找到了索引值为 5 的行数据。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
3-4层高度就可以满足B+Tree 存储千万级的数据。所以B+Tree 相较于B树和二叉树来说,最大的优势在于查询效率高
通过二级索引查询商品数据的过程
主键索引的B+Tree 和二级索引的 B+Tree 区别如下:
- 主键索引下,叶子节点中存储的是所有完整数据
- 二级索引下,叶子节点存放的是主键值,不是实际数据。所有要查询两张表,也就是回表操作
建表如下:
我们以为例:
sql
select * from product where product_no = '0002';
具体流程: 先检索二级索引中B+Tree的索引值,找到对应主键值。通过回表操作,再到主键索引中的B+Tree中查询数据信息。也就是说要查两个 B+Tree 才能查到数据。
这里出现了一个特例:覆盖索引 当查询的数据能子啊二级索引的B+Tree里的叶子节点里查询到,这时就不用再查主键索引,例如:
sql
select id from product where product_no = '0002';
这种不需要回表,在二级索引的B+Tree就能查询到结果的过程就叫:覆盖索引
为什么 MySQL InnoDB 选择 B+tree 作为索引的数据结构?
我们来比较一下B+Tree/BTree/二叉树/Hash之间的特点:
- B+Tree vs B Tree B+Tree只在叶子节点存储数据,而B树的非叶子节点也要存储数据,所以B+Tree的单个节点的数据量更小,在相同磁盘I/O次数下,能查询更多的节点。 另外:B+Tree叶子节点用的是双向链表连接,适合MySQL中常见的基于范围的顺序查找。
- B+Tree vs 二叉树 在磁盘中,数据是按页(Page,默认 16KB)读取的。二叉树每个节点只有两个分支,存储千万级数据时,树高可能达到 20-30 层。
代价:每找一个节点就要进行一次磁盘 I/O。查一条数据要读 20 次盘,性能不可接受。
B+Tree 的优势:它是多叉树,每个节点(页)能存上百个索引,千万级数据只需 3-4 层,仅需 3-4 次 I/O。
- B+Tree vs Hash Hash 索引在 WHERE id = 1 时无敌快,但实际业务中充斥着大量的范围查询、排序和模糊匹配(LIKE 'abc%')。
代价:Hash 后的数据是无序的,遇到 id > 100 只能全表扫描。
B+Tree 的优势:天然有序,且叶子节点互联。
按物理存储分类
常见的有:聚簇索引(主键索引)、二级索引(辅助索引)
- 主键索引的B+Tree的叶子节点存放是是实际数据,所有完整的用户记录都存放在叶子节点中
- 二级索引的B+Tree的叶子节点主要存放的是主键值
按字段特性分类
常见的有:主键索引、唯一索引、普通索引、前缀索引
- 主键索引 建立在主键字段上的索引,通常在创建表的时候一起创建,以后在那个表稚嫩那个有一个主键索引,索引列表的只不允许有NULL 创建主键索引的方式如下:
sql
CREATE TABLE table_name (
....
PRIMARY KEY (index_column_1) USING BTREE
);
- 唯一索引 建立在UNIQUE字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,允许有NULL
创建唯一索引的方式如下:
sql
CREATE TABLE table_name (
....
UNIQUE KEY(index_column_1,index_column_2,...)
);
- 普通索引 创建普通索引的方式如下:
sql
CREATE TABLE table_name (
....
INDEX(index_column_1,index_column_2,...)
);
- 前缀索引 前缀索引是指对字符类型的字段的前几个字符创建的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为:char vachar binary varbinary的列上 使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。 创建前缀索引的方式如下:
sql
CREATE TABLE table_name(
column_list
,
INDEX(column_name(length))
);
按字段个数分类
常见的有:单列索引、联合索引
- 建立在单列上的索引称为单列索引:主键索引
- 建立在多列上的索引称为联合索引
联合索引
将多个字段组合成一个索引,该索引就成为联合索引 比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name),创建联合索引的方式如下:
sql
CREATE INDEX index_product_no_name ON product(product_no, name);
联合索引的B+Tree 示意图如下:
用了两个字段的值作为B+Tree的key值。当联合索引查询数据时,先按照product_no 字段比较,当product_no 字段相同时再按name字段比较。 这就是最左前缀匹配原则,按照最左优先的方式进行索引的匹配。
最左前缀匹配原则
假设当前有一个联合索引(a,b,c) 符合最左前缀匹配原则的查询原则:
sql
where a = 1;
where a = 1 and b = 2;
where a = 1 and b = 2 and c = 3;
不符合:
sql
where b = 2;
where c = 3;
where b = 2 and c = 3;
范围查询的特殊规则: 理解范围查询之前,得先搞清楚联合索引的排序规则,联合索引(a,b,c)再B+树中的排序是:先a,若a相同再按b,b相同再按c。
当遇到>,<这种范围查询时,会停止匹配
sql
where a > 1 and b = 2 and c = 3;
所谓的停止匹配:只有a能用上联合索引,b和c不行。因为a经过范围查询筛选后,不同a值之间的b和c时无序的。当a=2与a=3,他们的b值之间没有任何关系。
遇到>=,<=,between这种范围查询,不会停止匹配。因为这些查询包含等值判断
sql
where a >= 1 and b = 2 and c = 3;
可以查询定位到a=1的数据,a=1内部的b和c是有序的。

索引下推
在学习前面的知识后,我们了解了对于联合索引(a,b)执行select * from table where a > 1 and b = 2时,只有a能用到索引。当我们找到满足a>1(a=2)的主键值时,还需去判断其他条件是否满足,这个判断是在哪里进行的呢?
- 在MySQL 5.6之前,只能从主键值开始一个个回表,回到主键索引上找出数据行,再对比b字段值
- 而MySQL 5.6之后引入的索引下推,可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

什么时候需要/不需要创建索引?
索引的优点是极大的加速了查询速度,但是索引也是有缺点的:
- 需要占用物理空间,数量越大,占用空间越大
- 常见索引和维护索引要耗费时间,这种时间随着数据量的增加而增大
- 会降低表的增删改查的效率,因为每次增删改索引,B+树为了维护索引有序性,都需要进行动态维护。
什么时候适用索引
- 字段有唯一性限制的,比如商品编码
- 经常用于where查询条件的字段,能够提高整个表的查询速度
- 经常用于GROUP BY 和 ORDER BY的字段,这样在查询的时候就不需要再去做一次排序。因为在建立索引之后在B+树中的记录都是排序好的
什么时候不需要创建索引
- WHERE 条件,GROUP BY,ORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。
- 字段中存在大量重复数据,不需要创建索引。比如性别字段,只有男女,无论查哪个字段都能得到另一字段数据。并且MySQL还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,他一般会忽略索引,进行全表扫面。
- 表数据很少的时候,不需要创建索引
- 经常更新的字段不需要。因为字段频繁修改,由于要维护B+树的有序性,那么基于需要频繁的重建索引。
优化索引的方式
前缀索引优化

含义:使用某个字段中字符串的前几个字符建立索引。 使用前缀索引可以减少索引字段的大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度
特例: order by 无法使用前缀索引 无法把前缀索引用作覆盖索引
一句话:简写索引字段,只留少部分前缀
覆盖索引优化
含义:SQL中query的所有字段,在索引B+树的叶子节点上都能找得到的哪些索引,从二级索引中查询得到记录,而不需要通过主键索引查询 获得,可以避免回表操作。
例:我们只需要查询商品的名称,价格,有什么方式可以避免回表? 我们可以建立一个联合索引,即商品ID、名称、价格。如果索引中存在这些数据,查询将不会再次检索主键索引,避免回表。
一句话:通过建立联合索引,减少回表操作,达到减少I/O的操作
主键索引最好是自增的

在 InnoDB 中,数据行是存储在聚簇索引(即主键索引)的 B+ 树叶子节点上的。B+ 树要求叶子节点的数据必须是有序的。
自增的优势:
在innodb插入新纪录是,只需要将新纪录加到B+树要求叶子节点的数据必须是有序的。
- 自增主键的优势:B+ 树有序插入 页分裂极少:因为新主键总是比旧主键大,所以总是顺序插入,不会引起页面的频繁分裂。 空间利用率高:每个页面都能填满,索引结构非常紧凑。 性能极佳:磁盘 I/O 是顺序写,效率非常高。
- 非自增主键(如随机 UUID)的劣势:页分裂与随机 I/O 如果主键是随机的(如 UUID),新插入记录的主键值可能位于 B+ 树的任意位置。 频繁页分裂:新记录可能会被插入到一个已经塞满的页面中间。为了腾出空间,InnoDB 必须将该页面分裂成两个,并重新移动数据。 空间碎片:页分裂会导致页面填充率低,索引变得"虚胖"且产生大量碎片。 随机 I/O:为了把数据插入到合适的位置,InnoDB 需要频繁加载和写入磁盘上不连续的页面,随机 I/O 导致性能急剧下降。
一句话:自增主键保证了B+ 树以最顺序、最紧凑、最少磁盘开销的方式增长。
防止索引失效
用上了索引,并不代表索引一定会派上用场。一下是几种常见索引失效的情况:
- 使用左或左右模糊匹配。也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
- 在查询条件中对索引列做计算,函数,类型转换操作。
- 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效
实际情况中,还会出现其他索引失效的场景。我们可以通过查看type字段查看所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
• All(全表扫描); • index(全索引扫描); • range(索引范围扫描); • ref(非唯一索引扫描); • eq_ref(唯一索引扫描); • const(结果只有一条的主键或唯一索引扫描)
all 是最坏的情况,因为采用了全表扫描的方式。index 和 all 差不多,只不过 index 对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。所以,要尽量避免全表扫描和全索引扫描。 range 表示采用了索引范围扫描,一般在 where 子句中使用 < 、>、in、between 等关键词,只检索给定范围的行,属于范围查找。从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。
ref 类型表示采用了非唯一索引,或者是唯一索引的非唯一性前缀,返回数据返回可能是多条。因为虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
eq_ref 类型是使用主键或唯一索引时产生的访问方式,通常使用在多表联查中。比如,对两张表进行联查,关联条件是两张表的 user_id 相等,且 user_id 是唯一索引,那么使用 EXPLAIN 进行执行计划查看的时候,type 就会显示 eq_ref。
const 类型表示使用了主键或者唯一索引与常量值进行比较,比如 select name from product where id=1。
总结
本篇,我们从了解什么是索引开始,再到索引的具体分类:按数据结构分;物理存储分;字段特性分;字段个数分。然后理解了索引不是必须的,究竟什么时候需要,什么时候不需要索引?进一步了解了有什么方式能够对索引进行优化。
希望能够帮助大家对索引有更好的了解,感谢感谢。