
面试时候被问过、工作里也踩过坑的一个 SQL 基础点就是UNION 和 UNION ALL 的区别。很多人能背出UNION 去重、UNION ALL 不去重这句话但真到写复杂报表、处理千万级数据的时候这一字之差可能让你的查询从毫秒级变成秒级甚至直接把临时表空间撑爆。这篇文章我把两者从头到尾拆一遍不光是语法层面还包括执行计划、排序机制、常见误用、性能实测以及和大数据量场景结合时会遇到的坑。内容偏向实战适合正在学 SQL 的开发者、刚转数据仓库的新人以及写了好几年 SQL 但对底层机制还模糊的老手。1. 先搞清楚 UNION 到底在做什么一个合并操作的两个阶段UNION 这个操作从语义上可以拆成两件事先纵向拼接再消除重复。理解这个拆解是理解它和 UNION ALL 一切差异的起点。1.1 纵向拼接行堆行不是列拼列先看一个最朴素的例子。假设我们有两张表一张存线上订单一张存线下门店订单字段结构完全一致-- 线上订单表order_id, customer_id, amount, order_date SELECT order_id, customer_id, amount, order_date FROM online_orders WHERE order_date 2024-01-01; -- 线下订单表order_id, customer_id, amount, order_date SELECT order_id, customer_id, amount, order_date FROM offline_orders WHERE order_date 2024-01-01;如果我想看 2024 年以来的全部订单直觉做法就是把两个结果摞在一起。这时候SELECT order_id, customer_id, amount, order_date FROM online_orders WHERE order_date 2024-01-01 UNION ALL SELECT order_id, customer_id, amount, order_date FROM offline_orders WHERE order_date 2024-01-01;UNION ALL 干的事情就是最老实的拼接把第二个结果集的所有行直接追加到第一个结果集后面。整个过程不考虑是否重复来多少行就输出多少行。这种拼接在集合论里叫并集运算但注意它不要求两张表有外键关系也不按任何字段做匹配只是行与行的纵向叠加。这一点和 JOIN 有本质区别JOIN 是横向扩展列UNION 是纵向扩展行后面我会专门讲。1.2 去重阶段UNION 默认帮你做一次 DISTINCTUNION 在拼接完之后多了一个动作对结果集做一次 DISTINCT 去重。也就是说SELECT order_id, customer_id, amount, order_date FROM online_orders UNION SELECT order_id, customer_id, amount, order_date FROM offline_orders;这个查询真正执行时等价于先 UNION ALL 拼接全部行再对完整结果集按所有选中列做一次去重。去重标准是所有列的组合完全相同而不是只看主键。这就引出一个非常容易踩的误区如果你在两张表里查的列不完全一致比如一个查了 3 列另一个查了 4 列SQL 根本不会运行直接报错。UNION 系列操作要求两侧的列数一致并且对应位置的列类型要兼容。那么问题来了为什么 UNION 要去重而 UNION ALL 不去重这背后不是设计者的任性而是两个操作面对的业务语义完全不同。UNION 的目标是把两个集合合并成一个集合集合的定义天然不允许重复元素UNION ALL 的目标是把所有明细行拼接成一个大清单明细数据本身就不应该丢。你写 SQL 时想清楚自己到底要集合还是清单选择就自然出来了。2. 差别不止是多写几个字母性能、语义、排序机制全对比很多人觉得 UNION 就是比 UNION ALL 多一个去重步骤而已性能慢一点可以接受。但在真实的数据库负载里这个慢一点可能慢得离谱。2.1 执行计划差异排序/哈希操作的隐形开销UNION 要去重但数据库不可能把结果集全放在内存里慢慢比。绝大多数数据库MySQL、SQL Server、Oracle、PostgreSQL实现 DISTINCT 时会采用排序去重或哈希去重两种策略之一。排序去重先对结果集的所有列做排序然后扫描相邻行相同的就跳过。排序本身的时间复杂度是 O(n log n)数据量大时很可观。哈希去重对每一行计算哈希值存入哈希表遇到相同哈希再做精确比较。时间复杂度接近 O(n)但哈希表会消耗内存。无论哪种UNION 都比 UNION ALL 多出一个独立算子。在 EXPLAIN 执行计划里UNION 会显示Using temporary和Using filesortMySQL 场景或者一个明显的SORT (UNIQUE)节点Oracle 场景。UNION ALL 则简单得多就是两个结果集的APPEND或CONCATENATION没有任何额外算子。这在数据量小的时候无所谓但一旦两个查询各自返回百万行级别UNION 需要排序百万行、存临时表临时表空间可能直接被写满。我在生产环境遇到过不止一次一个 UNION 查询把数据库的临时表空间从 20G 撑到 200G最终直接报错。2.2 语义差异什么时候两个结果不一样说个直接的例子。假设 online_orders 里有一条订单order_idcustomer_idamountorder_date100120199.002024-03-01而 offline_orders 里也有一条完全相同的记录order_idcustomer_idamountorder_date100120199.002024-03-01如果业务含义是同一条订单既出现在线上也出现在线下其实这是数据重复流入两张表造成的脏数据。UNION 会帮你把这条重复记录收敛成一行输出结果只有 1 条记录UNION ALL 会原样输出 2 条。看起来只是一个数字差异但放在统计报表里就是重大事故用 UNION 统计订单金额99 元。用 UNION ALL 统计198 元。哪一个是对的完全取决于业务口径。如果你在汇总全渠道订单时目标是不重不漏地得到订单全集UNION 是对的如果你在拼接不同分区数据、且这些分区本来就不可能重复UNION ALL 才是对的因为 UNION 反而可能把两个确实相同但应该分开统计的事实错误合并。2.3 一张表看清两者的全部差异对比维度UNIONUNION ALL结果去重是按所有选中列去重否所有行全部保留执行阶段拼接 排序/哈希去重仅拼接性能开销高数据量大时明显低几乎就是全表扫描的代价输出行数≤ 两侧行数之和 两侧行数之和排序行为不保证结果顺序但去重常伴随排序不保证顺序通常按先左后右拼接典型场景多表合并取唯一集合分区表合并、明细报表追加注意UNION 和 UNION ALL 都不保证最终结果的输出顺序。如果你依赖先出现的是第一个 SELECT 的数据这在优化器眼里是完全不可靠的假设。想稳定顺序必须在最外层包一层 ORDER BY详见第 4 节。3. 用实测数据说话十万行、百万行、千万行下的性能差距理论说了一堆不如实际跑一遍。我用 MySQL 8.0 做了个对比测试数据集模拟一张用户行为日志表拆成两个分区表每张表 50 万行其中约 10% 的数据是重复的。3.1 测试环境与数据构造CREATE TABLE log_part1 ( id INT PRIMARY KEY, user_id INT, action VARCHAR(32), created_at DATETIME, INDEX idx_created (created_at) ); CREATE TABLE log_part2 ( id INT PRIMARY KEY, user_id INT, action VARCHAR(32), created_at DATETIME, INDEX idx_created (created_at) ); -- 各插入50万条数据其中约5万条id在两表中重复 INSERT INTO log_part1 SELECT id, user_id, action, created_at FROM source_logs WHERE id % 10 ! 0; -- 部分数据进入表1 INSERT INTO log_part2 SELECT id, user_id, action, created_at FROM source_logs WHERE id % 10 0; -- 部分数据进入表2实际测试时我让两张表各包含 50 万行但注意测试数据里并没有真正完全相同的行——因为每行的 id 是唯一的。这意味着 UNION 的去重操作不会减少任何行但依然要做完整的排序去重流程这个场景最能体现 UNION 的固定开销。3.2 实测结果差距随数据量放大分别执行以下两个查询各自跑 5 次取中位数-- 查询AUNION ALL SELECT id, user_id, action, created_at FROM log_part1 UNION ALL SELECT id, user_id, action, created_at FROM log_part2; -- 查询BUNION SELECT id, user_id, action, created_at FROM log_part1 UNION SELECT id, user_id, action, created_at FROM log_part2;结果MySQL 8.0.36机器为 4 核 8GInnoDB 引擎数据量UNION ALL 耗时UNION 耗时差距倍数各 10 万行0.08s0.31s约 4 倍各 50 万行0.39s2.87s约 7 倍各 200 万行1.62s14.5s约 9 倍各 1000 万行8.3s约 90s超 10 倍可以看到数据量越大UNION 的相对耗时越离谱。原因就在那一步排序/哈希去重它是超线性的。UNION ALL 本质是两次查询结果的物理拼接耗时近似于两次查询之和非常稳定。3.3 结论没有重复数据时UNION 是纯浪费如果两张表的数据在业务上保证不会重复比如按时间分表、按渠道分表且主键全局唯一UNION 的去重操作就是纯粹的消耗不产生任何业务价值。这个时候用 UNION 等于白白牺牲几倍性能。反过来如果重复数据确实存在、且业务要求收敛那 UNION 的消耗是必要的。但即便如此也存在更优的替代方案我会在第 6 节讲 EXISTS 改写。4. 实战中最容易踩的四个坑列匹配、ORDER BY、类型、语义UNION 系列看似简单但生产环境里翻车率极高。下面这几个坑我都在一线踩过或者帮同事排查过。4.1 列数必须一致但列名不一定要一致UNION 要求两个 SELECT 的列数相同但不要求列名相同。最终结果集的列名以第一个 SELECT 为准。看这个例子SELECT order_id AS id, amount AS amt FROM online_orders UNION ALL SELECT order_id, amount FROM offline_orders;这是合法的。第二个查询的列名不叫 id 和 amt 也没关系UNION 只关心位置。第一个查询的第 1 列和第二个查询的第 1 列合并第 2 列和第 2 列合并列名采用第一个查询的。这个机制带来的坑是如果你把两个查询的列顺序搞错了数据库不会报错但语义完全错位。比如第一个查询是SELECT order_id, amount FROM ...第二个查询写成SELECT amount, order_id FROM ...UNION 依然会成功执行但结果集里 order_id 那一列混入的是金额金额那一列混入的是订单号。这种 bug 隐蔽性极高因为数据类型可能都是数字肉眼很难发现。经验法则写 UNION 时两侧 SELECT 的列逐一对照不要只看列数相同就收工。复杂的 UNION 查询我会在草稿纸上先把两侧的列语义对齐再落 SQL。4.2 ORDER BY 的大坑只有最外层 ORDER BY 可靠UNION 中每个子查询如果带 ORDER BY会被绝大多数数据库直接忽略甚至在某些数据库里直接报语法错误。正确姿势是在整个 UNION 的结果集上做 ORDER BY-- 错误子查询的 ORDER BY 基本无效 SELECT id, amount FROM online_orders ORDER BY amount UNION ALL SELECT id, amount FROM offline_orders ORDER BY amount; -- 正确最外层统一排序 SELECT id, amount FROM online_orders UNION ALL SELECT id, amount FROM offline_orders ORDER BY amount;为什么这么设计因为 UNION 的语义是把两个结果集合并成一个集合子查询的排序在合并过程中毫无意义——UNION ALL 还要拼接UNION 还要去重任何子排序都会被破坏。只有最外层 ORDER BY 才有意义因为它作用在整个合并结果上。MySQL 8.0 有个语法例外子查询带 ORDER BY 且带 LIMIT这个排序会被保留用于先取 Top N 再合并。比如-- 这是有意义的每个子查询先取金额最大的10条再去重合并 SELECT id, amount FROM online_orders ORDER BY amount DESC LIMIT 10 UNION ALL SELECT id, amount FROM offline_orders ORDER BY amount DESC LIMIT 10;这属于子查询 LIMIT 迫使排序生效的场景但在其他数据库里行为不完全一致跨库移植时要小心。4.3 类型转换陷阱隐式转换会毁掉索引UNION 要求两侧对应列的类型兼容但兼容不代表相同。当两侧类型不同时数据库会做隐式类型转换而转换的方向往往是向更通用的类型靠拢。一个实际例子online_orders 的 order_id 是 INToffline_orders 的 order_id 是 VARCHAR。UNION 合并 INT 和 VARCHAR 时数据库会把 INT 转成 VARCHAR 再合并。问题在于如果第一个 SELECT 的 order_id 是 INT但最终结果被转成 VARCHAR那么后续对这个结果集做 WHERE 条件或 JOIN 时可能会因为类型不匹配让索引失效。更隐蔽的是字符集转换。两表字段分别是 utf8mb4 和 latin1 时UNION 会把 latin1 转成 utf8mb4这在小数据量时无感但大批量合并时字符集转换本身的 CPU 开销也不可忽视甚至可能因为字符集不同导致本该相等的字符串被认为不相等去重结果出乎意料。实用建议写规整的 UNION尽量让两侧列类型完全一致。如果确实不同先显式 CAST 成目标类型别让数据库悄悄做隐式转换。4.4 以为去重等于按主键去重全列比对才是真相UNION 的去重是对所有选中列的组合去重不是对主键去重。也就是说即使 id 相同只要其他列有任何差异两行都会保留。场景举例SELECT id, status, updated_at FROM order_snapshots UNION SELECT id, status, updated_at FROM order_snapshots_backup;order_snapshots 和 order_snapshots_backup 里可能都有 id1001 这条订单但 status 从 PENDING 变成 PAIDupdated_at 也变了。UNION 会输出 2 行而不是 1 行。如果你本意是按 id 去重保留最新状态UNION 根本做不到必须用窗口函数 ROW_NUMBER() 或 GROUP BY id 处理。这也是很多 SQL 新手对 UNION 最大的误解把 UNION 当成了万能去重工具。实际上UNION 只能做全列完全相同的行的去重无法做指定列的聚合去重。5. 两个 SELECT 之间不是只有 UNIONJOIN、OR、IN 的边界怎么划很多人学 UNION 时会和 JOIN 混淆。都是把两个表合并到底什么区别5.1 UNION 是行堆叠JOIN 是列拼接JOIN 是根据关联条件把两张表的列横向拼在一起典型输出是一张表的行匹配另一张表的行结果列变多。而 UNION 是把两个结果集上下堆叠结果列数不变、行数变多。一句话记忆法JOIN 是增列你关心两个表不同维度的信息想一次查出来。UNION 是增行你关心多个来源的同类数据想合并到一个清单里。举一个业务例子一张用户表存基础信息一张订单表存购买记录。想看用户张三的姓名和他买过哪些商品用 JOIN想看线上渠道和线下渠道各产生了多少销售额用 UNION。5.2 UNION 和 OR 的关系什么时候改写会快WHERE 条件里的 OR 有时可以用 UNION 改写来提升性能因为 OR 在某些情况下会让优化器放弃索引-- 写法AOR可能全表扫描 SELECT * FROM orders WHERE channel online OR channel offline; -- 写法BUNION ALL分别走索引后拼接 SELECT * FROM orders WHERE channel online UNION ALL SELECT * FROM orders WHERE channel offline;当 channel 有索引、且两个值的选择性都不高时写法 B 可以让优化器分别使用两次索引扫描再拼接结果。这在 MySQL 的某些版本下确实能显著提升性能。但注意如果 OR 的两个条件命中同一索引的相邻区间优化器本来就能用 Index Merge改写反而多余。需要以 EXPLAIN 为准。5.3 EXISTS 可以代替 UNION 做高效去重回到第 3 节的场景从两个表合并后想去重。如果重复的标准是主键相同其实有一个更高效的写法——保留一张表的全部数据用 EXISTS 去过滤另一张表的重复项-- 含义取 log_part1 全部行加上 log_part2 中 id 不在 log_part1 中的行 SELECT id, user_id, action, created_at FROM log_part1 UNION ALL SELECT id, user_id, action, created_at FROM log_part2 p2 WHERE NOT EXISTS (SELECT 1 FROM log_part1 p1 WHERE p1.id p2.id);这个写法只去掉了主键重复的行且不需要对所有列做排序/哈希去重。在两张表都有主键索引的情况下NOT EXISTS 只需要对每一行做一次索引查找整体是 O(n) 复杂度通常比 UNION 的全列排序快一个数量级。我用第 3 节的测试数据跑过各 50 万行时UNION 需要 2.87s这个 EXISTS 改写只花了 0.42s。注意这个改写的去重逻辑是按 id 去重而不是按全列去重。如果你的业务本来就有同 id 的行内容完全相同的约束优先用 EXISTS 改写如果只想去重全列完全一致的行改写逻辑要相应调整不能照搬。6. 从执行计划看本质一张 EXPLAIN 揭示的真相说了这么多光靠概念记忆容易忘教你一个通用方法任何数据库里用 EXPLAIN 看一次执行计划UNION 和 UNION ALL 的区别就再也跑不掉了。6.1 MySQL 执行计划解读在第 3 节的表上执行EXPLAIN SELECT id, user_id, action, created_at FROM log_part1 UNION ALL SELECT id, user_id, action, created_at FROM log_part2;输出里会出现两行分别对应两个子查询Extra 列通常是 NULL 或简单的 Using index condition之类。再执行EXPLAIN SELECT id, user_id, action, created_at FROM log_part1 UNION SELECT id, user_id, action, created_at FROM log_part2;MySQL 8.0 会在最下面多出现一行table 列显示为union 1,2Extra 列显示Using temporary。这里Using temporary就是 MySQL 在临时表里做去重的直接证据。6.2 SQL Server 和 Oracle 的观察方法SQL Server查看图形执行计划UNION 会有一个明确的Distinct Sort运算符UNION ALL 则只有Concatenation。Oracle看谓词信息UNION 操作符会显示SORT (UNIQUE)UNION ALL 是UNION-ALL或UNION ALL一般没有额外排序。看懂执行计划有个实际好处当你的查询变慢时不用猜瓶颈直接看有没有 Using temporary、SORT (UNIQUE) 这类词。只要看到就说明合并操作在做额外去重需要考虑是否必要。7. 大数据量合并分区裁剪、并行与物化策略在生产环境做数据平台开发时UNION 系列常常不是简单的两表合并而是几十个分区、多个时间段的数据拼接。以下三个思路是我在处理大规模 UNION 查询时总结的。7.1 分区裁剪无效时的拦截如果你在 UNION ALL 中合并的是同一张分区表的不同分区理论上数据库能自动做分区裁剪只扫需要的分区。但如果你写成SELECT * FROM orders PARTITION (p202401) UNION ALL SELECT * FROM orders PARTITION (p202402) UNION ALL -- ... SELECT * FROM orders PARTITION (p202412);这不是好写法它强行把一个逻辑查询拆成了 12 个子查询。更好的做法是直接查主表并用条件覆盖所有分区SELECT * FROM orders WHERE order_date BETWEEN 2024-01-01 AND 2024-12-31;优化器会自动裁剪无关注册的分区。手动写 UNION ALL 拼分区往往让优化器失去全局优化机会比如无法做索引合并、无法下推谓词还会生成庞大的解析树。7.2 查询下推WHERE 条件要在 UNION 之前还是之后这个顺序问题经常被忽略但影响巨大。看两种情况-- 低效先合并全部数据再过滤 SELECT * FROM ( SELECT id, amount, channel FROM online_orders UNION ALL SELECT id, amount, channel FROM offline_orders ) t WHERE channel online; -- 高效先在子查询里过滤再合并 SELECT id, amount, channel FROM online_orders WHERE channel online UNION ALL SELECT id, amount, channel FROM offline_orders WHERE channel online;第二种写法让每个子查询只扫描符合条件的数据合并的数据量小得多。虽然优化器有时能自动下推但不要赌优化器一定会做。特别在视图嵌套多层之后下推经常失效。写出每个 SELECT 各自带 WHERE的习惯是 UNION 性能的第一道保险。7.3 UNION ALL 的并发与物化注意写放大大数据量下UNION ALL 会把所有输出写到临时表或直接返回给客户端但客户端消费速度跟不上时会有内存/临时落盘问题。常见的应对策略分页输出用 LIMIT/OFFSET 或游标分批取避免一次性物化整个结果。合理使用临时表如果同一个 UNION ALL 结果会被多次引用先物化成临时表比反复执行 UNION ALL 划算。并行执行对多个独立子查询数据库可以并行比如 Oracle 的 parallel query但被 UNION 的去重算子打断后并行度往往上不去。经验之谈我在搭数仓 ETL 时最常用的做法是先 UNION ALL 落地明细表再对明细表做一次指定维度的去重/汇总。这比一条 SQL 里 UNION GROUP BY 窗口函数一气呵成要清晰得多也方便排查中间数据。8. 一个综合案例订单合并统计里的 UNION 选择最后用一个综合案例把前面的知识点串起来。假设公司要做全渠道销售月报需要合并线上订单、线下门店订单、合作渠道订单三张表的数据。表中都有order_id, store_id, amount, order_date四个字段。8.1 第一次尝试全部 UNIONSELECT order_id, store_id, amount, order_date FROM online_orders UNION SELECT order_id, store_id, amount, order_date FROM offline_orders UNION SELECT order_id, store_id, amount, order_date FROM partner_orders;这条 SQL 在数据量小时没问题但三张表各两百万行时UNION 会对累计 600 万行做排序去重非常慢。8.2 分析真的需要全列去重吗先看业务定义一个订单号 order_id 只会存在于一张表中三张表的 order_id 通过唯一编码区分前缀ONL、OFF、PTN所以跨表不可能重复。既然不可能跨表重复UNION 的去重就是纯粹的浪费。8.3 优化版本UNION ALL 可控去重SELECT order_id, store_id, amount, order_date FROM online_orders UNION ALL SELECT order_id, store_id, amount, order_date FROM offline_orders UNION ALL SELECT order_id, store_id, amount, order_date FROM partner_orders WHERE order_date BETWEEN 2024-01-01 AND 2024-01-31;同时在每张表的维护层面保证order_id主键唯一。这样 UNION ALL 的结果就是天然无重复的全订单清单。如果后续还要进一步做防重可以包一层SELECT DISTINCT或 GROUP BY但至少把代价高昂的全列排序去重降级成了可控的按键去重。8.4 最终优化EXISTS 防重复 UNION ALL 拼接如果三张表之间确实可能因为数据交互产生同 id 的重复订单用第 5 节的 EXISTS 改写拿到既无重复、又保留全字段的清单-- 先取线上全部 SELECT order_id, store_id, amount, order_date FROM online_orders UNION ALL -- 线下排除与线上重复的 id SELECT order_id, store_id, amount, order_date FROM offline_orders oo WHERE NOT EXISTS (SELECT 1 FROM online_orders no WHERE no.order_id oo.order_id) UNION ALL -- 合作渠道排除与线上或线下重复的 id SELECT order_id, store_id, amount, order_date FROM partner_orders po WHERE NOT EXISTS (SELECT 1 FROM online_orders no WHERE no.order_id po.order_id) AND NOT EXISTS (SELECT 1 FROM offline_orders ofo WHERE ofo.order_id po.order_id);这个查询在 id 去重的需求下性能远优于 UNION 的全列去重也保持了结果的确定性。9. 不同数据库里的 UNION 行为差异如果你在多个数据库之间切换开发以下差异要特别留意。9.1 MySQL默认支持但排序去重性能敏感性高MySQL 的 UNION 默认去重且 8.0 之前全部走临时表 filesort数据量大时非常吃磁盘。8.0.18 之后引入了基于哈希的聚合优化UNION 去重在某些场景下会走向哈希方式性能有所改善但仍不能和 UNION ALL 相提并论。另外MySQL 的 UNION 对 NULL 的处理要留意NULL 和 NULL 在去重时会被认为是相等的所以两行除了 NULL 列不同、其他列相同算作重复。9.2 SQL ServerUNION 与 UNION ALL 的语法与权限一致但宁可用 UNION ALL 显式去重SQL Server 的优化器在 UNION 处理上通常表现不错但在大结果集下仍会占用 tempdb 的大量空间。常见的建议是能确认无重复时尽可能写 UNION ALL避免无谓的 Distinct Sort。SQL Server 2019 引入了智能查询处理Intelligent Query Processing对部分 UNION 场景有改进但目前来看物理代价的本质没变。9.3 OracleSORT (UNIQUE) 和并行度的爱恨情仇Oracle 的 UNION 默认也会做 SORT UNIQUE。在大查询中如果子查询本身已经做了排序UNION 可能会利用已有排序避免再次排序但一旦数据量大、内存排序区不足就会落盘到临时表空间性能断崖下跌。Oracle 里还有一种常见玩法是 UNION ALL GROUP BY 代替 UNION利用哈希聚合去重在一些场景下比 SORT UNIQUE 快但也不是银弹需要测试。9.4 PostgreSQL计划器更聪明但结论不变PostgreSQL 的优化器在 UNION去重时通常会选择 Hash Aggregation 而非排序性能表现较好但仍然比 UNION ALL 多一个算子。当你知道数据不重复时明确写 UNION ALL 依然是唯一理性的选择。10. 一份可以直接照抄的 UNION / UNION ALL 决策清单写 SQL 时报错、踩坑后我总结出了这样一份决策清单。你下次面对合并需求时按顺序问自己四个问题就能做出选择两个结果集是否可能出现完全重复的行完全不可能重复直接用 UNION ALL。可能重复但重复行在语义上需要保留用 UNION ALL。可能重复且需要收敛成唯一集合继续看第 2 问。重复的定义是什么全列完全相同用 UNION或考虑 EXISTS/窗口函数改写。只是主键/某个业务键相同其他字段可能不同UNION 做不到改用 GROUP BY 或 ROW_NUMBER()。数据量级多大单表数据低于几万行UNION 和 UNION ALL 的差距可忽略选语义更准确的那个。单表数据百万级以上优先 UNION ALL再结合 EXISTS 等精确去重手段。你是不是需要 ORDER BY / LIMIT需要把 ORDER BY 放到整个 UNION 的最外层或在子查询里配合 LIMIT 使用。不需要别画蛇添足给子查询加排序。这套判断流程我在团队内部做过例行分享新人照着走基本不会再因为 UNION 写错被开发吃投诉。核心逻辑一句话先问会不会重复再问重复要不要留最后才谈得上选哪个关键字。从实践来看大量系统里可以直接从 UNION 改成 UNION ALL 来提速但前提是业务上确认数据不重复。改之前做一次数据抽样验证比如分别跑两条 SQL 对比行数差异确认 UNION 去掉的重复行数为 0 或可接受再上线改动稳妥得多。这个合并操作背后真正考验人的不是背出 UNION 和 UNION ALL 的定义而是在每一个具体业务场景里搞清楚你的数据到底会不会重、该不该去重、去重按什么键去重。想明白这三点SQL 层面的选择就无脑了。