您的位置:首页 > 数据库 > MySQL

MySQL创建高性能的索引

2017-06-28 17:13 465 查看
标签(空格分隔): 高性能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 哈希索引的限制:

哈希索引只包含哈希值和行指针,而 不存储字段值,所以不能使用索引中的值来避免读取行。

哈希索引数据并不是按照索引顺序存储的,所以也就无法用于排序。

哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值。

哈希索引只支持等值比较查询,包括=、IN()、<=>(注意<>和<=>是不同的操作,<=>可用于比较NULL)。也不支持任何范围查询。

访问哈希索引的数据非常快,除非有很多哈希冲突。出现冲突时需要遍历列表。

如果哈希冲突很多的话,一些索引维护操作的代价也会很高。删除一行时,需要遍历哈希值链表中的每一行。

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查询也无须再做排序和将按组进行聚合计算了。

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

  总的来说,编写查询语句时应该尽选择合适的索引以避免单选查找、尽可能地使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。这与“三星”评价系统是一致的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息