高性能MySQL读书摘要(五)创建高性能的索引

索引优化应该是最有效的优化手段了。”最优”的索引有时比一个“好的”索引性能要好两个数量级。创建一个真正的“最优”的索引经常需要重写查询,所以本章和下一章关系密切。

5.1 索引基础

索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列。创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的。

5.1.1 索引的类型

在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。

B-Tree索引

实际上很多存储引擎使用的是B+Tree,即每一个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的范围遍历。底层的存储引擎也可能使用不同的存储结构,例如,NDB集群存储引擎内部实际上使用了T-Tree结构存储这种索引,即使其名字是BTREE;InnoDB则使用的是B+Tree。
存储引擎以不同的方式使用B-Tree索引,性能也各有不同,各有优劣。例如,MyISAM使用前缀压缩技术使得索引更小,但InnoDB则按照原数据格式进行存储。再如MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
B-Tree通常意味着所有的值都是按照顺序存储的,并且每一个叶子页到根的距离相同。图5-1展示了B-Tree索引的抽象表示。
在这里插入图片描述
叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他节点页(不同引擎的“指针”类型不同)。图5-1中仅绘制了一个节点和其对应的叶子节点,其实在根节点和叶子节点之间可能又很多层节点页。树的深度和表的大小直接相关。但是建议树的深度不要超过4层,即一般表大小不要大于500w行记录。

B-Tree 对索引列是顺序组织存储的,所以很适合查找范围数据,所以mysql不适合用uuid作主键。例如,在一个基于文本域的索引树上,按字母顺序传递连续的值进行查找是非常合适的(这里与字符集和对应的排序集有关)。所以像“找出所有以I到K开头的名字”这样的查找效率会非常高。

假设有如下的数据表:

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,  -- 这个对应java中的什么类型可以试下。
    key(last_name,first_name,dob)

对于表中的每一行数据,索引中包含了last_name,first_name和dob列的值,图5-2显示了该索引是如何组织数据的存储的。

在这里插入图片描述
请注意,索引对多个值进行排序的依据是CREAT TABLE语句中定义索引时列的顺序。看一下最后两个条目,两个人的姓和名都一样,则根据他们的出生日期来排列顺序。可以使用B-Tree索引的查询类型。B-Tree索引适用于全键值、键值范围或键值前缀查找。其中键前缀查找只适用于根据最左前缀的查找。前面所述的索引对如下类型的查询有效。

  1. 全值匹配
    全值匹配指的是和索引中的所有列进行匹配。
  2. 匹配最左前缀
    前面提到的索引可用于查找所有姓为Allen的人,即只使用索引的第一列。
  3. 匹配列前缀
    例如前面提到的索引可用于查找姓在Allen和Barrymore之间的人。这里也只使用了索引的第一列。
  4. 精确匹配某一列并范围匹配另外一列
    前面提到的索引页可以用于查找所有姓为Allen,并且名字是字母K开头的人。即第一列last_name全匹配,第二列first_name范围匹配。
  5. 只访问索引的查询
    B-Tree通常可以支持“只访问索引的查询”,即查询只需访问索引,而无须访问数据行。后面我们将单独讨论这种“覆盖索引”的优化。

下面是一些关于B-Tree索引的限制:

  1. 如果不是按照索引的最左列开始查找,则无法使用索引。
  2. 不能跳过索引中的列。
  3. 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。如有查询WHERE last_name=’Smith’ AND first_name LIKE ‘J%’ AND dob = ‘1976-12-23’,这个查询条件只能使用索引的前两列,因为这里LIKE是一个范围条件。
    到这里读者应该明白,前面提到的索引列的顺序是多么重要:这些限制都和索引列的顺序有关。在优化的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。

哈希索引

哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。在MySQL中,只有Memory引擎显示支持哈希索引。这也是Memory引擎表的默认索引类型,Memory引擎同时也支持B-Tree索引。
哈希索引有以下限制:

  1. 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。
  2. 哈希索引数据并不是按照索引值顺序存储的,采用内存中随机桶的方式,所以也就无法用于排序。
  3. 哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值得。例如,在数据列(A,B)上建立哈希索引,如果查询只有数据列A,则无法使用该索引。
  4. 哈希索引只支持等值比较查询,包括=,IN(),。也不支持任何范围查找。
  5. 访问哈希索引的数据非常快,除非有很多哈希冲突。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。
  6. 如果哈希冲突很多的话,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值得链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。
    InnoDB引擎有一个特殊的功能叫做“自适应哈希索引”。当InnoDB注意到某些索引值被使用得非常频繁时,它会在内存中基于B-Tree索引之上再创建一个哈希索引,这样就让B-Tree索引也具有哈希索引的一些优点,比如快速的哈希查找。这是一个完全自动的、内部的行为,用户无法控制或者配置,不过如果有必要,完全可以关闭该功能。
    有时可以自己用单独一个列保存另外一个列的哈希值,但建议用CRC32()或者MD5()函数返回值的一部分来作为自定义哈希函数。不要使用SHA1()和MD5()作为哈希函数。因为这两个函数计算出来的哈希值时非常长的字符串,会浪费大量空间,比较时也会更慢。

处理哈希冲突。当使用哈希索引进行查询的时候,必须在WHERE子句中包含常量值。

SELECT id FROM url WHERE url_crc=CRC32("http://www.mysql.com") AND url = "http://www.mysql.com";

空间数据索引

MyISAM表支持空间索引,可以用作地理数据存储。和B-Tree索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。查询时,可以有效地使用任何维度来组合查询。必须使用MySQL的GIS相关函数如MBRCONTAINS()等来维护数据。MySQL的GIS支持不完善。建议用开源关系数据库系统中对GIS的解决方案做的比较好的PostgreSQL的PostGIS。

全文检索

全文索引是一种特殊的类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。它有许多需要注意的细节。如停用词、词干和复数、布尔搜索等。全文索引更类似于搜索引擎做的事情,而不是简单的WHERE条件匹配。
在相同的列上同时创建全文索引和基于值得B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST操作,而不是普通的WHERE条件操作。

其他索引类别

TokuDB使用分形树索引,这是一类较新开发的数据结构,既有B-Tree的很多优点,也避免了B-Tree的一些缺点。如果通读完本章,可以看到很多关于InnoDB的主题,包括聚簇索引,覆盖索引等。多数情况下,针对InnoDB的讨论也都适用于TokuDB。

5.2 索引的优点

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

  1. 索引大大减少了服务器需要扫描的数据量。
  2. 索引可以帮助服务器避免排序和临时表。
  3. 索引可以将随机I/O变成顺序I/O。
    可以参考Lahdenmaki和Mike Leach编写的Relational Database Index Design and the Optimizers一书,该书详细介绍了如何评价一个索引是否适合某个查询的“三星系统”:索引将相关记录放到一起则获得一星;如果索引中的数据顺序和查找中的排列顺序一致则获得二星;如果索引中的列包含了查询中需要的全部列获得“三星”。
    索引并不是最好的工具。对于非常小的表,大部分情况下简单的全表扫描更高效。对于中到大型的表,索引就非常有效。但对于特大型的表,建立和使用索引的代价将随之增长。则需要使用分区技术分出查询需要的一组数据,而不是一条记录一条记录地匹配。如果表的数量特别多(如TB级别),定位单条记录的意义不大,所以经常会使用块级别元数据技术来替代索引。可以建立一个元数据信息表,用来查询需要用到的某些特性。如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录“哪个用户的信息存储在哪个表中“的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧(分库分表)。

5.3 高性能索引策略

5.3.1 独立的列

如果查询中的列不是独立的,则MySQL就不会使用索引。“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。
如:SELECT actor_id FROM actor WHERE actor_id + 1 = 5;

5.3.2 前缀索引和索引选择性

有时候需要索引很长的字符串,这样让索引变得大且慢。通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样会降低索引的选择性。一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOG、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的“基数”应该接近于完整列的“基数”。

5.3.3 多列索引

集中精力优化索引的顺序。

5.3.4 选择合适的索引列顺序

在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY和DISTINCT等子句的查询需求。对于如何选择索引的列顺序有一个经验法则:将选择性最高的列放到索引最前列,但通常不如避免随机IO和排序那么重要。我们知道有时排序需要占用很多时间。当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化WHERE条件的查找。然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值得分布有关。可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。

以下面的查询为例:

SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 584;

是应该创建一个(staff_id,customer_id)索引还是应该颠倒一下顺序?可以跑一些查询来确定在这个表中值得分布情况,并且确定哪个列的选择性更高。先用下面的查询预测一下。看看各个WHERE条件的分支对应的数据基数有多大:

在这里插入图片描述
根据前面的经验法则:应该将索引列customer_id放到前面,因为对应条件值的customer_id数量更小。我们再来看看对于这个customer_id的条件值,对应的staff_id列的选择性如何:
在这里插入图片描述
这样做有一个地方需要注意,查询的结果非常依赖于选定的具体值。如果按上述办法优化,可能对其他一些条件值得查询不公平,服务器的整体性能可能变得更糟,或者其他某些查询的运行变得不如预期。

如果是从诸如pt-query-digest这样的工具的报告中提取“最差”查询,那么再按上述办法选定的索引顺序往往是非常高效的。如果没有类似的具体查询来运行,那么最好还是按经验法则来做,因为经验法则则考虑的是全局基数和选择性,而不是某个具体查询:
在这里插入图片描述
customer_id的选择性更高,所以答案是将其作为索引列的第一列:

ALTER TABLE payment ADD KEY(customer_id, staff_id);

下面是一个我们遇到过的真实案例,在一个用户分享购买商品和购买经验的论坛上,这个特殊表上的查询运行得非常慢:
在这里插入图片描述
这个查询看似没有建立合适的索引,但是看下EXPLAIN的结果:

在这里插入图片描述
MySQL为这个查询选择了索引(groupId,userId),如果不考虑列的基数,这看起来是一个非常合理的选择,但如果考虑一下user ID 和 group ID条件匹配的行数,可能就会有不同的想法了。
在这里插入图片描述

5.3.5 聚簇索引

​聚簇索引不是单独的索引类型,而是一种数据存储方式。InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。
​当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中。术语“聚簇”表示数据行和相邻的键值紧凑地存在一起。InnoDB将通过主键聚集数据,这也就是说图5-3中的“被索引的列”就是主键列。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录。包含相邻键值得页面可能会相距甚远。所以,聚簇主键可能对性能有帮助,但也可能导致严重的性能问题。
在这里插入图片描述
聚集的数据有一些重要的优点:

  1. 可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚簇索引,则每封邮件可能导致一次磁盘I/O。
  2. 数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。
  3. 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。

如果在设计表和查询时能充分利用上面的优点,那就能极大地提升性能。同时,聚簇索引也有一些缺点:
4. 聚簇数据最大限度地提高了I/O密集型应用的性能,但如果数据全部都放在内存中,则访问顺序就没有那么重要,聚簇索引则没啥优势。这是充分利用了不同的存储介质。
5. 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。
6. 更新聚簇索引的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。
7. 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临"页分裂”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将给页分裂成两页来容纳该行,这就是页分裂。页分裂会导致表占用更多的磁盘空间。
8. 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
9. 二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。

最后一点可能让人有些疑惑,为什么二级索引(辅助索引)需要两次索引查找?答案在于二级索引中保存的“行指针”的实质。要记住,二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值,然后根据这个值去聚簇索引中查找到对应的行。这里做了重复的工作:两次B-Tree查找而不是一次。

InnoDB的数据分布

因为InnoDB支持聚簇索引,所以使用非常不同的方式存储同样的数据。
在这里插入图片描述
该图显示了整个表,而不是只有索引(前面的图只有索引)。因为在InnoDB中,聚簇索引“就是”表,所以不像MyISAM那样需要独立的行存储。MyISAM是按照数据插入的顺序存储在磁盘上的。
​聚簇索引的每一个叶子节点都包含了主键值、事务ID、用于事务和MVCC的回滚指针以及所有的剩余列(在这个例子是col2)。如果主键是一个列前缀索引,InnoDB也会包含完整的主键列和剩下的其他列。
​还有一点与MyISAM的不同的是,InnoDB的二级索引和聚簇索引很不相同。InnoDB二级索引的叶子节点中存储的不是“行指针”,而是主键值,并以此作为指向行的“指针”。这样的策略减少了当出现行移动或者数据分页时二级索引的维护工作。使用主键值当做指针会让二级索引占用更多的空间,换来的好处是,InnoDB在移动行时无须更新二级索引中的这个“指针”。
​图中展示了B-Tree的叶子节点结构,InnoDB的非叶子节点包含了索引列和一个指向下一级节点的指针。这对聚簇索引和二级索引都适用。

在InnoDB表中采用主键顺序或者单调递增插入行

以下内容是重点。
如果使用InnoDB表并没有什么数据需要聚集,那么可以定义一个代理键作为主键,这种主键的数据应该和应用无关,最简单的方法是使用AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入,对于根据主键做关联操作的性能也会更好。从性能的角度考虑,使用UUID来作为聚簇索引则会很糟糕:它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。如:
我们测试了这两个表的设计。首先,我们在一个有足够内容容纳索引的服务器上向这两个表各插入100万条记录。然后向这两个表继续插入300万条记录,使索引的大小超过服务器的内存容量,如下:
在这里插入图片描述
插入的时间考虑:
注意到向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大。这一方面是由于主键字段更长;另一方面毫无疑问是由于页分裂和碎片导致的。
为了明白为什么会这样,来看看往第一个表中插入数据时,索引发生了什么变化。
在这里插入图片描述
上图所示,因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改),下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加锁,主键页就会近似于被顺序的记录填满,这也正是所期望的结果(然而,二级索引页可能是不一样的)。
对比一下向第二个使用了UUID聚簇索引的表插入数据,看看有什么不同,如下图5-11所示。
因为新行的主键值不一定比之前插入的大,所以InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置–通常是已有数据的中间位置–并且分配空间。这会增加很多的额外工作,并导致数据分布不够优化。下面是使用UUID的缺点:

  1. 写入的目标页可能已经刷到磁盘上并从缓存中移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机IO。
  2. 因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。而频繁的页分裂,页会变得稀疏并被不规则地填充,所以最终数据会有碎片。

在把这些随机值载入到聚簇索引以后,也许需要做一次OPTIMIZE TABLE来重建表并优化页的填充。

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

在这里插入图片描述
下面是顺序插入主键的坏处。
在这里插入图片描述

覆盖索引

​通常大家都会根据查询的WHERE条件来创建合适的索引,不过这只是索引优化的一个方面。设计优秀的索引应该考虑到整个查询,而不单单是WHERE条件的部分。如果索引的叶子节点中已经包含要查询的数据,那么就没有必要回查表。如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称为覆盖索引。
覆盖索引时非常有用的工具,能够极大地提高性能。考虑一下如果查询只需要扫描索引而无须回表,会带来以下好处:

  1. 索引条目通常远小于数据行大小,所以如果只需要读取索引,那MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放在内存中。
  2. 因为索引时按照列值顺序存储的(至少在单个页内是如此),所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少很多。
  3. 一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用。
  4. 由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可避免对主键索引的二次查询。
    ​不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储列的值,而哈希索引、空间索引和全文索引等都不存在索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引。

当发起一个被索引覆盖的查询时,在EXPLAN的Extra列可以看到"Using index"的信息。例如,表sakila.inventory有一个多列索引(store_id,film_id)。MySQL如果只需访问这两列,就可以使用这个索引做覆盖索引,如下所示:

EXPLAIN SELECT store_id, film_id FROM sakila.inventory

在这里插入图片描述
索引覆盖查询还有很多陷阱可能会导致无法实现优化。MySQL查询优化器会在执行查询前判断是否有一个索引能进行覆盖。假设索引覆盖了WHERE条件中的字段,但不是整个查询涉及的字段。如果条件为假(false),MySQL 5.5 和更早的版本也总是会回表获取数据行,尽管并不需要这一行且最终会被过滤掉。

很容易把Extra列的“Using index”和type列的“index”搞混淆。其实这两者完全不同,type列和覆盖索引毫无关系;它只是表示这个查询访问数据的方式,或者说是MySQL查找行的方式。MySQL手册中称之为连接方式(join type)。

索引无法进行覆盖的原因

在这里插入图片描述
上面这种情况无法使用覆盖索引,有两个原因:

  1. 因为查询选择了表中的所有列,没有任何索引能覆盖这个查询。不过,理论上MySQL还有一个捷径可以利用:WHERE条件中的列是由索引可以覆盖的,因此MySQL可以使用该索引找到对应的actor并检查title是否匹配,过滤之后再读取需要的数据行。
  2. MySQL不能在索引中执行LIKE操作。这是底层存储引擎API的限制,MySQL 能在索引中做最左前缀匹配的LIKE比较,因为该操作可以转换为简单的比较操作,但是如果是通配符开头的LIKE查询,存储引擎就无法做比较匹配。这种情况下,MySQL服务器只能提取数据行的值而不是索引值来做比较。

利用索引往往有两个解决方案,重写查询并巧妙设计索引。先将索引扩展至覆盖三个数据列(artist,titile,prod_id),然后按如下方式重写查询:

在这里插入图片描述
我们把这种方式叫做延迟关联,因为延迟了对列的访问,在翻页时,比较靠后的页,一般也使用延迟关联的方式进行查询。在查询的第一阶段MySQL可以使用覆盖索引,在FROM子句的子查询中找到匹配的prod_id,然后根据这些pro_id值在外层查找匹配获取需要的所有列值。虽然无法使用索引覆盖整个查询,但总比完全无法利用索引覆盖的好。

这样优化的效果取决于WHERE条件匹配返回的行数。假设这个products表有100w行,我们来看一下上面两个查询在三个不同的数据集上的表现,每个数据集都包含100w行。

  1. 第一个数据集,Sean Carrey出演了30000部作品,其中有20000部的标题中包含了Apollo。
  2. 第二个数据集,Sean Carrey出演了30000部作品,其中40部的标题中包含了Apollo。
  3. 第三个数据集,Sean Carrey出演了50部作品,其中10部的标题中包含了Apollo。

在这里插入图片描述
下面是对结果的分析:

  1. 在示例1中,查询返回了一个很大的结果集,因此看不到优化的效果。大部分时间都花在读取和发送数据上了。
  2. 在示例2中,经过索引过滤,尤其是第二个条件过滤后只返回了很少的结果集,优化的效果非常明显:在这个数据集上性能提高了5倍,优化后的查询的效率主要得益于只需要读取40行完整的数据行,而不是原查询中需要的30000行。
  3. 在示例3中,显示了子查询效率反而下降的情况。因为索引过滤时符合第一个条件的结果集已经很小,所以子查询带来的成本反而比从表中直接提取完整行更高。

在大多数存储引擎中,覆盖索引只能覆盖那些只访问索引中部分列的查询。不过可以更进一步优化InnoDB。回想一下,InnoDB的二级索引的叶子节点都包含了主键的值,这意味着InnoDB的二级索引可以有效地利用这些“额外”的主键列来覆盖查询。也就是说如果查询的WHERE条件中包含覆盖索引列,则也可以使用包含Id。如下。
在这里插入图片描述

5.3.7 使用索引扫描来做排序

​MySQL有两种方式可以生成有序的结果:(1)通过排序操作;(2)或者按索引顺序扫描;如果EXPLAIN 出来的type列的值为“index”。则说明MySQL出来的type列的值为“index”,则说明MySQL使用了索引扫描来做排序。如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在I/O密集型的工作负载时。
​MySQL可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务。只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则MySQL都需要执行排序操作,而无法利用索引排序。

有一种情况下ORDERY BY子句可以不满足索引的最左前缀的要求, 就是前导列为常量的时候,如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以“弥补”索引的不足。

在这里插入图片描述
在这里插入图片描述

冗余索引带来的性能问题

​MySQL允许在相同列上创建多个索引。重复索引是指在相同的列上按照相同的顺序创建相同类型的索引。应该避免这样创建重复索引,发现以后也应该立即移除。

有时会在不经意间创建了重复索引,例如下面的代码:

CREATE TABLE test (
ID INT NOT NULL PRIMARY KEY,
A INT NOT NULL,
B INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
) ENGINE=InnoDB;

上面的写法其实创建了三个重复的索引,事实上,MySQL的唯一限制和主键限制都是通过索引实现的。

冗余索引和重复索引有一些不同。如果创建了索引(A,B),再创建索引(A)就是冗余索引。因为这只是前一个索引的前缀索引。还有一种情况是将一个索引扩展为(A,ID),其中ID是主键,对于InnoDB来说主键列已经包含在二级索引中了,所以这也是冗余的。但有时出于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询的性能。例如,如果在整数列上游一个索引,现在需要额外增加一个很长的VARCHAR列来扩展该索引,那性能可能会急剧下降。
在这里插入图片描述
解决冗余索引和重复索引的方法很简单,删除这些索引就可以,但首先要做的就是找出这样的索引。
​在决定哪些索引可以被删除的时候要非常小心。回忆一下,在前面的InnoDB的示例表中,因为二级索引的叶子节点包含了主键值,所以在列(A)上的索引就相当于在(A,ID)上的索引。如果有像WHERE A = 5 ORDER BY ID 这样的查询,这个索引会很有作用。但如果将索引扩展为(A,B),则实际上就变成了(A,B,ID),那么上面查询的ORDER BY子句就无法使用该索引做排序,而只能用文件排序了。

未使用的索引

有一些完全没有使用的索引,建议删除。有两个工具可以帮助定位未使用的索引。最简单有效的办法是在Percana Server或者MariaDB中先打开userstates服务器变量(默认是关闭的),然后让服务器正常运行一段时间,再通过查询INFORMATION_SCHEMA.STATISTICS就能查询到每个索引的使用频率。

索引和锁

索引可以让查询锁定更少的行。如果你的查询从不访问那些不需要的行,那么就会锁定更少的行,从两个方面来看这对性能都有好处。

但是MySQL有时会使用间隙锁锁住不需要的数据。如果不能使用索引查找和锁定行的话就可能会更糟糕。MySQL会做全表扫描并锁住所有的行。
关于InnoDB、索引和锁有一些很少有人知道的细节:InnoDB在二级索引上使用共享(读)锁,但访问主键索引需要排他(写)锁。这消除了使用覆盖索引的可能性,并且使得SELECT FOR UPDATE 比 LOCK IN SHARE MODE或非锁定查询要慢很多。

5.4 索引案例学习

如果MySQL使用某个索引进行范围查询,也就无法再使用另一个索引(或者是该索引的后续字段)进行排序了。

5.4.1 支持多种过滤条件

现在需要看看哪些列拥有很多不同的取值,哪些列在WHERE子句中出现得最频繁。在有更多不同值的列上创建索引的选择性会更好。一般来说这样做是对的,因为可以让MySQL更有效地过滤掉不需要的行。
​country列的选择性不高,但可能很多查询都会用到。sex列的选择性肯定很低,但也会在很多查询中用到。所以考虑到使用的频率,还是建议在创建不同组合索引的时候将(sex, country)列作为前缀。
在这里插入图片描述
这个案例显示了一个基本的原则:考虑表上所有的选项。当设计索引时,不要只为现有的查询考虑需要哪些索引,还需要考虑对查询进行优化。如果发现某些查询需要创建新索引,但是这个索引又会降低另一些查询的效率,那么应该想一下是否能优化原来的查询。应该同时优化查询和索引以找到最佳的平衡,而不是闭门造车去设计最完美的索引。

5.4.2 避免多个范围条件

​在这个案例中,优化器的特性是影响索引策略的一个很重要的因素。如果未来版本的MySQL能够实现松散索引扫描,就能在一个索引上使用多个范围条件,那也就不需要为上面考虑的这类查询使用IN()列表了。

5.4.3 优化排序

我们经常会遇到在翻页时,越往后面的分页,MySQL需要花费大量的时间来扫描丢弃的数据,需要的成本就越高。

SELECT <cols> FROM profiles WHERE sex = 'M' ORDER BY rating LIMIT 100000,10;

我们能想到的办法有反范式化、预先计算和缓存等方式,当然也可以限制用户翻页数。

但是最好的办法是延迟关联,通过使用覆盖索引查询返回需要的主键,再根据这些主键关联原表获得需要的行。这可以减少MySQL扫描那些要丢弃的行数。下面这个查询显示了如何高效地使用(sex,rating)索引进行排序和分页:

SELECT <col> FROM profiles INNER JOIN( SELECT <primary key cols> FROM profiles WHERE x.sex = 'M' ORDER BY rating LIMIT 100000,10 AS x USING<primary key cols>);

5.5 维护索引和表

5.5.1 找到并修复损坏的表

MyISAM存储引擎,如果出现表损坏,将是致命性的。如果InnoDB引擎的表出现了损坏,那么一定是发生了严重的错误,InnoDB自身的设计保证了它并不容易被损坏。如果发生损坏,一般要么是数据库的硬件问题例如内存或者磁盘问题(有可能),抑或是InnoDB本身的缺陷(不太可能)。常见的类似错误通常是由于尝试使用rsync备份InnoDB导致的。不存在任何查询能让InnoDB表损坏,也不用担心暗处有"陷阱”。如果某条查询导致InnoDB数据的损坏,那一定是遇到了bug,而不是查询的问题。

如果遇到了数据损坏,一定要找到损坏的原因而是修复,因为可能会再次损坏。可以通过设置innodb_force_recovery参数进行InnoDB的强制恢复模式来修复数据,更多细节可以参考MySQL手册。另外,还可以使用开源的InnoDB数据恢复工具箱直接从InnoDB数据文件恢复除数据。

猜你喜欢

转载自blog.csdn.net/gonghaiyu/article/details/107805165