5

索引(key)是存储引擎用于快速找到记录的一种数据结构。它和一本书中目录的工作方式类似——当要查找一行记录时,先在索引中快速找到行所在的位置信息,然后再直接获取到那行记录。
在MySql中,索引是在存储引擎层而不是服务器层实现的,所以不同的存储引擎对索引的实现和支持都不相同。

B-TREE索引

B-TREE索引是使用最多的索引。很多存储引擎采用的都是B-TREE数据结构的变体实现该索引,例如InnoDB使用的是B+TREE,即每个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点范围遍历。
不同存储引擎使用B-TREE索引的方式也不同。例如MyISAM使用前缀压缩技术使索引更小,而InnoDB则按照原数据格式进行存储。再如MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
B-TREE中的所有值都是按顺序存储的,每个叶子页到根的距离相同。下图展示了InnoDB中的B-TREE索引是如何工作的:
B-Tree数据结构

当查找一行记录时,存储引擎会先在索引中搜索。从索引的根节点开始,通过比较节点页的值和要查找的值逐层进入下层节点,最底层叶子节点的指针指向的是被索引的数据。这样的查找方式避免了全表扫描,加快访问数据的速度。此外因为B-Tree对索引列是顺序存储的,所以也很适合查找范围数据。
下面是一个使用B-Tree索引的例子,有如下数据表:

CREATE TABLE People (
    last_name varchar(50) not null,
    first_name varchar(50) not null,
    dob date not null,
    gender enum('m','f') not null,
    key(last_name,first_name,dob)
)

这个建表语句在last_name、first_name、dob列上建立了一个联合索引,下图展示了该索引的存储结构。
图片描述

可以看到,联合索引中的索引项会先根据第一个索引列进行排序,第一个索引列相同的情况下,会再按照第二个索引列进行排序,依次类推。根据这种存储特点,B-Tree索引对如下类型的查找有效:

  1. 全值匹配:查找条件和索引中的所有列相匹配

  2. 匹配最左前缀:查找条件只有索引中的第一列

  3. 匹配列前缀:只匹配某一列值的开头部分。这里并不一定只能匹配第一个索引列的前缀。例如在确定第一个索引列的值时,也可以在第二个索引列上匹配列前缀。在上面例子中,对于查找姓为Allen,名为J开头的人,也可以应用到索引。

  4. 匹配范围值,或者精确匹配某一列并范围匹配另外一列:例如查找姓在Allen和Barrymore之间的人,或者查找姓为Allen,名字在某一个范围内的人。

  5. 只访问索引的查询,即要查询的值在索引中都包含,只需要访问索引就行了,无需访问数据行。这种索引被称作覆盖索引。

  6. 对于上面列出的查询类型,索引除了可以用来查询外,还可以用来排序。

下面是B-Tree索引的一些限制:

  1. 如果不是从索引的最左列开始查找,则无法使用索引。例如直接查找名字为Bill的人,或查找某个生日的人都无法应用到上面的索引,因为都跳过了索引的第一个列。此外查找姓以某个字母结尾的人,也无法使用到上面的索引。

  2. 不能在中间跳过索引中的某个列,例如不能查找姓为Smith,生日为某个特定日期的类。这样的查询只能使用到索引的第一列。

  3. 如果查询中有某个列的范围查询,则该列右边的所有列都无法使用索引优化查找。例如有查询WHERE last_name='Smith' AND first_name LIKE 'J%' AND dob='1976-12-23',这个查询只能使用到索引的前两列,而不能使用整个索引。

通过上面列出的这些条件,可见对于一个B-TREE联合索引,索引列的顺序非常重要。

哈希索引

如果在列上建立哈希索引,则针对每一行数据,存储引擎会根据所有的索引列计算出一个哈希码,每个行计算出的哈希码会组成一个哈希表,同时在哈希表中存储了指向每个数据行的指针。只有精确匹配全部索引行的查询条件才能利用到哈希索引。
例如有如下数据表,在表中fname列上建立了一个哈希索引:

CREATE TABLE testhash(
    fname VARCHAR(50) NOT NULL,
    lname VARCHAR(50) NOT NULL,
    KEY USING HASH(fname)
);

表中包含如下数据:
图片描述

假设索引使用的哈希函数对fname字段的各个值生成如下哈希码:
图片描述

则索引中的哈希表结构如下:
图片描述

对于下面的这条查询:

select lname from testhash where fname = 'Peter';

MySql先计算Peter的哈希码为8784,根据这个哈希码得到“指向第3行数据的指针”。然后获取第三行数据,判断第三行的fname是否为Peter,以确保就是要查找的行(防止哈希冲突影响)。
因为哈希索引中的哈希表只包含指向对应记录行的指针,所以索引的结构十分紧凑,查找的速度也非常快。但也有下面这些限制:

  1. 哈希索引中不包含任何列的值,所以无法利用哈希索引避免读取行。

  2. 哈希索引无法用于排序。

  3. 不支持部分索引列匹配查找。必须是使用全部索引列的查询条件才能使用哈希索引优化查询。

  4. 只能支持等值比较,不支持范围查找。

  5. 哈希冲突会影响索引性能。当有哈希冲突时,必须遍历每个哈希值相同索引项,找到对应的数据行与其进行比较,直到找到对应数据或遍历结束。

InnoDB中有一个功能叫“自适应哈希索引”,当InnoDB注意到某些索引值使用的非常频繁时,会在B-Tree索引之上再建立一层哈希索引,以加速查找效率。这是完全自动的内部行为,用户无法干预。
下面是借助哈希算法优化索引的一个例子:
假设需要存储大量的URL,并根据URL进行搜索查找。如果直接使用B-Tree索引存储URL,因为URL一般都比较长,存储的内容就会很大。可以在表中新建一个列url_crc,这个列存储的值为crc32(url),并在url_crc列上建立一个B-Tree索引。这样只需要很小的索引就可以为超长的列建立索引,性能会提高很多。注意要使用下面形式的查询语句进行查询,因为有可能会有哈希冲突,所以还要对url进行等值比较。

select id from url where url='https://segmentfault.com/write?draftId=1220000010978320' and
 url_crc=crc32('https://segmentfault.com/write?draftId=1220000010978320');

独立的列

索引列不能是表达式的一部分,也不能是函数的参数,否则将无法使用索引进行查询优化。列入下面两个查询都无法利用到索引:

SELECT actor_id FROM actor WHERE actor_id+1=5;
SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col)<=10;

虽然有些表达式很简单,但MySql也无法解析方程式。应该养成简化where条件的习惯,始终将索引列单独放在比较符号的一侧。

前缀索引

如果在很长的字符串列上建立索引,会导致索引变得大且慢。这时可以考虑只根据列的开始部分字符建立索引,这样可以大大节约索引空间,从而提高索引效率。
对于BLOB、TEXT或很长的VARCHAR类型的列,必须使用前缀索引,因为MySql不允许索引这些列的完整长度。
在使用前缀索引时,要注意保证索引的选择性。索引的选择性是指不重复的索引值和数据表记录总数的比值。索引的选择性越高则查询效率越高,因为选择性高的索引在查找时会过滤掉更多的行。所以建立前缀索引时,选取的字符串前缀长度不能太短。

多列索引

像下面这样在每个列上都建立一个单独的索引是非常错误的:

CREATE TABLE t(
    t1 INT,
    t2 INT,
    t3 INT,
    KEY(t1),
    KEY(t2),
    KEY(t3)
)

当出现对多个索引列做相交(AND)操作的查询时,代表需要一个包含所有相关列的联合索引,而不是多个独立的单列索引。
在MySql官方提供的示例数据库sakila中,表film_actor在字段film_id和actor_id上各有一个单列索引,对于下面这条查询语句,这两个单列索引都不是很好的选择:

SELECT film_id,actor_id FROM film_actor WHERE actor_id=1 OR film_id=1;

在老的MySql版本中,这个查询会使用全表扫描。但在MySql5.0之后,查询能够同时使用这两个单列索引进行扫描,然后将结果合并,相当于转换成下面这条查询:

SELECT film_id,actor_id FROM film_actor WHERE actor_id=1 
UNION
SELECT film_id,actor_id FROM film_actor WHERE film_id=1;

在MySql5.7中,执行上面查询的执行计划如下图所示:
图片描述

从执行计划的type字段可以看到,MySql同时使用了两个索引,并将各自的查询结果合并。并且Extra字段描述了使用索引的详细信息。
虽然MySql在背后对查询进行了优化,使其可以同时利用两个单列索引。但是这需要耗费大量的CPU和内存资源,所以直接将查询改写成UNION的方式会更好。像这种两个列上都有索引的情况,用union代替or会得到更好的效果(注意要求两个列上都建有索引,如果没有索引,用union代替or反而会降低效率)。
如果在EXPLAIN中看到有索引合并,那就应该好好检查一下查询和表的结构,看看是不是已经是最优的。

联合索引列的顺序

将选择性最高的列放到索引的最前列虽然是一条重要的参考准则,但通常不如避免随机IO和排序那么重要。所以在设计索引时,还要考虑到WHERE子句中的排序、分组和范围条件等其他因素,这些因素可能对查询的性能造成非常大的影响。

聚簇索引

因为是存储引擎负责实现索引,所以对于不同的存储引擎,聚簇索引的实现方式有所区别。这里讨论的是InnoDB下的聚簇索引。
所谓聚簇索引,就是表中的数据行实际上是存放在聚簇索引的叶子页中。因为表中的数据行只有一份,所以一个表只能有一个聚簇索引。在InnoDB中,唯一的聚簇索引就是主键索引。如果没有定义主键,InnoDB会选择一个非空索引代替。如果连一个非空索引都没有,则InnoDB会隐式定义一个主键来作为聚簇索引。也就是说,对于InnoDB存储引擎,它肯定是有且仅有一个聚簇索引的存在,并且InnoDB存储引擎的表结构就是通过聚簇索引组织的。
下面定义了一个表结构:

CREATE TABLE layout_test (
    col1 int NOT NULL,
    col2 int NOT NULL,
    PRIMARY KEY(col1),
    KEY(col2)
); 

对于这个表结构,在InnoDB中建立的聚簇索引结构如下图所示:
图片描述

首先聚簇索引跟之前介绍过的索引一样,采用的都是B-Tree的数据结构。不同的是,聚簇索引在叶子节点中,不仅保存了索引列的值,还保存了TID(事务ID)、RP(回滚指针)和表中所有非主键的列的值(这个例子中只有一个非主键列col2)。非叶子节点中还是只包含索引列的值(就是主键)和指向下一个节点的指针。由此可以看到,在InnoDB中,整个表结构是用聚簇索引来维护的。
对于这里的二级索引(非主键列,这里是col2上的索引),它的叶子节点并不保存指向数据行的指针,而是保存了行的主键值。这样当出现数据行的物理位置改变时,无需更新二级索引中的这个“指针”。
如果使用的是InnoDB存储引擎,则应该保证主键是按照数据行的插入顺序递增的。可以定义一个代理键作为主键,这种主键的数据可以和应用无关,最简单的方式是使用AUTO_INCREMENT自增列。因为如果主键的值是顺序增长的,那么每次insert操作其实就是简单的把本次插入的记录放到上一条记录的后面,当达到一个页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出的部分空间用于以后修改),本次要插入的记录就会写到新的页中。这种顺序写入的方式操作简单,并且不会导致页分裂,会有很高的效率。
相反,如果主键是一个随机的值,InnoDB就无法简单的总是把新行插入到索引的最后,而是要为新的行寻找合适的位置(通常是已有数据的中间位置),这会导致分配空间、页分裂等很多额外的工作。

覆盖索引

如果一个索引包含所有需要查询的字段,就称之为“覆盖索引”。由于在索引的叶子节点中已经包含了要查询的全部数据,所以就可以从索引中直接获取查询结果,而没必要再回表查询。
索引一般远远小于数据行的大小,如果只需要访问索引,就会极大减少数据访问量。而且索引是按照顺序存储,所以在进行范围查询时会比随机从磁盘读取每一条数据的I/O要少的多。由此看出,覆盖索引能够极大的提高查询性能。
sakila数据库中包含了由store_id和film_id组成的一个联合索引,如下图所示:
图片描述

如果只查询store_id和film_id这两列,就可以使用这个索引做覆盖索引。
图片描述

EXPLAIN的Extra列如果是Using index,则代表这个查询使用到了覆盖索引。注意type字段和是否为覆盖索引毫无关系。

利用索引做排序

只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向都一样时,才能使用索引对结果进行排序。一般情况ORDER BY子句和查找型查询的限制是一样的,都需要满足索引的最左前缀要求。但有一种特殊情况,如果在WHERE子句或JOIN子句中对索引前导列指定了常量,则order by子句可以不满足索引的最左前缀的要求。
在表rental中,有一个在列(rental_date,inventory_id,customer_id)上建立的联合索引rental_date,下图描述了rental表的结构:
图片描述

可以利用rental_date索引为下面的查询做排序:
图片描述

虽然这里的order by子句不满足索引的最左前缀的要求,但由于在WHERE子句中指定了索引rental_date的第一列为一个常量,所以仍然可以用于排序。
下面这个查询也可以利用rental_date索引进行排序,因为ORDER BY子句中使用的两列就是索引的最左前缀。

. . .WHERE rental_date>'2005-05-25' ORDER BY rental_date,inventory_id;

下面的查询就无法使用索引进行排序:
图片描述

虽然在ORDER BY子句中满足了最左索引前缀的要求,但由于查询使用了两种不同的排序方向,所以仍然无法使用索引进行排序。注意EXPLAIN中的type字段只是说明了这个SQL利用到了索引进行查询,Extra字段才描述了有没有使用到索引进行排序。
下面的查询语句中ORDER BY子句中的两个列都是逆序的,但仍然可以使用索引进行排序。所以是否能利用到索引进行排序与DESC和ASC无关,只要每个列的排序方向都相同,就可以利用到索引进行排序。

. . .WHERE rental_date='2005-05-25' 
ORDER BY inventory_id DESC,customer_id DESC

poype
425 声望79 粉丝