我们知道,InnoDB 中索引分为两种,聚簇索引和二级索引,每个索引对应一颗B+树,其中聚簇索引的叶子节点存放完整的用户数据,而二级索引的叶子节点只存放 索引列+主键 数据,并且叶子节点按照索引列值从小到大排序链接,具体的,大家可以自行查阅资料补充知识点。
当我们执行像这么一条查询语句时
SELECT
*
FROM
t1
WHERE
key1 = "a"
假设 t1 中有2个索引,分别是聚簇索引和二级索引 idx_key1 (key1),(忽略执行计划)那么将会利用 idx_key1 来执行查询语句,从 idx_key1 索引中查找匹配 key1= "a" 的第一条记录,索引 idx_key1 除了记录k1的值外,还记录了对应的主键值,但是由于 SELECT "*" 需要查询所有用户数据,而这里的只有值 (key1,id),所以需要拿着主键值立刻回表查询该行的完整数据,然后返回给客户端。请记住,是立刻回表查询,而不是等查询到所有匹配的记录再一次性回表,当然 InnoDB 为了优化这个情况,用了一些策略来减少回表次数,这里忽略。
回表:二级索引中匹配到的记录中会有对应的索引列和主键值,根据这个主键值回到聚簇索引(也就是主键索引)中查询对应的完整用户记录,这个过程就是回表。
所以假设索引 idx_key1 中匹配了10条数据,那么将会产生10次回表,而回表的代价是很大的,回表意味着产生页面IO,会影响性能。
如果我们改一下这条查询语句:
SELECT
key1, id
FROM
t1
WHERE
key1 = "a"
那么只需要到二级索引中查找匹配的记录,则不需要回表,对性能更加友好。
假设表 t1 有2个索引,分别是聚簇索引和二级索引(联合索引) idx_key_part (key_part1, key_part2, key_part3) 。
当我们执行像这么一条查询语句时
SELECT
*
FROM
t1
WHERE
key_part2 = "a"
虽有联合索引,但是访问方法仍然是 ALL 全表扫描,因为在索引 idx_key_part 中,数据是先按照 key_part1 排序,再按照 key_part2 排序,最后按照 key_part3 排序的,当查询条件是 key_part2=a,InnoDB无法知道 key_part2=a 的记录分布在哪,需要全表扫描匹配。
举个例子,假设索引 idx_key_part (key_part1, key_part2, key_part3) 中有4条数据:
(1,b,X)
(1,c,X)
(2,c,X)
(3,a,X)
,在忽略了第一个元素的情况下,第二个元素单独抽出来是(b,c,c,a),如我们所见是无序的,所以只能全表扫描,挨个匹配。
这种情况下 InnoDB 不会利用索引 idx_key_part ,因为与其查询二级索引,还要花费回表的代价,还不如直接查询聚簇索引来的快。
但是如果我们把查询修改以下,改为
SELECT
key1, key2, key3, id
FROM
t1
WHERE
key2 = "a"
则可以利用到 idx_key_part 索引,因为我们虽然WHERE子句中无法利用索引,但是 idx_key_part 记录了 key1, key2, key3, id,与直接查聚簇索引相比,聚簇索引存储的元素更多,这就意味着一次页面IO能够读取的记录更加少,会产生更多的IO,所以 InnoDB 会选择 idx_key_part 来查询,也就是 INDEX 访问方法,也就是大家说的索引覆盖。
我们通过 EXPLAIN 分析一下 InnoDB 是不是确实这样做的:
简单说一下联表的原理是先选择一个驱动表,对驱动表执行查询结果,每匹配一条查询结果就立刻访问一次被驱动表。比如下列查询语句
SELECT
*
FROM
t1, t2
WHERE
t1.m1>1 AND t1.m1=t2.m2 AND t2.n2<'d'
假设选择 t1 作驱动表,查询 t1 后的结果集中符合 t1.m1>1 的有2条记录,分别是
t1.m1 = 2
t1.m1 = 3
那么每匹配到一条,就要立刻访问 t2 表一次,分别对 t2 表执行查询
SELECT * FROM t2 WHERE t2.m2=2 AND t2.n2<'d';
SELECT * FROM t2 WHERE t2.m2=3 AND t2.n2<'d';
请注意,是每匹配到一条t1记录,就立刻查询t2一次,而不是全部查询出来再执行查询。假设表 t2 建立了索引 idx_m1_m2_n2,很显然,上面2条查询语句无法利用这个索引,但是如果我们把查询改成
SELECT
t1.*, t2.m2
FROM
t1, t2
WHERE
t1.m1>1 AND t1.m1=t2.m2 AND t2.n2<'d'
虽然WHERE子句无法利用二级索引,但是 InnoDB 还是选择二级索引 idx_m1_m2_n2 来执行查询,因为可以使用 INDEX 访问方法,代价比 ALL 低,再说一遍,INDEX 比 ALL 效率更高的原因是,二级索引中每页存储的数据量更多,即使对二级索引进行全扫描,产生页面IO也比聚簇索引的更少。
总而言之,最好不要使用 * 作为查询列表,而是查询真正使用的列,增加 INDEX 作为查询方法的概率。
上面提到,联表查询中每匹配到驱动表中的一条数据,就要访问一次被驱动表。如果这个被驱动表数据特别多而且不能使用索引,相当于要访问这个表N次(N取决于表页数),这个IO代价也忒大了!!
所以 InnoDB 为了减少对被驱动表的访问次数,想了个策略。就是 Join Buffer ,在执行查询前申请一块固定大小的内存。
先把若干条驱动表的结果集中的记录放到 Join Buffer 中,然后扫描被驱动表,每条被驱动表的记录一次性和 Join Buffer 中的多条驱动表记录作匹配,这个匹配的内存发生在内存中,这样可以显著减少被驱动表的IO代价。
Join Buffer 并不会存放驱动表记录的所有列,而是存放查询列表和过滤条件中的列
所以,我们写查询的时候尽量不要写 SELECT *, 而是只写真正需要使用的列,提高 Join Buffer 的利用率,SELECT 的列越少,可以存储的条数越多,发生的IO次数也就越少!!
Join Buffer 可以通过系统变量进行配置,默认265KB