MySQL(二)——索引

索引

平衡二叉树:左右节点的层级相差不大于1、左节点小于本节点,本节点小于右节点,最多拥有两个子节点

B树:

  • 枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2);
  • 子节点数可以大于2,每个节点包含的关键字多了,减少了层级。

B+树:

  • 非叶子节点不存关键字数据,所以每个非叶子节点存储的关键字数更多,树的层级更少(磁盘每4k为一块)
  • 所有的关键字数据地址都存在叶子节点上,所以每次查找的次数都相同,更稳定。
  • 叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,紧密型更高
  • 全节点遍历更快,有利于全表扫描

如何定位记录所在的页

  • 目录项记录页,该页中存放的数据只有(主键)索引的起始值和对应子节点目录(数据)页的页号信息
  • innodb采用了b+树来做索引,所有的叶子节点是所有的用户数据页,也就是真正存数据的页,而所有的非叶子节点都是目录项记录页,当数据量越来越大时,目录项记录页也会越来越多,此时需要一个更上层的节点来保存该层所有目录项记录页的索引。最终顶层是一个根节点。如下图:
  • 当进行索引查询的时候,根据索引项,从根节点(单个目录项记录页)通过二分法找到对应的子节点的目录项记录页,在再这个页中再通过二分法找到下一个节点的目录项记录页,直到找到叶子节点,即数据页,再通过二分法找到找到对应的槽,再在槽里遍历找到该数据。牛逼!

聚簇索引(主键索引,叶子节点存放所有的数据)

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

  2. B+树的叶子节点存储的是完整的用户记录。

    所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。

我们把具有这两种特性的B+树称为聚簇索引,所有完整的用户记录都存放在这个聚簇索引的叶子节点处。这种聚簇索引并不需要我们在MySQL语句中显式的使用INDEX语句去创建(后边会介绍索引相关的语句),InnoDB存储引擎会自动的为我们创建聚簇索引。另外有趣的一点是,在InnoDB存储引擎中,聚簇索引就是数据的存储方式(所有的用户记录都存储在了叶子节点),也就是所谓的索引即数据,数据即索引。

二级索引(非聚簇索引也叫辅助索引)

  • 使用其他列字段的大小作为排序依据(作为索引)
  • 叶子节点存储的不是完整的用户记录,而是索引列+主键
  • 目录页记录的不是主键+页号,而是索引列+主键+页号
  • 查找到用户记录后,根据查找到的主键再去聚簇索引中查找一遍完整的用户记录也就是回表,需要2棵b+树

联合索引

如果根据c2和c3来建索引,则按照c2和c3的大小进行排序

  • 先把各记录的页按照c2进行排序
  • 如果c2相同则根据c3排序
  • 叶子节点由c2、c3、主键组成
  • 如果要查询的数据刚好都在索引列里面,则不会回表,直接从二级索引的叶子节点中取数据。
  • 如果是ab联合索引,查询条件中b在前,a在后,那mysql会通过查询优化器进行优化,先用a来搜索。但是如果只用b来进行查询,则不会走索引,因为没办法通过b的大小来二分查找。

注意事项

  • 每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有用户记录,也没有目录项记录。
  • 随后向表中插入用户记录时,先把用户记录存储到这个根节点中。
  • 根节点中的可用空间用完时继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页,比如页a中,然后对这个新页进行页分裂的操作,得到另一个新页,比如页b。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页b中,而根节点便升级为存储目录项记录的页。
  • 一个B+树索引的根节点自诞生之日起,便不会再移动。这样只要我们对某个表建立一个索引,那么它的根节点的页号便会被记录到某个地方,然后凡是InnoDB存储引擎需要用到这个索引的时候,都会从那个固定的地方取出根节点的页号,从而来访问这个索引

MyISAM中的索引方案

MyISAM的索引方案虽然也使用树形结构,但是却将索引和数据分开存储:

  • 将表中的记录按照记录的插入顺序单独存储在一个文件中,称之为数据文件。这个文件并不划分为若干个数据页,有多少记录就往这个文件中塞多少记录就成了。我们可以通过行号而快速访问到一条记录。MyISAM 记录也需要记录头信息来存储一些额外数据,我们以index_demo表为例,看一下这个表中的记录使用MyISAM作为存储引擎在存储空间中的表示:

  • 由于在插入数据的时候并没有刻意按照主键大小排序,所以我们并不能在这些数据上使用二分法进行查找。

  • 使用MyISAM存储引擎的表会把索引信息另外存储到一个称为索引文件的另一个文件中。MyISAM会单独为表的主键创建一个索引,只不过在索引的叶子节点中存储的不是完整的用户记录,而是主键值 + 行号的组合。也就是先通过索引找到对应的行号,再通过行号去找对应的记录!

CREATE TALBE 表名 (
    各种列的信息 ··· , 
    [KEY|INDEX] 索引名 (需要被索引的单个列或多个列)
)
ALTER TABLE 表名 ADD [INDEX|KEY] 索引名 (需要被索引的单个列或多个列);
ALTER TABLE 表名 DROP [INDEX|KEY] 索引名;

索引的代价

  • 空间上的代价

    这个是显而易见的,每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间,一棵很大的B+树由许多数据页组成,那可是很大的一片存储空间呢。

  • 时间上的代价

    每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值从小到大的顺序排序而组成了双向链表。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收啥的操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,这还能不给性能拖后腿么?

匹配左边的列

最好不要跨过中间索引查询前后索引,这样只会用到最左侧的连续索引。

匹配列前缀

  • 字符串排序,根据第一个字符的大小排序,如果相同则根据第二个,依次类推。
  • 最好使用a = 'test%' 而不是a = '%test' 或者 a= '%test%'
  • 如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+树索引,因为第一个列查出来的是范围,只有在第一列相同的情况下,第二列才做了排序,所以用不到这个索引。
  • 如果最左边的列是精确,第二列是范围,是可以用到索引,如果再有第三列是范围,则只能遍历了

排序

  • order by后的列的顺序也必须按照索引列的顺序给出,不然用不了索引。且排序方式必须一致,不能一个desc一个asc
  • 如果包含了非同一个索引的列,也不能使用索引
  • 不能使用别的函数,比如UPPER或者a*2=4 可以改成a=4/2

分组

如果是索引字段按顺序进行分组,天然就是排好序的,可以直接分组。

回表

  • 根据索引查数据时,由于数据记录再磁盘中的存储是相连的,顺序IO速度很快。而他们所对应的主键往往是不连续的,所以用这些不连续的id值到聚簇索引中去查完整记录时,大概率分布在不同的数据页,就变成随机IO了
  • 需要回表的记录越多,使用二级索引的性能就越低,甚至让某些查询宁愿使用全表扫描也不使用二级索引。比方说name值在AsaBarlow之间的用户记录数量占全部记录数量90%以上,那么如果使用idx_name_birthday_phone_number索引的话,有90%多的id值需要回表,这不是吃力不讨好么,还不如直接去扫描聚簇索引(也就是全表扫描)。回表的记录越少,性能提升就越高,越倾向二级索引+回表

覆盖索引

  • 最好返回结果只查索引值

列的基数

  • 在记录行数一定的情况下,列的基数越大,该列中的值越分散,列的基数越小,该列中的值越集中。最好为那些列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。

索引列的类型尽量小

  • 数据类型越小,在查询时进行的比较操作越快(这是CPU层次)
  • 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/O带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。

索引字符串值的前缀

  • 由于字符串长的话占用的空间会比较多,如果整个字符串做索引,b+树占用的空间会很大,做字符串比较也会占用更多的时间,所以可以用前缀几个字符来做索引,比如前10个字符进行索引,则匹配的时候只查出前10个字符匹配上的数据,然后再回表查完整数据。减少比较时间,节约了空间。
  • 索引列前缀的方式无法支持使用索引排序。

主键插入顺序

插入时不要忽大忽小,不然会造成页面分裂和记录位移,会造成性能损耗。最好是自增主键。

总结

B+树索引适用于下边这些情况:

  • 全值匹配
  • 匹配左边的列
  • 匹配范围值
  • 精确匹配某一列并范围匹配另外一列
  • 用于排序
  • 用于分组

在使用索引时需要注意下边这些事项:

  • 只为用于搜索、排序或分组的列创建索引
  • 为列的基数大的列创建索引
  • 索引列的类型尽量小
  • 可以只对字符串值的前缀建立索引
  • 只有索引列在比较表达式中单独出现才可以适用索引
  • 为了尽可能少的让聚簇索引发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT属性。
  • 定位并删除表中的重复和冗余索引
  • 尽量使用覆盖索引进行查询,避免回表带来的性能损耗。

区(extent)

  • 每16kb为一页,连续64个页就是一个区,默认占用1MB,每256个区被划分成一个组。
  • 相邻页的物理位置可能离的很远,所以在进行读写的时候是随机IO,速度并不快
  • 为了使用顺序IO,引入了区的概念,连续的64个区,在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位了,而是区。甚至数据非常多的时候可以一次性分配多个连续的区。
  • 叶子节点和非叶子节点分别拥有各自的区,这些区的集合就是一个,一个索引会有一个叶子节点段和一个非叶子节点段。
  • 碎片区:由于直接分配一个区就是占用了1mb的内存,对于小表会造成浪费,所以提出了碎片区的概念,这个区中并不是所有的页都是同一个段的,可能是不同的段。碎片区直属于表空间,不属于任何一个段。只有当某个段已经占用了32个碎片区页面后,才会以完整的区为单位来分配存储空间。

访问方法

  • const:主键或唯一约束索引的等值匹配,如果是多个列,则每一个列需要与常数进行等值比较
  • ref:普通的二级索引,如果是key is null,不管是唯一二级索引还是普通二级索引,都是ref形式,因为NULL值的数量不限制.如果最左边的连续索引全都是等值比较,则还是ref,否则就不是了
  • ref_or_null:普通的等值比较或者key is null则成为ref_or_null
  • range:对二级索引进行等值或者某个范围的值
  • index:如果可以直接通过遍历某个索引的子节点的记录来比较查询条件,且返回值包含在索引列中,不需要回表。把这种遍历二级索引记录成为index
  • all:扫全表

索引合并:

Intersection

某个查询可以使用多个二级索引,将从多个二级索引中查询到的结果取交集。MySQL在某些特定的情况下才可能会使用到Intersection索引合并:

  • 情况一:二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹配,不能出现只匹配部分列的情况。为什么?因为只有每个列全都等值匹配的情况下,才会对主键排序。只有在这种情况下根据二级索引查询出的结果集是按照主键值排序的。
  • 情况二:主键列可以是范围匹配

why?Intersection索引合并会把从多个二级索引中查询出的主键值求交集,如果从各个二级索引中查询的到的结果集本身就是已经按照主键排好序的,那么求交集的过程就很easy啦。

按照有序的主键值去回表取记录有个专有名词儿,叫:Rowid Ordered Retrieval,简称ROR

Union合并

Intersection索引合并类似,MySQL在某些特定的情况下才可能会使用到Union索引合并。其实就是union取并集,intersection取交集

  • 情况一:二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹配,不能出现只出现匹配部分列的情况。
  • 情况二:主键列可以是范围匹配
  • 情况三:使用Intersection索引合并的搜索条件

Sort-Union合并

先按照二级索引记录的主键值进行排序,之后按照Union索引合并方式执行的方式称之为Sort-Union索引合并,这种Sort-Union索引合并比单纯的Union索引合并多了一步对二级索引记录的主键值排序的过程。

你可能感兴趣的:(MySQL(二)——索引)