单表访问方法

首先创建一个表:

CREATE TABLE single_table (
 id INT NOT NULL AUTO_INCREMENT,
 key1 VARCHAR(100),
 key2 INT,
 key3 VARCHAR(100),
 key_part1 VARCHAR(100),
 key_part2 VARCHAR(100),
 key_part3 VARCHAR(100),
 common_field VARCHAR(100),
 PRIMARY KEY (id),
 KEY idx_key1 (key1),
 UNIQUE KEY idx_key2 (key2),
 KEY idx_key3 (key3),
 KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;

我们为这个 single_table 表建立了1个聚簇索引和4个二级索引,分别是:

  • 为 id 列建立的聚簇索引。
  • 为 key1 列建立的 idx_key1 二级索引。
  • 为 key2 列建立的 idx_key2 二级索引,而且该索引是唯一二级索引。
  • 为 key3 列建立的 idx_key3 二级索引。
  • 为 key_part1 、 key_part2 、 key_part3 列建立的 idx_key_part 二级索引,这也是一个联合索引。

然后我们需要为这个表插入10000行记录,除 id 列外其余的列都插入随机值就好了,具体的插入语句我就不写了,自己写个程序插入吧(id列是自增主键列,不需要我们手动插入)。

访问方法的概念

对于单个表的查询来说,MySQL把查询的执行方式大致分为下边两种:

  • 使用全表扫描进行查询

    这种执行方式很好理解,就是把表的每一行记录都扫一遍嘛,把符合搜索条件的记录加入到结果集就完了。不管是啥查询都可以使用这种方式执行,当然,这种也是最笨的执行方式。

  • 使用索引进行查询

    因为直接使用全表扫描的方式执行查询要遍历好多记录,所以代价可能太大了。如果查询语句中的搜索条件可以使用到某个索引,那直接使用索引来执行查询可能会加快查询执行的时间。使用索引来执行查询的方式五花八门,又可以细分为许多种类:

    • 针对主键或唯一二级索引的等值查询
    • 针对普通二级索引的等值查询
    • 针对索引列的范围查询
    • 直接扫描整个索引

MySQL 执行查询语句的方式称之为 访问方法 或者 访问类型。同一个查询语句可能可以使用多种不同的访问方法来执行,虽然最后的查询结果都是一样的,但是执行的时间可能差远了。

下边细细道来各种 访问方法 的具体内容。

const

有的时候我们可以通过主键列来定位一条记录,比方说这个查询:

select * from single_table where id = 1438;

MySQL 会直接利用主键值在聚簇索引中定位对应的用户记录,就想这样:

image-20230301141220383

B+ 树叶子节点中的记录是按照索引列排序的,对于的聚簇索引来说,它对应的 B+ 树叶子节点中的记录就是按照 id 列排序的。

所以这样根据主键值定位一条记录的速度贼快。类似的,我们根据唯一二级索引列来定位一条记录的速度也是贼快的,比如下边这个查询:

SELECT * FROM single_table WHERE key2 = 3841;

这个查询的执行过程的示意图就是这样:

image-20230301141532586

可以看到这个查询的执行分两步,第一步先从 idx_key2 对应的 B+ 树索引中根据 key2 列与常数的等值比较条件定位到一条二级索引记录,然后再根据该记录的 id 值到聚簇索引中获取到完整的用户记录。

这种通过主键或者唯一二级索引列来定位一条记录的访问方法定义为: const,意思是常数级别的,代价是可以忽略不计的。不过这种 const 访问方法只能在主键列或者唯一二级索引列和一个常数进行等值比较时才有效,如果主键或者唯一二级索引是由多个列构成的话,索引中的每一个列都需要与常数进行等值比较,这个const 访问方法才有效(这是因为只有该索引中全部列都采用等值比较才可以定位唯一的一条记录)。

对于唯一二级索引来说,查询该列为 NULL 值的情况比较特殊,比如这样:

SELECT * FROM single_table WHERE key2 IS NULL;

因为唯一二级索引列并不限制 NULL 值的数量,所以上述语句可能访问到多条记录,也就是说 上边这个语句不可以使用 const 访问方法来执行

ref

有时候我们对某一个普通的二级索引列与场数进行等值比较,比如这样:

SELECT * FROM single_table WHERE key1 = 'abc';

对于这个查询,我们当然可以选择全表扫描来逐一对比搜索条件是否满足要求,我们也可以先使用二级索引找到对应记录的 id 值,然后再回表到聚簇索引中查找完整的用户记录。由于普通二级索引并不限制索引列值的唯一性,所以可能找到多条对应的记录,也就是说使用二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。如果匹配的记录较少,则回表的代价还是比较低的,所以 MySQL 可能选择使用索引而不是全表扫描的方式来执行查询。

把这种搜索条件为二级索引列与常数等值比较,采用二级索引来执行查询的访问方法称为: ref 。我们看一下采用 ref 访问方法执行查询的图示:

image-20230301143512738

从图示中可以看出,对于普通的二级索引来说,通过索引列进行等值比较后可能匹配到多条连续的记录,而不是像主键或者唯一二级索引那样最多只能匹配1条记录,所以这种 ref 访问方法比 const 差了那么一丢丢,但是在二级索引等值比较时匹配的记录数较少时的效率还是很高的(如果匹配的二级索引记录太多那么回表的成本就太大了),跟坐高铁差不多。不过需要注意下边两种情况:

  • 二级索引列值为 NULL 的情况
    • 不论是普通的二级索引,还是唯一二级索引,它们的索引列对包含 NULL 值的数量并不限制,所以我们采用key IS NULL 这种形式的搜索条件最多只能使用 ref 的访问方法,而不是 const 的访问方法。
  • 对于某个包含多个索引列的二级索引来说,只要是最左边的连续索引列是与常数的等值比较就可能采用 ref 的访问方法,比方说下边这几个查询:
    SELECT * FROM single_table WHERE key_part1 = 'god like';
    SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 = 'legendary';
    SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 = 'legendary' AND key_part3 = 'penta kill';
    

但是如果最左边的连续索引并不全部是等值的比较的话,它的访问方法就不能称为 ref 了,比方说这样:

SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 > 'legendary';

ref_or_null

有时候我们不仅想找出某个二级索引列的值等于某个常数的记录,还想把该列的值为 NULL 的记录也找出来,就像下边这个查询:

SELECT * FROM single_demo WHERE key1 = 'abc' OR key1 IS NULL;

当使用二级索引而不是全表扫描的方式执行该查询时,这种类型的查询使用的访问方法就称为 ref_or_null ,这个 ref_or_null 访问方法的执行过程如下:

image-20230301145105339

可以看到,上边的查询相当于先分别从 idx_key1 索引对应的 B+ 树中找出 key1 IS NULL 和 key1 = 'abc' 的两个连续的记录范围,然后根据这些二级索引记录中的 id 值再回表查找完整的用户记录。

range

我们之前介绍的几种访问方法都是在对索引列与某一个常数进行等值比较的时候才可能使用到( ref_or_null 比较奇特,还计算了值为 NULL 的情况),但是有时候我们面对的搜索条件更复杂,比如下边这个查询:

SELECT * FROM single_table WHERE key2 IN (1438, 6328) OR (key2 >= 38 AND key2 <= 79);

我们当然还可以使用全表扫描的方式来执行这个查询,不过也可以使用 二级索引 + 回表 的方式执行,如果采用 二级索引 + 回表 的方式来执行的话,那么此时的搜索条件就不只是要求索引列与常数的等值匹配了,而是索引列需要匹配某个或某些范围的值,在本查询中 key2 列的值只要匹配下列3个范围中的任何一个就算是匹配成功。此处所说的使用索引进行范围匹配中的 索引 可以是聚簇索引,也可以是二级索引

  • key2 的值是 1438
  • key2 的值是 6328
  • key2 的值在 38 和 79 之间。

all

最直接的查询执行方式就是我们已经提了无数遍的全表扫描,对于 InnoDB 表来说也就是直接扫描聚簇索引,设计 MySQL 的大叔把这种使用全表扫描执行查询的方式称之为: all 。