案例分析:

select
c.*
from
hotel_info_original c
left join
hotel_info_collection h
on
c.hotel_type=h.hotel_type
and
c.hotel_id =h.hotel_id
where
h.hotel_id is null

  这个sql是用来查询出 c 表中有 h 表中无的记录,所以想到了用 left join 的特性(返回左边全部记录,右表不满足匹配条件的记录对应行返回 null)来满足需求,不料这个查询非常慢。先来看查询计划:

  rows代表这个步骤相对上一步结果的每一行需要扫描的行数,可以看到这个sql需要扫描的行数为35773*8134,非常大的一个数字。

  在EXPLAIN结果中,第一行出现的表就是驱动表。

NLJ 算法

  即 Nested Loop Join,就是扫描一个表(外表,也叫驱动表),每读到一条记录,就根据 join 字段上的索引去另一张表(内表)里查找。内表(一般是带索引的表)被外表(也叫驱动表,一般为小表,不仅相对其他表为小表,而且记录数的绝对值也小,不要求有索引)驱动,外表返回的每一行都要在内表中检索与其匹配的行,因此整个返回的结果集不能太大(大于 1 万不适合)。

  驱动表:就是在嵌套循环连接和哈希连接中,用来最先获得数据,并以此表的数据为依据,逐步获得其他表的数据,直至最终查询到所有满足条件的数据的第一个表。驱动表不一定是表,有可能是数据集,即由某个表中满足条件的数据行,组成子集合后,再以此子集合作为连接其他表的数据来源。这个子集合,才是真正的驱动表,有时候为了简洁,直接将最先按照条件或得子集合的那张表叫做驱动表。我们常说,驱动表一定是小表,指的是根据条件获得的子集合一定要小,而不是说实体表本身一定要小,大表如果获得的子集合小,一样可以简称这个大表为驱动表。

  如果有三个及以上的表,则会先使用 NLJ 算法得到一、二个表的结果集,并将该结果集作为外层数据,遍历结果集到后第三个表中查询数据。

一个简单的嵌套循环联接(NLJ)算法,循环从第一个表中依次读取行,取到每行再到联接的下一个表中循环匹配。这个过程会重复多次直到剩余的表都被联接了。假设表t1、t2、t3用下面的联接类型进行联接:

Table Join Type
t1 range
t2 ref
t3 ALL

  如果使用的是简单NLJ算法,那么联接的过程像这样:

for each row in t1 matching range {
for each row in t2 matching reference key {
for each row in t3 {
if row satisfies join conditions,
send to client
}
}
}

  因为NLJ算法是通过外循环的行去匹配内循环的行,所以内循环的表会被扫描多次。

  由此可知道,on a.id = b.aid 代表着驱动表无法使用此索引,是给被驱动表用的。

BLJ 算法

  即 Block Nested-Loop Join,是MySQL 自己创建的方式。将指定的外层键对应的被驱动表缓存起来以提高性能。

  Join操作使用内存(join_buffer_size):应用程序经常会出现一些两表(或多表)Join的操作需求,MySQL在完成某些 Join 需求的时候(all/index join),为了减少参与Join的“被驱动表”的读取次数以提高性能,需要使用到 Join Buffer 来协助完成 Join操作(具体 Join 实现算法请参考:MySQL中的 Join基本实现原理)。当 Join Buffer太小,MySQL不会将该 Buffer存入磁盘文件,而是先将Join Buffer中的结果集与需要 Join 的表进行 Join操作,然后清空 Join Buffer中的数据,继续将剩余的结果集写入此 Buffer中,如此往复。这势必会造成被驱动表需要被多次读取,成倍增加 IO访问,降低效率。

for each row in t1 matching range {
for each row in t2 matching reference key {
store used columns from t1, t2 in join buffer
if buffer is full {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions,
send to client
}
}
empty buffer
}
}
} if buffer is not empty {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions,
send to client
}
}
}

  对上面的过程解释如下:
    1. 将t1、t2的联接结果放到缓冲区,直到缓冲区满为止;
    2. 遍历t3,内部再循环缓冲区,并找到匹配的行,发送到客户端;
    3. 清空缓冲区;
    4. 重复上面步骤,直至缓冲区不满;
    5. 处理缓冲区中剩余的数据,重复步骤2。

  设S是每次存储t1、t2组合的大小,C是组合的数量,则t3被扫描的次数为:

    (S * C)/join_buffer_size + 1
  由此可见,随着join_buffer_size的增大,t3被扫描的次数会较少,如果join_buffer_size足够大,大到可以容纳所有t1和t2联接产生的数据,t3只会被扫描1次。

实例1:

mysql> show create table c;
+-------+---------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+---------------------------------------------------------------------------------------------------------------------+
| c | CREATE TABLE `c` (
`id` int(11) NOT NULL,
`name` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-------+---------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec) mysql> show create table d;
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------+
| d | CREATE TABLE `d` (
`id` int(11) NOT NULL,
`score` int(11) DEFAULT NULL,
`stuid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec) mysql> explain select c.id,d.score from c,d where c.id=d.stuid;
+----+-------------+-------+------+---------------+------+---------+------+------+--------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+--------------------------------+
| 1 | SIMPLE | c | ALL | NULL | NULL | NULL | NULL | 42 | |
| 1 | SIMPLE | d | ALL | NULL | NULL | NULL | NULL | 61 | Using where; Using join buffer |
+----+-------------+-------+------+---------------+------+---------+------+------+--------------------------------+
2 rows in set (0.00 sec)

  MySQL 会根据条件选用不同的执行策略。比如说在上面的 d 和 c 表中,如果按照当前的 c 和 d 的结构,执行 explain 之后,是 c 驱动 d 表,因为 c 表较小。

  那么如果在c的id上加一个index之后,mysql就会采用d驱动c表了。

  【因为此时,在Nested Loop Join算法中,内部循环可以使用c表上的索引,加速执行c表的查询。内部查询每加快一点,对整个join来说都是效率上比较大的提升】

mysql> alter table c add index(id);
Query OK, 0 rows affected (0.94 sec)
Records: 0 Duplicates: 0 Warnings: 0 mysql> explain select c.id,d.score from c,d where c.id=d.stuid;
+----+-------------+-------+------+---------------+------+---------+--------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+--------------+------+-------------+
| 1 | SIMPLE | d | ALL | NULL | NULL | NULL | NULL | 61 | |
| 1 | SIMPLE | c | ref | id | id | 4 | test.d.stuid | 1 | Using index |
+----+-------------+-------+------+---------------+------+---------+--------------+------+-------------+
2 rows in set (0.00 sec)

实例2:

  表结构:

create table `user_group` (
`user_id` int(11) NOT NULL,
`group_id` int(11) not null,
`user_type` int(11) not null,
`gmt_create` datetime not null,
`gmt_modified` datetime not null,
`status` varchar(16) not null,
key `idx_user_group_uid` (`user_id`)
) engine=innodb default charset=utf8; create table `group_message` (
`id` int(11) not null auto_increment,
`gmt_create` datetime not null,
`gmt_modified` datetime not null,
`group_id` int(11) not null,
`user_id` int(11) not null,
`author` varchar(32) not null,
`subject` varchar(128) not null,
primary key (`id`),
key `idx_group_message_author_subject` (`author`,`subject`(16)),
key `idx_group_message_author` (`author`),
key `idx_group_message_gid_uid` (`group_id`,`user_id`)
) engine=innodb auto_increment=97 default charset=utf8; create table `group_message_content` (
`group_msg_id` int(11) not null,
`content` text NOT NULL,
KEY `group_message_content_msg_id` (`group_msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  查询:

explain
select
m.subject msg_subject,
c.content msg_content
from
user_group g,
group_message m,
group_message_content c
where
g.user_id = 1
and
m.group_id = g.group_id
and
c.group_msg_id = m.id\G

  结果:

*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: g
type: ref
possible_keys: user_group_gid_ind,user_group_uid_ind,user_group_gid_uid_ind
key: user_group_uid_ind
key_len: 4
ref: const
rows: 2
Extra: *************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: m
type: ref
possible_keys: PRIMARY,idx_group_message_gid_uid
key: idx_group_message_gid_uid
key_len: 4
ref: example.g.group_id
rows: 3
Extra: *************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ref
possible_keys: idx_group_message_content_msg_id
key: idx_group_message_content_msg_id
key_len: 4
ref: example.m.id
rows: 2
Extra:

  MySQL Query Optimizer 选择了 user_group 作为驱动表,首先利用我们传入的条件 user_id 通过 该表上面的索引 user_group_uid_ind 来进行 const 条件的索引 ref 查找,然后以 user_group 表中过滤出来的结果集的 group_id 字段作为查询条件,对 group_message 循环查询,然后再通过 user_group 和 group_message 两个表的结果集中的 group_message 的 id 作为条件 与 group_message_content 的 group_msg_id 比较进行循环查询,才得到最终的结果。没啥特别的,后一个引用前一个的结果集作为条件。

  如果去掉 group_message_content 上面的 idx_group_message_content_msg_id 这个索引,然后再看看会是什么效果:

*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: g
type: ref
possible_keys: idx_user_group_uid
key: idx_user_group_uid
key_len: 4
ref: const
rows: 2
Extra: *************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: m
type: ref
possible_keys: PRIMARY,idx_group_message_gid_uid
key: idx_group_message_gid_uid
key_len: 4
ref: example.g.group_id
rows: 3
Extra: *************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 96
Extra: Using where; Using join buffer

  我们看到不仅仅 group_message_content 表的访问从 ref 变成了 ALL,此外,在最后一行的 Extra信息从没有任何内容变成为 Using where; Using join buffer,也就是说,对于从 ref 变成 ALL 很容易理解,没有可以使用的索引的索引了嘛,当然得进行全表扫描了,Using where 也是因为变成全表扫描之后,我们需要取得的 content 字段只能通过对表中的数据进行 where 过滤才能取得,但是后面出现的 Using join buffer 是一个啥呢?

  我们知道,MySQL 中有一个供我们设置的参数 join_buffer_size ,这里实际上就是使用到了通过该参数所设置的 Buffer 区域。那为啥之前的执行计划中没有用到呢?

  实际上,Join Buffer 只有当我们的 Join 类型为 ALL(如示例中),index,rang 或者是 index_merge 的时候 才能够使用,所以,在我们去掉 group_message_content 表的 group_msg_id 字段的索引之前,由于 Join 是 ref 类型的,所以我们的执行计划中并没有看到有使用 Join Buffer。

join 优化

  用小结果集驱动大结果集,尽量减少join语句中的Nested Loop的循环总次数;
  优先优化Nested Loop的内层循环,因为内层循环是循环中执行次数最多的,每次循环提升很小的性能都能在整个循环中提升很大的性能;
  对被驱动表的join字段上建立索引;
  当被驱动表的join字段上无法建立索引的时候,设置足够的Join Buffer Size

参考:

  http://www.jasongj.com/2015/03/07/Join1/ #强烈推荐读,本文没写全里面的 Hash Join、Merge Join 等分析

  http://www.cnblogs.com/weizhenlu/p/5970392.html

  http://blog.csdn.net/ghsau/article/details/43762027

  http://database.51cto.com/art/200904/117947.htm

  http://blog.csdn.net/ys_565137671/article/details/6361730

MySQL-join的实现原理、优化及NLJ算法的更多相关文章

  1. MySQL join的实现原理及优化思路

    Join 的实现原理 在MySQL 中,只有一种Join 算法,也就是Nested Loop Join,没有其他很多数据库所提供的Hash Join,也没有Sort Merge Join.顾名思义,N ...

  2. MySQL Join 的实现原理

    在寻找Join 语句的优化思路之前,我们首先要理解在MySQL 中是如何来实现Join 的,只要理解了实现原理之后,优化就比较简单了.下面我们先分析一下MySQL 中Join 的实现原理.在MySQL ...

  3. Mysql Join语法以及性能优化

    引言 内外联结的区别是内联结将去除所有不符合条件的记录,而外联结则保留其中部分.外左联结与外右联结的区别在于如果用A左联结B则A中所有记录都会保留在结果中,此时B中只有符合联结条件的记录,而右联结相反 ...

  4. 转 Join的实现原理及优化思路

    前言 前面我们已经了解了MySQLQueryOptimizer的工作原理,学习了Query优化的基本原则和思路,理解了索引选择的技巧,这一节我们将围绕Query语句中使用非常频繁,且随时可能存在性能隐 ...

  5. MySQL JOIN原理

    先看一下实验的两张表: 表comments,总行数28856 表comments_for,总行数57,comments_id是有索引的,ID列为主键. 以上两张表是我们测试的基础,然后看一下索引,co ...

  6. MySQL JOIN原理(转)

    先看一下实验的两张表: 表comments,总行数28856 表comments_for,总行数57,comments_id是有索引的,ID列为主键. 以上两张表是我们测试的基础,然后看一下索引,co ...

  7. mysql join 底层原理

    你知道 Sql 中 left join 的底层原理吗? 2019-09-10阅读 7130 https://cloud.tencent.com/developer/column/2367   01.前 ...

  8. mysql join语句的执行流程是怎么样的

    mysql join语句的执行流程是怎么样的 join语句是使用十分频繁的sql语句,同样结果的join语句,写法不同会有非常大的性能差距. select * from t1 straight_joi ...

  9. mysql join 和left join 对于索引的问题

    今天遇到一个left join优化的问题,搞了一下午,中间查了不少资料,对MySQL的查询计划还有查询优化有了更进一步的了解,做一个简单的记录: select c.* from hotel_info_ ...

随机推荐

  1. Js计算指定日期加上多少天,加多少月,加多少年的日期

    function DateAdd(interval,number,date) { /* * 功能:实现VBScript的DateAdd功能. * 参数:interval,字符串表达式,表示要添加的时间 ...

  2. 10款CSS3按钮 - 程序员再也不用为按钮设计而发愁了...

    这次主要给大家分享10款风格各异的CSS3按钮,如果你希望你的页面也能有很炫的样式,那么我相信这10款CSS3按钮就非常适合你,而且每一款都整理了源代码供参考,一起来看看吧. 1.绚丽的CSS3发光按 ...

  3. JQuery效率问题

    1,前言 我们开发了一个专题系统,生成了JSON的数据格式,采用JQuery动态插入HTML中,在前期的使用中,没有太大的问题,效率还可以接受,但是最近可能由于网络加之页面设计问题,我们的JS效率比较 ...

  4. Eclipse关掉项目SVN的链接

    有时候 svn 会导致 eclipse 反应很慢,可以关掉 svn项目信息展现. 1. 点击项目文件夹,右键出现项目信息 2. 选择team项 3. Disconnect.

  5. POI的一些配置

    引用:http://apps.hi.baidu.com/share/detail/17249059 POI中可能会用到一些需要设置EXCEL单元格格式的操作小结: 先获取工作薄对象: HSSFWork ...

  6. [Javascript] Check both prop exists and value is valid

    Sometime you need to check one prop exists on the object and value should not be ´null´ or ´undefine ...

  7. Python 各种测试框架简介(三):nose

    转载:https://blog.csdn.net/qq_15013233/article/details/52527260 摘要 这里将从(pythontesting.net)陆续编译四篇 Pytho ...

  8. 解决Android sdk无法下载的问题

    由于android官网在国内无法正常访问,在安装android sdk时经常会出现http://dl.google.com拒绝访问的情况.为了解决这个问题,建议使用国内镜像源,这里推荐几个: 1.mi ...

  9. Hbase总结(六)hbase37个笔试题

    下面试题是摘自互联网的基础上自己加了选项说明解释便于自己以后看时方便节省时间 1. HBase来源于哪篇博文? C A The Google File System B MapReduce C Big ...

  10. WIN10系统如何隐藏底部搜索框

    右击任务栏,搜索,可以切换三种模式,建议还是显示搜索图标,因为这个搜索还是能比较快速定位到系统功能的,只不过显示搜索框的话比较占地方,不方便