MySQL创建高性能的索引

标签(空格分隔): 高性能MYSQL 第五章 创建高性能的索引


  在MySQL中,索引是在存储引擎层而不是服务器层实现的。不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。下面我们先来看看MySQL支持的索引类型,以及它们的优点和缺点。

  1. 索引的种类

    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()函数,或自定义哈希函数。
      

  2. 索引的优点

      索引可以让服务器快速定位到表的指定位置。最常见的B-Tree索引,按照顺序存储数据,所以MySQL可以用来做ORDER BY和GROUP BY操作。
      总结下来索引有如下三个优点:
     1. 索引大大减少了服务器需要扫描的数据量。
     2. 索引可以帮助服务避免排序和临时表。
     3. 索引可以将随机I/O变为顺序I/O。

      索引评价的“三星系统”:索引将相关的记录放到一起则获得一星;如果索引中的数据顺序和查找中的排列顺序一致则获得二星;如果索引中的列包含了查询中需要的全部列则获得“三星”。
      

  3. 高性能的索引策略

     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
  4. 索引案例学习

      对于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);

      对越靠后的分页越有效

  5. 维护索引和表

    对于InnoDB

    重建表(会重新组织数据)

    mysql> ALTER TABLE innodb_tb1 ENGINE=INNODB;

    更新索引统计信息

    mysql> ANALYZE TABLE
  6. 总结

      最后值得总的回顾一个这些特性以及如何使用B-Tree索引。
    在选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住:

    1. 单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机I/O要快很多,不过这一点仍然成交)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的快中包含尽可能多所需要的行。使用索引可以创建位置引用以提升效率。

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

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

        总的来说,编写查询语句时应该尽选择合适的索引以避免单选查找、尽可能地使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。这与“三星”评价系统是一致的。

你可能感兴趣的:(高性能mysql)