SQL从入门到出门 第12篇 集合运算
本篇介绍SQL:2016(ISO/IEC 9075:2016)标准中定义的集合操作符(UNION
、INTERSECT
和EXCEPT
),以及六种主流数据库中的实现及差异:Oracle、MySQL、Microsoft SQL Server、PostgreSQL、Db2、SQLite。
我们知道,SQL 基于关系代数;因此 ,SQL 中的表或查询结果与集合理论中的集合非常类似,表或查询结果中的行相当于集合中的元素。不过,它们之间也存在一些不同之处:
- 集合理论中的集合不允许存在重复的数据,但是 SQL 允许。因此,SQL 中的集合也被称为多重集合(multiset);
- 多重集合与集合理论中的集合都是无序的,但是 SQL 查询结果可以通过ORDER BY子句进行排序。
在 SQL 标准中,提供了针对多重集合的集合操作符:UNION
、INTERSECT
和EXCEPT
。它们分别表示集合的并集()、交集()和差集()运算。
SQL 中的集合操作符要求参与运算的表或查询结果具有相同数量的列,以及对应列的类型必须匹配或兼容。某些数据库产品可能会为此尝试执行类型转换。
并集(UNION)
UNION
操作符用于将两个查询结果合并成一个结果集,包含了第一个查询结果以及第二个查询结果中的数据:
SELECT column1, column2
FROM table1
UNION [DISTINCT | ALL]
SELECT col1, col2
FROM table2;
其中,DISTINCT 表示将合并后的结果集进行去重;ALL 表示保留结果集中的重复记录;如果省略,默认为 DISTINCT。参考以下示意图:
MySQL、PostgreSQL 支持
UNION DISTINCT
语法,其他 4 个数据库只支持简写的UNION
。
来看一个示例:
SELECT d.department_id
FROM departments d
UNION
SELECT e.department_id
FROM employees e;
首选,分别执行两个查询,获取 departments 表中的部门编号和 employees 表中的部门编号;然后将它们进行合并操作,并且排除重复的记录。最终结果返回了 28 条记录。
以下语句执行了类似的操作,只是不进行重复值的删除:
SELECT d.department_id
FROM departments d
UNION ALL
SELECT e.department_id
FROM employees e;
查询返回了 134 条记录,包括重复值。
UNION
基于行进行纵向组合,JOIN
基于列进行横向组合。
如果要对集合运算后的数据进行排序,需要将ORDER BY
子句写在最后;UNION
之前的查询中不能出现排序操作:
-- Error case
SELECT d.department_id
FROM departments d
ORDER BY d.department_id
UNION
SELECT e.department_id
FROM employees e;
正确的写法如下:
SELECT d.department_id
FROM departments d
UNION
SELECT e.department_id
FROM employees e
ORDER BY department_id;
对于分组操作,每个 SELECT 查询都可以包含一个GROUP BY
,但是它们只针对各自进行分组;如果想要对最终结果进行分组,需要在外层增加一个 SELECT 语句:
SELECT tmp.department_id
FROM (SELECT d.department_id
FROM departments d
UNION
SELECT e.department_id
FROM employees e) tmp
ORDER BY tmp.department_id;
最后,我们可以使用多个集合操作符合并多个结果集:
SELECT column1, column2
FROM table1
UNION [DISTINCT | ALL]
SELECT col1, col2
FROM table2
UNION [DISTINCT | ALL]
SELECT c1, c2
FROM table3;
交集(INTERSECT)
INTERSECT
操作符用于返回两个查询结果中的共同部分,即同时出现在第一个查询结果和第二个查询结果中的数据:
SELECT column1, column2
FROM table1
INTERSECT [DISTINCT | ALL]
SELECT col1, col2
FROM table2;
其中,DISTINCT 表示将最终的结果集进行去重;ALL 表示保留结果集中的重复记录;如果省略,默认为 DISTINCT。参考以下示意图:
注意中间的示意图,虽然 result1 中包含两个 1,但是最终结果只有一个 1;因为 result2 中的 1 只能和 result1 中的一个 1 匹配。
MySQL 不支持
INTERSECT
操作符;
只有 PostgreSQL 支持INTERSECT DISTINCT
语法,其他 4 个数据库只支持简写的INTERSECT
;
只有 PostgreSQL 和 Db2 支持INTERSECT ALL
语法。
以下示例使用INTERSECT
获取所有经理的员工编号:
SELECT m.employee_id
FROM employees m
INTERSECT
SELECT e.manager_id
FROM employees e
ORDER BY employee_id;
对于 MySQL,以上示例可以通过JOIN
实现:
SELECT DISTINCT
m.employee_id
FROM employees m
JOIN employees e
ON (m.employee_id = e.manager_id)
ORDER BY employee_id;
差集(EXCEPT)
EXCEPT
操作符用于返回出现在第一个查询结果中,但不在第二个查询结果中的数据:
SELECT column1, column2
FROM table1
EXCEPT [DISTINCT | ALL]
SELECT col1, col2
FROM table2;
其中,DISTINCT 表示将最终的结果集进行去重;ALL 表示保留结果集中的重复记录;如果省略,默认为 DISTINCT。参考以下示意图:
对于EXCEPT
,只要 result2 中出现的数据(1),result1 中所有匹配的数据(重复出现的 1)都会被去除;对于EXCEPT ALL
,result2 中出现的数据,在 result1 中只会消除一次(保留了一个 1 和两个 2)。
MySQL 不支持
EXCEPT
操作符;
Oracle 使用关键字MINUS
替代EXCEPT
,Db2 同时支持MINUS
和EXCEPT
;
只有 PostgreSQL 支持EXCEPT DISTINCT
语法,其他 4 个数据库只支持简写的EXCEPT
或MINUS
;
只有 PostgreSQL 和 Db2 支持EXCEPT ALL
语法。
以下查询用于检索那些目前没有员工的部门编号:
SELECT d.department_id
FROM departments d
EXCEPT -- For Oracle, use MINUS
SELECT e.department_id
FROM employees e
ORDER BY department_id;
对于MySQL,可以通过LEFT JOIN
实现以上功能:
SELECT d.department_id
FROM departments d
LEFT JOIN employees e
ON (e.department_id = d.department_id)
WHERE e.department_id IS NULL
ORDER BY department_id;
查询中的WHERE
子句正好找出左表(departments)在右表(employees)中不存在对应数据的那些记录。
集合运算优先级
我们已经介绍了 SQL 中的 3 种集合操作符:UNION
、INTERSECT
以及EXCEPT
。实际使用中,我们可以通过多个集合操作符对多个查询结果进行组合。此时,需要注意它们之间的优先级和执行顺序:
- 相同的集合操作符按照从左至右的顺序执行;
-
INTERSECT
的优先级高于UNION
和EXCEPT
,但是 Oracle 和 SQLite 未支持; - 使用括号可以明确指定执行的顺序。
首先,看一下相同的操作符的执行顺序。以下示例使用了两个UNION
操作符,其中一个增加了 ALL 选项:
SELECT 1 FROM employees
UNION ALL
SELECT 1 FROM employees
UNION
SELECT 1 FROM employees;
最终的结果只有一条记录(一个 1),因为最后的UNION
去除了重复的数据。与此不同的是,下面的示例返回了多条重复的记录(108 个 1):
SELECT 1 FROM employees
UNION
SELECT 1 FROM employees
UNION ALL
SELECT 1 FROM employees;
接下来,看一个不同的操作符示例:
-- Not for MySQL
SELECT 1 FROM employees
UNION ALL
SELECT 1 FROM employees
INTERSECT
SELECT 1 FROM employees;
除了Oracle 和 SQLite 之外,其他 3 个数据库的查询结果返回了多条重复的数据(108 个 1),因为INTERSECT
先执行,UNION ALL
最后保留的重复值。
Oracle 和 SQLite 中,所有集合操作符优先级相同。
最后,我们使用括号来修改多个集合操作符的执行顺序:
-- Not for MySQL or SQLite
(
SELECT 1 FROM employees
UNION ALL
SELECT 1 FROM employees
)
INTERSECT
SELECT 1 FROM employees;
-- For SQLite only
SELECT *
FROM (SELECT 1 FROM employees
UNION ALL
SELECT 1 FROM employees)
INTERSECT
SELECT 1 FROM employees;
查询结果只返回一条记录。
数据库 | 集合操作符 | 描述 |
---|---|---|
Oracle | OK | Oracle 差集运算使用MINUS 替代EXCEPT Oracle 不支持 UNION 的 DISTINCT 选项,使用默认行为 Oracle 不支持 INTERSECT 和MINUS 的 DISTINCT | ALL 选项 Oracle 中所有集合操作符优先级相同 |
MySQL | OK | MySQL 不支持INTERSECT 操作符 MySQL 不支持 EXCEPT 操作符 |
SQL Server | OK | SQL Server 不支持 UNION 的 DISTINCT 选项,使用默认行为 SQL Server 不支持 INTERSECT 和EXCEPT 的 DISTINCT | ALL 选项 |
PostgreSQL | OK | |
Db2 | OK | Db2 同时支持EXCEPT 和MINUS Db2 不支持 UNION 的 DISTINCT 选项,使用默认行为 |
SQLite | OK | SQLite 不支持 UNION 的 DISTINCT 选项,使用默认行为 SQLite 不支持 INTERSECT 和EXCEPT 的 DISTINCT | ALL 选项 SQLite 中所有集合操作符优先级相同 |
欢迎留言讨论!