MySQL系列(11)— 索引

索引

参考:web

MySQL 索引

索引 就是为了提升数据查询的效率,就像书的目录同样,咱们能够借助目录快速找到某个知识点所在的页。一样,对于数据库的表而言,索引其实就是表数据的“目录”。算法

索引 是在 MySQL 存储引擎层中实现的,因此每一种存储引擎支持的索引不必定相同,即便多个存储引擎支持同一种类型的索引,其底层的实现也可能不一样。下面这张表格展现了不一样的存储引擎所支持的索引类型。数据库

索引类型 InnoDB 引擎 MyISAM 引擎 Memory 引擎
B+Tree 索引 Y Y Y
Hash 索引 N N Y
R-Tree 索引 N Y N
Full-Text 索引 N Y N

B+Tree索引Hash索引是比较经常使用的两个索引数据存储结构:性能优化

  • B+Tree索引是经过B+树实现的,是有序排列存储,因此在排序和范围查找方面都比较有优点。服务器

  • Hash索引适合 key-value 键值对查询,不管表数据多大,查询数据的复杂度都是O(1),且直接经过 Hash 索引查询的性能比其它索引都要高。但缺点是,由于不是有序的,因此哈希索引作区间查询的速度很慢。因此,哈希表结构适用于只有等值查询的场景。markdown

B+Tree 索引

数据页

B+Tree 索引是经过 B+ 树实现的,能够经过 平衡二叉树、B树、B+树、B*树 这篇文章了解 B+ 树的数据结构原理。数据结构

InnoDB 磁盘管理的最小单位是,B+Tree 索引中的每一个节点就是一个数据页。在研究 B+Tree 索引前,先回顾下前面学过的一些关于页的知识。oop

页结构的 File Header 部分记录的信息以下表所示:post

这里记住以下几个比较重要的信息:性能

  • FIL_PAGE_OFFSET:当前页的页号,每一个页都有一个惟一编号
  • FIL_PAGE_PREV:双向链表中指向当前页的上一个页
  • FIL_PAGE_NEXT:双向链表中指向当前页的下一个页
  • FIL_PAGE_TYPE:页的类型,索引和数据都是存放在 FIL_PAGE_INDEX(0x45BF)这种类型的页中,就是数据页。

数据页中存放的就是一行行记录,如常使用的 Compact 行记录格式以下图所示:

其中记录头部分记录的信息以下表所示:

这里记住两个比较重要的信息:

  • record_type:记录的类型:
    • 0:普通的用户记录
    • 1:目录项记录
    • 2:最小记录
    • 3:最大记录
  • next_record:指向页中下一条记录

有了上面这些信息,就能够造成一个简单的双向链表来存储数据,页与页之间就经过 FIL_PAGE_PREVFIL_PAGE_NEXT 连成双向链表。页中存放的就是一行行记录,每行记录经过 next_record 链接起来造成一个单项链表,每一个页中都会有一个最小记录(Infimum,record_type=2),以及最大记录(Supremum,record_type=3),而后就是普通的用户记录(record_type=0)。

image.png

B+Tree 索引的造成过程

仍是之前面测试使用的 account 表为例,咱们先觉得 id 列建立主键索引为例来讲明。

CREATE TABLE `account` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `card` varchar(60) NOT NULL COMMENT '卡号',
  `name` varchar(60) DEFAULT NULL COMMENT '姓名',
  `balance` int(11) NOT NULL DEFAULT '0' COMMENT '余额',
  PRIMARY KEY (`id`),
  UNIQUE KEY `account_u1` (`card`) USING BTREE,
  KEY `account_n1` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='帐户表';
复制代码

每当为某个表建立一个 B+Tree 索引的时候,都会为这个索引建立一个根节点页面,这个根节点页面建立后便不会再移动。这个根节点的页号会被记录起来,而后在访问这个表须要用这个索引的时候,就会取出这个根节点页面,从而来访问这个索引。

例如建立主键索引时,建立了一个页号为30的根节点页面,插入数据时,首先会插入到根节点页面中。

须要注意的是,一行记录不仅包含用户记录,还包含隐藏的事务ID(trx_id)、回滚指针(roll_pointer)等,这些就没有展现在图中了。

image.png

假设一个页最多存储 3 条记录就满了。

这时再插入一条 id=4 的记录,根节点页面已满,就会新分配一个页(页35),而后将根节点中的全部记录复制到这个新分配的页中。因为页 35 已满,因此会再分配一个新的页(页38),而后将新的记录插入页 38 中。

image.png

但这里是有点问题的,索引上的记录是顺序排列的,并且要求 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。页 35 中最大的记录是 id=5,若是插入一条 id > 5 的记录,插入到页 38 中就没有问题,而这里插入的是 id=4 这条记录。因此插入 id=4 这条记录时还伴随着 页分裂,就是把 id=5 这条记录移动到页 38 中,而后再把 id=4 这条记录插入到页 35 中。

image.png

这个过程代表了在对页中的记录进行增删改操做的过程当中,会经过一些诸如记录移动的操做来保证下一个数据页中记录的主键值始终大于上一个页中记录的主键值,这个过程也能够称为页分裂

存储用户记录的页在物理存储上可能并不挨着(把这种页称为用户记录页),因此若是想从这么多页中根据主键值快速定位某些记录所在的页,就须要给它们作个目录。

这时候根节点页就会变成目录页,里面的记录的类型为record_type=1,也就是目录项记录。目录项记录跟普通记录的结构相似,只不过它的数据部分只存储了主键值页号,每一个用户记录页都会对应一个目录项记录,这个目录项记录的主键值就是这个用户记录页中主键值最小的记录。

这时,跟节点页中就会有两条目录项记录,第一条记录的页号为 35,id=1;第二条记录的页号为 38,id=5。

image.png

随着用户记录不断插入,用户记录页愈来愈多,目录页中的记录也满了,这时要再插入一个目录项记录就放不下了。例以下面最后一条记录。

image.png

其实跟前面是相似的,也会伴随着页分裂的操做。根节点页始终不动,它会把全部记录复制到一个新分配的页中。这时能够看到目录页就有两层了。

image.png

上面这幅图如今看起来就像一个倒过来的树,这其实就是 B+树,B+ 树就是一种用来组织数据的数据结构。

不管是存放用户记录的数据页,仍是存放目录项记录的数据页,都把它们存放到 B+ 树这个数据结构中了。从图中能够看出来,用户记录页都存放在B+树的最底层的节点上,这些节点也被称为叶子节点叶节点,其他用来存放目录项的节点称为非叶子节点或者内节点,其中B+树最上边的那个节点就称为根节点

在索引中查找记录

上面已经造成一个 B+ 数索引了,假设如今要查找 id=11 这条记录,这时就会按以下步骤来查找:

  • 首先IO读取索引的根节点页(页30)到内存中,而后在内存中遍历根节点页中的记录项,这些记录能够根据主键划分几个区间:(Infimum, 1),[1, 15),[15,Supremum)。id=11 落在 [1, 15) 这个区间,因此定位到 id=1 这条记录,对应的页号是 52。

  • 接着IO读取页 52 到内存中,一样的遍历页中的记录,这时 id=11 落在 [10, Supremum) 这个区间,所以定位到 id=10 这条记录,对应的页号是 45。

  • 接着IO读取页 45 到内存中,再遍历页中的记录,就能够定位到 id=11 这条用户记录了。

须要注意的是,无论是目录页仍是用户记录页,页中都会有一个 Page Directory,就是页目录,经过页目录就能够经过二分法快速定位到页中的一条记录,而不是从左往右一条条遍历。关于 Page Directory 请参考:MySQL系列(4)— InnoDB数据页结构

image.png

注意B+树索引并不能找到一个给定键值的具体行,能找到的只是被查找数据行所在的。而后数据库把读入到内存,再在内存中进行查找,最后获得要查找的数据。因此上面的步骤中会有IO操做。

从上面查找记录的过程能够看出,磁盘IO的次数等于 B+ 树的高度,也就是说IO的次数将取决于 B+ 树的高度,而磁盘 IO 每每是数据库性能的瓶颈。B+Tree 索引最高会有多少层呢?

前面咱们只是假设每一个页最多存放3条记录,其实一个 16KB 的页存放的记录数量是很是大的。假设存放用户记录的叶子节点数据页能够存放100条用户记录,而存放目录项记录的内节点数据页能够存放1000条目录项记录:

  • 若是 B+Tree 有1层,也就是只有1个用于存放用户记录的节点,最多能存放 100 条记录。

  • 若是 B+Tree 有2层,最多能存放 1000×100=10万 条记录。

  • 若是 B+Tree 有3层,最多能存放 1000×1000×100=1亿 条记录。

  • 若是 B+Tree 有4层,最多能存放 1000×1000×1000×100=1000亿 条记录。

一张表通常来讲不多会超过1亿条记录,更不用说 1000亿 了。因此通常状况下,B+Tree 都不会超过4层

咱们经过主键值去查找某条记录最多只须要作4个页面内的查找(查找3个目录项页和一个用户记录页),又由于在每一个页面内有 Page Directory(页目录),因此在页面内又能够经过二分法实现快速定位记录。

因此有了索引以后,根据索引查找数据是很是快的。而没有索引,就只能全表扫描,读取每一个页到内存中遍历,就会有不少次的磁盘IO,这个性能就很是低下了。

聚簇索引

上面介绍建立的ID主键索引其实就是一种聚簇索引,最主要的特征即是 B+树的叶子节点存储的是完整的用户记录,也就是存储了一行记录中全部列的值(包括隐藏列)。

除此以外,聚簇索引使用记录主键值的大小进行记录和页的排序,主要表如今如下几个方面:

  • 页内的记录是按照主键的大小顺序排成一个单向链表。
  • 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。
  • 存放目录项记录的页分为不一样的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。

InnoDB 存储引擎表都会有主键,若是咱们没有为某个表显式的定义主键,而且表中也没有定义惟一索引,那么InnoDB会自动为表添加一个 row_id 的隐藏列做为主键。所以,InnoDB 始终会自动建立聚簇索引,在 InnoDB 中,聚簇索引就是数据的存储方式,全部的数据都是存储在这颗 B+树的叶子节点上,这也就是 索引即数据,数据即索引

辅助索引

InnoDB 在建立表时,默认会建立一个主键的聚簇索引,而除此以外的其它索引都属于辅助索引,也被称为二级索引非聚簇索引

聚簇索引只能在搜索条件是主键值时才能发挥做用,由于目录页中存储的都是主键,B+树中的数据都是按照主键进行排序的。若是咱们要根据其它的非主键列来查询,好比前面 account 表中的 name 列,这时就能够再建一个 name 列的辅助索引。

辅助索引与聚簇索引最大的区别在于叶子节点存储的就再也不是完整的用户记录了,而是索引列+主键值,目录页中存储的也是索引列的值,同时,辅助索引使用索引列的大小进行记录和页的排序。

例以下面就是为 name 列建立的一个辅助索引。能够看到最底层的叶子节点就只包含 name + id 列的数据,同时数据是按照 name 列的大小排序的,目录页中存储的也是 name 列的值。

image.png

这时,再根据 name 列查找数据时,就会用上这个辅助索引了,查找过程跟聚簇索引的查找过程是相似的。最主要的区别在于利用辅助索引查找到的数据不是完整的用户记录,因此找到叶子节点上的记录后,还会根据对应的主键值回到主键索引上再根据主键值找到对应的完整记录,这个过程也称为回表

例如查找 name=H 的记录,就会定位到页69,name=H 对应的主键 id=11,而后就会回表在聚簇索引上查找 id=11 这条完整记录。

利用辅助索引查找的时候,也并不是必定须要回表,若是咱们查找的数据在辅助索引上都已经存在了,就不会回表了。例如SQL select name, id where name='H' 只查询 name、id 的值,就不会回表了,由于这个辅助索引上已经包含了要查找的全部列,只有索引上不包含要查找的列时,才会回表再查一遍。

联合索引

咱们也能够同时以多个列的大小做为排序规则,同时为多个列创建索引,多个列创建的索引称为联合索引,其本质上也是一个辅助索引或二级索引。

多个列创建的联合索引,叶子节点中存储的就是这几个索引列+主键值,例如为 name、balance 列创建索引,那叶子节点上存储的就是 name、balance、id 这几列,目录页存储的就是 name、balance + 页号。

联合索引会先根据第一列排序,第一列相同的再根据第二列排序,以此类推。例如 name、balance 的联合索引,会先以 name 列排序存储,name 列值相同的再按 balance 列排序。

MyISAM 中的索引

InnoDB 引擎表中聚簇索引既包含了索引目录又包含了完整数据,索引和数据是一块儿存在一颗B+树上的。

MyISAM 引擎表则是将索引和数据分开存储:

  • 用户数据按照记录的插入顺序单独存储在一个文件中,称之为数据文件,也就是 .MYD 为后缀的文件。这个文件并不划分数据页,全部记录都按照插入顺序插入就好了,而后经过每行数据的物理地址来快速访问到一条记录。

  • 索引信息则另外存储到一个单独的索引文件中,就是 .MYI 为后缀的文件。MyISAM 会单独为表的主键建立一个索引,只不过在索引的叶子节点中存储的不是完整的用户记录,而是主键值 + 物理地址的组合。也就是先经过索引找到行对应的物理地址,再经过物理地址去找对应的记录。

也就是说,MyISAM 引擎中创建的索引至关于所有都是二级索引,不管是为主键仍是其它列建立的索引,都须要根据物理地址 回表,到数据文件中查找完整的用户记录。

image.png

索引总结

索引规则

根据前面的学习,咱们先总结熟悉下 InnoDB 引擎的 B+ 树索引规则。

  • 每一个索引都对应一棵 B+树,B+ 树通常最多不超过4层,最底层的是叶子节点,其他的是内节点。全部用户记录都存储在B+树的叶子节点,全部目录项记录都存储在内节点。

  • InnoDB 存储引擎会自动为主键创建聚簇索引,聚簇索引的叶子节点包含完整的用户记录。

  • 能够根据实际需求建立 二级索引,二级索引的叶子节点仅包含索引列 + 主键,因此若是想经过二级索引来查找完整的用户记录,会有 回表 操做,也就是在经过二级索引找到主键值以后再到聚簇索引中查找完整的用户记录。

  • B+树索引中,每层数据页节点都是按照索引列值从小到大的顺序排序而组成了双向链表,每一个页内的记录(不管是用户记录仍是目录项记录)都是按照索引列值从小到大的顺序而造成了一个单向链表。

  • 联合索引 的页面和记录先按照联合索引前边的列排序,若是该列值相同,再按照联合索引后边的列排序。

  • 经过索引查找记录是从B+树的根节点开始,一层一层向下搜索。因为每一个页面都按照索引列的值创建了 Page Directory,因此在这些页面中的查找也很是快。

记住索引列的顺序性是很是重要的,索引自己的特征以及不少查询的性能优化和限制都和索引的顺序有关系。

索引优势

索引主要就是为了提高数据库的查询性能,总结下来主要有以下几个优势:

  • 索引大大减小了服务器须要扫描的数据量,经过索引能够快速定位到一条记录。并且由于索引列存储了实际的值,因此有些查询只使用索引就可以完成所有查询,而无需回表。

  • 索引能够帮助服务器避免排序和临时表,由于索引是按照索引列排序的,数据已经排好序了,因此对于范围查询、排序 ORDER BY、分组 GROUP BY 是很是有用的。

  • 索引能够将随机 I/O 变为顺序 I/O,由于数据就是按索引列排序的。

索引代价

首先要明确,索引并非越多越好,索引的使用是有必定代价的。

  • 空间上的代价

每建立一个索引都要为它创建一棵 B+树,每一棵 B+树 的每个节点都是一个数据页,一个页默认会占用16KB的存储空间。一张表数据越多,这颗 B+树就会越大,占用的空间就会越多。

  • 时间上的代价

每次对表中的数据进行增、删、改操做时,都须要去修改各个B+树索引。B+树 每层节点都是按照索引列的值从小到大排序连成的双向链表,节点中的记录也是按照索引列的值从小到大排序而造成的一个单向链表。而增、删、改操做可能会对节点和记录的排序形成破坏,因此存储引擎须要额外的时间进行一些记录移位,页面分裂、页面回收等操做来维护好节点和记录的排序。因此索引建的越多,每次增、删、改操做索引所耗费的时间就会越多。

因此,一个表上索引建的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。只有正确使用和建立索引,才能总体提高性能,不然只会拔苗助长。

只有当索引对查找到记录带来的好处大于其带来的额外工做时,索引才是有效的。对于很是小的表,大部分状况下简单的全表扫描更高效。对于中到大型的表,索引就很是有效。

索引应用

全值匹配

全值匹配就是查询条件中的列和索引中的列一致。

例如为 (card, name, balance) 建立的一个联合索引 idx_cnb,假设一个查询语句把这三列都用上了:

SELECT * FROM account WHERE card = 'A' AND name = 'A' and balance = 0;
复制代码

首先要知道 idx_cnb 这个索引是一个联合索引,这个索引首先按照 card 列排序,card 列相同的再按照 name 列排序,name 列相同的再按照 balance 列排序。

因此这个查询语句会先查找 card = 'A' 的记录,再从这些记录中快速找出 name='A' 的记录,若是 card 和 name 都相同,还会用上 balance 列。

WHERE 子句中的几个搜索条件的顺序对查询结果是没有什么影响的,MySQL查询优化器会自动优化SQL语句,而后根据要使用的索引,来决定先使用哪一个查询条件,后使用哪一个查询条件。

匹配最左前缀

对于联合索引,能够只使用左边的部分列,能够不用包含所有联合索引中的列,但只能是左边连续的列。

例以下面的查询语句就会使用 idx_cnb 这个联合索引,但只使用了索引中的前两个列。

SELECT * FROM account WHERE card = 'A' AND name = 'A';
复制代码

若是只使用了中间的列,则用不上这个联合索引。例以下面的SQL根据 name 查询,由于 idx_cnb 索引是先安装 card 列排序的,在 card 列相同的状况下才会使用 name 列排序。因此没法跳过 card 列直接根据 name 列查找数据。

SELECT * FROM account WHERE name = 'A';
复制代码

再好比下面的SQL,则只会使用到 idx_cnb 索引的第一列,由于 balance 是先根据 name 列排序后再排序的,因此对于 card=A 的数据,balance 可能并非有序的。因此要将全部 card=A 的数据查询到内存后再筛选出 balance=0 的数据 。

SELECT * FROM account WHERE card = 'A' AND balance = 0;
复制代码

匹配列前缀

匹配列前缀就是只匹配某一列的值的开头部分。

例以下面的查询,只匹配 card 为 A 开头的记录,也能够用 idx_cnb 索引来快速定位记录。

SELECT * FROM account WHERE card LIKE 'A%';
复制代码

但若是只给出后缀或者中间的某个字符串,则没法使用索引。例以下面的查询,查找 card 为 A 结尾的记录,由于并不知道 A 结尾以前的顺序,因此就没办法使用索引。

SELECT * FROM account WHERE card LIKE '%A';
复制代码

匹配范围值

匹配范围值就是利用索引的有序性,能够很是方便的查找在某个范围内的记录。

例以下面的SQL语句,根据 card 列进行范围查找,就可使用上 idx_cnb 索引。

SELECT * FROM account WHERE card > 'A' AND card < 'H';
复制代码

但须要注意的是,若是对联合索引多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用上索引。由于第一列使用范围查询后,第二列并非有序的,要知道是在第一列值相同的状况下,才用第二列排序。

例以下面的查询,先查询了 card 在 (A,B) 之间的记录,此时可能会有多条 card 不一样的记录,因此这些记录中的 name 并非有序的。因此须要先找到 card 在 (A, B) 之间的记录,再一条条过滤出 name > A 的记录。因此这个查询只用到了 idx_cnb 索引的 card 列,没用到 name 列。

SELECT * FROM account WHERE card > 'A' AND card < 'H' AND name > 'A';
复制代码

精确匹配某一列并范围匹配另一列

上一小节说的是若是第一列是范围查询,第二列也是范围查询时,第二列不会走索引。

但若是左边的列是精确匹配的,后面的列是范围查询则能够用上索引,由于左边的列精确匹配后,后边的列就是排好序的。

例以下面的查询,card 列是精确匹配,以后对 name 列进行范围查找,这个查询会用上 idx_cnb 索引的 card、name 两列。

SELECT * FROM account WHERE card = 'A' AND name > 'A';
复制代码

排序和分组

咱们常常会使用 ORDER BY 子句来对记录排序,通常状况下,数据库只能把记录都加载到内存中,再用一些排序算法在内存中对这些记录进行排序。有的时候可能查询的结果集太大以致于不能在内存中进行排序的话,还可能要使用磁盘空间来存放中间结果,排序操做完成后再把排好序的结果集返回到客户端。在MySQL中,把这种在内存中或者磁盘上进行排序的方式统称为文件排序(filesort),文件排序的性能通常就比较低了。

可是若是 ORDER BY 子句里使用到了索引列,就有可能省去在内存或文件中排序的步骤。

例以下面的查询就会使用到 idx_cnb 索引,由于 card,name 已经排好序了,这个查询就能够直接从 idx_cnb 索引中提取数据,而后回表查询。(固然了,idx_cnb 索引已经包含了整张表的数据,因此不会有回表这一步了)

SELECT * FROM account ORDER BY card, name;
复制代码

一样的,ORDER BY 也能够只使用部分的B+树索引列,当联合索引左边列的值为精确匹配时,也可使用后边的列进行排序。例以下面的查询:

SELECT * FROM account ORDER BY card, balance;

SELECT * FROM account WHERE card='A' ORDER BY name;
复制代码

须要注意的是,ORDER BY 子句后边的列的顺序必须按照索引列的顺序来,不然也是用不了索引的。例以下面的查询:

SELECT * FROM account ORDER BY name, card;
复制代码

使用联合索引进行排序时,要求各个排序列的排序顺序是一致的,要么各个列都是ASC升序,要么都是DESC降序。

由于若是一个按 ASC 升序,一个按 DESC 降序,这与索引中的顺序始终都是反的,并且若是加上 LIMIT 之类的限制条件,只能排好序以后才能肯定具体的记录。因此 MySQL 认为这种状况还不如文件排序来的快,就不会使用索引。

例以下面的查询语句:

SELECT * FROM account ORDER BY name ASC, card DESC;
复制代码

若是用来排序的多个列不是一个索引里的,这种状况也不能使用索引进行排序,缘由跟上面的是相似的。假设 acount 表还有其它列,例以下面的查询,country 列不属于 idx_cnb ,因此这个查询排序也用不上 idx_cnb 这个索引。

SELECT * FROM account ORDER BY name, country;
复制代码

分组和排序在使用索引的方式上是相似的,就不在赘述了。

相关文章
相关标签/搜索