在 MySQL 中,索引是一种帮助存储引擎快速获取数据的数据结构,形象的说就是索引是数据的目录。它一般是以包含索引键值和一个指向索引键值对应数据记录物理地址的指针的节点的集合的清单的形式存在。通过使用索引, MySQL 可以在不需要扫描整个表的情况下快速找到与查询条件匹配的记录。
索引的目的在于提高查询效率,可以类比字典,比如当我们要查 “mysql” 这个单词,我们肯定需要定位到 ‘m’ 字母,然后从下往下找到 ‘y’ 字母,再找到剩下的 “sql”。如果没有索引,那么我们可能需要把所有单词看一遍才能找到想要的。
在 MySQL 中,索引是一种帮助存储引擎快速获取数据的数据结构,形象的说就是索引是数据的目录。它一般是以包含索引键值和一个指向索引键值对应数据记录物理地址的指针的节点的集合的清单的形式存在。通过使用索引, MySQL 可以在不需要扫描整个表的情况下快速找到与查询条件匹配的记录。
在 MySQL 索引设计中,核心目标是有效平衡数据检索的速度与存储效率。就像图书目录帮助我们快速找到特定章节,MySQL索引使数据库能迅速定位数据。但数据库的挑战更为复杂,因为它需要处理各种查询类型,如等值查询、范围查询和模糊查询等。
为了有效平衡数据检索速度与存储效率,MySQL 通过其 InnoDB 存储引擎采取了以下具体措施:
MySQL 通过上述措施,在保证数据检索速度的同时,优化存储效率和减少磁盘I/O成本,实现了数据管理的高效平衡。这些技术不仅提高了查询性能,也保证了数据的安全性和一致性。
从数据结构的角度来看,MySQL 常见索引有 B+Tree 索引、HASH 索引、Full-Text 索引。
InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引擎采用最多的索引类型。
MySQL 索引的实现采用的是 B+ 树,B+ 树是 B- 树的变体,也是一棵多路搜索树。B+ 树相较于 B- 树最主要的特点是:数据只出现在叶子节点;所有叶子节点增加了一个链指针。
在 B+ Tree 中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储 key 值信息,这样可以大大加大每个节点存储的 key 值数量,降低 B+ 树的高度。
B+ 树的叶子节点上有指针进行相连,因此在做数据遍历的时候,只需要对叶子节点进行遍历即可,这个特性使得 B+ 树非常适合做范围查询。
默认情况下,如果不指定索引类型,MySQL 将创建 B+ 树索引。下面显示了基于表的存储引擎允许的索引类型:
存储引擎 | 允许的索引类型 |
---|---|
InnoDB | BTREE |
MyISAM | BTREE |
MEMORY/HEAP | HASH, BTREE |
为什么 MySQL InnoDB 选择 B+tree 作为索引的数据结构呢 ?
MySQL 选择使用 B+树作为索引结构,主要是因为 B+树提供了许多适合数据库索引的优点:
Hash 在做等值查询的时候效率很高,搜索时间复杂度为 O(1)。但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。
对于有 N 个叶子节点的 B+Tree,其搜索复杂度为 O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。
在实际的应用当中, d 值是大于 100 的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。
而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。
并且如果二叉树受插入顺序影响特殊化为一个链表,相当于全表扫描。
平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。但是平衡二叉树可是每个节点只存储一个键值和数据的,并且每次新增数据,平衡二叉树都会进行大量的平衡判断。
B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。
另外,B+Tree 叶子节点采用的是双向链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。
记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。
因此,InnoDB 的数据是按「数据页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。
数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16KB,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。
数据页包括七个部分,结构如下图:
这 7 个部分的作用如下:
在 File Header 中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下图所示:
采用链表的结构是让数据页之间不需要是物理上的连续的,而是逻辑上的连续。
数据页中的记录按照「主键」顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。
因此,数据页中有一个页目录,起到记录的索引作用,就像我们书那样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应的章节的页数,而数据页中的页目录就是为了能快速找到记录。
那 InnoDB 是如何给记录创建页目录的呢?页目录与记录的关系如下图:
页目录创建的过程如下:
从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。
聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据 ;
InnoDB 里的 B+ 树中的每个节点都是一个数据页,结构示意图如下:
以上图为例子,实现快速查找主键为 6 的记录,:
可以看到,在定位记录所在哪一个页时,也是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。
因为表的数据都是存放在聚簇索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚簇索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个。
InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:
一张表只能有一个聚簇索引,那为了实现非主键字段的快速搜索,就引出了二级索引(非聚簇索引/辅助索引),它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据。
二级索引的 B+ 树如下图,数据部分为主键值:
因此,如果某个查询语句使用了二级索引,但是查询的数据不是主键值,这时在二级索引找到主键值后,需要去聚簇索引中获得数据行,这个过程就叫作「回表」,也就是说要查两个 B+ 树才能查到数据。不过,当查询的数据是主键值时,因为只在二级索引就能查询到,不用再去聚簇索引查,这个过程就叫作「索引覆盖」,也就是只需要查一个 B+ 树就能找到数据。
索引的分类,可以根据角度的不同来分,在前面的内容,我们已经了解到了按 “数据结构(Hash 索引、B+ Tree 索引)” 以及按 “物理存储(聚合索引、二级索引)” 两种角度的索引分类,那么MySQL 中还存在着哪些形式的索引呢。
从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。
主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
在创建表时,创建主键索引的方式如下:
CREATE TABLE table_name ( .... PRIMARY KEY (index_column_1) USING BTREE );
唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
在创建表时,创建唯一索引的方式如下:
CREATE TABLE table_name ( .... UNIQUE KEY(index_column_1,index_column_2,...) );
建表后,如果要创建唯一索引,可以使用这面这条命令:
CREATE UNIQUE INDEX index_name ON table_name(index_column_1,index_column_2,...);
普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
在创建表时,创建普通索引的方式如下:
CREATE TABLE table_name ( .... INDEX(index_column_1,index_column_2,...) );
建表后,如果要创建普通索引,可以使用这面这条命令:
CREATE INDEX index_name ON table_name(index_column_1,index_column_2,...);
前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。
使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
在创建表时,创建前缀索引的方式如下:
CREATE TABLE table_name( column_list, INDEX(column_name(length)) );
建表后,如果要创建前缀索引,可以使用这面这条命令:
CREATE INDEX index_name ON table_name(column_name(length));
从字段个数的角度来看,索引分为单列索引、联合索引(复合索引)。
通过将多个字段组合成一个索引,该索引就被称为联合索引。
比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name),创建联合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
联合索引范围查询:
联合索引有一些特殊情况,并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询,也就是可能存在部分字段用到联合索引的 B+Tree,部分字段没有用到联合索引的 B+Tree 的情况。
这种特殊情况就发生在范围查询。联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配。也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。
最左前缀匹配原则:在 MySQL 建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。比如我们配置了一个 A、B、C 三个字段的联合索引,我们用 A、AB、ABC 的方式都是可以走到联合索引的,但如果是 AC、BC、C 的这种情况则不会使用索引。
MySQL 允许我们使用 CREATE INDEX 语句在指定的表上为指定的列创建索引
CREATE [UNIQUE] INDEX index_name [USING {BTREE | HASH}] ON table_name (column_list) [algorithm_option | lock_option];
说明:① UNIQUE 关键字表明此索引为唯一索引,它是可选的;② index_name 是索引的名字。一个表中不应该出现两个相同名字的索引;③ table_name 是表的名字;④ column_list 是表中的列名。多个列名使用逗号分隔;⑤ USING 子句指定索引的类型。可选值:BTREE,HASH。 它是可选的 ⑥ algorithm_option 指定删除索引的算法;⑦ lock_option 指定删除索引的并发控制策略。
其中 algorithm_option 它使用以下的语法:
ALGORITHM [=] {DEFAULT | INPLACE | COPY}
ALGORITHM 子句是可选的。默认为 INSTANT。如果不支持 INSTANT,则使用 INPLACE。
使用 DEFAULT 和省略 ALGORITHM 子句效果相同。
以下是对各个算法的说明:
lock_option 指定删除索引的并发控制策略。它使用以下的语法:
LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}
LOCK 子句是可选的。以下是对各个并发策略的说明:
DEFAULT
给定 ALGORITHM 子句(如果有)和 ALTER TABLE 操作的最大并发级别:如果支持,则允许并发读取和写入。如果不是,则允许并发读取(如果支持)。如果不是,则强制执行独占访问。
NONE
如果支持,允许并发读取和写入。否则,会发生错误。
SHARED
如果支持,允许并发读取但阻止写入。即使存储引擎支持给定 ALGORITHM 子句(如果有)和 ALTER TABLE 操作的并发写入,写入也会被阻止。如果不支持并发读取,则会发生错误。
EXCLUSIVE
强制执行独占访问。即使存储引擎支持给定 ALGORITHM 子句(如果有)和 ALTER TABLE 操作的并发读/写,也会这样做。
在 MySQL 内部,CREATE INDEX 语句被映射为 ALTER TABLE ... ADD INDEX ... 语句。
在 MySQL 数据库中,我们可以使用 DROP INDEX 从表中删除已有的索引。
DROP INDEX index_name ON table_name [algorithm_option | lock_option];
在 MySQL 中,我们可以使用 SHOW INDEXES 命令从表中查询索引信息
SHOW INDEXES FROM db_name.table_name;
或者
SHOW INDEXES FROM table_name IN db_name;
MySQL 查询优化器是 MySQL 数据库服务器的一个组件,它为 SQL 语句制定最佳执行计划。 MySQL 优化器通常根据索引基数进行决策。 有时候,虽然你创建了索引,但是你的 SQL 语句却不一定使用索引。 这是因为 MySQL 查询优化器的做出了它认为的更优的选择。
MySQL 允许我们使用 USE INDEX 语句建议查询优化器去使用指定的命名索引。
SELECT column_list FROM table_name # 如果 MySQL 查询优化器要使用索引,则必须使用索引列表 index_list 中的一个索引 USE INDEX (index_list) WHERE condition;
同样的有时候,虽然我们创建了索引,但是我们的 SQL 语句却不一定使用索引。 这是因为 MySQL 查询优化器的做出了它认为的更优的选择。
这里我们可以是使用 FORCE INDEX 子句告诉 MySQL 查询优化器必须使用指定的索引
SELECT * FROM table_name FORCE INDEX (index_list) WHERE condition;
在有些时候因为使用上的一些瑕疵就会导致索引的失效,无法达到我们使用索引的预期效果,下面介绍几种索引失效的场景:
索引设计不合理或者缺少索引都会对数据库和应用程序的性能差生障碍,设计高效的索引需要遵循一些原则和最佳实践,以确保查询性能的同时避免不必要的资源消耗。以下是索引设计的一些核心原则:
遵循这些设计原则可以帮助开发者和数据库管理员创建出既能满足查询需求又能优化性能的索引策略。正确的索引策略能够在提升查询性能和控制资源消耗之间找到一个平衡点。