mysql 8 新特性三 Hash Join / 联接查询算法之Hash Join (五)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mysql 8 新特性三 Hash Join / 联接查询算法之Hash Join (五)相关的知识,希望对你有一定的参考价值。

参考技术A mysql8以前 的 join 算法只有 nested loop 这一种,在 MySQL8 中推出了一种新的算法 hash join,比 nested loop 更加高效。mysql8中的部分NLJ算法已经取消,hash join 是它的的替代方案。像属于NLJ的BNLJ、SNLJ都会被Hash join替代!不过基于索引的INLJ算法还是存在的,所以实际使用中可以对比下INLJ和Hash Join的查询性能然后做出选择。

个人觉得mysql8这个hash join也只能算是一个锦上添花的功能,顶多是代替了没有加索引时默认走的BNLJ算法,提高了join的性能下限。说白了就是给不懂加索引的mysql新用户提高下join性能。其实也不绝对,不过我有做 INLJ和Hash Join 对比实验,Hash Join 很有可能比需要在内部表建立索引的INLJ算法性能要好!毕竟当INLJ需要回表查的时候性能会大幅度下降,这时候Hash Join绝对值得一试的,当然具体两者之间的选择还请自己实际测试下。

创建user和book表

可以看看下列语句的执行计划,Extra 出现了 Using join buffer (hash join) 说明该语句使用到了hash join。这里还使用了 IGNORE index(index_user_id)禁用索引,不然使用的是INLJ。

那么,使用Hash Join会分为下面2个阶段:

1、build 构建阶段:从参与join的2个表中选一个,选择占空间小的那个表,不是行数少的,这里假设选择了 user 表。对 user表中每行的 join 字段值进行 hash(a.id ) 计算后放入内存中 hash table 的相应位置。所有行都存放到 hash table 之后,构建阶段完成。

溢出到磁盘在构建阶段过程中,如果内存满了,会把表中剩余数据写到磁盘上。不会只写入一个文件,会分成多个块文件。

2、probe 探测阶段:对 book 表中每行中的 join 字段的值进行 hash 计算:hash(b.user_id) 拿着计算结果到内存 hash table 中进行查找匹配,找到一行就发给 client。这样就完成了整个 join 操作,每个表只扫描一次就可以了,扫描匹配时间也是恒定的,非常高效。

散列连接的内存使用可以使用join_buffer_size系统变量来控制;散列连接使用的内存不能超过这个数量。当散列连接所需的内存超过可用的数量时,MySQL通过使用磁盘上的文件来处理这个问题(溢出到磁盘)。

如果发生这种情况,您应该知道,如果散列连接无法容纳在内存中,并且它创建的文件超过了为open_files_limit设置的数量,则连接可能不会成功。

为避免此类问题,请执行以下任一更改:
1、增加join_buffer_size,以便哈希连接不会溢出到磁盘。
在MySQL 8.0.19及更高版本中, 设置 optimizer_switch 变量值 hash_join=on or hash_join=off 的方式已经失效了

2、增加open_files_limit。若数据量实在太大内存无法申请更大的join_buffer,就只能溢出到磁盘上了。我们可以增加open_files_limit,防止创建的文件超过了为open_files_limit设置的数量而join失败。

必须使用format=tree(8.0.16的新特性)才能查看hash join的执行计划:

创建几张测试表

从MySQL 8.0.18开始,MySQL对每个连接都有一个等连接条件的任何查询都使用散列连接,并且没有可应用于任何连接条件的索引,例如:

在MySQL 8.0.20之前,如果任何一对连接的表没有至少一个等连接条件,就不能使用Hash Join,并且使用了较慢的BNLJ。而 在MySQL 8.0.20和更高版本中,hash join可以用于未包含等值连接条件的查询

甚至是笛卡尔积的join

Semijoin也行

还有 antijoin

Mysql 优化器内部JOIN算法hash join Nestloopjoin及classic hash join CHJ过程详解

Mysql hash join之classic hash join CHJ过程详解

hash join的历史

优化器里的hash join算法在SQL Server、Oracle、postgress等数据库早已实现,而Mysql在8.0.18之后才支持。在8.0.18之前mysql只支持嵌套循环关联(nested loop join),这其中最简单就是简易嵌套循环关联simple nestloop join,随后mysql做了改进进而支持block nestloop join, index nestloop join and batched key access等算法,这也是hash join算法被推迟实现的部分原因。

hash join的概述

提到hash join之前自然得说Nestloopjoin,以两个表的关联为例,它其实是个双层循环,先遍历外层的表(n条),再拿每次对应的值去匹配、循环遍历内部的表(M条)。这样显然会有M*n的计算复杂度。如果能将外部表先装载到内存,然后再做内部表的匹配、遍历,计算的复杂度就会大大降低,这就是hash join的思想。

hash join的类型

hash join 从具体实现上又分为:

1经典hash join(In-Memory Join或classic hash join或CHJ)

2 磁盘分区hash join (On-Disk Hash Join)

3 Grace Hash Join

4 混合hash join(hybrid hash join)

hash join应用场景

hash join主要应用在以下条件:

1两个或多个表至少包含一个等值条件关联时。

2 关联的字段上没有索引。

3 关联条件可以是原始字段或者表达式(如 T1.col1+T1.col2 = T2.col3+T2.col4).

4 关联条件等号的两表只能出现一张表。

5 补充,可能使用到hash join的SQL写法示例:

 CREATE TABLE t1 (t1_1 INT, t1_2 INT);
 CREATE TABLE t2 (t2_1 INT, t2_2 INT)
 SELECT * FROM t1 JOIN t2 ON (t1.t1_1 = t2.t2_1);
 SELECT * FROM t1 JOIN t2 ON (t1.t1_1 = t2.t2_1 AND t1.t1_2 = t2.t2_2);
 SELECT * FROM t1 JOIN t2 ON (t1.t1_1 = t2.t2_1 AND t2.t2_2 > 43);
 SELECT * FROM t1 JOIN t2 ON (t1.t1_1 + t1.t1_2 = t2.t2_1);
 SELECT * FROM t1 JOIN t2 ON (FLOOR(t1.t1_1 + t1.t1_2) = CEIL(t2.t2_1 = t2.t2_2));

In-Memory Join classic hash join

经典hash join(classic hash join)简称CHJ,该算法由两部分构成,一是构建哈希表(hash table)过程,二是探测、匹配(probe)过程。

以有如下查询和表格结构为例:

CREATE TABLE t1 (foo INT);

CREATE TABLE t2 (bar INT);

insert into t1 values(12);

insert into t2 values(34);

… ...  -- 插入数据省略

SELECT * FROM t1 JOIN t2 on (t1.foo = t2.bar);

构建哈希表:

1 当两个表以含等值条件方式关联时其中一个表会被指定为构建表,该表会以哈希表(哈希的值来自于等值条件里的字段)的形式读入内存。

2 假设这里t1表被指定为构建表,那么将会通过哈希函数产生哈希表,这里t1的foo字段是是哈希函数里的键。

探测阶段:

此时join里的另外个表即t2作为探测表,在构建完成后,开始从t2探测表作为输入。这时以t2的bar (t1、t2两表关联时t2的字段)作为哈希的键,同时该键是用来匹配内存里的t1生成的哈希表。一旦匹配到记录则意味着找到目标,这个按照每一行匹配的过程就做探测过程。

In-Memory Join过程示意

构建阶段示例

探测阶段示例​​​​

执行计划查看In-Memory Join

EXPLAIN ANALYZE
SELECT CountryCode, country.Name AS Country,
city.Name AS City, city.District
FROM world.country IGNORE INDEX (Primary)
INNER JOIN world.city IGNORE INDEX (CountryCode)
ON city.CountryCode = country.Code
WHERE Continent = 'Asia';

-- 数据库来自mysql官网示例数据库world
-- 结果
EXPLAIN
-> Inner hash join (world.city.CountryCode = world.country.`Code`)  (cost=13870.82 rows=595) (actual time=58.807..134.662 rows=1766 loops=1)
    -> Table scan on city  (cost=0.80 rows=4046) (actual time=30.193..95.386 rows=4079 loops=1)
    -> Hash
        -> Filter: (world.country.Continent = 'Asia')  (cost=30.90 rows=34) (actual time=21.446..28.489 rows=51 loops=1)
            -> Table scan on country  (cost=30.90 rows=239) (actual time=21.435..28.338 rows=239 loops=1)

执行示例

 过程介绍

Step1:Country的Code字段会被哈希函数哈希并保存在关联缓存(即哈希表)内(join buffer)。

Step2:然后通过和step1里一样的哈希函数哈希的字段CountryCode进行表扫描(table scan)遍历city表里的每一行。

以上是关于mysql 8 新特性三 Hash Join / 联接查询算法之Hash Join (五)的主要内容,如果未能解决你的问题,请参考以下文章

MySQL 8.0发布,你熟悉又陌生的Hash Join?

mysql 8 中的hash join

理论学习 | MySQL 8.0.18 +的hash join学习

Mysql 优化器内部JOIN算法hash join Nestloopjoin及classic hash join CHJ过程详解

MySQL Hash Join前世今生

MySQL Hash Join前世今生