MySQL优化 - 索引优化

时间:2023-03-09 19:02:34
MySQL优化 - 索引优化

索引(在MySQL中也叫做“键(key)”)是存储引擎用于快速找到记录的一种数据结构。

索引对于良好的性能非常关键,尤其是当表的数据量越来越大时,索引对性能(查询)的影响愈发重要。

  • 索引的类型
  • 索引的优点(大致分为以下三点)
    • 索引大大减少了服务器需要扫描的数据量。
    • 索引可以帮助服务器避免排序和临时表。
    • 索引可以将随机I/O变为顺序I/O。
  • 如何创建高性能的索引
    • 索引列不能是表达式的一部分,也不能是函数的参数,如下是不恰当的写法
       mysql> SELECT * FROM TB1 WHERE num + 1 = 5;
       mysql> SELECT * FROM TB1 WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(ctime) < 10;
    • 在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能,如下是不恰当的写法
       mysql> CREATE TABLE TB4(
      -> c1 INT,
      -> c2 INT,
      -> c3 INT,
      -> key(c1),
      -> key(c2),
      -> key(c3));
       mysql> EXPLAIN SELECT * FROM TB4 WHERE c1=1 OR c2=1 OR c3=1\G
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB4
      partitions: NULL
      type: ALL
      possible_keys: c1,c2,c3
      key: NULL
      key_len: NULL
      ref: NULL
      rows: 6
      filtered: 42.13
      Extra: Using where
    • 当不需要考虑排序和分组时,将选择性最高的列放在最左边通常是最好的,可以使用以下方法来查看基数和选择性(这里对比TB1表中的namenum字段):
       mysql> SELECT COUNT(DISTINCT name)/COUNT(*) AS name_selectivity,
      -> COUNT(DISTINCT num)/COUNT(*) AS num_selectivity,
      -> COUNT(*)
      -> FROM TB1\G
      *************************** 1. row ***************************
      name_selectivity: 0.3479
      num_selectivity: 0.0000
      COUNT(*): 1750001

      可以看出name字段的选择性更高,所以将其作为索引列的第一列

       mysql> ALTER TABLE TB1 ADD KEY(name, num);
    • 使用覆盖索引(查询列要被所建的索引覆盖)。索引条目通常远小于数据行大小,所以如果只需要读取索引,可以极大地减少数据访问量,如下(TB1表中有一个多列索引name和num):
       mysql> EXPLAIN SELECT name,num FROM TB1\G
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: ref
      possible_keys: IDX
      key: IDX
      key_len: 66
      ref: const,const
      rows: 15
      filtered: 100.00
      Extra: Using index

      上面的语句会使用到覆盖索引,Extra列可以看到Using index的信息。下面的例子没有任何索引能够覆盖这个查询,有两个原因,一是查询从表中选择了 所有列(*),二是MySQL不能再索引中执行LIKE操作:

       mysql> EXPLAIN SELECT * FROM TB1 WHERE name='test1' AND nk LIKE '%a%'\G
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: ref
      possible_keys: IDX
      key: IDX
      key_len: 66
      ref: const,const
      rows: 15
      filtered: 100.00
      Extra: Using index condition # 在MySQL的5.6以下版本会显示Using index where

      我们可以通过重写查询设计索引来解决上面的查询语句,先将索引拓展至覆盖三个数据列(id,name,nk),然后按照如下方式重写查询(延迟关联):

       mysql> EXPLAIN SELECT TB1.* FROM TB1 JOIN
      -> (
      -> SELECT id FROM TB1 WHERE name='test1' AND nk LIKE '%a%'
      -> ) AS t1 ON
      -> t1.id = TB1.id\G
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: ref
      possible_keys: PRIMARY,IDX,IDX1
      key: IDX
      key_len: 62
      ref: const
      rows: 15
      filtered: 11.11
      Extra: Using where; Using index

      还有一种情况,在name字段有二级索引(除了聚簇索引,如果表上有主键,该主键索引就是聚簇索引。如果未定义主键,则取第一个唯一索引而且只含非空列作为主键,并使用它作为聚簇索引。如果没有这样的列,InnoDB就自己产生一个这样的ID值,它有六个字节,而且是隐藏的,使其作为聚簇索引,其他索引都为二级索引),虽然该索引列不包括主键id列,但也能够对id列进行覆盖查询,如下:

       mysql> EXPLAIN SELECT id,name FROM TB1 WHERE name='test2'\G
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: ref
      possible_keys: IDX,IDX1
      key: IDX
      key_len: 62
      ref: const
      rows: 15
      filtered: 100.00
      Extra: Using index
    • 使用索引对结果做排序(当索引的列顺序和ORDER BY子句的顺序一致,并且所有列的排序方向(倒序或顺序)都一样时,可以使用索引对结果做排序),如下(假设TB1表中有一个多列索引(num,name,nk)):
       mysql> EXPLAIN SELECT * FROM TB1 WHERE num='1' ORDER BY name, nk\G       # 即使ORDER BY子句不满足作引的最左前缀要求,也可用于查询排序,因为索引的第一列(num)被指定为一个常数
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: ref
      possible_keys: IDX2
      key: IDX2
      key_len: 4
      ref: const
      rows: 872985
      filtered: 100.00
      Extra: Using index condition
       mysql> EXPLAIN SELECT * FROM TB1 WHERE num>'1' ORDER BY num, name\G        # 这个查询也没问题,ORDER BY使用的两列就是索引的最左前缀
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: range
      possible_keys: IDX2
      key: IDX2
      key_len: 4
      ref: NULL
      rows: 1
      filtered: 100.00
      Extra: Using index condition

      下面是一些反例:

       mysql> EXPLAIN SELECT * FROM TB1 WHERE num>'1' ORDER BY name, nk\G         # 这个查询是错误的,索引的第一列(num)被指定为一个范围,且ORDER BY不满足最左前缀
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: range
      possible_keys: IDX2
      key: IDX2
      key_len: 4
      ref: NULL
      rows: 1
      filtered: 100.00
      Extra: Using index condition; Using filesort
       mysql> EXPLAIN SELECT * FROM TB1 WHERE num='1' ORDER BY name DESC, nk ASC\G    #  这个查询使用了两种不同的排序方向,但索引是顺序排序的
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: ref
      possible_keys: IDX2
      key: IDX2
      key_len: 4
      ref: const
      rows: 872985
      filtered: 100.00
      Extra: Using index condition; Using filesort
       mysql> EXPLAIN SELECT * FROM TB1 WHERE num='1' ORDER BY name, ctime\G    #  这个查询的ORDER BY 引用了一个不再索引的中列(ctime)
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: ref
      possible_keys: IDX2
      key: IDX2
      key_len: 4
      ref: const
      rows: 872985
      filtered: 100.00
      Extra: Using index condition; Using filesort
       mysql> EXPLAIN SELECT * FROM TB1 WHERE num='1' ORDER BY nk\G        #  这个查询的WHERE 和ORDER BY 中的列无法组合成索引的最左前缀
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: ref
      possible_keys: IDX2
      key: IDX2
      key_len: 4
      ref: const
      rows: 872985
      filtered: 100.00
      Extra: Using index condition; Using filesort
       mysql> EXPLAIN SELECT * FROM TB1 WHERE num='1' AND name IN('test1','test2') ORDER BY nk\G      #  这个查询在name列上有多个等于条件,对于排序来说,也是范围查询
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: TB1
      partitions: NULL
      type: range
      possible_keys: IDX2
      key: IDX2
      key_len: 66
      ref: NULL
      rows: 30
      filtered: 100.00
      Extra: Using index condition; Using filesort
    • MyISAM使用前缀压缩减少索引的大小,从而让更多的索引可以放入内存中,在某些情况能极大的提高性能。MyISAM压缩每个索引块的方法是:先保存索引块中的一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余的不同后缀部分,把这部分存储起来即可。例如,索引块中的第一个值是“test”,第二个值是“test88888”,那么第二个值的前缀压缩后存储的是类似“4,88888”这样的形式。压缩块使用更少空间,但代价是某些操作可能更慢。因为每个值的压缩前缀都依赖前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描,顺序的扫描速度不错,但如果是倒序扫描(DESC)就不是很好了,所以在块中查找某一行的操作平均都需要扫描半个索引块。
    • 避免重复索引(在相同的列上按照相同的顺序创建的相同类型的索引),如下:
       mysql> CREATE TABLE TB5(
      -> ID INT NOT NULL PRIMARY KEY,
      -> A INT NOT NULL,
      -> B INT NOT NULL,
      -> UNIQUE(ID),
      -> INDEX(ID)
      -> ) ENGINE=InnoDB;

      PS:MySQL的唯一限制和主键限制都是通过索引实现的,因此上面的写法实际上在相同的列上创建了三个重复的索引。通常没有理由这样做,除非是在同一列上创建不同类型的索引来满足不同的查询需求。

    • 避免冗余索引(创建了索引(A,B),在创建索引(A)就是冗余索引),如下:
       mysql> CREATE TABLE TB6(
      -> A INT NOT NULL,
      -> B INT NOT NULL,
      -> INDEX IDX(A,B),
      -> INDEX IDX1(A)
      -> ) ENGINE=InnoDB;

      PS:对于B-Tree索引来说,索引(A,B)也可以当作索引(A)来使用,但如果在创建索引(B,A)则不是冗余索引。