SQL从入门到出门 第8篇 多表连接查询

时间:2021-06-08 20:01:12

本篇介绍SQL:2016(ISO/IEC 9075:2016)标准中定义的多表连接查询语句(JOIN),以及六种主流数据库中的实现及差异:Oracle、MySQL、Microsoft SQL Server、PostgreSQL、Db2、SQLite。

到目前为止,我们所使用的查询都只是针对一个表进行数据检索。但是在实际应用中,常常需要从两个或更多的表中同时返回相关联的信息。

例如,我们在查询员工信息时,只能看到他/她所在的部门编号,如果想要同时显示部门的名称,就需要连接 employees 表和 departments 表中的信息。关于这两个表的结构和关联关系,可以参考前面的文章:SQL从入门到出门 第3篇 初级查询

我们先来看一下如何解决上面的查询问题:

SELECT departments.department_id, employees.department_id, department_name, employees.first_name, employees.last_name FROM employees INNER JOIN departments ON employees.department_id = departments.department_id;

首先,我们引入了新的关键字:INNER JOINONINNER JOIN表示内连接,随后会介绍外连接,这里表示要连接 employees 表和 departments 表;ON用于指定连接条件,这里表示 employees 中的部门编号等于departments 的部门编号。

以上查询的结果如下:
SQL从入门到出门 第8篇 多表连接查询

对于内连接,只有满足连接条件的行才会被返回。我们可以看到结果共计 106 行,每一行中的两个 department_id 都相等;但是 employees 表总共有107 行。这是因为有一个员工(employee_id = 178)的部门编号为空,所以没有返回该条记录(使用外连接可以处理这种情况)。

另外,我们在SELECT列表中的某些字段名之前加上了表名限定,例如 departments.department_id,这是因为两个表中都存在部门编号,必须明确指定需要显示哪个表中的信息。但是,如果某个字段只存在一个表中,可以省略表名,例如 department_name。

为了便于了解字段的来源,建议总是加上表名限定。但是如果总是写上表的全名,输入时非常麻烦,也不便于阅读,因此我们引入了表的别名。

使用表别名

我们修改一下上面的示例,加上表别名:

SELECT d.department_id, e.department_id, d.department_name, e.first_name, e.last_name FROM employees AS e INNER JOIN departments AS d ON e.department_id = d.department_id;

通过在表名后面加上关键字AS,我们为 employees 表指定了别名 e,为 departments 表指定了别名 d。然后在查询的其他地方使用了表别名。

Oracle 不支持在表别名中使用AS关键字,而是直接使用空格:
      FROM employees e
     INNER JOIN departments d

表别名另外两个非常有用的场景就是自连接(self join)和子查询(subquery),我们在后面介绍相关概念时再给出示例。

连接语法标准

在 SQL 历史中,定义了两种多表连接的语法:

  • ANSI SQL/92 标准,使用JOINON关键字指定表的连接条件
  • ANSI SQL/86 标准,使用FROMWHERE关键字指定表的连接条件

我们在前面的内连接示例中,使用的是 ANSI SQL/92 标准的语法格式。如果使用 ANSI SQL/86 标准,其查询语句如下:

SELECT d.department_id, 
       e.department_id,
       d.department_name,
       e.first_name,
       e.last_name
  FROM employees e, departments d
 WHERE e.department_id = d.department_id;

我们推荐使用JOINON,它们的语义更明显,更符合SQL的声明性;当WHERE中包含多个查询条件,又用于指定表的连接关系时,显得比较混乱。

接下来的示例中我们会同时给出这两种连接语法的语句,方便大家参考。

连接类型

SQL中的连接主要包括以下类型:

  • INNER JOIN,内连接
  • LEFT OUTER JOIN,左外连接
  • RIGHT OUTER JOIN,右外连接
  • FULL OUTER JOIN,全外连接
  • CROSS JOIN,交叉连接
  • NATURAL JOIN,自然连接
  • Self Join,自连接

其中,左外连接、右外连接以及全外连接统称为外连接(OUTER JOIN)。

内连接

内连接使用关键字INNER JOIN表示,也可以简写成JOIN; 内连接只返回两个表中匹配的数据行。参考以下示意图(基于两个表的 id 进行连接):
SQL从入门到出门 第8篇 多表连接查询

其中,id = 1 和 id = 3 是两个表中匹配的数据,因此内连接返回了 2 行记录。前面已经给出了内连接的示例,不再重复。

左外连接

左外连接使用关键字LEFT OUTER JOIN表示,也可以简写成LEFT JOIN; 左外连接返回左表中所有的数据行;对于右表中的数据,如果没有匹配的值,返回空值。参考以下示意图(基于两个表的 id 进行连接):
SQL从入门到出门 第8篇 多表连接查询

其中,id = 2 在 table1 中存在,在 table2 中不存在;左外连接仍然会返回该记录,只是对于 table2 中的列,返回的是空值。

假如我们想要查看部门和其员工信息,考虑到某些部门可能还没有员工,如果使用内连接,无法显示这些部门,因此使用左外连接:

SELECT d.department_id, 
       e.department_id,
       d.department_name,
       e.first_name,
       e.last_name
  FROM departments d
  LEFT JOIN employees e
    ON e.department_id = d.department_id
 WHERE d.department_id IN (110, 120, 130);

查询结果如下:
SQL从入门到出门 第8篇 多表连接查询

可以看出 Treasury 部门和 Corporate Tax 部门目前没有员工。

以上示例使用 ANSI SQL/86 标准语法实现如下:

SELECT d.department_id, 
       e.department_id,
       d.department_name,
       e.first_name,
       e.last_name
  FROM departments d, employees e
 WHERE d.department_id = e.department_id(+)
   AND d.department_id IN (110, 120, 130);

注意WHERE条件中,等号右侧的 (+) 表示 employee 表中的数据为空时,仍然返回 departments 中的记录。

目前,只有 Oracle 和 Db2 支持 ANSI SQL/86 标准的外连接语法

右外连接

右外连接使用关键字RIGHT OUTER JOIN表示,也可以简写成RIGHT JOIN; 右外连接返回右表中所有的数据行;对于左表中的数据,如果没有匹配的值,返回空值。参考以下示意图(基于两个表的 id 进行连接):
SQL从入门到出门 第8篇 多表连接查询

也就是说:

table1 RIGHT JOIN table2

等价于

table2 LEFT JOIN table1

假如我们想要查看部门和其员工信息,考虑到某些员工可能还没有分配部门,如果使用内连接,无法显示这些员工,因此使用右外连接:

SELECT d.department_id, 
       e.department_id,
       d.department_name,
       e.first_name,
       e.last_name
  FROM departments d
 RIGHT JOIN employees e
    ON d.department_id = e.department_id
 WHERE e.employee_id IN (176, 177, 178);

查询结果如下:
SQL从入门到出门 第8篇 多表连接查询

其中,Kimberely, Grant 没有分配部门,显示为空值。

同样,给出 ANSI SQL/86 标准的语法示例:

SELECT d.department_id, 
       e.department_id,
       d.department_name,
       e.first_name,
       e.last_name
  FROM departments d, employees e
 WHERE d.department_id(+) = e.department_id
   AND e.employee_id IN (176, 177, 178);

SQLite 不支持右外连接,可以使用等效的左外连接

全外连接

全外连接使用关键字FULL OUTER JOIN表示,也可以简写成FULL JOIN

全外连接等效于左外连接加上右外连接,返回左表和右表中所有的数据行。参考以下示意图(基于两个表的 id 进行连接):
SQL从入门到出门 第8篇 多表连接查询

需要注意的是,对于重复的行(id = 3),只返回一次记录。

假如我们想要查看部门和其员工信息,需要同时考虑到某些部门可能还没有员工,而且某些员工可能还没有分配部门,使用全外连接:

SELECT d.department_id, 
       e.department_id,
       d.department_name,
       e.first_name,
       e.last_name
  FROM departments d
  FULL JOIN employees e
    ON d.department_id = e.department_id
 WHERE e.employee_id IN (176, 177, 178)
    OR d.department_id IN (110, 120, 130);

查询结果如下:
SQL从入门到出门 第8篇 多表连接查询

MySQL 和 SQLite 不支持全外连接
ANSI SQL/86 标准的语法没有对应的全外连接实现

对于内连接和外连接,如果满足以下条件,可以使用USING替代ON,简化连接条件的输入:

  • 连接条件是等值连接,即 t1.col1 = t2.col1
  • 两个表中的列必须同名同类型,即 t1.col1 = t2.col1

对于上文中的内连接查询示例,由于 employees 表和 departments 表中的 department_id 字段名称和类型都相同,可以使用USING简写成以下语句:

SELECT d.department_id, e.department_id, d.department_name, e.first_name, e.last_name FROM employees e INNER JOIN departments d USING (department_id);

注意USING后面必须使用括号引用公共的字段,可以包含多个公共字段,使用逗号分隔。

SQL Server 不支持连接查询中的USING关键字
只有 Oracle 要求使用USING时,公共字段不允许加表名前缀(遵循SQL标准要求):
     SELECT d.department_id, e.department_id
需要改成:
     SELECT department_id, department_id

交叉连接

交叉连接使用关键字CROSS JOIN表示,也称为笛卡尔积(Cartesian product)。

两个表的笛卡尔积相当于一个表的所有行和另一个表的所有行两两组合,结果的数量为两个表的行数相乘。假如第一个表有 100 行,第二个表有 200 行,它们的交叉连接将会产生 100 × 200 = 20000 行结果。

参考以下示意图(基于两个表的 id 进行连接):
SQL从入门到出门 第8篇 多表连接查询

将部门与员工进行交叉连接的语法如下(我也不知道为什么要这样做):

SELECT d.department_id, e.department_id, d.department_name, e.first_name, e.last_name FROM departments d CROSS JOIN employees e;

结果如下,共计 2889 条记录:
SQL从入门到出门 第8篇 多表连接查询

对于 ANSI SQL/86 标准的语法,交叉连接就是不指定连接条件:

SELECT d.department_id, 
       e.department_id,
       d.department_name,
       e.first_name,
       e.last_name
  FROM departments d, employees e;

自然连接

前面我们介绍了如果使用两个表中的同名同类型字段进行等值连接时,可以使用USING关键字进行简写。

进一步来说,如果USING子句中包含了两个表中所有的这种同名字段,可以使用自然连接(NATURAL JOIN)表示。

举例说明,employees 表和 departments 表拥有 2 个同名字段:department_id 和 manager_id,如果要基于这 2 个字段进行等值连接,可以使用自然连接:

SELECT department_id, d.department_name, e.first_name, e.last_name FROM departments d NATURAL JOIN employees e;

如果使用USING关键字,它的等价形式如下:

SELECT department_id, d.department_name, e.first_name, e.last_name FROM departments d JOIN employees e USING (department_id, manager_id);

查询返回的结果满足以下条件:员工的经理也是他/她所在部门的经理。
SQL从入门到出门 第8篇 多表连接查询

SQL Server 不支持自然连接,可以使用JOIN ON替代
Db2 不支持自然连接,可以使用JOIN USING或者JOIN ON替代

自连接

有一种特殊的连接,称为自连接(Self join),它是指连接操作符的两边都是同一个表,即把一个表和它自己进行连接。

自连接本质上并没有什么特殊之处,主要用于处理那些对自己进行了引用的表。例如 employees 表中的 manager_id,它是一个外键列,指向了 employees 自身的 employee_id。

如果要显示员工姓名以及他们经理的姓名,可以通过 employee 表的自连接实现:

SELECT e.first_name AS employee_first_name, e.last_name AS employee_last_name, m.first_name AS manage_first_name, m.last_name AS manage_last_name FROM employees m JOIN employees e ON m.employee_id = e.manager_id;

字段的别名可以明确信息的含义,查询结果如下:
SQL从入门到出门 第8篇 多表连接查询

最后,给出一个多表连接的示例。

SELECT d.department_name, e.first_name, e.last_name, j.job_title FROM departments d INNER JOIN employees e ON d.department_id = e.department_id INNER JOIN jobs j ON j.job_id = e.job_id;

我们加上了第3个表(jobs),通过 job_id 列进行内连接,并且输出了员工的职位名称:
SQL从入门到出门 第8篇 多表连接查询

对于多个表的连接,按照出现的顺序进行连接;如有必要,可以使用括号 () 修改连接的顺序:

SELECT d.department_name, e.first_name, e.last_name, j.job_title FROM departments d INNER JOIN (employees e INNER JOIN jobs j ON j.job_id = e.job_id) ON d.department_id = e.department_id;
数据库 多表连接查询 描述
Oracle OK Oracle 中的表别名不支持AS关键字
Oracle 支持 ANSI SQL/86 标准的外连接语法
MySQL OK MySQL 不支持全外连接
SQL Server OK SQL Server 不支持自然连接和JOIN USING
PosgreSQL OK
Db2 OK Db2 支持 ANSI SQL/86 标准的外连接语法
DB2 不支持自然连接
SQLite OK SQLite 不支持右外连接和全外连接