关系型数据库通常将不同的实体对象和它们之间的联系存储在多个表中,例如电商系统中使用的产品表、用户表、订单表以及订单明细表等。当我们查看某个订单信息时,需要同时从这几个表中查找关于该订单的相关数据。
本文比较五种主流数据库实现的多表连接查询功能,包括 MySQL、Oracle、SQL Server、PostgreSQL 以及 SQLite。
功能 | MySQL | Oracle | SQL Server | PostgreSQL | SQLite |
---|---|---|---|---|---|
内连接 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
左外连接 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
右外连接 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
全外连接 | ❌ | ✔️ | ✔️ | ✔️ | ✔️ |
交叉连接 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
自然连接 | ✔️ | ✔️ | ❌ | ✔️ | ✔️ |
自连接 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
连接的语法与类型
在 SQL 标准的发展过程中产生了两种连接查询语法:
- ANSI SQL/86 标准使用 FROM 和 WHERE 子句指定表的连接查询。
- ANSI SQL/92 标准使用 JOIN 和 ON 子句指定表的连接查询。
下面我们分别介绍这两种连接查询语法。
员工表(employee)中存储了员工的信息和员工所在部门的编号,同时部门的信息存储在部门表(department)中。如果我们想要知道某个员工所在部门的名称,就需要同时查询员工表和部门表。以下示例使用 FROM 和 WHERE 子句实现了这两个表的连接查询:
SELECT d.dept_id, e.dept_id, d.dept_name, e.emp_name
FROM employee e, department d
WHERE e.dept_id = d.dept_id
AND e.emp_id = 1;
其中,FROM 子句用于指定查询的表,逗号表示连接两个表。WHERE 子句既指定了过滤条件(e.emp_id=1),又指定了连接两个表的条件(e.dept_id= d.dept_id),也就是员工表中的部门编号等于部门表中的编号。另外,我们在查询中还通过表别名(e 和 d)指明了字段来自哪个表。该查询返回的结果如下:
dept_id|dept_id|dept_name|emp_name
-------|-------|---------|--------1| 1|行政管理部 |刘备
通过使用部门编号作为连接查询的条件,我们同时获得了员工和员工所在部门的信息。
对于以上连接查询示例,我们同样可以使用 JOIN 和 ON 子句实现:
SELECT d.dept_id, e.dept_id, d.dept_name, e.emp_name
FROM employee e
JOIN department d ON (e.dept_id = d.dept_id)
WHERE e.emp_id = 1;
其中,JOIN 子句表示连接员工表和部门表,ON 子句指定了连接两个表的条件,WHERE 子句指定了查询的过滤条件。查询返回的结果和上面的示例相同。
💡推荐使用 JOIN 和 ON 子句进行连接查询,因为这种方式的语义更明确,更符合 SQL 的声明性。对于 FROM 和 WHERE 连接查询语法,WHERE 子句同时用于指定查询的过滤条件和表的连接条件,逻辑显得比较混乱。另外,并不是所有的连接查询类型都支持 FROM 和 WHERE 连接查询的语法。
SQL 支持的连接查询包括内连接、外连接、交叉连接、自然连接以及自连接等。其中,外连接又可以分为左外连接、右外连接以及全外连接。下面我们详细介绍一下 SQL 中的各种连接类型。
内连接
内连接(Inner Join)查询返回两个表中满足连接条件的数据。内连接使用关键字 INNER JOIN 表示,也可以简写成 JOIN。内连接的原理如下图所示(连接条件为两个表的 id 相等)。
其中 id 等于 1 和 3 的记录是两个表中满足连接条件的数据,因此内连接返回了这 2 个记录。
等值连接
连接查询中的 ON 子句与 WHERE 子句类似,可以支持各种条件运算符。其中最常用的是等号(=)运算符,这种连接查询也被称为等值连接。例如:
SELECT e.emp_name AS "员工姓名", j.job_title "职位名称"
FROM employee e
JOIN job j ON (e.job_id = j.job_id)
WHERE e.emp_id = 1;
我们通过职位编号连接了员工表和职位表,查询返回的结果如下:
员工姓名|职位名称
-------|-------刘备 |总经理
等值连接返回两个表中连接字段值相等的数据,我们最常使用的连接就是等值连接。
非等值连接
除等号运算符外,连接条件中也可以使用其他比较运算符或者逻辑运算符,例如>=、!=、BETWEEN、AND 等,这种连接查询被称为非等值连接。例如:
SELECT e.emp_name AS "员工姓名", e.salary "月薪"
FROM employee e
JOIN job j ON (e.job_id != j.job_id AND e.salary BETWEEN j.min_salary AND j.max_salary)
WHERE j.job_title = '开发经理';
我们将职位编号不相等以及员工的月薪位于“开发经理”月薪的范围之内作为连接条件,返回当前月薪属于开发经理级别但不是开发经理的员工。查询返回的结果如下:
员工姓名|月薪
-------|--------孙尚香 |12000.00
“孙尚香”的月薪位于开发经理级别,但是她的实际职位是财务经理。
外连接
外连接查询可以分为左外连接、右外连接以及全外连接。
左外连接
左外连接(Left Outer Join)查询首先返回左表中的全部数据。之后,如果右表中存在满足连接条件的数据,就返回该数据;如果没有相应的数据,就返回空值。左外连接使用关键字 LEFT OUTER JOIN 表示,也可以简写成 LEFT JOIN。左外连接的原理如下图所示(连接条件为两个表的 id 相等)。
其中,id 等于 2 的记录只存在表 table1 中。左外连接仍然会返回左表中的记录;而对于右表 table2 中的 price 字段,则返回了空值。
如果我们想要统计每个部门中的员工人数。考虑到某些部门可能还没有员工入职,使用内连接无法显示这些部门,此时我们可以使用左外连接查询。例如:
SELECT d.dept_name AS "部门名称", count(e.emp_id) AS "员工人数"
FROM department d
LEFT JOIN employee e ON (e.dept_id = d.dept_id)
GROUP BY d.dept_name;
其中,LEFT JOIN 表示左外连接,连接条件为两个表中的部门编号相等。查询返回的结果如下:
部门名称 |员工人数
--------|-------
行政管理部| 3
人力资源部| 3财务部 | 2研发部 | 9销售部 | 8保卫部 | 0
虽然“保卫部”目前还没有任何员工,但是查询结果仍然返回了该部门。
如果我们想要找出哪些部门没有员工,可以在以上左外连接的基础上增加一个过滤条件:
SELECT d.dept_id AS "部门编号",d.dept_name AS "部门名称"
FROM department d
LEFT JOIN employee e ON (e.dept_id = d.dept_id)
WHERE e.emp_id IS NULL;
左外连接返回了所有的部门信息。如果某个部门没有员工,对应的 e.emp_id 字段就是空值。查询返回的结果如下:
部门编号|部门名称
-------|-------6|保卫部
另外,只有 Oracle 实现了 ANSI SQL/86 标准的左外连接语法。例如:
-- Oracle
SELECT d.dept_id AS "部门编号",d.dept_name AS "部门名称"
FROM department d, employee e
WHERE d.dept_id = e.dept_id(+)
AND e.emp_id IS NULL;
其中,WHERE 子句右侧的(+)运算符表示右表中可能缺少相应的数据,也就表示这是一个左外连接查询。
右外连接
右外连接(Right Outer Join)查询首先返回右表中的全部数据。如果左表中存在满足连接条件的数据,就返回该数据;如果没有相应的数据,就返回空值。右外连接使用关键字 RIGHT OUTER JOIN 表示,也可以简写成 RIGHT JOIN。右外连接的原理如下图所示(连接条件为两个表的 id 相等)。
其中,id 等于 5 的数据只存在表 table2 中。右外连接仍然会返回右表中的记录;而对于左表 table1 中的 name 字段,则返回了空值。简而言之:
table1 RIGHT JOIN table2
等价于:
table2 LEFT JOIN table1
右外连接和左外连接可以相互转换。因此,前面统计员工人数的示例也可以使用等价的右
外连接查询实现:
SELECT d.dept_name AS "部门名称", count(e.emp_id) AS "员工人数"
FROM employee e
RIGHT JOIN department d ON (e.dept_id = d.dept_id)
GROUP BY d.dept_name;
其中,RIGHT JOIN 表示右外连接,连接条件是两个表中的部门编号相等。
另外,在 Oracle 中也可以使用 ANSI SQL/86 标准的右外连接语法:
-- Oracle
SELECT d.dept_name AS "部门名称", count(e.emp_id) AS "员工人数"
FROM department d, employee e
WHERE e.dept_id(+) = d.dept_id
GROUP BY d.dept_name;
注意查询条件中(+)运算符所在的位置。
全外连接
全外连接(Full Outer Join)查询相当于左外连接加上右外连接。查询同时返回左表和右表中所有的数据。如果右表或者左表中存在满足连接条件的数据,就返回该数据;如果没有相应的数据,就返回空值。全外连接使用关键字 FULL OUTER JOIN 表示,也可以简写成 FULL JOIN。
全外连接的原理如下图所示(连接条件为两个表的 id 相等)。
查询结果包含了两个表中所有的 id。对于左表中不存在的数据(id=5)以及右表中不存在的数据(id=2)。分别为相应的字段返回了空值。
假如公司组织了一次活动,需要将所有的员工进行分组。每个组的信息存储在表 t_group 中,员工的分组信息存储在 t_emp_group 中。现在我们想要知道哪些组还没有分配员工,以及哪些员工还没有被分配到任何组。为此,我们可以使用全外连接查询:
-- Oracle、Microsoft SQL Server、PostgreSQL 以及 SQLite
SELECT g.group_name, eg.emp_id
FROM t_group g
FULL JOIN t_emp_group eg ON (eg.group_id = g.group_id)
WHERE g.group_id IS NULL OR eg.emp_id IS NULL;
我们使用两个表中的 group_id 字段作为连接条件,同时在 WHERE 子句中指定了想要返回的数据。查询返回的结果如下:
group_name|emp_id
----------|------| 8| 12| 16| 20| 23五组 |
查询结果显示“五组”还没有分配任何员工,工号为 8、12、16、20 以及 23 的员工还没有被分配到任何组。
MySQL 目前不支持全外连接。
另外,ANSI SQL/86 标准语法不支持全外连接。
交叉连接
交叉连接也被称为笛卡儿积(Cartesian Product),使用关键字 CROSS JOIN 表示。两个表的交叉连接将一个表的所有数据行和另一个表的所有数据行进行两两组合,返回结果的数量为两个表中的行数相乘。例如,一个 100 行数据的表和一个 200 行数据的表进行交叉连接查询将会产生 20 000 行数据。交叉连接的原理如下图所示:
table1 中存在 3 条记录,table2 中也存在 3 条记录,因此这两个表交叉连接的结果总共包含 9 条记录。交叉连接使用的场景比较少,一般用于生成大量测试数据。
我们介绍一个利用交叉连接生成数字序列的方法,首先创建一个示例表 t_number:
CREATE TABLE t_number(n INTEGER PRIMARY KEY);
INSERT INTO t_number VALUES (0);
INSERT INTO t_number VALUES (1);
INSERT INTO t_number VALUES (2);
INSERT INTO t_number VALUES (3);
INSERT INTO t_number VALUES (4);
INSERT INTO t_number VALUES (5);
INSERT INTO t_number VALUES (6);
INSERT INTO t_number VALUES (7);
INSERT INTO t_number VALUES (8);
INSERT INTO t_number VALUES (9);
t_number 表中存储了数字 0~9。我们在以下查询中将 t_number 表多次和它自己进行交叉连接:
SELECT hundrand.n * 100 + ten.n * 10 + one.n AS n
FROM t_number hundrand
CROSS JOIN t_number ten
CROSS JOIN t_number one
ORDER BY n;
查询返回的结果如下:
n
---0123
...
997
998
999
查询返回了一个 0~999 的数字序列。显然,我们可以通过更多的自连接生成更大的数字序列。
对于 ANSI SQL/86 标准,交叉连接指的是不指定表的连接条件。例如:
SELECT hundrand.n * 100 + ten.n * 10 + one.n AS n
FROM t_number hundrand, t_number ten, t_number one
ORDER BY n;
💡如果表中的数据量比较大,交叉连接可能会导致查询结果的数据量急剧膨胀,从而引起性能问题。我们通常应该指定连接条件,避免产生交叉连接。
自然连接
如果连接查询同时满足以下条件,我们可以使用 USING 替代 ON 来简化连接条件的输入:
- 连接条件是等值连接。
- 两个表中的连接字段名称相同,类型也相同。
例如,上文中的等值连接查询可以使用 USING 关键字简化如下:
-- Oracle、MySQL、PostgreSQL 以及 SQLite
SELECT e.emp_name AS "员工姓名", j.job_title "职位名称"
FROM employee e
JOIN job j USING (job_id)
WHERE e.emp_id = 1;
其中,USING 表示使用两个表中的公共字段(job_id)进行等值连接。另外,查询语句中出现的公共字段无须添加表名限定。
除 Microsoft SQL Server 外,其他 4 种数据库都支持 USING 关键字。
进一步来说,如果等值连接条件中包含了两个表中所有同名同类型的字段,查询语句可以继续进行简化。例如,员工表和职位表中只存在 1 个同名同类型的字段(job_id),因此上面的示例可以进一步修改为下面这样:
-- Oracle、MySQL、PostgreSQL 以及 SQLite
SELECT e.emp_name AS "员工姓名", j.job_title "职位名称"
FROM employee e
NATURAL JOIN job j
WHERE e.emp_id = 1
AND job_id = 1;
其中,NATURAL JOIN 表示自然连接,我们同时省略了连接条件,表示使用两个表中的所有同名同类型字段进行等值连接。
同样,除 Microsoft SQL Server 外的其他 4 种数据库都支持自然连接。
自连接
自连接(Self Join)查询是指一个表和它自己进行连接查询。自连接本质上并没有什么特殊之处,主要用于处理那些对自身进行了外键引用的表。例如,员工表中的经理字段(manager)引用了员工表自身的编号字段(emp_id)。如果我们想要查看员工以及他的经理,可以通过自连接查询实现:
SELECT e.emp_name AS "员工姓名",m.emp_name AS "经理姓名"
FROM employee e
LEFT JOIN employee m ON (m.emp_id = e.manager)
WHERE e.emp_id = 9;
由于自连接中同一个表(employee)出现了两次,我们必须使用表别名进行区分。其中别名 e 代表了员工,别名 m 代表了经理。查询返回的结果如下:
员工姓名|经理姓名
-------|-------赵云 |刘备
另外,我们在查询中使用了左外连接,因为员工表中存在没有上级经理的员工(刘备)。