高性能MySQL(第三版)第五章:创建高性能的索引

第五章 创建高性能的索引

  • 5.1索引基础
    • 5.1.1 索引的类型
      • 5.1.1.1 B-Tree索引
      • 5.1.1.2 哈希索引
      • 5.1.1.3 空间数据索引(R-Tree)
      • 5.1.1.4 全文索引
  • 5.2 索引的优点
  • 5.3 高性能的索引策略
    • 5.3.1 独立的列
    • 5.3.2 前缀索引和索引选择性
    • 5.3.3 多列索引
    • 5.3.4 选择合适的索引列顺序(B-Tree场景)
    • 5.3.5 聚簇索引(主要关注InnoDB)
    • 5.3.6 覆盖索引
    • 5.3.8 压缩(前缀压缩)索引(MyISAM)
    • 5.3.9 冗余和重复索引
    • 5.3.10 未使用的索引
    • 5.3.11 索引和锁
    • 5.4 索引案例学习
    • 5.4.1 支持多种过滤条件
    • 5.4.2 避免多个范围条件
    • 5.4.3 优化排序
  • 5.5 维护和索引表
    • 5.5.1 找到并修复顺坏的表
    • 5.5.2 更新索引统计信息
    • 5.5.3 减少索引和数据的碎片
  • 5.6总结

  • 索引是存储引擎用于快速找到记录的一种数据结构。
  • 对良好的性能非常关键。尤其是表中的数据较大时,索引对性能的影响愈发重要。
  • 对查询性能优化最有效的手段。轻易将查询性能提高几个数量级。

5.1索引基础

  • 索引,又叫key(键)
  • 存储引擎使用索引,先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行。
  • 可以包含一个或多个列的值。如果包含多个列,那么列的顺序也很重要,因为MySQL只能高效地使用索引的最左前缀列。

最左前缀:就是KEY(id, name, sex)中,id在id、name、sex里面是写在左边的,id就叫最左前缀

5.1.1 索引的类型

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

5.1.1.1 B-Tree索引

实际上很多存储引擎使用的是B+Tree,即每一个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的范围遍历。

  • innoDB使用B+Tree
  • B-Tree意味着所有的数据都是按顺序存储,适合查找某一范围内的数据,对数据排序。
  • 可加快访问数据的速度,存储引擎不再需要扫描全表,而是从根节点开始进去搜索。
  • 既然索引按顺序存储,也就意味着它对数据排序了,索引对数据排序的依据是按照定义索引的顺序来排序,例如KEY(id, name, sex),先对id排序,再是name,最后是sex。

B-Tree索引适用于全键值键值范围键前缀查找,其中键前缀查找只适用于根据最左前缀的查找。

  • 全值匹配。全值匹配指的是和索引中的所有列进行匹配,例如,前述示例中的索引可用于查找姓名为Cuba Allen、出生于1960-01-01的人。
  • 匹配最左前缀。示例索引可用于查找所有姓为Allen的人,即只使用索引的第一列。
  • 匹配列前缀。也可以只匹配某一列的值的开头部分,例如,示例索引可用于查找所有以J开头的姓的人,这里也只使用了索引的第一列。
  • 匹配范围值。例如,示例索引可用于查找姓在Allen和Barrymore之间的人,这里也只使用了索引的第一列。
  • 精确匹配某一列并范围匹配另外一列。示例索引也可用于查找所有姓为Allen,并且名字是字母K开头的人,即第一列last_name全匹配,第二列first_name范围匹配。
  • 只访问索引的查询。B-Tree通常可以支持“只访问索引的查询”,即查询只需要访问索引,而无须访问数据行。

因为索引树中的结点是有序的,所以除了按值查找之外,索引还可以用于查询中的ORDER BY操作。一般来说,如果B-Tree可以按照某种方式查找到值,那么也可以按照这种方式用于排序。所以,如果ORDER BY子句满足前面列出的几种查询类型,则这个索引也可以满足对应的排序需求。

限制

  • 如果不是按照索引的最左列开始查找,则无法使用索引。例如,示例索引中无法用于查找名字为Bill的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列。类似的,也无法查找姓氏以某个字母结尾的人。
  • 不能跳过索引中的列。例如,示例索引无法用于查找姓为Smith并且在某个特定日期出生的人。如果不指定first_name,则mysql只能使用索引的第一列。
  • 如果索引中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。例如,示例索引中有如下查询语句WHERE last_name = ‘Smith’ AND first_name LIKE ‘J%’ AND dob = ‘1976-12-23’,此查询只能使用索引的前两列,因为这里LIKE是一个范围条件。如果范围查询列值的数量有限,那么可以通过使用多个等于条件来代替范围条件。

上述限制都和索引列的顺序有关,在优化性能时,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。也有些限制并不是B-Tree本身导致的,而是mysql优化器和存储引擎使用索引的方式导致的。

想要了解B-Tree和B+Tree可以参考博客:https://blog.csdn.net/u010727189/article/details/79399384

5.1.1.2 哈希索引

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

在mysql中,只有Memory引擎显式支持哈希索引。这也是Memory引擎表的默认索引类型,Memory引擎同时也支持B-Tree索引。Memory引擎支持非唯一哈希索引,如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中。

索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,也让哈希索引查找的速度非常快。

限制

  • 只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。但由于在内存中,对性能的影响并不明显。
  • 并不是按照索引值顺序存储的,所以无法用于排序。
  • 不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值。
  • 只支持等值比较查询,包括=、IN()、<=>。也不支持任何范围查询。
  • 访问哈希索引的数据非常快,除非有很多哈希冲突(不同索引列值却有相同的hash值)。当出现哈希冲突时,存储引擎必须遍历所有的行指针,逐行比较,直到找到对应的行。
  • 如果哈希冲突很多的话,一些索引维护操作的代价也非常高。例如删除一行时,需遍历对应哈希值的每一行。

应用场景:
数据仓库应用中有一种经典的“星型”schema,需要管理很多查找表

InnoDB的自适应哈希索引(adaptive hash index):
当InnoDB注意到某些值被使用的非常频繁时,会在内存中基于B-Tree索引之上在创建一个hash索引,从而拥有hash索引的优点,如快速的hash查找。这是一个完全自动的,内部的行为,用户无法控制或者配置,但可以关闭。

创建自定义哈希索引:

  • 如果存储引擎不支持hash索引,可以模仿像InnoDB一样创建hash索引。
  • 思路:在B-Tree基础上创建一个伪哈希索引,即将要索引的列删除索引,对其创建一个被索引哈希列,里面存放原索引列每一行数据的哈希值。
  • 缺陷:需要维护哈希值,可以手动维护,也可以使用触发器实现。
  • 使用时不要使用SHA1()和MD5()作为哈希函数,因为这两个函数计算出来的哈希值非常长,浪费大量空间而且比较时也会更慢。可以使用CRS32()。
  • 如果数据表非常大,CRS32()会出现大量的数据冲突,可以自行实现一个简单的64位哈希函数,这个函数要返回整数,而不是字符串。也可以使用MD5()函数返回值的一部分来作为哈希函数。
SELECT CONV(RIGHT(MD5("str"), 16), 16, 10) AS HASH64
  • 使用哈希索引进行查询时,必须在WHERE子句包含对应列值,因为可能会有哈希冲突从而选出多个不同的数据。

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

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

5.1.1.4 全文索引

  • 查找的是文中关键词,而不是直接比较索引中的值。
  • 与其他索引完全不一样,需注意如停用词、词干和复数、布尔搜索等细节。
  • 更类似于搜索引擎做的事,而不是简单的WHERE条件匹配。
  • 在相同列上同时创建全文索引和基于值的B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST操作

为什么索引结构默认使用B-Tree,而不是hash,二叉树,红黑树?

  • hash:虽然可以快速定位,但是没有顺序,IO复杂度高。
  • 二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。
  • 红黑树:树的高度随着数据量增加而增加,IO代价高。

5.2 索引的优点

优点:

  • 大大减少服务器需要扫描的数据量
  • 帮助服务器避免排序和临时表(B-Tree会将相关的列值存储在一起,便于ORDER BY 和GROUP BY进行排序)
  • 可以将随机I/O变为顺序I/O

索引适合某个查询的“三星系统”:

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

索引并不是最好的解决方案:

  • 非常小的表,大部分情况下全表扫描更高效。
  • 中到大型的表,索引非常有效。
  • 特大型的表,建立和使用索引的代价随之增长,需要区分出查询需要的一组数据,如分区技术。
  • 表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录“哪个用户的信息存储在哪个表中”的元数据,这样在查询时就可以直接忽略掉那些不包含指定用户信息的表。对大型系统是一个常用的技巧.
  • 对于TB级别的数据,定位单条记录的意义不大,所以经常使用块级元数据技术来代替索引.

5.3 高性能的索引策略

5.3.1 独立的列

索引列不能是表达式的一部分,也不能是函数的参数。因为MySQL无法自动解析

WHERE column + 1 = 5 AND TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10
无法使用索引

因此在写SQL语句时应该养成简化WHERE条件的习惯,始终将索引列单独放在比较符的一侧。

5.3.2 前缀索引和索引选择性

当索引是很长的字符列时,会让索引变得大且慢。一个策略是模拟的哈希索引,另一个策略是前缀索引。

索引选择性:

  • 指不重复的索引值(也称基数,cardinality)和数据表的记录总数(#T)的比值
  • 选择性越高则查询效率越高,唯一索引的选择性是1,性能是最好的。

前缀索引:

  • 索引开始的部分字符串,即可节约索引空间,也可提高索引效率,但会降低索引的选择性。
  • 一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。
  • 针对BLOB,TEXT或很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。
  • 选择前缀长度的诀窍:保持较高的选择性(接近于索引完整列),同时又不能太长。换句话说,前缀的基数应接近于完整列的基数。

方法:
先计算出完整列的选择性:SELECT COUNT(DISTINCT col)/COUNT(*) FROM mytable , 再与平均选择性和最差选择性比较。

  • 平均选择性:在一个查询中针对不同前缀长度进行计算
SELECT COUNT(DISTINCT LEFT(col, 3))/COUNT(*) AS sel3,    
	COUNT(DISTINCT LEFT(col, 4))/COUNT(*) AS sel4,  
 	...
 FROM mytable;
  • 最差选择性:针对平均选择性选出的多个前缀长度,考虑其数据分布很不均匀下的选择性。

缺点:

  • MySQL无法使用前缀索引做ORDER BY和GROUP BY
  • 无法做覆盖扫描

应用场景:
针对很长的十六进制唯一ID,如保存网站的会话(SESSION),可采用长度为8的前缀索引,而且对上层应用完全透明。

有时使用后缀索引也有用途,如找到某个域名的所有电子邮件地址。但MySQL原生不支持反向索引,可以通过触发器把字符串反转后存储,并基于此建立前缀索引。

5.3.3 多列索引

在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查性能。

MySQL5.0及以后版本引入“索引合并(index merge)"的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。对示例查询同时使用两个单列索引进行扫描,并将结果进行合并,可以通过EXPLAIN的Extra看到过程。这算法有三个变种:

  • OR条件的联合(union)
  • AND条件的相交(intersection)
  • 组合前两种情况的联合及相交

更早版本的MySQL只能使用其中的某一个单列索引,而这种情况下没有哪一个独立的单列索引是非常有效的。对示例查询使用全表扫描,除非改成UNION的方式。

-- 两个单列索引的查询
mysql> SELECT film_id, actor_id FROM sakila.film_actor   
 	-> WHERE actor_id = 1 OR film_id = 1;

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

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

如果在EXPLAIN中看到有索引合并,应检查下查询和表的结构以达到最优。也可以通过参数optimizer_switch来关闭索引合并功能,或使用INGORE INDEX提示让优化器忽略掉某些索引。

5.3.4 选择合适的索引列顺序(B-Tree场景)

正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要。

选择索引列顺序的经验法则:

  • 当不需要考虑排序和分组时,将选择性最高的列放到索引最前列
  • 性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关。
  • 如果某些索引值的选择性非常小,即匹配的范围非常大,说明该索引基本没什么用。该特殊情况可能会摧毁整个应用的性能。

5.3.5 聚簇索引(主要关注InnoDB)

聚簇索引不是一种单独的索引类型,而是一种数据存储的方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行

当表有聚簇索引时,它的数据行实际上存放在索引中的叶子页(leaf page)中,但节点也只包含了索引列。术语的“聚簇”表示数据行和相邻的键值紧凑地存放在一起(Oracle中为索引组织表)。如图,被索引的列是主键列
高性能MySQL(第三版)第五章:创建高性能的索引_第1张图片

  • 一个表只能有一个聚簇索引,因为无法同时将数据行存放在两个不同的地方
  • MySQL内建的存储引擎不支持选择索引作为聚簇索引,InnoDB将通过主键聚集数据,其默认使用聚簇索引:
    如果没有定义主键,InnoDB会选择一个唯一的非空索引。
    如果没有唯一的非空索引,InnoDB会隐式定义一个主键。
  • InnoDB只聚集在同一个页面的记录,包含相邻键值的页面可能会相距甚远。

优缺点

优点(设计表和查询时充分利用可极大地提升性能):

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

缺点:

  • 最大限度地提高了I/O密集型应用的性能,但如果数据全部存放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势。
  • 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。如果不是按照主键顺序加载数据,加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。
  • 更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。
  • 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分裂(page split)”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这会导致表占用更多的磁盘空间。
  • 可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
  • 二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
  • 二级索引访问需要两次索引查找,而不是一次。因为二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。(InnoDB的自适应哈希索引能够减少这样的重复工作)
    高性能MySQL(第三版)第五章:创建高性能的索引_第2张图片

InnoDB:

  • 由于采用了聚簇索引,其保存了整个表
  • 聚簇索引每个叶子节点都包含了主键值、事务ID、用于事务和MVVC的回滚指针以及所有的剩余列。
  • 二级索引的叶子节点存储的不是"行指针",而是主键值,并以此作为指向行的“指针”。即叶子节点包含被索引的列和主键列。这样的策略会让二级索引占用更多的空间,但减少了当出现行移动或者数据页分裂时二级索引的维护工作,因为无须更新二级索引中的指针。

MyISAM

  • 采用了独立的行存储,按照数据插入的顺序存储在磁盘上
  • 主键索引和其他索引在结构上一样,主键索引是一个名为PRIMARY的唯一非空索引。

在InnoDB表中按主键顺序插入行:

  • 如果没有数据需要聚集,建议定义一个代理键作为主键,并且主键的数据应该和应用无关。最简单是使用AUTO_INCREMENT自增列,这样可以保证数据行是按顺序写入的,对于根据主键做关联操作的性能更好。
  • 最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于I/O密集型的应用,比如使用UUID作为聚簇索引可能会带来糟糕的性能,它使得聚簇索引的插入完全随机,使得插入行的时间更长,而且索引占用的空间更大。因为主键的字段更长,还由于页分裂和碎片导致。

根据顺序id插入数据
每条记录都存储在上一条记录的后面,当达到页的最大填充因子时(InnoDB默认为页大小的15/16,留出部分空间用于以后修改),下一条记录会插入新的页中。一旦数据按照这种顺序的方式加载,主键页就会被近似于被顺序的记录填满(二级索引页可能是不一样的)
造成更坏结果的场景:
1.对于高并发工作负载,可能会造成明显的争用。因为所有的插入都发生在这里,可能导致间隙锁竞争。
2.AUTO_INCREMENT锁机制也可能会被争用,需考虑重新设计表或者应用,或者更改innodb_autoinc_lock_mode配置。

使用随机id插入数据:
新行的主键值不一定比之前插入的大,因此需要为新行找到合适的位置——通常是已有数据的中间位置——并分配空间。这会增加很多额外的工作,并导致数据分布不够优化。

使用随机id插入数据缺点:

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

5.3.6 覆盖索引

设计优秀的索引应该考虑到整个查询,而不单单是WHERE条件部分

覆盖索引:一个索引包含(覆盖)所有需要查询的字段的值,查询只需要扫描索引而无须回表读取数据行

好处:

  • 索引条目通常小于数据行大小,如果只需要读取索引会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花在数据拷贝上。覆盖索引对I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中(尤其是MyISAM能压缩索引)
  • 索引是按照列值顺序存储的(至少在单个页内是如此 ),所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的IO要少得多。
  • 一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占了数据访问中的最大开销。
  • 覆盖索引对使用了聚簇索引的InnoDB的表非常有用。InnoDB的二级索引在叶子节点保存了行的主键值,所以如果二级节点能够覆盖查询,则可以避免对主键索引的二次查询

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

无法使用覆盖索引的原因:

  • 没有任何索引能够覆盖这个查询。
  • 不能在索引执行LIKE操作。

可以使用延迟关联和覆盖索引,因为延迟了对列的访问。先在查询第一阶段使用覆盖索引,再在外层查询所要获取的列值。

InnoDB的二级索引的叶子节点包含了主键的值,这意味着二级索引可以有效地利用这些主键列来覆盖查询。

-- last_name字段有二级索引,虽然该索引的列不包括逐渐actor_id,但也能用于对actor_id做覆盖查询
mysql>EXPLAIN SELECT actor_id, last_name     
    -> FROM sakila.actor WHERE last_name = "HOPPER"\G

使用InnoDB的表通过主键查询所有列,并不是覆盖查询,虽然聚簇索引的叶子节点包含了所有列的数据,但它只是一种数据存储方式,并不算索引。

5.3.8 压缩(前缀压缩)索引(MyISAM)

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

  • 默认只压缩字符串,通过设置也能压缩整数。
  • 压缩每个索引块的方法:先完全保存索引块的第一个值,然后将其他值和第一个值比较得到相同的前缀字节数和剩余的不同后缀部分,再把这部分存储起来。MyISAM对指针也采用类似的压缩方式。
  • 压缩块使用更少的空间,代价是某些操作可能更慢。因为每个值都依赖前面的值,无法使用二分查找只能从头开始扫描,而对倒序的扫描性能更差。
  • 对CPU密集型应用,因为扫描经常要随机查找,不推荐使用该索引。
  • 在CREATE TABLE语句中制定PACK_KEYS参数来控制索引压缩的方式。

5.3.9 冗余和重复索引

冗余索引:在相同列上创建多个索引。MySQL需要单独维护重复的索引,并且优化器在查询时也需要逐个考虑,可能会影响性能。

  • (A)是(A,B)的冗余索引,(B,A)和(B)则不是(A,B)的冗余索引,只针对B-Tree索引来说.
  • (A,ID)也是冗余索引,因为对InnoDB主键列已经包含在二级索引中。
  • 其他类型的如哈希索引也不会是B-Tree的冗余索引。
  • 增加新索引会导致INSERT,UPDATE等操作的速度变得更慢,特别是新增索引达到了内存瓶颈的时候。

重复索引:在相同列上按照相同顺序创建的相同类型的索引。应该避免这种操作,常见错误做法是对一个主键添加唯一限制和查询索引,这属于三个重复的索引。(如果索引的类型不同,并不算重复索引)

大多数情况下都不需要冗余索引,应该尽量扩展已有的索引而不是创建新的索引。除非扩展已有的索引会导致其变得太大,从而影响其他使用该索引查询的性能。

假如在整数列上有一个查询,现在需要额外增加很长的VARCHAR列来扩展该索引,可能会导致性能急剧下降。特别是有查询把这个索引当作覆盖查询,或者是MyISAM表并且有很多范围查询。

-- Q1查询:
SELECT count(*) FROM userinfo WHERE state_id=5;
-- Q2查询:
SELECT state_id, city, address FROM userinfo WHERE state_id=5;
-- Q2的查询速度会比Q1慢,最简单的办法是扩展索引变成覆盖查询:
ALTER TABLE userinfo DROP key state_id, ADD KEY state_id_2 (state_id, city, address);
-- 索引扩展后,Q2运行更快,但Q1变慢了。如果想要两个查询都变得更快,就需要两个索引,尽管这是冗余的。

解决冗余和重复索引的办法只需要删除它们。找出这些索引的办法:

  • 写一些复杂的访问INFORMATION_SCHEMA表的查询(服务器如果有大量的数据或表,可能会导致性能问题)
  • 第三方工具。

由于二级索引包含了主键值,因此(A)相当于(A,ID)WHERE A=5 ORDER BY ID这样的查询很有用但如果(A)扩展为(A,B)相当于(A,B,ID),前面的查询就无法使用该索引排序,而只能用文件排序。

5.3.10 未使用的索引

找出未使用的索引,并且删除!不过有些索引的功能相当于唯一约束,虽然一直没被查询使用,但是是用于避免产生重复数据的。

5.3.11 索引和锁

索引可以锁定更少的行。如果查询从不访问那些不需要的行,那么就会锁定更少的行:

  • 虽然InnoDB的行锁效率很高,内存使用也很少,但是锁定行的时候仍然会带来额外的开销。
  • 锁定超过需要的行会增加锁争用并减少并发性。

InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量。但只有当InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效。

  • 如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层后,MySQL服务器才能应用WHERE子句。而这时候InnoDB已经锁住了这些行(包含有没被索引的行数据,这些是要在服务器层被过滤掉的,因为索引只在存储引擎层工作),到适当的时候才释放。
  • MySQL5.0及新版本,InnoDB可以在服务器端过滤掉行就释放锁;但在早期版本,只有在事务提交后才能释放锁。

如果不使用索引查找和锁定行的话,MySQL可能会做全表扫描并锁住所有的行,而不管是否需要。

InnoDB在二级索引上使用共享(读)锁,但访问主键索引需要排他(写)锁,这消除了使用覆盖索引的可能性,并且使得SELECT FOR UPDATELOCK IN SHARE MODE或非锁定查询要慢得多。

5.4 索引案例学习

设计一个在线约会网站,用户信息表包括国家、地区、城市、性别、眼睛颜色等等。网站必须支持上面这些特征的各种组合来搜索用户,还必须允许根据用户的最后在线时间、其他会员对用户的评分等对用户进行排序并对结果进行限制。

使用索引排序,还是先检索数据再排序?使用索引排序会严格限制索引和查询的设计。

5.4.1 支持多种过滤条件

先看哪些列拥有不同的取值,哪些列在WHERE子句中出现得最频繁。

country和sex选择性通常比较低,考虑到使用频率,建议将(sex, country)作为查询前缀

  • 即使查询没有使用sex列,也可在查询条件中新增AND SEX IN(‘m’, ‘f’)来绕过,因为只有加上了sex这个条件,MySQL才能够匹配索引的最左前缀。但如果列有太多的值而导致IN()列表太长,或则IN()的数量太多导致有太多的组合,则不建议使用该技巧。
  • 基本原则之一:考虑表上的所有选项。当设计索引时,不要只为现有的查询考虑需要哪些索引,还需要考虑对查询进行优化。如果发现某些查询需要创建新索引,但是这个查询又会降低另一些查询的效率,那么应该考虑优化原有的查询,在优化查询和索引找到最佳的平衡,而不是一味追求最完美索引

考虑其他常见的WHERE组合列表,并需要了解哪些组合在没有合适索引的情况下会很慢。
(sex, country, age)、(sex, country, region, age)(sex, country, region, city, age)都很常见

  • 这会需要大量的索引。如果想尽量重用索引,可以使用前面提到的IN()技巧
  • 如果没有指定这个字段搜索,就需要定义一个全部国家列表,或者国家的全部地区列表,来确保索引前缀有同样的约束(组合所有国家、地区、性别将会是一个非常大的条件)

为一些生僻的搜索条件(比如has_pictures,eye_color,eduaction)来设计索引

  • 这些列选择性高,使用也不频繁,可以选择忽略,让MySQL多扫描一些额外的行。
  • 或者在age列的前面加上这些列,在查询是使用IN()技巧来处理搜索时没有这些列的场景。

为什么要将age列放在最后?age列有什么特殊的地方?
尽可能让MySQL使用更多的索引列因为查询只能使用索引的最左前缀,直到遇到第一个范围条件。前面的列都是等于条件,age列则大多是范围条件。

  • 虽然可以用IN()来代替范围查询,例如age IN(18, 19, 20),但不是所有的范围查询都可以转换。
  • 基本原则之二:尽可能将需要做范围查询的列放到索引后面,以便优化器能使用尽可能多的索引列。

5.4.2 避免多个范围条件

假设有一个last_online列并希望通过下面的查询显示在过去几周上线过的用户:

WHERE eye_color IN('brown', 'blue', 'hazel')    
	AND hair_color IN('black', 'red', 'blonde', 'brown')    
	AND sex IN("M", "F")    
	AND last_online > DATE_SUB(NOW(), INTERVAL 7 DAY)    
	AND age BETWEEN 18 AND 25;
-- MySQL会将age>18和age IN(18,19)都认为是范围查询(通过EXPLAIN查看),但两种访问效率是不同的,因为第二个查询是多个
等值条件查询。对MySQL来说,无法在使用范围查询后面的其他索引列,但对多个等值范围查询没有这个限制。

这个查询有两个范围条件,MySQL无法同时使用它们。
如果无法将age字段转换为一个IN()的列表,并且要求对这两个维度的范围查询的速度很快,很遗憾没有一个直接的办法解决该问题,但可以将其中的一个范围查询转换成一个简单的等值比较:

  • 事先计算好一个active列,这个字段由定时任务来维护。当用户每次登陆时,将对应值设置为1,并且将过去连续7天未登陆的用户的值设置为0。
  • 这个方法可以使用(active, sex, country, age)索引。active并不是完全精确的,因为对这类查询的精度要求并不高。如果需要精确次数,可以把last_online列放到WHERE子句,但不加入到索引中。所以这个查询条件没法使用任何索引,但因为这个条件的过滤性不高,即使在索引中加入该列也没有太大的帮助,或者说缺乏合适的索引对该查询的影响也不明显。
  • 如果用户系统同时看到活跃和不活跃用户,可以在查询中使用IN()列表。另一个可选方案是为不同的组合创建单独的索引,至少要包含(active, sex, country, age),(active, country, age),(sex, country, age)和(country, age),这些索引对某个具体的查询来说可能是更优化的,但是考虑到索引的维护额额外的空间占有代价,并不是一个好策略。

5.4.3 优化排序

对选择性非常低的列,可以增加一些特殊的索引来做排序。例如,可以创建(sex, rating),这个查询同时使用了ORDER BY和LIMIT,如果没有索引会很慢

SELECT  FROM profiles WHERE sex="M" ORDER BY rating LIMIT 10

即使有索引,如果用户界面上要翻页,并且翻页翻到比较靠后时查询也可能非常慢:

SELECT  FROM profiles WHERE sex="M" ORDER BY rating LIMIT 10000, 10;

无论如何创建索引,这种查询都是严重的问题。因为随着偏移量的增加,MySQL需要花费大量的时间来扫描需要丢弃的数据。反范式化、预先计算和缓存可能是解决这类查询的仅有策略。一个更好的办法是限制用户能够翻页的数量,而实际上这对用户体验的影响并不大,因为用户很少真正在乎搜索结果的第10000页。

优化这类索引另一个较好的办法是使用延迟关联,通过使用覆盖索引查询返回需要的主键,再根据这些主键关联原表获得需要的行。这可以减少MySQL扫描那些需要丢弃的行数。

SELECT  FROM profiles INNER JOIN (  
	SELECT  FROM profiles   
	WHERE sex="M" ORDER BY rating LIMIT 10000, 10
) AS x USING();

5.5 维护和索引表

维护表的三个目的:找到并修复损坏的表,维护准确的索引统计信息,减少碎片

5.5.1 找到并修复顺坏的表

损坏的索引会导致查询返回错误的结果或莫须有的主键冲突问题,严重时还会导致数据库崩溃。

尝试运行CHECK TABLE来检查是否发生了表损坏(注意有些引擎不支持该命令),通常能够找出大多数的表和索引的错误。

修复表错误的办法:

  • 可以使用REPAIR TABLE来修复损坏的表(注意有些引擎不支持该命令)。
  • 如果存储引擎不支持REPAIR TABLE,也可通过一个不做任何操作的ALTER来重建表,如修改表的存储引擎为当前引擎:ALTER TABLE innodb_dbl ENGINE=INNODB;
  • 将数据导出一份,然后再重新导入。
  • 使用第三方工具
  • 如果损坏的是系统区域,或者是表的"行数据"区域,而不是索引,那么之前的办法就没有用了。只能从备份中恢复表,或者尝试从损坏的数据文件中尽可能恢复数据。

如果InnoDB引擎的表出现了损坏,那么一定是发现了严重的错误,需要立刻调查下原因。因为InnoDB的设计一般不会出现损坏。如果发生损坏,可能是数据库的硬件问题,或者在MySQL外部操作了数据文件,亦或是InnoDB的缺陷(不太可能)。不存在任何查询让InnoDB损坏。

如果出现了数据损坏,最重要的是找出原因,而不是简单的修复,否则很有可能会不断的损坏。可以通过设置innodb_force_recovery参数进入InnoDB的强制恢复数据模式来修复数据。

5.5.2 更新索引统计信息

MySQL的查询优化器通过两个API来了解存储引擎的索引值分布信息:

  • records_in_range(),通过传入两个边界值获取在这个范围大概有多少条记录。对某些存储引擎如MyISAM返回精确值,对InnoDB返回一个估算值。
  • info(),返回各种类型的数据,包括索引的基数(每个键值有多少条记录)

如果存储引擎向优化器提供的扫描行数信息是不准确的数据,或者执行计划本身太复杂而无法精确地获取各个阶段匹配的行数,那么优化器会使用索引统计信息来估算扫描行数。

MySQL优化器使用的是基于成本的模型,而衡量成本的主要指标就是一个查询需要扫描多少行。如果表没有统计信息,或者统计信息不准确,优化器很可能做出错误的决定。通过运行ANALYZE TABLE来重新生成统计信息解决这个问题。而每种存储引擎实现的统计信息的方式不同,需要进行ANALYZE TABLE的频率和每次运行的成本也不同:

  • Memory引擎根本不存储索引统计信息
  • MyISAM将索引统计信息存储在磁盘中,ANALYZE TABLE需要进行一次全索引扫描来计算索引基数,在整个过程中需要锁表。
  • 直到MySQL5.5版本,InnoDB也不在磁盘存储索引统计信息,而是通过随机的索引访问进行评估并将其存储在内存中。

使用SHOW INDEX FROM table;命令来查看索引的基数(cadinality)。基数显示了存储引擎估算索引列有多少个不同的取值。在MySQL5.0及以后的版本,可以通过INFORMATION_SCHEMA.STATISTICS表很方便地查询到这些信息,不过如果服务器的库表非常多,从这里获取元数据的速度会非常慢,而且会给MySQL带来额外的压力。

InnoDB的统计信息:

  • 该引擎通过抽样的方式来计算统计信息,首先随机地读取少量的索引页面,然后以此为样本计算索引的统计信息。老版本中样本页数是8,新版本可以设置innodb_stats_sample_pages来设置样本页的数量。理论上越大的值可以帮助生成更准确的索引信息,特别是对某些超大数据表来说。
  • 会在表首次打开,或者执行ANALYZE TABLE,抑或表的大小发生非常大的变化(该变化超过十六分之一或者新插入20亿行)的时候计算索引的统计信息。
  • 会在打开某些INFORMATION_SCHEMA表,或者使用SHOW TABLE STATUS和SHOW INDEX,抑或MySQL客户端开启自动补全功能的时候都会触发索引统计信息的更新。
    如果服务器上有大量的数据,可能会导致严重的问题,尤其是IO比较慢的时候,客户端或者监控程序触发索引信息采样更新时可能会导致大量的锁,并给服务器带来额外的压力。可以关闭innodb_stats_on_metadata参数来避免上面提到的问题。

5.5.3 减少索引和数据的碎片

索引碎片化

  • B-Tree索引可能会碎片化,这会降低查询的效率。碎片化的索引可能会以很差或者无序的方式存储在磁盘上。
  • 根据设计,B-Tree需要随机磁盘访问才能定位到叶子页,所以随机访问是不可避免的。然而,如果叶子页在物理分布上是顺序且紧密的,那么查询的性能就会更好。否则对于范围查询、索引覆盖扫描等操作来说,速度可能会降低很多倍;对于索引覆盖扫描这一点更明显

表的数据存储碎片化(比索引碎片化更加复杂):
类型:

  • 行碎片(Row fragmentation):数据行被存储为多个地方的多个片段中。及时查询只从索引中访问一行记录,也会导致性能下降。
  • 行间碎片(Intra-row fragmentation):指逻辑上顺序的页,或者行在磁盘上不是顺序存储的。行间碎片对诸如全表扫描和聚簇索引扫描之类的操作有很大的影响,因为这些操作原本能够从磁盘上的顺序存储的数据收益。
  • 剩余空间碎片(Free space fragmentation):数据页中有大量的空余空间。这会导致服务器读取大量不需要的数据,造成浪费。

对MyISAM表,这三类碎片化都可能发生;但InnoDB不会出现短小的行碎片,它会移动短小的行并重写到一个片段中

重新整理数据方式

  • OPTIMIZE TABLE
  • 导出再导入
  • 排序算法重建索引(针对MyISAM)
  • “在线”添加和删除索引的功能,可以通过先删除,然后在重新创建索引来消除索引碎片(针对最新版本InnoDB)
  • 通过一个不做任何操作的ALTER TABLE ENGINE = ;来重建表(针对不支持OPTIMIZE TABLE的引擎)

    应该通过一些实际测量而不是随意假设来确定是否需要消除索引和表的碎片化,还要考虑数据是否已达到稳定状态(如果进行碎片整理将数据压缩到一起,可能会导致后续的更新操作触发一系列的页分裂和重组,对性能造成不良的影响直到数据达到新的稳定状态)

    5.6总结

    • MySQL和存储引擎访问数据的方式,加上索引的特性,使得索引成为一个影响数据访问的有利而灵活的工作(无论数据是在磁盘还是在内存中)
    • 大多数情况下都会使用B-Tree索引,其他类型的索引大多只适用于特殊目的。
    • 选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住
    1. 单行访问是很慢的。如果服务器从存储中读取一个数据块只是为了获取其中的一行,那么就浪费了很多工作。最好的读取的块中能包含尽可能多需要的行。使用索引可以创建位置引用提高效率。
    2. 按顺序访问范围数据是很快的。有两个原因:顺序IO不需要多次磁盘寻道而比随机IO快很多;如果服务器能够按需要的顺序读取数据,就不再需要额外的排序操作,并且GROUP BY查询也无须再做排序和将行按组进行聚合计算。
    3. 索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎就不需要再回表查找行,避免了大量的单行访问。
    • 编写查询语句应该尽可能选择合适的索引以避免单行查找,尽可能地使用数据原生顺序而避免额外的数据排序操作,并尽可能使用索引覆盖查询。
    • 对某些查询不可能创建一个“三星”索引,必须要有所取舍,或者寻求替代策略(例如反范式话、或者提前计算汇总表)
    • 理解索引的工作原理来创建最适合的索引
    • 判断为一个系统创建的索引的合理性:按响应时间对查询进行分析。
    1. 找出那些消耗时间最长的或给服务器带来最大压力的查询
    2. 检查这些查询的schema,SQL和索引结构
    3. 判断是否有查询扫描了太多的行,是否做了很多额外的排序或者使用了临时表,是否使用随机IO访问数据,或者是有太多回表查询那些不在索引中的列的操作。
    • 如果一个查询无法从所有可能的索引中获益,则应该看看是否可以创建一个更合适的索引来提升性能。如果不行,也要尝试是否可以重写该查询,将其转化成一个能够高效利用现有索引或者新创建索引的查询。

    你可能感兴趣的:(数据库)