Join
,翻译成中文就是
连接
。相信很多小伙伴在初学连接的时候有些一脸懵逼,理解了连接的语义之后又可能不明白各个表中的记录到
底是怎么连起来的,以至于在使用的时候常常陷入下边两种误区:
- 误区一:业务至上,管他三七二十一,再复杂的查询也用在一个连接语句中搞定。
- 误区二:敬而远之,上次 DBA 那给报过来的慢查询就是因为使用了连接导致的,以后再也不敢用了。
我们先来介绍一下
MySQL
中支持的一些连接语法。
连接简介
连接的本质
为了故事的顺利发展,我们先建立两个简单的表并给它们填充一点数据:
mysql> CREATE TABLE t1 (m1 int, n1 char(1));
Query OK, 0 rows affected (0.02 sec)
mysql> CREATE TABLE t2 (m2 int, n2 char(1));
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO t1 VALUES(1, 'a'), (2, 'b'), (3, 'c');
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> INSERT INTO t2 VALUES(2, 'b'), (3, 'c'), (4, 'd');
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
我们成功建立了
t1
、
t2
两个表,这两个表都有两个列,一个是
INT
类型的,一个是
CHAR(1)
类型的,填充好数据的两个表长这样:
mysql> SELECT * FROM t1
连接
的本质就是把各个连接表中的记录都取出来依次匹配的组合加入结果集并返回给用户。所以我们把
t1
和
t2
两个表连接起来的过程如下图所示:
这个过程看起来就是把
t1
表的记录和
t2的记录连起来组成新的更大的记录,所以这个查询过程称之为连接查询。连接查询的结果集中包含一个表中的每一条记录与另一 个表中的每一条记录相互匹配的组合,像这样的结果集就可以称之为
笛卡尔积
。因为表
t1
中有
3
条记录,表
t2
中也有
3条记录,所以这两个表连接之后的笛卡尔积就有
3×3=9
行记录。在
MySQL
中,连接查询的语法也很随意,只要在
FROM
语句后边跟多个表名就好了,比如我们把
t1
表和
t2
表连接起来的查询语句可以写成这样:
连接过程简介
如果我们乐意,我们可以连接任意数量张表,但是如果没有任何限制条件的话,这些表连接起来产生的
笛卡尔积
可能是非常巨大的。比方说
3
个
100行记录的表连接起来产生的
笛卡尔积
就有
100×100×100=1000000
行数据!所以在连接的时候过滤掉特定记录组合是有必要的,在连接查询中的过滤条件可以分成两种:
- 涉及单表的条件
这种只设计单表的过滤条件我们之前都提到过一万遍了,我们之前也一直称为
搜索条件
,比如
t1.m1 > 1
是只针对
t1
表的过滤条件,
t2.n2 < 'd'
是只针对
t2
表的过
滤条件。
- 涉及两表的条件
这种过滤条件我们之前没见过,比如
t1.m1 = t2.m2
、
t1.n1 > t2.n2
等,这些条件中涉及到了两个表,我们稍后会仔细分析这种过滤条件是如何使用的哈。
下边我们就要看一下携带过滤条件的连接查询的大致执行过程了,比方说下边这个查询语句:
SELECT * FROM t1, t2 WHERE t1.m1 > 1 AND t1.m1 = t2.m2 AND t2.n2 < 'd';
在这个查询中我们指明了这三个过滤条件:
- t1.m1 > 1
- t1.m1 = t2.m2
- t2.n2 < 'd'
那么这个连接查询的大致执行过程如下:
1.
首先确定第一个需要查询的表,这个表称之为
驱动表。怎样在单表中执行查询语句我们在前一章都唠叨过了,只需要选取代价最小的那种访问方法去执行单表查询 语句就好了(就是说从
const
、
ref
、
ref_or_null
、
range
、
index
、
all
这些执行方法中选取代价最小的去执行查询)。此处假设使用
t1
作为驱动表,那么就需要到
t1表中找 满足
t1.m1 > 1
的记录,因为表中的数据太少,我们也没在表上建立二级索引,所以此处查询
t1
表的访问方法就设定为
all吧,也就是采用全表扫描的方式执行单表 查询。关于如何提升连接查询的性能我们之后再说,现在先把基本概念捋清楚哈。所以查询过程就如下图所示:
我们可以看到,t1表中符合t1.m1 > 1的记录有两条。
2.
针对上一步骤中从驱动表产生的结果集中的每一条记录,分别需要到
t2
表中查找匹配的记录,所谓
匹配的记录
,指的是符合过滤条件的记录。因为是根据
t1
表中的
记录去找
t2
表中的记录,所以
t2
表也可以被称之为
被驱动表
。上一步骤从驱动表中得到了
2
条记录,所以需要查询
2
次
t2
表。此时涉及两个表的列的过滤条件
t1.m1 =
t2.m2
就派上用场了:
- 当t1.m1 = 2时,过滤条件t1.m1 = t2.m2就相当于t2.m2 = 2,所以此时t2表相当于有了t2.m2 = 2、t2.n2 < 'd'这两个过滤条件,然后到t2表中执行单表查询。
- 当t1.m1 = 3时,过滤条件t1.m1 = t2.m2就相当于t2.m2 = 3,所以此时t2表相当于有了t2.m2 = 3、t2.n2 < 'd'这两个过滤条件,然后到t2表中执行单表查询。
所以整个连接查询的执行过程就如下图所示:
也就是说整个连接查询最后的结果只有两条符合过滤条件的记录:
从上边两个步骤可以看出来,我们上边唠叨的这个两表连接查询共需要查询
1
次
t1
表,
2
次
t2
表。当然这是在特定的过滤条件下的结果,如果我们把
t1.m1 > 1这个条件去掉,那么从
t1
表中查出的记录就有
3
条,就需要查询
3
次
t2
表了。也就是说在两表连接查询中,驱动表只需要访问一次,被驱动表可能被访问多次。
内连接和外连接
为了大家更好理解后边内容,我们先创建两个有现实意义的表,
CREATE TABLE student (
number INT NOT NULL AUTO_INCREMENT COMMENT '学号',
name VARCHAR(5) COMMENT '姓名',
major VARCHAR(30) COMMENT '专业',
PRIMARY KEY (number) ) Engine=InnoDB CHARSET=utf8 COMMENT '学生信息表';
CREATE TABLE score (
number INT COMMENT '学号',
subject VARCHAR(30) COMMENT '科目',
score TINYINT COMMENT '成绩',
PRIMARY KEY (number, score)
) Engine=InnoDB CHARSET=utf8 COMMENT '学生成绩表';
我们新建了一个学生信息表,一个学生成绩表,然后我们向上述两个表中插入一些数据,为节省篇幅,具体插入过程就不唠叨了,插入后两表中的数据如下:
现在我们想把每个学生的考试成绩都查询出来就需要进行两表连接了(因为
score
中没有姓名信息,所以不能单纯只查询
score
表)。连接过程就是从
student表中取出记 录,在
score
表中查找
number
相同的成绩记录,所以过滤条件就是
student.number = socre.number
,整个查询语句就是这样:
从上述查询结果中我们可以看到,各个同学对应的各科成绩就都被查出来了,可是有个问题,
史珍香
同学,也就是学号为
20180103的同学因为某些原因没有参加考试,所 以在
score
表中没有对应的成绩记录。那如果老师想查看所有同学的考试成绩,即使是缺考的同学也应该展示出来,但是到目前为止我们介绍的
连接查询是无法完成这样 的需求的。我们稍微思考一下这个需求,其本质是想:
驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集
。为了解决这个问题,就有了内连 接
和
外连接
的概念:
- 对于内连接的两个表,驱动表中的记录在被驱动表中找不到匹配的记录,该记录不会加入到最后的结果集,我们上边提到的连接都是所谓的内连接。
- 对于外连接的两个表,驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集。
在
MySQL
中,根据选取驱动表的不同,外连接仍然可以细分为
2
种:
- 左外连接 : 选取左侧的表为驱动表。
- 右外连接 : 选取右侧的表为驱动表。
可是这样仍然存在问题,即使对于外连接来说,有时候我们也并不想把驱动表的全部记录都加入到最后的结果集。这就犯难了,有时候匹配失败要加入结果集,有时候 又不要加入结果集,这咋办,有点儿愁啊。。。噫,把过滤条件分为两种不就解决了这个问题了么,所以放在不同地方的过滤条件是有不同语义的:
- WHERE子句中的过滤条件
WHERE
子句中的过滤条件就是我们平时见的那种,不论是内连接还是外连接,凡是不符合
WHERE
子句中的过滤条件的记录都不会被加入最后的结果集。
- ON子句中的过滤条件
对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配
ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个 字段使用
NULL
值填充。
需要注意的是,这个
ON
子句是专门为外连接驱动表中的记录在被驱动表找不到匹配记录时应不应该把该记录加入结果集这个场景下提出的,所以如果把
ON子句放到 内连接中,
MySQL
会把它和
WHERE
子句一样对待,也就是说:
内连接中的
WHERE
子句和
ON
子句是等价的
。
一般情况下,我们都把只涉及单表的过滤条件放到
WHERE
子句中,把涉及两表的过滤条件都放到
ON
子句中,我们也一般把放到
ON
子句中的过滤条件也称之为
连接条件
。
小贴士: 左外连接和右外连接简称左连接和右连接,所以下边提到的左外连接和右外连接中的
`
外
`
字都用括号扩起来,以表示这个字儿可有可无。
左(外)连接的语法
左(外)连接的语法还是挺简单的,比如我们要把t1表和t2表进行左外连接查询可以这么写:
SELECT * FROM t1 LEFT [OUTER] JOIN t2 ON 连接条件 [WHERE 普通过滤条件];
其中中括号里的
OUTER
单词是可以省略的。对于
LEFT JOIN类型的连接来说,我们把放在左边的表称之为外表或者驱动表,右边的表称之为内表或者被驱动表。所以上述例子中
t1
就是外表或者驱动表,
t2
就是内表或者被驱动表。需要注意的是,
对于左(外)连接和右(外)连接来说,必须使用
ON
子句来指出连接条件
。了解了左(外)连接的基本语法之后,再次回到我们上边那个现实问题中来,看看怎样写查询语句才能把所有的学生的成绩信息都查询出来,即使是缺考的考生也应该被放到结果集中:
mysql> SELECT s1.number, s1.name, s2.subject, s2.score FROM student AS s1 LEFT JOIN score AS s2 ON s1.number = s2.number;
右(外)连接的语法
右(外)连接和左(外)连接的原理是一样一样的,语法也只是把
LEFT
换成
RIGHT
而已:
SELECT * FROM t1 RIGHT [OUTER] JOIN t2 ON 连接条件 [WHERE 普通过滤条件];
只不过驱动表是右边的表,被驱动表是左边的表,具体就不唠叨了。
内连接的语法
内连接和外连接的根本区别就是
在驱动表中的记录不符合
ON
子句中的连接条件时不会把该记录加入到最后的结果集
,我们最开始唠叨的那些连接查询的类型都是内连接。不过之前仅仅提到了一种最简单的内连接语法,就是直接把需要连接的多个表都放到
FROM
子句后边。其实针对内连接,
MySQL提供了好多不同的语法,我们以
t1
和
t2表为例瞅瞅:
SELECT * FROM t1 [INNER | CROSS] JOIN t2 [ON 连接条件] [WHERE 普通过滤条件];
- SELECT * FROMt1 JOIN t2;
- SELECT * FROMt1 INNERJOIN t2;
- SELECT * FROMt1 CROSS JOIN t2;
上边的这些写法和直接把需要连接的表名放到
FROM
语句之后,用逗号
,
分隔开的写法是等价的:
SELECT * FROM t1, t2;
现在我们虽然介绍了很多种内连接
的书写方式,不过熟悉一种就好了,这里我们推荐
INNER JOIN
的形式书写内连接(因为
INNER JOIN
语义很明确嘛,可以和
LEFT
JOIN
和
RIGHT JOIN
很轻松的区分开)。这里需要注意的是,
由于在内连接中
ON
子句和
WHERE
子句是等价的,所以内连接中不要求强制写明
ON
子句
。
我们前边说过,连接的本质就是把各个连接表中的记录都取出来依次匹配的组合加入结果集并返回给用户。不论哪个表作为驱动表,两表连接产生的笛卡尔积肯定是一 样的。而对于内连接来说,由于凡是不符合
ON
子句或
WHERE子句中的条件的记录都会被过滤掉,其实也就相当于从两表连接的笛卡尔积中把不符合过滤条件的记录给踢出去,所以
对于内连接来说,驱动表和被驱动表是可以互换的,并不会影响最后的查询结果
。但是对于外连接来说,由于驱动表中的记录即使在被驱动表中找不到符 合
ON
子句连接条件的记录,所以此时驱动表和被驱动表的关系就很重要了,也就是说
左外连接和右外连接的驱动表和被驱动表不能轻易互换
。
小结
上边说了很多,给大家的感觉不是很直观,我们直接把表
t1
和
t2
的三种连接方式写在一起,这样大家理解起来就很
easy
了:
连接的原理
上边贼啰嗦的介绍都只是为了唤醒大家对
连接
、
内连接
、
外连接
这些概念的记忆,这些基本概念是为了真正进入本章主题做的铺垫。真正的重点是
MySQL采用了什么样的 算法来进行表与表之间的连接,了解了这个之后,大家才能明白为啥有的连接查询运行的快如闪电,有的却慢如蜗牛。
基于块的嵌套循环连接( Block Nested-Loop Join)
扫描一个表的过程其实是先把这个表从磁盘上加载到内存中,然后从内存中比较匹配条件是否满足。现实生活中的表可不像
t1
、
t2
这种只有
3条记录,成千上万条记录都 是少的,几百万、几千万甚至几亿条记录的表到处都是。内存里可能并不能完全存放的下表中所有的记录,所以在扫描表前边记录的时候后边的记录可能还在磁盘上, 等扫描到后边记录的时候可能内存不足,所以需要把前边的记录从内存中释放掉。我们前边又说过,采用
嵌套循环连接算法的两表连接过程中,被驱动表可是要被访问好 多次的,如果这个被驱动表中的数据特别多而且不能使用索引进行访问,那就相当于要从磁盘上读好几次这个表,这个
I/O
代价就非常大了,所以我们得想办法:
尽量减 少访问被驱动表的次数
。
当被驱动表中的数据非常多时,每次访问被驱动表,被驱动表的记录会被加载到内存中,在内存中的每一条记录只会和驱动表结果集的一条记录做匹配,之后就会被从 内存中清除掉。然后再从驱动表结果集中拿出另一条记录,再一次把被驱动表的记录加载到内存中一遍,周而复始,驱动表结果集中有多少条记录,就得把被驱动表从 磁盘上加载到内存中多少次。所以我们可不可以在把被驱动表的记录加载到内存的时候,一次性和多条驱动表中的记录做匹配,这样就可以大大减少重复从磁盘上加载 被驱动表的代价了。所以设计
MySQL
的大叔提出了一个
join buffer
的概念,
join buffer就是执行连接查询前申请的一块固定大小的内存,先把若干条驱动表结果集中的 记录装在这个
join buffer
中,然后开始扫描被驱动表,每一条被驱动表的记录一次性和
join buffer中的多条驱动表记录做匹配,因为匹配的过程都是在内存中完成 的,所以这样可以显著减少被驱动表的
I/O
代价。使用
join buffer
的过程如下图所示:
最好的情况是join buffer
足够大,能容纳驱动表结果集中的所有记录,这样只需要访问一次被驱动表就可以完成连接操作了。设计
MySQL
的大叔把这种加入了join buffer
的嵌套循环连接算法称之为
基于块的嵌套连接
(
Block Nested-Loop Join
)算法。
这个join buffer
的大小是可以通过启动参数或者系统变量
join_buffer_size
进行配置,默认大小为
262144
字节
(也就是
256KB
),最小可以设置为
128
字节。当然,对于 优化被驱动表的查询来说,最好是为被驱动表加上效率高的索引,如果实在不能使用索引,并且自己的机器的内存也比较大可以尝试调大
join_buffer_size的值来对连 接查询进行优化。
另外需要注意的是,驱动表的记录并不是所有列都会被放到join buffer
中,只有查询列表中的列和过滤条件中的列才会被放到
join buffer中,所以再次提醒我们,最 好不要把
*
作为查询列表,只需要把我们关心的列放到查询列表就好了,这样还可以在
join buffer
中放置更多的记录呢哈。