第5章,创建高性能的索引

5.1.1 索引的类型

创建自定义哈希索引。如果存储引擎不支持哈希索引,则可以模拟像InnoDB一样创建哈希索引,这可以享受一些哈希索引的便利,例如只需要很小的索引就可以为超长的键创建索引。

思路很简单:在B-Tree基础上创建一个伪哈希索引。这和真正的哈希索引不是一回事,因为还是使用B-Tree进行查找,但是它使用哈希值而不是键本身进行索引查找。你需要做的就是在查询的WHERE子句中手动指定使用哈希函数。

如果数据表非常大,CRC32()会出现大量的哈希冲突,则可以考虑自己实现一个简单的64位哈希函数。这个自定义函数要返回整数,而不是字符串。一个简单的办法可以使用MD5()函数返回值的一部分来作为自定义哈希函数。 要避免冲突问题,必须在WHERE条件中带入哈希值和对应列值。

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

☆ InnoDB上创建哈希索引的方法。

5.2 索引的优点

如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录“哪个用户的信息存储在哪个表中”的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧。事实上,Infobright就是使用类似的实现。对于TB级别的数据,定位单条记录的意义不大,所以经常会使用块级别元数据技术来替代索引。

☆ 我的理解是这样的,将一些元数据存放在元数据信息表中,那么什么是元数据呢,可能是用户分布信息,那个用户存放在那个库,那么表中。这样在查询数据时,就可以直接到对应的库,表中查找。

5.3.2 前缀索引和索引选择性

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

☆ 只要仔细想想,这两个缺点是前缀索引所固有的。

5.3.4 选择合适的索引列顺序

当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化WHERE条件的查找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在WHERE子句中只使用了索引部分前缀列的查询来说选择性也更高。然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关。

☆ 影响性能的因素有很多,索引列的选择性只是其中之一。

5.3.5 聚簇索引

对于高并发工作负载,在InnoDB中按主键顺序插入可能会造成明显的争用。主键的上界会成为“热点”。因为所有的插入都发生在这里,所以并发插入可能导致间隙锁竞争。另一个热点可能是AUTO_INCREMENT锁机制;如果遇到这个问题,则可能需要考虑重新设计表或者应用,或者更改innodb_autoinc_lock_mode配置。如果你的服务器版本还不支持innodb_autoinc_lock_mode参数,可以升级到新版本的InnoDB,可能对这种场景会工作得更好。

☆ 这是特别需要注意的。

5.3.7 使用索引扫描来做排序

MySQL有两种方式可以生成有序的结果:通过排序操作;或者按索引顺序扫描。如果EXPLAIN出来的type列的值为“index”,则说明MySQL使用了索引扫描来做排序(不要和Extra列的“Using index”搞混淆了)。

只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则,MySQL都需要执行排序操作,而无法利用索引排序。

☆ 使用索引排序的注意项。

5.3.9 冗余和重复索引

还有一种情况是将一个索引扩展为(A,ID),其中ID是主键,对于InnoDB来说主键列已经包含在二级索引中了,所以这也是冗余的。

☆ 这个是我没有想到的。

5.3.11 索引和锁

换句话说,底层存储引擎的操作是“从索引的开头开始获取满足条件actor_id<5的记录”,服务器并没有告诉InnoDB可以过滤第1行的WHERE条件。注意到EXPLAIN的Extra列出现了“Using where”,这表示MySQL服务器将存储引擎返回行以后再应用WHERE过滤条件。

mysql> EXPLAIN SELECT actor_id FROM sakila.actor
-> WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE;
+----+-------------+-------+-------+---------+--------------------------+
| id | select_type | table | type | key | Extra |
+----+-------------+-------+-------+---------+--------------------------+
| 1 | SIMPLE | actor | range | PRIMARY | Using where; Using index |
+----+-------------+-------+-------+---------+--------------------------+

☆ 那么是不是可以这样理解呢,当Extra出现‘Using where; Using index’,是不是就表示存储引擎利用索引获取数据后,然后MYSQL服务器再对返回的数据进行筛选呢,我想是这样的。

5.4.1 支持多种过滤条件

这个“诀窍”就是:如果某个查询不限制性别,那么可以通过在查询条件中新增AND SEX IN(’m’,’f’)来让MySQL选择该索引。这样写并不会过滤任何行,和没有这个条件时返回的结果相同。但是必须加上这个列的条件,MySQL才能够匹配索引的最左前缀。这个“诀窍”在这类场景中非常有效,但如果列有太多不同的值,就会让IN()列表太长,这样做就不行了。

这里提到可以在索引中加入更多的列,并通过IN()的方式覆盖那些不在WHERE子句中的列。但这种技巧也不能滥用,否则可能会带来麻烦。因为每额外增加一个IN()条件,优化器需要做的组合都将以指数形式增加,最终可能会极大地降低查询性能。

☆ 如何更好的利用索引(更好的利用INNODB中的最左前缀索引)。

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

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

mysql> SELECT < cols> 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>);

☆ 如何解决分页中的大量翻页的问题,这里提供了一些方法。有意思有两个,一个是限制大量翻页,二是通过先查询页面记录的ID值,然后再通过ID值获取页面的记录。

5.5.1 找到并修复损坏的表

损坏的索引会导致查询返回错误的结果或者莫须有的主键冲突等问题,严重时甚至还会导致数据库的崩溃。如果你遇到了古怪的问题——例如一些不应该发生的错误——可以尝试运行CHECK TABLE来检查是否发生了表损坏(注意有些存储引擎不支持该命令;而有些引擎则支持以不同的选项来控制完全检查表的方式)。CHECK TABLE通常能够找出大多数的表和索引的错误。

可以使用REPAIR TABLE命令来修复损坏的表,但同样不是所有的存储引擎都支持该命令。如果存储引擎不支持,也可通过一个不做任何操作(no-op)的ALTER操作来重建表,例如修改表的存储引擎为当前的引擎。下面是一个针对InnoDB表的例子:

mysql> ALTER TABLE innodb_ tbl ENGINE= INNODB;

如果InnoDB引擎的表出现了损坏,那么一定是发生了严重的错误,需要立刻调查一下原因。InnoDB一般不会出现损坏。InnoDB的设计保证了它并不容易被损坏。如果发生损坏,一般要么是数据库的硬件问题例如内存或者磁盘问题(有可能),要么是由于数据库管理员的错误例如在MySQL外部操作了数据文件(有可能),抑或是InnoDB本身的缺陷(不太可能)。常见的类似错误通常是由于尝试使用rsync备份InnoDB导致的。不存在什么查询能够让InnoDB表损坏,也不用担心暗处有“陷阱”。如果某条查询导致InnoDB数据的损坏,那一定是遇到了bug,而不是查询的问题。

☆ CHECK TABLE命令和ALTER的替代写法,应该知道这个东西,这样当发生了数据错误的时候,还知道通过这个命令去解决问题。

5.5.2 更新索引统计信息

InnoDB会在表首次打开,或者执行ANALYZE TABLE,抑或表的大小发生非常大的变化(大小变化超过十六分之一或者新插入了20亿行都会触发)的时候计算索引的统计信息。

一旦关闭索引统计信息的自动更新,那么就需要周期性地使用ANALYZE TABLE来手动更新。否则,索引统计信息就会永远不变。如果数据分布发生大的变化,可能会出现一些很糟糕的执行计划。

☆ 优化器需要用到索引统计信息,一是构建索引统计信息需要花费时间,二是过期的索引统计信息会导致一些查询的性能问题,所以在优化时,需要兼具这两者。

5.5.3 减少索引和数据的碎片

可以通过执行OPTIMIZE TABLE或者导出再导入的方式来重新整理数据。这对多数存储引擎都是有效的。对于一些存储引擎如MyISAM,可以通过排序算法重建索引的方式来消除碎片。老版本的InnoDB没有什么消除碎片化的方法。不过最新版本InnoDB新增了“在线”添加和删除索引的功能,可以通过先删除,然后再重新创建索引的方式来消除索引的碎片化。

对于那些不支持OPTIMIZE TABLE的存储引擎,可以通过一个不做任何操作(no-op)的ALTER TABLE操作来重建表。只需要将表的存储引擎修改为当前的引擎即可:

mysql>ALTER TABLE <table> ENGINE = <engine>;

☆ 减少索引和数据的碎片的方法,需要有所了解。

5.6 总结

在选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住: 单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机I/O要快很多,不过这一点仍然成立)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的块中能包含尽可能多所需要的行。使用索引可以创建位置引用以提升效率。

按顺序访问范围数据是很快的,这有两个原因。第一,顺序I/O不需要多次磁盘寻道,所以比随机I/O要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUP BY查询也无须再做排序和将行按组进行聚合计算了。

索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎就不需要再回表查找行。这避免了大量的单行访问,而上面的第1点已经写明单行访问是很慢的。

总的来说,编写查询语句时应该尽可能选择合适的索引以避免单行查找、尽可能地使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。

☆ 索引优化的一个总的指导原则。