标签(空格分隔): 高性能MYSQL 第五章 创建高性能的索引
在MySQL中,索引是在存储引擎层而不是服务器层实现的。不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。下面我们先来看看MySQL支持的索引类型,以及它们的优点和缺点。
1.1. B-Tree索引
当人们谈论索引的时候,多半说的是B-Tree索引,它使用B-Tree数据结构来存储数据。我们使用术语“B-Tree”,是因为MySQL在CREATE TABLE和其他语句中 使用该关键字。实际上,底层在存储引擎也可能使用不同的存储结构,InnoDB使用的是B+Tree。
InnoDB按照数据格式对索引进行存储,再根据主键引用被索引的行。B-Tree通常意味着所有的值都是按顺序存储的,并且每一个叶子到根的距离相同。
B-Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下查找。叶子节点比较特别,它们的指针指向的是被索引的数据,面不是其他的节点页(不同引擎的“打针”类型不同)。树的深度和表的大小直接相关。
B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据。所以像“找出所有以I到K开头的名字”这样的查找效率会非常高。
(B-Tree的具体实现请自行百度)
B-Tree索引对如下类型的查询有效:
覆盖索引,索引包含查询所有的数据,无须访问数据行
因为索引树中的节点是有序的,所以除了按值查找之外,索引还可能用于查询中的ORDER BY操作。
B-Tree索引的限制:
如果不是按照索引的最左列开始查找(跳过了某些列),则无法使用索引。
1.2. 哈希索引
哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有列的查询才有效。在MySQL中,只有Memory引擎显示支持哈希索引。值得 一提的是 ,Memory引擎是支持非唯一哈希索引的,如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希目中。
1.2.1 哈希索引的限制:
1.2.2 创建自定义哈希索引
思路很简单:在B-Tree基础上创建一个伪哈希索引。这和真正的 哈希索引不是一回事,因为还是使用B-Tree进行查找,但是它使用哈希值而不是键本身进行索引查找。你需要做的就是在查询的WHERE子句中手动指定使用哈希函数。
例:
mysql> SELECT ID FROM url WHERE url="http://www.mysql.com"
AND url_crc=CRC32("http://www.mysql.com");
这样做的性能会非常高,但缺陷是需要维护哈希值。可以手动维护,也可以 使用触发器实现
。如果采用这种方式,记住不要使用SHA1()和MD5()作为哈希函数。因为这两个函数计算出来 的哈希值是非常长的字符串,会浪费大量空间,比较时也会更慢。
CRC32)返回的是32位整数,当索引有93000条记录时出现冲突的概率是1%。减少冲突可以使用FNV64()函数,或自定义哈希函数。
索引可以让服务器快速定位到表的指定位置。最常见的B-Tree索引,按照顺序存储数据,所以MySQL可以用来做ORDER BY和GROUP BY操作。
总结下来索引有如下三个优点:
1. 索引大大减少了服务器需要扫描的数据量。
2. 索引可以帮助服务避免排序和临时表。
3. 索引可以将随机I/O变为顺序I/O。
索引评价的“三星系统”:索引将相关的记录放到一起则获得一星;如果索引中的数据顺序和查找中的排列顺序一致则获得二星;如果索引中的列包含了查询中需要的全部列则获得“三星”。
3.1. 独立的列
索引不能是表达式的一部分,也不能是函数的参数。
// 该查询都不能使用索引
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
mysql> SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;
3.2. 前缀索引和索引选择性
索引的选择性是指,不重复的索引值(也称为基数,cardinality)和数据表的记录总数(#T)的比值,范围从1/#T到1之间。索引的选择性超高则查询效率超高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的“基数”应该接近于完事列的“基数”。
下面以实例讲解:
mysql> SELECT COUNT(*) AS cnt, city
-> FROM sakila.city_demo GROUP BY city ORDER BY cnt DESC LIMIT 10;
+-----+----------------+
| cnt | ciyt |
+-----+----------------+
| 65 | London |
| 49 | hiroshima |
| 48 | Teboksary |
| 48 | Pak Kret |
| 48 | yaound |
| 47 | Tel Aviv-Jaffa |
| 47 | Shimoga |
| 45 | Cabuyao |
| 45 | Callao |
| 45 | Bislig |
注意到,上面每个值都出现了45~65次。现在查找到最频繁出现的城市前缀,从3个前缀字母到7个字母,经过实验后发现前缀长度为7时比较合适:
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 7) AS pref
-> FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
+-----+----------------+
| cnt | ciyt |
+-----+----------------+
| 70 | Santiag |
| 68 | San Fel |
| 65 | London |
| 61 | Valle d |
| 49 | Hiroshi |
| 48 | Teboksa |
| 48 | Pak Kre |
| 48 | Yaound |
| 47 | Tel Avi |
| 47 | Shimoga |
计算合适的前缀长度的另外一个办法就是计算完整列的选择性,并合前缀的选择性接近于完整列的选择性。下面显示如何计算完整列的选择性:
msyql> SELECT COUNT(DISTINCT city)/COUNT(*) FROM sakila.city_demo;
+-------------------------------+
| COUNT(DISTINCT ciyt)/COUNT(*) |
+-------------------------------+
| 0.0312 |
+-------------------------------+
下面给出了如何在同一个查询中计算不同前缀长度的选择性:
mysql> SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) AS sel3,
COUNT(DISTINCT LEFT(city, 4))/COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(city, 5))/COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(city, 6))/COUNT(*) AS sel6,
COUNT(DISTINCT LEFT(city, 7))/COUNT(*) AS sel7
FROM sakila.city_demo;
+--------+--------+--------+--------+--------+
| sel3 | sel4 | sel5 | sel6 | sel7 |
+--------+--------+--------+--------+--------+
| 0.0239 | 0.0293 | 0.0305 | 0.0309 | 0.0310 |
+--------+--------+--------+--------+--------+
查询显示当前缀长度到达7的时候,再增加前缀长度,选择性提升的幅度已经很小了。
注意:要同时考量平均值和选择性,也就是上面的两种评价。
创建前缀索引:
mysql> ALTER TABLE sakila.city_demo ADD KEY(city(7));
MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。
3.3. 多列索引
在MySQL5.0和更新的版本中,查询能够同时使用多个单列索引进行扫描,并将结果进行合并。这种算法有三个变种:OR条件的聚合(union),AND条件的相交(intersection),纵使前两种情况的联合及相交。
mysql> EXPLAIN SELECT flim_id, actor_id FROM sakila.film_actor
-> WHERE actor_id = 1 OR film_id = 1\G
************************1. row************************
...
possible_key: PRIMARY,idx_fk_film_id
key:PRIMARY,idx_fk_film_id
...
Extra:Using union(PRIMARY,idx_fk_film_id); Using Where
索引合并策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建得很糟糕。
另一种方法是改为UNION查询。
3.4. 选择合适的索引列顺序
在B-TREE索引中,将选择性最高的列放到索引最前列,在某些场景可能有帮助,但通常不如避免随机IO和排序那么重要。
性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布 有关。
3.5. 聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个 结构中保存了B-Tree索引和数据行。
3.6. 覆盖索引
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。覆盖不需要满足最左前缀的要求。
这里延伸出一种查询优化方式,叫做“延迟关联”
mysql> SELECT * FROM products WHERE actor='SEAN CARREY'
-> AND title like '%APOLLO%%';
假设有一个 索引覆盖一个数据列(actor,title,prod_id),重写后的查询:
mysql> SELECT *
-> FROM products
-> JOIN (
-> SELECT prod_id
-> FROM products
-> WHERE actor='SEAN CARREY' AND title LIKE '%APOLLO%'
-> ) AS t1 ON (t1.prod_id=products.prod_id);
在查询的第一阶段MySQL可以使用覆盖索引,在FROM子句的子查询中找到匹配的prod_id,然后根据这些prod_id值在外层查询匹配获取需要的所有列值。
3.7. 使用索引扫描来做排序
MySQL有两种方式可以生成有序的结果:通过排序操作;或者按索引顺序扫描;如果EXPLAIN出来的type列的值为index,则说明MySQL使用了索引扫描来做排序。
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需要的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据(全行记录的数据)的速度通常要比顺序地全表扫描慢,尤其是在I/O密集型的工作负载时。
只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则MySQL都需要执行排序操作,而无法利用索引排序。
注解:type不为index时,也可能是使用索引排序的,当EXTRA出现“Using filesort”时就需要优化。
3.8. 索引和锁
索引可以让查询锁定更少的行。如果你 的查询从不访问那些不需要的行,那么就会锁定更少的行。
InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量。但这只胡当InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效。如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层以后,MySQL服务器才能应用WHERE子句 。
这时已经无法避免锁定行了,InnoDB可以在服务器端过滤掉行后就释放锁,但是在早期MySQL版本中,InnoDB只胡在事务提交后才能释放锁。
关于InnoDB、索引和锁有一些很少有人知道的细节:InnoDB在二级索引上使用共享(读)锁,但访问主键索引需要排他(写)锁。这消除了消除了使用覆盖索引的可能性,并且使得SELECT FOR UPDATE男LOCK IN SHARE MODE或非锁定查询要慢很多。
行级锁又分共享锁和排他锁。
共享锁:
名词解释:共享锁又叫做读锁,所有的事务只能对其进行读操作不能写操作,加上共享锁后在事务结束之前其他事务只能再加共享锁,除此之外其他任何类型的锁都不能再加了。
// 用法:
mysql> SELECT `id` FROM table WHERE id in(1,2) LOCK IN SHARE MODE
// 结果集的数据都会加共享锁
排他锁:
名词解释:若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束之前,其他事务不能对其进行加任何锁,其他进程可以读取,不能进行写操作,需等待其释放。
// 用法:
mysql> SELECT `id` FROM mk_user WHERE id=1 FOR UPDATE
对于IN和范围查询,从EXPLAIN的输出很难区分MySQL是要查询范围值,还是查询列表值。EXPLAIN使用同样的词“range”来描述这两种情况。
从EXPLAIN的结果是无法区分这两者的,但可以从值的范围和多个等于条件来得出不同。在我们看来,IN查询就是多个等值条件查询。
我们不是挑剔:这两种访问效率是不同的。对于范围条件查询,MySQL无法再使用范围列后面的其他索引列了,但是对于“多人等值条件查询”则没有这个限制。
排序优化
使用延迟关联
注意:测试前请关系缓存
mysql> SET SESSION query_cache_type=0;
mysql> SELECT * FROM tt_test
> ORDER BY score, value, id
> LIMIT 100000, 100;
重写后的查询:
mysql> SELECT * FROM tt_test
> INNER JOIN(
> SELECT id FROM tt_test
> ORDER BY score, VALUE, id
> LIMIT 100000, 100
> )t USING(id);
对越靠后的分页越有效
对于InnoDB
重建表(会重新组织数据)
mysql> ALTER TABLE innodb_tb1 ENGINE=INNODB;
更新索引统计信息
mysql> ANALYZE TABLE
最后值得总的回顾一个这些特性以及如何使用B-Tree索引。
在选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住:
单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机I/O要快很多,不过这一点仍然成交)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的快中包含尽可能多所需要的行。使用索引可以创建位置引用以提升效率。
按顺序访问范围数据是很快的,这有两个原因。第一,顺序I/O不需要多次磁盘寻道,所以比随机I/O要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUP BY查询也无须再做排序和将按组进行聚合计算了。
索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎就不需要再回表查找行。这避免了大量的单选访问,而上面的第上点已经写明单选访问是很慢的。
总的来说,编写查询语句时应该尽选择合适的索引以避免单选查找、尽可能地使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。这与“三星”评价系统是一致的。