MySQL索引

Posted by AlstonWilliams on February 17, 2019

索引一直都是老生常谈的话题.之前对它也是一知半解.这两天,查询了大量资料,并在阅读了«High Performance MySQL»的相关章节后,终于对它有了一个比较清晰地认识.所以在这里记录下来.

我对MySQL并不精通,所以即使呕心沥血地写下这篇文章,其中难免还有一些错误.请各位在阅读的时候,保持怀疑的太多,去查阅更多资料.如果发现哪里有错误,请在评论区指正,大家一起进步!

实现索引的数据结构:B-Tree和B+Tree

在介绍索引之前,我们首先介绍两个重要的数据结构:B-Tree和B+Tree.因为索引的很多操作都跟它们的特性有关.

B-Tree

B-Tree的结构如下图所示:

我们可以看到,每个节点都是这种形式:

其中pi,1 <= i <= 4表示指向子节点的指针,子节点的形式跟上面的相同.

ki,1<= i <= 3表示第i个key,在ki左边的pi,其指向的子节点中的key均小于ki,于此类似,在ki右边的pi+1,其指向的子节点中的key均大于ki

我们可以看到,B-Tree实际上就是一个扩展了的BST,即每个节点可以有多个key.

同时,它也是一棵自平衡树,这也就意味着,在进行插入或删除操作时,为了满足其特性,可能会对它的结构进行调整.那么B-Tree都有哪些特性呢?

  • 全部叶子节点都在相同的高度上
  • B-Tree用属于minimum degree ‘t’来定义.这里degree跟树的degree的概念是一样的
  • 除根节点外的其他节点,至少要包含t-1个key.根节点最少可以包含1个key
  • 包括根节点的全部节点,最多能包含2t-1个key
  • 一个节点的子节点的数量等于key的数量+1
  • 一个节点中的key都是以升序存放的.k1和k2之间的p2所指向的子节点包含了k1和k2之间的全部key
  • B-Tree和其它平衡BST一样,查询,插入,删除操作的时间复杂度均为O(logn)

我们知道,B-Tree主要的应用场景就是文件系统和数据库.我们也知道,硬盘的速度是远慢于内存的,而数据库又把数据保存到硬盘中.所以,如果我们能尽可能地减少访问磁盘的次数,就能大幅提高数据库的性能.

通过使用B-Tree,并将节点的大小设置为磁盘上一页的大小,我们不就可以尽可能地减少访问磁盘的次数?

更多关于B-Tree的资料,请自行在wikipedia中查找.

B+Tree

B+Tree是对B-Tree的一种优化,它的结构跟B-Tree其实差不多.比如,在B-Tree中的那个例子,换成B+Tree就是:

我们可以看到,跟B-Tree的区别在于:

  • 孩子节点包含了双亲节点的key
  • 叶子节点的最右侧指针指向了其兄弟节点

我们拿一张表举个例子:

上图中的数据,包括row address,都是我随意编造的.

上图中的数据,用B-Tree表示可能是这样:

我们可以看到,每个节点,除了保存有id之外,还保存有那一行的地址.

这个地址肯定是必须的,但是我们可不可以放在其他位置呢?把它放在中间节点的话,树的深度肯定比放在叶子节点深.因为放在叶子节点的话,中间节点的key肯定多嘛!

所以,用B+Tree表示就是:

由于这个例子中,数据较少,B+Tree的优势不是太明显.

那么为什么要让叶子节点的最右侧指向其兄弟节点呢?

因为在SQL中,我们经常有范围查询的需求,这样做的话,就不用回溯了.范围查询就方便了好多,对吧?

其他数据结构

除了这两种数据结构以外,还有其他的,如哈希表等,但是这些都是用在特殊的索引中的,没有B-Tree和B+Tree通用.

索引的分类

索引主要可以按照底层数据结构,主次,用处来分类.

按照底层数据结构划分

  • B-Tree indexes:顾名思义,这种索引的内部的数据结构是B-Tree.实际上,大多数Storage Engine使用的实际上是B+Tree.
  • Hash indexes:Hash indexes的内部数据结构是哈希表,它会为索引中的每一列对应的值计算一个对应的哈希值,存放在哈希表中,用于精确匹配.
  • Spatial(R-Tree) indexes:这种索引是用于存储地理信息的,具体我也不太清楚
  • Full-text indexes:这种索引是用在全文检索中的,它是一种特殊的索引,它会在text中查找keyword,而不是把它和index中对应的列对应的值做比较.它有很多不同之处,如分词等.

按照主次划分

  • Clustered index(primary index):它和Primary Index是一样的意思,就是将Primary Key相近的row放在一起.默认情况下,会为Primary Key建立Clustered index.如果这张表中并没有Primary key,则挑选第一个非空且不重复的列建立Clustered index.如果这张表中既无Primary key又无非空不重复的列,则MySQL会为我们创建一个隐藏的id并建立Clustered index.从上面我们对Clustered Index的介绍,结合B-Tree的特性,我们可以很容易地知道Clustered Index是用B-Tree indexes实现的.
  • Secondary Index:它和Clustered Index是相对的,除Clustered Index外的其他Index,比如UNIQUE Indexes等都属于Secondary Indexes.

按照用法划分

  • MultiColumn Indexes:顾名思义,这个就是在多个列上创建的索引.
  • Covering Indexes:其实Covering Indexes是MultiColumn Indexes的一种特殊形式,它要求索引中包含了查询条件和要查询的列,这样查询时效率相对高一些

特殊的索引

  • Packed Indexes:这种索引是MyISAM中提供的为了节省空间的一种索引,比如说,'Theory'和'Theoristic'会被存储为:Theo,<4, ry>,<4, ristic>

索引的实现

索引是在Storage Engine层面实现的,而不是在Database层面实现的这里我们主要介绍B-Tree index在MyISAM和InnoDB这两种Storage Engine中的实现.

B-Tree index在MyISAM中的实现

假设有下面这种表:

其中<First_name,Last_name>是这张表的联合主键.

那么,MyISAM会建立如下图所示的索引:

上图中最重要的一点是,每个节点的key中的下半部分,是row address.那么MyISAM为什么要这样做呢?

因为在MyISAM中,数据和索引是分开存放的,即数据在一个文件中,索引在一个文件中,MyISAM会先将索引加载到内存中,然后根据Row Address去查找对应的Row.

B-Tree index在InnoDB中的实现

还是上面的那张表,InnoDB会建立如下图所示的索引:

我们可以看到,跟B-Tree index在MyISAM中的实现的差异在于:

  • 在InnoDB中,采用B+Tree来实现
  • 叶子节点存储的是数据而非Row Address

这是为什么呢?

因为在InnoDB中,是把索引和数据存放在一起的,所以有这种结构.

Notice

在这里我们仅仅只是介绍了B-Tree indexes的实现,其他索引的实现,比如Secondary Indexes,跟这个有一定的差距,但是差距不大,到时我们会介绍.

各种索引的优缺点

B-Tree indexes

从上面对B-Tree indexes的实现的介绍,我们可以很清楚地看到B-Tree的有点,它适用于下面的这种查询:

  • 完全匹配
  • leftmost prefix匹配
  • Column prefix匹配
  • 范围查询
  • 一部分精确匹配一部分范围匹配

上面的这几种匹配都是什么意思?

假设我们有一个索引,它包含了下面几列:(first_name, last_name, country).

当我们执行下面这条SQL时,就是完全匹配:

select * from user where first_name = ‘first_name’ and last_name = ‘last_name’ and country = ‘country’

我们可以看到,在上面的SQL中,查询条件包含了索引中的全部的列.

当我们执行下面的这个SQL,就是leftmost prefix匹配:

select * from user where first_name = ‘first_name’

我们可以看到,这条SQL中,只用到了上面的索引中的第一列.

实际上,查询条件为where first_name = … **或where first_name = … and last_name = …where first_name = … and last_name = … and country = …**都是leftmost prefix.

当我们执行下面这条SQL,就是Column prefix:

select * from user where first_column like ‘first%’

我们可以看到,在这个模糊查询中,’%’在最右侧.

当我们执行下面这条SQL,就是范围匹配:

select * from user where first_name = … and last_name > …

在’范围查询’以及’一部分精确匹配,一部分范围匹配’这两种情况中,索引只会匹配到第一个范围查询条件.

那么,B-Tree indexes的缺点是什么呢?

  • 细心的读者已经注意到了,上面我们写的全部SQL中的where部分,都是严格按照索引中列的顺序从左到右匹配的.这是因为,如果不满足这种条件,索引是没用的.我们考虑一下B-Tree index的实现就很容易理解.

  • 不能跳过索引中的列进行匹配,比如,下面的where clause就不会用到索引:where first_name = … and country = …

  • 范围查询关键字右侧的列是用到不到的.比如,如果我们的where clause 是这样的:where first_name = … and last_name like ‘A%’ and country = ‘china’,那么,即使在where clause中写了索引中全部的列,’country’也是不会被用到的.因为like是一个范围查询关键字.

Hash indexes

Hash indexes的优点是,时间复杂度仅为O(1).这是由于哈希算法固有的特性.

另一个优点是,它并不跟B-Tree indexes一样,严格要求索引中列的顺序.

但是,它的缺点也很明显:它只能进行完全匹配.因为计算哈希值时需要索引中的全部列.

就是这个缺点导致它只能被有限的Storage Engine支持,目前只有Memory Storage Engine支持它.

另外,由于索引只包含哈希值和row address,所以这可能导致更多的I/O.

MySQL也无法用Hash Indexes来对Row进行排序.

如果冲突很多的话,维护索引的操作就会比较慢.

Covering Indexes

上文中我们也已经说过了,Covering Indexes就是包含要查询的数据的Index.比如,我们有如下索引:(first_name, last_name, age).那么下面的SQL就会应用Covering Indexes:

select first_name, last_name, age from user where first_name = …

而下面的这条SQL就不会应用Covering Indexes:

select * from user where first_name = …

由于Covering Indexes是用B-Tree实现的,所以B-Tree indexes具有的优缺点,Covering Indexes都有.

由于要查询的数据都包含在索引中,所以,MySQL就不需要根据去读取Row的数据,直接根据索引中的数据,就能得到我们需要的全部数据,对于MyISAM这种需要再次根据Row Address来寻找的Storage Engine,这特别有用.

Secondary Indexes

Secondary Indexes也采用了B-Tree,但是它的叶子节点中既不是Row Address,也不是那一行的地址,而是那一行的primary key,所以,Secondary Indexes在查询时,需要查询两次,先找到primary key,再根据primary key去找对应的row.

用索引来排序

MySQL可以使用任何相同的索引来排序和查找Rows.

但是,只有在满足这种条件时,才能根据索引来排序结果:

  • order by clause中的列和索引中的列完全匹配
  • 全部的列都能按照相同的顺序来排列
  • 如果一个查询join了多个表,Order by clause中的列都是第一张表中的.

用索引来排序跟用索引查询有相同的限制,它必须满足leftmost prefix.

如果where clause或join clause中也有条件,则它们中的列也算入leftmost prefix.

比如,下面两条语句都会使用索引来排序:

select * from user order by first_name asc, last_name asc select * from user where first_name = … order by first_name asc

## 参考资料

«High Performance MySQL, 3rd Edition»