《高性能MySQL》第五章阅读笔记

索引(在MySQL中也叫做键(key))是存储引擎用于快速找到记录的一种数据结构。索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但是当数据量逐渐增大时,性能则会急剧下降。

索引的类型

索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同的存储引擎的索引的工作方式不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层实现也可能不同。

B树(B-Tree)索引

当人们谈论索引的时候,如果没有特别指明类型,那多半说的就是B树索引,它使用B树数据结构或者及其变种来存储数据(例如,Innodb使用的是B+树)。我们使用术语“B树”,是因为MySQL在create table和其他语句中也是用该关键字。存储引擎以不同的方式使用B树索引,性能也各有不同,各有优劣。
假设有如下数据表:

create table People (
	last_name varchar(50) not null,
	first_name varchar(50) not null,
	dob date not null,
	gender enum('m', 'f') not null,
	key(last_name, first_name, dob)
);

对于表中的每一行数据,索引中包含了last_name、first_name和dob列的值,下图显示了该索引是如何组织数据的存储的(该图只展示了索引部分的结构,B+树索引中的叶子结点关联的数据行并未画出):
《高性能MySQL》第五章阅读笔记_第1张图片
从上图可见,索引对多个值进行排序的依据是create table语句中定义索引时列的顺序。可以看到最后两个条目,两个人的姓名一模一样,则根据他们的出生日期来排列顺序。
能使用B树索引查询的类型:

  • 全值匹配
    全值匹配是指和索引中的所有列进行匹配。
  • 匹配最左前缀
    即按照索引顺序匹配索引最左边的列,例如前面提到的索引,可用于查找所有姓氏为Basinger,名字为Vivien的人。
  • 匹配列前缀
    即匹配某一列的值的开头部分,例如前面提到的索引,可用于查找姓氏以B开头的人,或者查找所有姓氏为Basinger,名字以V开头的人。
  • 匹配范围值
    例如查找姓在Allen和Barrymore之间的人,或者查找所有姓氏为Allen,名字在字面B和V之间的人。
  • 精确匹配某一列并范围匹配另外一列
    就如前面提到的,也可以用于查询姓氏是Allen,并且名字是以字面K开头的所有人,即第一列last_name全匹配,第二列first_name范围匹配
  • 只访问索引的查询
    B树索引通常可以支持只访问索引的查询,即查询只需要访问索引,而无需访问数据行。

B树索引的限制:

  • 如果不是按照索引的最左列开始查找,则无法使用索引。
  • 不能跳过索引中的列
  • 如果查询中有某个列的范围查找,则其右边所有列都无法使用索引优化查询。例如:有查询where last_name=‘Smith’ and first_name like ‘J%’ and dob = ‘1976-12-23’,这个查询只能使用索引的前两列,因为这里like是一个范围条件。

看到这里大家应该明白,索引列的顺序是多么的重要。这些限制都和索引的列有关系。在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。

哈希索引

哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每一个数据行的指针。
哈希索引自身只需要存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。
哈希索引的限制:

  • 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。
  • 哈希索引数据并不是按照索引值顺序排序的,所以也就无法用于排序。
  • 哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。
  • 哈希索引只支持等值比较查询,包含IN、=、<=>(注意<>和<=>是不同的操作),也不支持任何的范围查询。
  • 访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列值可能会有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历该哈希值对应的链表中的所有的行指针,逐行进行比较,直到找到所有符合条件的行。
  • 如果哈希冲突很多的话,一些所有维护操作的代价也很高。例如,如果在某个选择性很低的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。

另外由于目前Innodb存储引擎大量的使用,因此需要针对Innodb存储引擎说明一下,Innodb存储引擎支持B+树索引、全文索引,但是Innodb并不支持哈希索引,Innodb中有一个特殊的功能叫做自适应哈希索引,是指Innodb引擎注意到某些索引值被使用得非常频繁时,它会在内存中基于B树索引智商再创建一个哈希索引,这样就让B树索引也具有哈希索引的一些优点。但是这是一个完全自动的、内部的行为,用户无法控制或者配置,不过如果有必要,可以关闭该功能。

索引优点

最常见的B树索引,按照顺序存储数据,所以MySQL可以用来做order by和group by操作。因为数据是有序的,所以B树也就会将相关列值都存储在一起。最后,因为索引中存储了实际的列值,所以某些查询只是用索引就能够完成全部查询。根据这些特点,总结下来索引有如下三个优点:

  1. 索引大大减少了服务器需要扫描的数据量。
    因为有了索引之后,存储引擎不用进行全表扫描。
  2. 索引可以帮助服务器避免排序和临时表。
    因为B树索引内的数据已经排好了序。
  3. 索引可以将随机I/O变为顺序I/O。
    因为B树索引中相关数据行存储在一个磁盘页面中。

评价一个索引的好坏可以使用三星准则:

  1. 索引将相关记录放在一起则获得一星;
  2. 索引中的数据顺序和查找中的排序顺序一致则获得二星;
  3. 索引中的列包含了查询中需要的全部列则获得三星。

索引并不总是会提升性能,只有当索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工作时,索引才是有效的。对于非常小的表,大部分情况下简单的全表扫描更高效,对于中到大型的表,索引的就非常有效。但是对于特大型的表,建立和使用索引的代价将随之增加。

高性能的索引策略

独立的列

独立的列是指索引列不能是表达式的一部分,也不能是函数的参数。
例如,下面这个查询无法使用actor_id列的索引:

select actor_id from sakila.actor where actor_id + 1 = 5;

前缀索引和索引选择性

索引的选择性是指,不重复的索引值(也称为基数,cardinality)和数据表的记录总数(#T)的比值,范围从1/#T到1之间。索引的选择性越高,则查询的效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引的选择性时1,这是最好的索引选择性,性能也是最好的。
有些时候需要索引很长的字符列,这会让索引变得大且慢。这种情况下通常可以索引索引列的开始部分字符,这样可以大大节约索引空间,从而提高索引效率,但是这样会降低索引的选择性。
如下是一个使用前缀索引的例子:

alter table sakila.city add key (city(7));

前缀索引是可以让索引更小、更快的有效办法。但是无法使用前缀索引做order by和group by操作。

多列索引

在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。MySQL5.0和更新版本引入了一种叫做索引合并(index merge)的策略(需要注意的是,索引合并如果使用不当会很容易造成慢查询),一定程序上可以使用表上的多个单列索引来定位指定的行。更早版本的MySQL只能使用其中某一个单列索引,然而很多情况下没有哪一个独立的单列索引是非常有效的。
使用explain语句分析sql时,可以在Extra列中看到是否使用了index merge。索引合并有时候是一种优化的结果,但是实际上更多时候说明了表中索引建的很糟糕:

  • 当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。
  • 当服务器需要对多个索引做联合操作时(通常有多个OR条件),通常需要耗费大量的CPU和内存资源在算法的缓存、排序和合并操作上。特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候。
  • 优化器不会爆这些计算到查询成本中,优化器之关心随机页面读取(?)。这会使得查询的成本被低估,导致该执行计划还不如直接走全表扫描。这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性,但如果是单独运行这样的查询则往往会忽略对并发性的影响。

如果在explain中看到有索引合并,应该好好检查一下查询和表的结构,看是不是已经是最优的。也可以通过参数optimization switch来关闭索引合并功能。也可以使用ignore index提示让优化器忽略掉某些索引。

选择合适的索引列顺序

在如何选择索引的列顺序有一个经验法则:将选择性最高的列放在索引的最前面。这个经验法则在某些时候有用,但通常不如避免随机I/O和排序那么重要。场景不同则选择不同,没有一个放之四海而皆准的法则。但是当不需要考虑排序和分组的情况下,将选择性最高的列放在前面通常是很好的。

聚集索引(聚簇索引)

聚集索引并不是一种单独的索引类型,而是一种数据存储方式。在Oracle中也叫做索引组织表(index organized table)。和聚集索引相对于的叫非聚集索引,在Oracle中也叫堆组织表(heap organized table)。当表有聚集索引时,他的数据行实际上存放在索引的叶子页。《高性能MySQL》中术语“聚集”,就是表示数据行和相邻的键值紧凑地存储在一起。说的通俗一点,就是数据行在磁盘上是按照索引顺序存放在一起,当然这只是定义,实际中例如Innodb存储引擎,只聚集在同一个磁盘页面中的记录,包含相邻键值的页面可能相距很远。也就是说Innodb中,同一个磁盘页面中的记录是因为在同一个页面中,所以是存储在一起的,但是逻辑相邻的磁盘页面由链表连接,实际上可能物理上相隔很远。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚集索引。
以下以Innodb存储引擎做说明。到目前为止,Innodb存储引擎并不支持选择哪个索引作为聚集索引,在定义了主键的情况下,在Innodb中将通过主键聚集数据,如果没有定义主键,Innodb会选择一个唯一的非空索引代替。如果没有这样的索引,Innodb会隐式定义一个主键来作为聚集索引。
聚集索引的优点:

  • 可以把相关数据保存在一起。
  • 数据访问更快
  • 使用覆盖索引扫描的查询可以直接使用叶节点中的主键值。

聚集索引缺点:

  • 聚集数据能最大限度地提高了I/O密集型应用的性能,但如果数据全部都存放在内存中,则访问的顺序就没有那么重要了,聚集索引也就没什么优势了。
  • 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到Innodb表中速度最快的方式(所以在Innodb存储引擎中,主键不推荐使用UUID,这是因为UUID型主键会造成大量的随机I/O操作,推荐使用和数据行没有关系的auto increment整形主键是因为整形存储空间比较少,而且插入数据时,都是按照主键顺序插入,速度会比较快)。
  • 更新聚集索引列的代价会很高,因为会强制Innodb将每个被更新的行移动到新的位置。
  • 基于聚集索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临页分裂的问题。所谓的页分裂问题就是指当行的主键值要求必须将这一行插入到某个已满的磁盘页中时,存储引擎会将该页分裂成两个页面来容纳该行。页分裂会导致表占用更多的磁盘空间以及为了维护B+树索引移动元素导致的开销。
  • 非聚集索引的叶子结点中包含了对应数据行的主键列。这可能会导致非聚集索引占用比较大的空间。
  • 非聚集索引访问需要两次索引查询,而不是一次。这是因为非聚集索引保存的行指针并不是指向数据行的物理位置的指针,而是行的主键值。这也就意味着通过非聚集索引查找行时,存储引擎需要找到非聚集索引的叶子结点获取对应的主键值,然后根据这个值去聚集索引中查找对应的行。在MySQL中之所以这样设计是为了避免数据行因为更新、插入等操作导致数据行需要移动时也同时需要维护非聚集索引。

InnoDB和MyISAM的数据分布对比
下面让我们来看看InnoDB和MyISAM是如何存储下面这个表的:

create table t (
	id int not null primary key auto_increment,
	name varchar(50) not null,
	sex enum('m', 'f') not null,
	key(name)
);

表中有4条记录:
1, zhangsan, m
3, lisi, m
5, wangwu, f
9, zhaoliu, m


MyISAM的数据分布比较简单,我们先介绍MyISAM的情况。MyISAM中的数据行和索引是分开存储的,也就是说MyISAM的索引都是非聚集索引。非聚集索引的主键索引和普通索引没有本质区别:

  • 有连续聚集的区域单独存储数据行。
  • 主键索引的叶子结点,存储主键,与对应行记录的指针
  • 普通索引的叶子结点,存储索引列,与对应行记录的指针

也就是说MyISAM的表是可以没有主键的。主键索引和普通索引是两棵独立的索引B+树,通过索引列查找时,先定位到B+树的叶子结点,再通过指针定位到行记录(也就是对应数据行移动时,需要维护索引)。MyISAM存储示意图如下所示:
《高性能MySQL》第五章阅读笔记_第2张图片

InnoDB存储引擎支持聚集索引,所以使用非常不同的方式存储同样的数据。InnoDB存储数据的示意图如下所示:
《高性能MySQL》第五章阅读笔记_第3张图片
如上图所示,InnoDB的主键索引与行记录是存储在一起的,并没有独立区域存储数据行。主键索引的叶子结点存储的是主键和对应的数据行而不是指针,因此InnoDB的PK查询是非常快的。另外需要注意的是InnoDB默认是在主键上使用聚集索引,如果没有定义主键会使用唯一的非空索引代替,如果没有这样的索引,会隐式定义一个主键来做聚集索引。也就是说InnoDB的主键是一定会存在的。
还有一个值得注意的问题是,顺序主键在高并发的情况下可能会造成明显的锁争用。

覆盖索引

所谓的覆盖索引是指一个索引中包含了所有需要查询的字段的值。
覆盖索引是非常有用的工具,能够极大的提高性能。覆盖索引带来的好处如下:

  • 索引条目通常远小于数据行的大小,所以如果只需要读取索引,那么MySQL就会极大的减少数据访问量。
  • 因为B树索引是按照列值顺序存储的(至少单个页内是如此),所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要小很多。
  • 由于InnoDB的聚集索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级索引能覆盖查询,则可以避免对主键索引的二次查询。
  • 一些存储引擎例如MyISAM在内存中只缓存索引,数据则依赖于操作系统缓存,因此要访问数据需要一次系统调用。这可能会导致严重的性能问题。

但是需要注意的是,并不是所有的存储引擎都支持覆盖索引。

总结

索引是一个复杂的问题,理解索引最好的办法就是结合示例,多做练习。高效的索引策略当然不止上面总结的这些示例,还比如有索引引发的锁、冗余和重复索引、使用索引扫描来做排序(B树索引是已经排好序的)等等策略。

参考:
《高性能MySQL》
1分钟了解MyISAM与InnoDB的索引差异

你可能感兴趣的:(MySQL)