高性能MySQL之创建高性能的索引(三)

版权声明:YETA https://blog.csdn.net/qq_28958301/article/details/89357293

索引(MySQL中也叫做“键”)是存储引擎用于快速找到记录的一种数据结构。

1. 索引基础

在MySQL中,存储引擎首先在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行,例如:

SELECT id, age FROM user WHERE age = 10;

如果在age列上建立索引,那么MySQL将使用该索引找到age=10的行,即MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行。

1.1 索引的类型

在MySQL中,索引是在存储引擎层而不是服务器层实现的。

1.1.1 B-Tree索引

B-Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。

B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据。例如,在一个基于文本域的索引树上,按字母顺序传递连续的值进行查找是非常合适的,如“找出所有以I到K开头的名字”。

注意,索引对多个值进行排序的依据是CREATE TABLE语句定义索引时列的顺序。

参考下列建表语句,可以使用B-Tree索引的查询类型有:

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)
);
  • 全值匹配

和索引中的所有列进行匹配,例如“查找姓名为Cuba Allen、出生于1960-01-01的人”。

  • 匹配最左前缀

只使用索引的第一列,例如“查找所有姓为Allen的人”。

  • 匹配列前缀

只匹配某一列的值的开头部分,例如“查找所有以J开头的姓的人”。

  • 匹配范围值

例如“查找姓在Allen和Barrymore之间的人”。

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

例如“查找所有姓为Allen,并且名字是字母K开头的人”。

  • 只访问索引的查询

查询只需要访问索引,而无须访问数据行。

B-Tree的限制:

  • 如果不是按照索引的最左列开始查找,则无法使用索引;
  • 不能跳过索引中的列;
  • 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。

这些限制都和索引列的顺序有关,在性能优化的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。

1.1.2 哈希索引

哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。

在MySQL中,只有Memory引擎显式支持哈希索引。

在数据仓库应用中有一种经典的“星型”schema,需要关联很多查找表,哈希索引就非常适合查找表的需求。

1.1.3 空间数据索引(R-Tree)

MyISAM表支持空间索引,可以用作地理数据存储。这类索引无须前缀查询,会从所有维度来索引数据。

1.1.4 全文索引

全文索引查找的是文本中的关键词,而不是直接比较索引中的值,类似于搜索引擎。

在相同的列上同时创建全文索引和基于值的B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST操作,而不是普通的WHERE条件操作。

2. 索引的优点

索引大大减少了服务器需要扫描的数据量;

索引可以帮助服务器避免排序和临时表;

索引可以将随机I/O变为顺序I/O。

3. 高性能的索引策略

3.1 独立的列

如果查询中的列不是独立的,那么MySQL就不会使用索引。

“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。

查询示例1:


查询1中type为ref表示非唯一性索引扫描,本质上也是一种索引访问,返回所有匹配某个单独值的行;查询2中type为index表示全索引文件扫描,将直接从索引数据结构中找寻数据。性能ref > index。

查询示例2:

3.2 前缀索引和索引选择性

有时候需要索引很长的字符列,这会让索引变得大且慢。

索引的选择性:不重复的索引值和数据表的记录总数的比值,越高则查询效率越高,唯一索引的选择性是1。

对于BLOB、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。

前缀索引的诀窍在于选择足够长的前缀以保证较高的选择性,同时又不能太长。

方式1:为了决定前缀的合适长度,需要找到最常见的值的列表,然后和最常见的前缀列表进行比较。

方式2:计算完整列的选择性,并使前缀的选择性接近于完整列的选择性。

创建前缀索引:

ALTER TABLE USER ADD KEY (city(2));

前缀索引是一种能使索引更小、更快的有效办法,但是缺点是:MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。

3.3 多列索引

一个常见的错误:为每个列创建独立的索引,或者按照错误的顺序创建多列索引。

在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。MySQL5.0和更新版本引入了一种叫“索引合并”的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。例如以下查询(已知在age和city列分别建立了单列索引):

索引合并策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建得很糟糕:

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

3.4 选择合适的索引列顺序

在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY和DISTINCT等子句的查询需求。

当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。

3.5 聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。

InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。

因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。

InnoDB通过主键聚集数据,如果没有定义主键,InnoDB会选择一个唯一的非空索引代替,如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。

聚簇索引的优点:

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

聚簇索引的缺点:

  • 如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚簇索引就什么优势了;
  • 插入速度严重依赖于插入顺序;
  • 更新聚簇索引的代价很高;
  • 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临页分裂的问题;
  • 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候;
  • 二级索引可能比想象的要更大,因为二级索引的叶子节点包含了引用行的主键列;
  • 二级索引访问需要两次索引查找,而不是一次。

使用InnoDB时应该尽可能按主键顺序插入数据,并且尽可能地使用单调增加的聚簇键的值来插入新行。

3.6 覆盖索引

MySQL可以使用索引来直接获取列的数据,这样就不再需要读取数据行。如果一个索引包含(覆盖)所有需要查询的字段的值,就称之为“覆盖索引”。

覆盖索引的优点:

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

覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都补存储索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引。

当发起一个被索引覆盖的查询时,在EXPLAIN的Extra列可以看到“Using index”的信息。

已知如下表结构:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `age` int(11) NOT NULL,
  `birthday` date NOT NULL,
  `city` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `age_city` (`age`,`city`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=111 DEFAULT CHARSET=utf8;

索引无法覆盖查询的例子:

重写查询:

这种方式叫延迟关联,因为延迟了对列的访问,在查询的第一阶段MySQL可以使用覆盖索引,在FROM字句的子查询中遭到匹配的id,然后根据这些id的值在外层查询匹配获取需要的所有列值。

3.7 使用索引扫描来做排序

MySQL有两种方式可以生成有序的结果:通过排序操作、或者按索引顺序扫描。

如果EXPLAIN出来的type列的值为“index”,则说明MySQL使用了索引扫描来做排序。

只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。

已知如下表结构:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `age` int(11) NOT NULL,
  `birthday` date NOT NULL,
  `city` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `age_city` (`age`,`city`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=111 DEFAULT CHARSET=utf8;

观察以下几个查询的EXPLAIN结果:

前两个查询没有出现文件排序操作。

3.8 压缩(前缀压缩)索引

MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。

默认只压缩字符串,可以通过参数设置对整数做压缩,在CREATE TABLE语句中指定PACK_KEYS参数来控制索引压缩的方式。

压缩块使用更少的空间,代价是某些操作可能更慢,因为每个值的压缩前缀都依赖前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。正序的扫描速度不错,但是如果是倒序扫描就不是很好了。

3.9 冗余和重复索引

MySQL允许在相同列上创建多个索引,但是需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。

重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。例如,PRIMARY(ID)和UNIQUE(ID)是重复索引,KEY(col)和FULLTEXT(col)则不是重复索引。

冗余索引是指如果创建了索引(A, B),再创建索引(A)就是冗余索引,而(B, A)或(B)则不是冗余索引。

3.10 索引和锁

索引可以让查询锁定更少的行。InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量。但这只有当InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效。如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层以后,MySQL服务器才能应用WHERE字句。

例如上述查询中,虽然仅仅会返回id为2到4的行,但是实际上获取了id为1到4的行的排他锁。

4. 总结

  • 单行访问是很慢的;
  • 按顺序访问范围数据是很快的;
  • 索引覆盖查询是很快的;
  • 尽可能选择合适的索引以避免单行查找;
  • 尽可能使用数据原生顺序从而避免额外的排序操作;
  • 尽可能使用索引覆盖查询。

猜你喜欢

转载自blog.csdn.net/qq_28958301/article/details/89357293