阅读正文之前一定要仔细阅读下边这些建议:

1. 最好使用电脑观看。

2. 如果你非要使用手机观看,那请把字体调整到最小,这样观看效果会好一些。

3. 碎片化阅读并不会得到真正的知识提升,要想有提升还得找张书桌认认真真看一会书,或者我们公众号的文章

4. 如果觉得不错,各位帮着转发转发,如果觉得有问题或者写的哪不清晰,务必私聊我~

5. 本公众号的文章都是需要被系统性学习的,在阅读本篇文章前最好已经阅读过下边几篇文章,要不然可能会有阅读不畅的体验:


连接查询

再次认识关系表

我们分别使用student_infostudent_score两个表来存储学生的基本信息和学生的成绩信息,其实合并成一张表也不是不可以,假设这种将两张表合并后的新表为student_merge,那它应该长这样:


student_merge表


查询简介(六)之连接查询_查询简介(六)之连接查询

有了这个合并后的表,我们就可以在一个查询语句中既查询到学生的基本信息,也查询到学生的成绩信息,比如这个查询语句:

SELECT numbername, major, subject, score FROM student_merge;

其中查询列表处的namemajor属于学生的基本信息,subjectscore属于学生的成绩信息,而number既属于成绩信息也属于基本信息,我们可以在一个对student_merge表的查询中很轻松的把这些信息都查询出来。但是别忘了一个学生可能会有很多门学科的成绩信息,也就是说每当我们想增加一个学生某门学科的成绩信息时,我们必须把他的基本信息再抄一遍,这种同一个学生的基本信息被冗余存储会带来下边的问题:

  • 问题一:浪费存储空间

  • 问题二:当修改某个学生的基本信息时必须修改多处,很容易造成信息的不一致,增大信息维护的困难

所以为了尽可能少的存储冗余信息,我们把表拆分成了student_infostudent_score表,但是这两张表之间有某种关系作为纽带,这里的关系指的可以通过student_score表的中的number值去student_info表中找到该条成绩记录对应的学生的基本信息。

内连接

拆分之后的表的确解决了数据冗余问题,但是查询数据却成了一个问题。截至目前为止,在我们介绍的查询方式中,查询结果只能是一个表中的一个列或者多个列,也就是说到目前为止还没有一种可以在一条查询语句中把numbernamemajorsubjectscore这几个信息都查询出来的方式。

小贴士:
虽然我们前边介绍的子查询可以在一个查询语句中涉及到多个表,比如这个查询语句:
`SELECT * FROM student_score WHERE number = (SELECT number FROM student_info WHERE name = '杜琦燕');`
但是子查询中对于`student_info`表的查询结果只是作为外层查询的一个搜索条件,换句话说,子查询的结果只是整个查询的一个中间状态,最后的查询结果中的列还是外层查询的查询列表上填写的列,本例中的外层查询列表是`*`,也就是`student_score`表中的所有列,所以整个查询语句的最后结果中只有`student_score`表中的所有列。

所以为了在一个查询语句中的结果中的到多个表的信息,我们需要引入一种全新的查询方式 --- 连接。为了以更简单的方式让大家理解表的连接,我们先建立两个简单的表并给它们填充一点数据:

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
mysql>

我们成功建立了t1t2两个表,这两个表都有两个列,一个是INT类型的,一个是CHAR(1)类型的,填充好数据的两个表长这样:

mysql> SELECT * FROM t1;
+------+------+
| m1   | n1   |
+------+------+
|
    1 | a    |
|    2 | b    |
|
    3 | c    |
+------+------+
3 rows in set (0.00 sec)
mysql> SELECT * FROM t2;
+------+------+
| m2   | n2   |
+------+------+
|
    2 | b    |
|    3 | c    |
|
    4 | d    |
+------+------+
3 rows in set (0.00 sec)
mysql>

其实在一个查询语句中同时得到t1t2这两个表中的数据的方式十分简单,直接把两个表名放到FROM语句之后,用逗号,分隔开就好了,就是这样:

查询简介(六)之连接查询_查询简介(六)之连接查询 _02

查询列表处的*代表从FROM语句后列出的表中选取每个列,上边的查询语句其实和下边这几种写法都是等价的:

  • 写法一:

    SELECT t1.m1t1.n1t2.m2t2.n2 FROM t1t2;

    这种写法是将t1t2表中的列名都显式的写出来,也就是使用了列的全限定名。

  • 写法二:

    SELECT m1, n1, m2, n2 FROM t1, t2;

    由于t1t2表中的列名并不重复,所以并不会造成让服务器懵逼的二义性,在查询列表上直接使用列名也是可以的。

  • 写法三:

    SELECT t1.*, t2.* FROM t1, t2;

    这种写法意思就是查询t1表的全部的列,t2表的全部的列。

知道了*号的意思后,我们再来看查询结果是怎么产生的。由于t1t2是两个独立的表,而展示结果的时候又需要展示到一起,所以在展示结果的时候谁都不能偏袒,只好把每个表中的记录全部都组合一遍,所以这个查询过程就是:

  1. t1表中的三条记录与t2表的第一条记录依次组合一遍,产生的效果就是这样:

  2. 继续将t1表中的三条记录与t2表的第二条记录依次组合一遍,产生的效果就是这样:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _03

  3. 继续将t1表中的三条记录与t2表的第三条记录依次组合一遍,产生的效果就是这样:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _04

这种把一个表中的记录和另一个表中的记录连起来的过程称为表的连接,一个表中的每一条记录与另一个表中的每一条记录得到全部可能的组合的过程称之为生成笛卡尔积。就像上边t1表中有3条记录,t2表中有3条记录,那这两个表连接而生成的笛卡尔积就有3×3=9行数据。

想连接两个表,除了像SELECT * FROM t1, t2这种在FROM语句后将各个表名用逗号,隔开的方式,下边这几种方式产生的效果是一样的:

  • 方式一:

    SELECT * FROM t1 INNER JOIN t2;
  • 方式二:

    SELECT * FROM t1 CROSS JOIN t2;
  • 方式三:

    SELECT * FROM t1 JOIN t2;

这些连接方式统统都被称为内连接,我们在之后说外连接的时候再详细唠叨的区别。虽然内连接有很多种书写方式,我们熟悉一种就好了。

如果我们乐意,我们可以连接任意数量张表,但是如果没有任何限制条件的话,这些表连接起来产生的笛卡尔积可能是非常巨大的。比方说3个100行记录的表连接起来产生的笛卡尔积就有100×100×100=1000000行数据!所以在连接的时候过滤掉特定组合是有必要的,比方说我们只想要t1表的m1列的值和t2表的m2列的值相同的记录组合,可以把这个限制条件直接加到WHERE子句中:

查询简介(六)之连接查询_查询简介(六)之连接查询 _05

这个带有WHERE条件的连接操作可以这么理解:

  1. 选取t2表中的第一条记录,这条记录的m2列的值是2,从t1表中查找m1列的值是2的记录,在t1表中只有一条记录符合这个条件,将这两条记录组合起来,效果就是这样:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _06

  2. 继续选取t2表中的第二条记录,这条记录的m2列的值是3,从t1表中查找m1列的值是3的记录,在t1表中只有一条记录符合这个条件,将这两条记录组合起来,效果就是这样:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _07

  3. 继续选取t2表中的第三条记录,这条记录的m2列的值是4,从t1表中查找m1列的值是4的记录,在t1表中没有任何记录符合这个条件,所以这个步骤不会有任何记录的组合加入到结果集,所以结果并不变:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _08

这个过程中,由于我们指定了t1表的m1列的值和t2表的m2列的值必须相同的条件,所以极大的减少了最后记录组合的条数。这种一个表中列的值和另一个表中列的值的关系也可以被称为连接条件,这两个列也可以被称为连接列。本例中展示的连接条件为两个连接列相等,我们也可以指定诸如>>=<>!=等等合法的布尔表达式,比如这样:

查询简介(六)之连接查询_查询简介(六)之连接查询 _09

不过我们最常用的连接条件就是两个连接列相等,所以后边的例子中都会用连接列相等作为连接条件。对于INNER JOINCROSS JOINJOIN这几种内连接的书写格式来说,除通过WHERE语句指定连接条件以外,还可以通过下边的这两种方式指定(以INNER JOIN为例):

  • 方式一:

    SELECT * FROM t1 INNER JOIN t2 ON t1.m1 = t2.m2;
  • 方式二:

    SELECT * FROM 表1 INNER JOIN 表2 USING 相同的列名

    这种方式表示的连接条件也是两个连接列相等,但是比较特殊的是,用于两个表中的连接列必须是同名的。我们上边t1t2表中由于没有用同名的列来做连接条件,所以暂时还不能使用这种方式举例子,等遇到了再说哈~

理论知识介绍完了,再回到我们现实中的student_infostudent_score表,我们希望在一条语句中同时把numbernamemajorsubjectscore这几个信息都查询出来,所以需要把student_infostudent_score表连接起来,连接的条件就是两个表的number列相等,所以我们可以这么写:

查询简介(六)之连接查询_查询简介(六)之连接查询 _10

你如果有疑惑的话可以翻上去对照着t1t2表的连接过程来理解这个查询的连接过程,这里就不赘述了。

连接查询产生的结果集就好像把散布到两个表中的信息被重新粘贴到了一个表,这个粘贴后的结果集可以方便我们分析数据,就不用两个老是两个表对照的看了。

多表连接

上边说过,如果我们乐意的话可以连接任意数量的表,我们再来创建一个简单的t3表:

mysql> CREATE TABLE t3 (m3 int, n3 char(1));
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO t3 VALUES(3'c'), (4'd'), (5'e')
;
Query OK, 3 rows affected (0.01 sec)
Records: 3  Duplicates: 0  Warnings: 0
mysql>

t1t2表的结构一样,t3表也是一个INT列,一个CHAR(1)列,现在我们看一下把这3个表连起来的样子:

查询简介(六)之连接查询_查询简介(六)之连接查询 _11

其实上边的查询语句也可以写成这样,用哪个取决于你的心情:

SELECT * FROM t1 INNER JOIN t2 ON t1.m1 = t2.m2 INNER JOIN t3 ON t1.m1 = t3.m3;

这种3个表的连接就相当于t1表和t2表连接之后的结果集再和t3表进行连接,所以我们可以分这么两个步骤理解这个过程:

  1. 先把t1表和t2表按照指定的t1.m1 = t2.m2连接条件连接起来的结果就是:

  2. 然后把t1表和t2表的连接产生的结果与t3表按照t1.m1 = t3.m3的连接条件进行连接,由于t3表的m3字段的值是2或者3的只有1条,所以最后整个查询的结果就是:

其实不管是多少个表的连接,本质上就是各个表的记录在符合连接条件下的自由组合

表的别名

我们前边曾经为列命名过别名,比如说这样:

查询简介(六)之连接查询_查询简介(六)之连接查询 _12

我们可以把别名用在ORDER BYGROUP BY等子句上,比如这样:

查询简介(六)之连接查询_查询简介(六)之连接查询 _13

与列的别名类似,我们也可以为表来定义别名,格式与定义列的别名一致,都是用空白字符或者AS隔开,这个在表名特别长的情况下可以让语句表达更清晰一些,比如这样:

查询简介(六)之连接查询_查询简介(六)之连接查询 _14

这个例子中,我们在FROM子句中给student_info定义了一个别名s1student_score定义了一个别名s2,那么在整个查询语句的其他地方就可以引用这个别名来替代该表本身的名字了。

连接中的普通搜索条件

在表的连接过程中,除了指定一个表中列的值和另一个表中列的值必须相同的连接条件以外,我们还可以指定普通的搜索条件,比方说我们在查询numbernamemajorsubjectscore这几个信息的时候,需要限制只查询及格(也就是score值不小于60分)并且专业为'软件工程'的记录,那我们可以这么写:

查询简介(六)之连接查询_查询简介(六)之连接查询 _15

其中的s1.major = '软件工程's2.score >= 60就是我们指定的普通搜索条件,这个过程可以这么理解:

  1. 首先对于s1(也就是student_info)表来说按照搜索条件s1.major = '软件工程'过滤掉不符合条件的记录,所以剩下这些记录:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _16

  2. 然后再使用这些s1表中符合条件的记录与s2(也就是studnet_score)表中的记录逐一进行匹配,并且要求符合s1.number = s2.numbers2.score >= 60这两个条件,所以最后整个查询的结果就是:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _17

其实连接条件本质上也是一个搜索条件,所以如果有多个搜索条件的话,它们在WHERE子句中的摆放顺序没有要求,也就是说我们把连接条件放在WHERE子句的最后也可以,就像这样:

查询简介(六)之连接查询_查询简介(六)之连接查询 _18

在连接查询中,也可以不指定连接条件,仅仅指定普通的搜索条件也可以,比方说这样:

查询简介(六)之连接查询_查询简介(六)之连接查询 _19


不过不指定连接条件的连接查询通常没啥意义~

自连接

我们上边说的都是多个不同的表之间的连接,其实同一个表也可以进行连接,准确的说是该表的副本之间也可以进行连接。比方说我们可以对两个t1表来生成笛卡尔积,就像这样:

mysql> SELECT * FROM t1, t1;
ERROR 1066 (42000): Not unique table/alias't1'
mysql>

咦,报了个错,这是因为不允许FROM子句中出现相同的表名,MySQL服务器会把名称一样的表当作是一个表处理。我们这里需要的是两张一模一样的t1表进行连接,所以为了把两个一样的表区分一下,需要为表定义别名。比如这样:

查询简介(六)之连接查询_查询简介(六)之连接查询 _20

这里我们为t1表定义了两个副本,一个是table1,另一个是table2,这里的连接过程就不赘述了。由于被链接的表其实是源自同一个表,所以这种连接也称为自连接。我们看一下这个自连接的现实意义,比方说我们想查看与'史珍香'相同专业的学生有哪些,那我们可以这么写:

查询简介(六)之连接查询_查询简介(六)之连接查询 _21

s1s2都是student_info表的一份副本,这个查询大致如下:

  1. 根据s1.name = '史珍香'搜索条件过滤s1表,最后的结果是这样(为节省篇幅,只保留重要的行):

    查询简介(六)之连接查询_查询简介(六)之连接查询 _22

  2. 将过滤后的s1表中的记录与s2表中的记录逐一组合,并且要遵循s1.major = s2.major的条件,所以两个表连接后的样子就是:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _23

    而我们只需要s2表的numbernamemajor这3个列的数据,所以最终的结果就长这样:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _24

左(外)连接和右(外)连接

我们重新看一下之前举过的这个例子:

查询简介(六)之连接查询_查询简介(六)之连接查询 _25

不知道你有没有发现,student_info表中一共记录了6名学生的基本信息,但是在连接查询之后只剩下了4名学生的信息,为啥呢?因为有2名学生可能由于他们的专业并没有考试或者他们缺考了之类的原因,在student_score表中并没有记录关于他们的成绩信息。即使他们没有成绩信息,我们也想在连接查询的时候把他们的基本信息打印出来,这该肿么办?

这时就需要正式的介绍一下外连接的概念了。为了简单起见,我们还是用之前介绍的t1t2表做例子,先看一个外连接查询的示例:

查询简介(六)之连接查询_查询简介(六)之连接查询 _26

我们知道t1一共有3条记录,但是由于第一条记录的m1列的值是1,无法在t2表中找到任意一条符合t1.m1 = t2.m2连接条件的记录,但是它仍然被加到了最后的结果集当中,由于在t2表中并没有与其匹配的记录,所以m2n2列的值都是NULL。这种即使某条记录在另一个表中没有任意一条记录与它匹配,它也可以被加入结果集中的连接查询被称为外连接。我们常用的外连接类型有下边这两种:

  • 左外连接

    基本结构如下:

    SELECT 查询列表 FROM 表1 LEFT JOIN 表2 ON 连接条件

    由于这种连接方式会保留表1出现在表2的左边,所以左外连接的意思就是保留中表1中所有待匹配的记录。上边的例子中使用的就是左外连接的语法,由于t1表在左边,所有最后的查询结果里保留了所有t1表中的记录。对于表1中匹配失败的记录,如果查询列表处有表2的字段,那么这些字段的值将用NULL值代替。

  • 右外连接两种,格式如下:

    SELECT 查询列表 FROM 表1 RIGHT JOIN 表2 ON 连接条件

    左外连接类似,只不过这种连接方式会保留表2中的所有带匹配的记录,比如这样:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _27

    这个列子中由于t2表在右边,所以保留t2表中所有待匹配的记录。

为了图个方便,对于左外连接右外连接,我们也可以省略名称中的字,也就是直接称呼为左连接右连接

知道了外连接的意思,内连接也就懂了,就是对于两个用于连接的表,任何一方的记录和另外一个表中任意一条记录的组合都不符合连接条件,那这条记录就不会被加入到最后的结果集

理论知识又唠叨完了,返回到我们查询学生信息的问题,使用外连接就可以查看到所有学生的基本信息和成绩信息了:

使用外连接注意事项
  1. 必须使用 ON 来明确连接条件

    我们知道在内链接查询方式中,可以将连接条件写入WHERE子句,但是外连接中是万万不允许的,比如这样是错误的:

    mysql> SELECT * FROM t1 LEFT JOIN t2 WHERE t1.m1 = t2.m2;
    ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WHERE t1.m1 = t2.m2' at line 1
    mysql>
  2. 普通搜索条件优先级更高

    如果外连接语句中除连接条件外还有普通搜索条件,比如这样:

    查询简介(六)之连接查询_查询简介(六)之连接查询 _28

    虽然这个查询中是左外连接查询,但是由于有普通搜索条件t1.m1 != 1,所以在进行连接之前,t1表中m1列的值是1的记录就被过滤掉了并不参与连接,所以该记录不会出现在最后的结果中。

连接查询与子查询的转换

有的查询需求既可以使用连接查询解决,也可以使用子查询解决,比如

SELECT * FROM student_score WHERE number IN (SELECT number FROM student_info WHERE major = '计算机科学与工程');

这个子查询就可以被替换:

SELECT s2.* FROM student_info AS s1 INNER JOIN student_score AS s2 WHERE s1.number = s2.number AND s1.major = '计算机科学与工程';

两种查询方式虽然都可以解决问题,但是效率上可能有差别,具体的差别我们在说查询优化的时候详细唠叨,现在我们知道如何转换这两种查询方式就好了。

总结

连接本质上就是各个表的记录在符合连接条件下的自由组合。一个表中的每一条记录与另一个表中的每一条记录得到全部可能的组合的过程称之为生成笛卡尔积,这个笛卡尔积非常庞大,没有什么意义,我们在连接表的时候通常都会指明连接条件,也就是一个表中列的值和另一个表中列的值的关系。

有两种非常重要的连接类型:

  • 内连接:对于两个用于连接的表,任何一方的记录和另外一个表中任意一条记录的组合都不符合连接条件,那这条记录就不会被加入到最后的结果集。

  • 外连接:即使某条记录没有在另一个表中任意一条记录与它匹配,它也可以被加入结果集中。通常外连接分为这两种类型:

    • 左外连接

    • 右外连接