首页 / SQLite 入门教程 / UNION 与 UNION ALL

SQLite 入门教程

UNION 与 UNION ALL

本教程共 50 篇 · 第 29 篇 · 更新于 2026-07-31

sqliteunionunion-all结果集合并去重

29. UNION 与 UNION ALL

本节目标:学完本章你能把两条或多条 SELECT 的结果”上下叠”成一张表,并清楚 UNION(去重)和 UNION ALL(保留重复)的区别,知道该在什么场景用哪一个。

前面学的 JOIN 是”左右拼”(横向加列),这一章的 UNION 是”上下拼”(纵向加行)。它能把多条 SELECT 查出来的结果,按行首尾相接合并成一张大结果集。典型用途:你有两张结构相似的表(比如 orders_2024orders_2025),想一起统计;或者同一条业务逻辑有两种取数方式(比如”已支付订单”和”已退款订单”),想合并后统一展示。

合并的前提条件

UNION 不是随便两条 SELECT 都能拼的,它有三个硬要求:

  1. 每条 SELECT 选出的列数必须相同
  2. 对应位置的列,含义和数据类型要能对应(SQLite 是动态类型,约束比静态类型数据库宽松,但位置要对上);
  3. 列出现的顺序要一致

列名以第一条 SELECT 为准,后面的 SELECT 列名不起决定作用,所以给第一条起好别名很重要。

UNION:合并并去重

UNION 会把多个结果集合并,并且自动去掉完全重复的行

sqlite> SELECT name FROM users WHERE age < 30
   ...> UNION
   ...> SELECT name FROM users WHERE age > 20;

假设有个用户 age=25,他既满足 <30 又满足 >20,在两份子结果里都出现。UNION 合并时会把这条重复记录只保留一份。换句话说,UNION 在合并的同时做了一次”去重”,等价于先拼起来再 DISTINCT

我们用全局示例表做个更贴近业务的例子:假设你想得到”所有出现过的人名清单”——不管是 users.name 还是 orders 备注里(这里简化演示,仅说明语法),只要列数、位置对齐即可:

sqlite> SELECT name FROM users
   ...> UNION
   ...> SELECT '客户' || user_id FROM orders;   -- 仅演示:第二句产出单列文本,与 name 对齐
Note

UNION 的”去重”是按整行所有列的值来判断的:只有两行每一列都相等,才算重复、合并成一行。只要有一列不同,就是两行。去重需要额外排序比对,所以有性能成本。

UNION ALL:合并但保留重复

UNION ALL 与 UNION 唯一区别:它不去掉重复行,左表的所有行、右表的所有行,原样首尾相接。

sqlite> SELECT name FROM users WHERE age < 30
   ...> UNION ALL
   ...> SELECT name FROM users WHERE age > 20;

同样那个 age=25 的用户,在结果里会出现两次(来自左、右各一次),因为 UNION ALL 不做去重。

该用哪个:去重 vs 性能

这是个关键取舍:

  • 如果你明确需要”不重复的并集”(比如”出现过的人名清单""去重的 ID 列表”),用 UNION
  • 如果你本来就知道不会有重复,或者重复也是有意义的(比如把两个时间段的流水原样拼起来对账),用 UNION ALL

为什么很多时候推荐 UNION ALL?因为去重要额外排序和比对,数据量一大,UNION 比 UNION ALL 慢不少。当你确定没有重复、或重复无所谓时,用 UNION ALL 既正确又更快。

-- 业务上两张表不会重叠(不同年份),直接 ALL 更高效
sqlite> SELECT user_id, amount FROM orders_2024
   ...> UNION ALL
   ...> SELECT user_id, amount FROM orders_2025
   ...> ORDER BY amount DESC;
Tip

实用技巧:拿不准时用 UNION ALL 先验证行数是否符合预期(它返回的行数 = 各子查询行数之和,最好预测);若发现多了重复行、而你又确实要去重,再改成 UNION。另外,ORDER BY 要写在整段 UNION 的最后,对整个合并结果排序,不能写在中间某条 SELECT 后还指望它只排那一段。

用 UNION 模拟”全外连接”思路

SQLite 不支持 FULL OUTER JOIN(也不支持 RIGHT JOIN),但有时你想要”两边没匹配上的行都要”。一个常见思路是用 LEFT JOIN 的结果,再 UNION 上”右表有、左表没有”的那部分。例如要列出”所有用户及其订单,以及所有订单及其用户(含孤立订单)”:

sqlite> SELECT u.name, o.amount
   ...> FROM users AS u
   ...> LEFT JOIN orders AS o ON o.user_id = u.id
   ...> UNION
   ...> SELECT u.name, o.amount
   ...> FROM orders AS o
   ...> LEFT JOIN users AS u ON u.id = o.user_id
   ...> WHERE u.id IS NULL;   -- 补上"订单有、但用户不存在"的孤儿订单

第一部分 LEFT JOIN 已经覆盖”所有用户 + 匹配订单”;第二部分专门补”孤儿订单”(user 为 NULL 的那些)。UNION 负责去重合并,避免重叠行。这正是 JOIN 章节提到的”用 LEFT + UNION 模拟全外连接”的做法。

Warning

常见坑:列数或列顺序不一致会直接报错。比如第一条 SELECT 选了 name, age 两列,第二条只选了 name 一列,UNION 会拒绝合并。另外,容易把 UNION 和 JOIN 搞混:UNION 是”上下加行、列数相同”,JOIN 是”左右加列、用条件对齐”,二者解决的问题完全不同,别在需要其中一种时误用另一种。

列名、类型与排序的细节

有几个容易忽略但很实用的细节:

第一,合并后结果集的列名,以第一条 SELECT 为准。后面 SELECT 里起的别名不会影响最终列名,所以别名要起在第一条上。例如 SELECT name AS 姓名 FROM users UNION SELECT name FROM ...,最终列名叫”姓名”。

第二,ORDER BY 和 LIMIT 必须写在整段 UNION 的最后,对整个合并结果生效。你不能写出”先排好第一段、再排好第二段、然后 UNION”——那样排序只对各自段有效、合并后顺序又被打乱。正确写法:

sqlite> SELECT name FROM users WHERE age < 30
   ...> UNION
   ...> SELECT name FROM users WHERE age > 20
   ...> ORDER BY name
   ...> LIMIT 10;

第三,虽然 SQLite 是动态类型(声明类型只是”类型亲和性”建议,实际存什么由值决定),但参与 UNION 的各段对应列,最好语义和数据类型对齐,否则可能出现”数字和文本被并在一列”导致排序或展示怪异。这是动态类型的便利,也是需要你自觉把关的地方。

一个完整实战取舍

假设运营要一份”本季度活跃用户 + 本季度新增用户”的名单去发券,两张来源可能重叠(既是活跃又是新增的人)。如果名单允许重复(一人收到两张券也无妨),用 UNION ALL 最直接;如果要求”一人只发一张、去重”,就用 UNION。这个选择不是语法问题,而是业务含义问题——先问清楚”重复有没有意义”,再决定去不去重。这正呼应前面说的:拿不准时先用 UNION ALL 看行数,再按需改 UNION。

类比小结

把 UNION 想成”把几叠内容相似的纸首尾摞成一厚叠”:

  • UNION = 摞之前先 toss 掉完全相同的纸(去重),结果更”干净”但更慢;
  • UNION ALL = 原样摞,快慢且保留全部,包括重复的纸。

记住口诀:要干净去重用 UNION,要完整高效用 UNION ALL;列数相同、顺序对齐是前提。 下一章我们学 CTE(WITH),它能把复杂查询拆成有名字的步骤,配合 UNION、JOIN、子查询都更清晰。