# UNION、INTERSECT 与 EXCEPT

> 使用 UNION、INTERSECT 和 EXCEPT 组合查询结果，可以保留或去除重复项。

---

LLMS 索引： [llms.txt](/llms.txt)

---

集合运算逐行组合两个结果集。三者都要求两侧查询返回相同数量的列，且类型兼容。`UNION` 返回任一侧出现的行（去除重复项）。`INTERSECT` 返回两侧都出现的行。`EXCEPT` 返回第一侧有而第二侧没有的行。每种运算都有 `ALL` 变体。`UNION ALL` 连接两个输入。`INTERSECT ALL` 保留每行在两侧出现次数中的较小值；`EXCEPT ALL` 从左侧次数中减去右侧次数，最小为零。后两种仍然需要匹配并统计行数。

## 本章要点 {#learn}

- 比较 `UNION` 和 `UNION ALL`，了解各自适用的场景
- `INTERSECT` 与 `INTERSECT ALL`
- `EXCEPT` 与 `EXCEPT ALL`
- 组合集合运算，以及运算优先级如何生效
- 性能：什么时候 `UNION ALL` 会明显更快

## 示例 {#example}

```sql {num="32.1" caption="UNION、INTERSECT 与 EXCEPT"}
-- UNION removes duplicates across the combined result
SELECT 1 AS n UNION SELECT 2 UNION SELECT 1;

-- UNION ALL keeps duplicates (cheaper, no sort/hash)
SELECT 1 AS n UNION ALL SELECT 1 UNION ALL SELECT 2;

-- INTERSECT: rows present in both
SELECT id FROM fruits WHERE id <= 3
INTERSECT
SELECT id FROM fruits WHERE id >= 2;

-- EXCEPT: rows in the first query but not the second
SELECT id FROM fruits WHERE id <= 4
EXCEPT
SELECT id FROM fruits WHERE id >= 3;

-- Order of evaluation: INTERSECT binds tighter than UNION / EXCEPT.
-- Use parentheses to be explicit:
(SELECT id FROM fruits WHERE id < 5)
UNION
(SELECT id FROM fruits WHERE id > 100);

-- ORDER BY applies to the combined result; column refs by position or alias
SELECT id AS n FROM fruits WHERE id < 3
UNION
SELECT id     FROM fruits WHERE id > 100
ORDER BY n;
```

`UNION` 会在完整行上进行排序或哈希，以去除重复项——这项工作确实存在，并会出现在 `EXPLAIN` 中。如果你知道输入不会产生重复项（例如谓词互不相交），优先使用 `UNION ALL`。对于大型查询，两者差异可能很大。

`INTERSECT` 和 `EXCEPT` 在应用代码中不太常见，但用于分析很方便：“点击了 A 和 B 的用户”“在目录 A 中而不在目录 B 中的产品”。

集合运算是对*行集合*的运算。各输入的列类型必须对齐——`int` 和 `text` 不能直接用集合运算合并。列名取自第一个查询。

## 运行示例 {#run}

运行方式（需要 `fruits`）：

```text
$ psql -f source/union-intersect-except.sql postgres
 n
---
 1
 2
(2 rows)

 n
---
 1
 1
 2
(3 rows)
 ...
```

## 常见陷阱 {#pitfalls}

- 对于不会产生重复项的查询，使用 `UNION` 而不是 `UNION ALL` 会把 CPU 浪费在去重上。除非可能出现你不想保留的重复项，否则选择 `UNION ALL`。
- 列数或类型不对齐：`SELECT id, name FROM t UNION SELECT id FROM t2` 会报错。用 `NULL::text` 补齐列，或对齐列。
- 集合运算后的 `ORDER BY` 针对*合并后的*结果，不能使用带表限定的列名。使用位置（`ORDER BY 1`）或第一个查询中的别名。
- `EXCEPT` 对 NULL 很敏感：`EXCEPT` 把两个 NULL 当作相等（就像 `DISTINCT`），所以左侧有 NULL、右侧也有 NULL 的行会被删除。这通常正是你想要的结果，但值得记住。

> [!TIP] 提示
> 如果你发现自己在写 `SELECT ... WHERE x = 1 UNION ALL SELECT ... WHERE x = 2 UNION ALL ...`，不妨考虑单个 `WHERE x IN (1, 2, 3)` 是否就够了。集合运算适用于确实不同、且需要合并结果的查询。

> [!NOTE] 动手练习
> 写两个 `SELECT` 查询 fruits，每个使用不同的 `WHERE` 子句，并用 `UNION` 合并。然后改成 `UNION ALL`，观察重复项数量的变化（如果有）。再试试在两者之间使用 `INTERSECT` 和 `EXCEPT`。

## 下载 {#downloads}

SQL 文件：[union-intersect-except.sql](/examples/source/union-intersect-except.sql) · [全部示例文件](/examples/examples.zip) · [准备工作](/#prerequisites)
