
在数据库开发这条路上摸爬滚打了十几年我几乎每次做代码评审时都能看到有人把union和union all混着用觉得差一个单词无所谓也见过很多项目为了“保险”所有合并查询都写union结果数据量一上来SQL 慢到让 DBA 想骂人。今天我就想把这俩关键字从头到尾聊透从语法差异、执行逻辑、性能表现到实际踩坑一次性讲清楚希望能帮你在日常开发里做出更合理的选择。先给还没完全入门的读者一个基本认知union和union all都是用来把多个查询结果“纵向拼接”的操作符。它们解决的核心问题是“我有两段数据结构相同我想把它们合成一个结果集返回”。区别只在于一个去重一个不去重。但就是这一个小小的差异直接影响 SQL 的执行计划、内存消耗和最终结果绝对不能拍脑袋乱写。这篇文章我会从最基础的概念讲起逐步深入到性能分析和进阶用法最后附上一份实战排错清单适合刚从入门到进阶的 SQL 开发者也适合正在做报表开发、数据迁移和需要频繁处理结果集合并的同学作为参考。1. 先搞清楚基本概念union 和 union all 到底在做什么1.1 最典型的使用场景把两个查询结果纵向拼起来union和union all的本质可以理解为“结果集合并”也叫“纵向拼接”。举个例子公司里有一个 2024 年的订单表orders_2024一个 2025 年的订单表orders_2025字段都是order_id, customer_id, amount, order_date。现在要拉一份所有订单的清单最直接的办法就是写两个查询再用union all把它们接起来。select order_id, customer_id, amount, order_date from orders_2024 union all select order_id, customer_id, amount, order_date from orders_2025;这是一个非常典型的合并查询场景。两条记录上下拼接最后返回的行数等于两个查询结果行数之和。union all在这里的理解成本最低它就是把两段数据直接摞在一起不做任何额外处理。如果你把union all换成unionselect order_id, customer_id, amount, order_date from orders_2024 union select order_id, customer_id, amount, order_date from orders_2025;那么数据库引擎会多做一步操作把合并后的结果集中完全重复的行去掉只保留一份。这里要强调一个很容易混淆的点很多人把union和join搞混。join是横向拼接把两张表的字段并在一起增加列数比如用户表和订单表通过customer_id关联最后得到一列用户信息加一列订单信息而union是纵向拼接增加的是行数字段数量和顺序必须保持一致。弄清楚这个方向性差异后面的很多问题就迎刃而解了。1.2 去重逻辑的本质差异union和union all最核心的分水岭就是“去重”。union会在合并结果后对每一行做一次完整的唯一性检查重复的行值只保留一次union all则完全跳过这一步有多少返回多少。这个“去重”听起来很简单但背后的代价并不小。为了判断两行是否重复数据库必须对结果集中的每一行进行逐列比较。如果结果集涉及大量字段或者字段本身是长度很大的文本类型这个比较过程会消耗大量 CPU 和内存资源。同时为了保证去重结果正确数据库通常还需要对结果进行排序或者哈希操作这一步也会引入额外的 I/O 开销。工作中最常见的例子是写报表 SQL。比如一张订单明细表里面有order_id和order_status两个字段。如果业务上同一订单号会对应多行明细你想知道一共有多少个订单号可以这样写select order_id from order_details union select order_id from order_details;这当然是一种很“绕”的写法实际项目里不会有人这么干。但它能直观说明一个问题union的去重是针对整个结果行的不是针对某一个字段的。select order_id只返回一个字段那去重时就会按这一个字段判断如果select后有多列比如select order_id, customer_id那么只有两列都相同才会被判定为重复。1.3 两个关键字在 SQL 标准中的位置从 SQL 语法角度讲union和union all都属于集合操作符与之并列的还有intersect交集和except差集但后两者在业务开发中的使用频率远低于union系。注意不同的数据库产品对集合操作符的语法支持有细微差别比如 MySQL 早期版本不支持exceptSQL Server 则完整支持PostgreSQL 语法也较为完整。union和union all在大部分主流数据库里都支持包括 MySQL、PostgreSQL、SQL Server、Oracle、达梦、人大金仓等。这一点让我在实际项目中基本不担心兼容性。不过当你用到intersect、except这种操作符时一定要先确认目标数据库的版本和兼容性情况否则上线阶段很容易碰到“语法不支持”的尴尬问题。2. 核心实操细节字段、排序、类型和 limit 的坑2.1 字段数量和顺序必须一致类型要兼容这是使用union系列操作符最基础也最容易出错的地方。两条查询语句的列数必须完全一致对应列的数据类型也必须兼容。所谓兼容是指两个类型能隐式转换比如int和varchar在某些数据库中会被隐式转换但有些数据库会直接报错。举个常见的错误例子select customer_id, customer_name from customers_2024 union all select customer_id, customer_name, register_channel from customers_2025;第一条查询返回 2 列第二条返回 3 列数据库立刻会报“列数不匹配”的错误。这种问题在开发环境基本一眼就能看出来但在地道的业务代码里如果查询是从多个模块拼接出来的字段就容易漏加或多加。再来看类型兼容性问题select amount from orders union all select customer_name from customers;amount是数值类型customer_name是字符串类型在大多数数据库里这种查询不会被执行因为第一列和第二列的类型不一致且无法安全转换。注意有些数据库比如 MySQL在严格模式下会直接报错而在非严格模式下可能发生隐式转换返回一个字符串化的数字或一个类型转换后的结果这种不确定性最容易坑人。所以我一直建议开发同学在写合并查询的时候把每个字段显式转换成目标类型比如都写成cast(amount as decimal(10,2))避免依赖数据库的隐式转换。2.2 空字符串和 null 在合并中的表现这是个非常经典的问题。union去重时null和null会被认为重复这一点大多数数据库是一致的。但空字符串和null不是一回事它们不会被判重。看个实际例子select a as col1 union select null as col1;这条查询返回两行a和null因为二者不同不会合并。再看这个select null as col1 union select null as col1;这条查询最终只返回一行null因为两个null被认为是重复的。这里有个易错点在union去重过程中某些数据库对于字符串字段的尾部空格处理存在差异。比如 SQL Server 的排序规则是忽略尾随空格的那么abc和abc 可能被认为是重复行而在 MySQL 的某些排序规则下abc和abc 进行比较时也可能因为字符集和排序规则的不同被判定为相同。这个差异非常隐蔽一旦数据源里出现脏数据就会导致合并结果行数不符合预期。我的建议是如果业务上需要精确区分子符串内容最好先把空白处理掉。比如用trim(col)统一去除首尾空格再合并。2.3 order by、limit 和 group by 放在什么位置这也是新手最容易犯迷糊的地方。union/union all的结果集整体排序和分页需要把order by写在最后一个查询语句的末尾而不是写在中间某个查询里。比如你想把两个班级的成绩合并并按分数从高到低排序select student_name, score from class_a union all select student_name, score from class_b order by score desc;这段 SQL 的意思是先把class_a和class_b的结果合并再对合并后的结果集按score降序排序。注意order by写在整个语句的最后它作用于union all产生的结果集而不是仅仅作用于class_b的查询结果。如果你在第一条查询里写了order by大多数数据库会直接报错因为中间查询的order by在集合操作里没有意义。这里可以稍微提一下某些数据库比如 Oracle在开启特定优化模式后可能允许语法通过但执行结果并不会符合你的预期所以最好统一按“最后一条查询末尾加order by”的规范来写。关于limit道理是一样的。MySQL 的写法是写在最后(select student_name, score from class_a order by score desc limit 10) union all (select student_name, score from class_b order by score desc limit 10);注意这里我用了括号。在 MySQL 中如果你想先对子查询分别取前 10 条再合并就需要加括号并各自写order by和limit否则order by和limit的作用范围就会变成整个合并后的结果集。这是一个非常高频的坑特别是在做分页报表、TopN 统计时不加括号和加括号结果完全不一样。group by也经常在合并场景里出现。如果你想对合并后的结果按某个字段分组统计同样要先把合并结果作为一个子查询再在外层做group by。举例select order_date, sum(amount) from ( select order_id, customer_id, amount, order_date from orders_2024 union all select order_id, customer_id, amount, order_date from orders_2025 ) t group by order_date;直接把group by写在某个子查询里通常不是你要的本意所以遇到这类需求时优先考虑使用子查询包一层。2.4 别名问题第一个查询决定了列名合并后的结果集列名默认使用第一个查询的列名这一点很多人容易忽略。看个例子select customer_id as cid from customers_2024 union all select customer_id from customers_2025;最后结果集返回的列名是cid不是customer_id。如果你在外部包装一层查询就会涉及字段别名的引用问题。比如select order_date from ( select order_date as dt from orders_2024 union all select order_date from orders_2025 ) x;外层select order_date会报错因为子查询返回的列名是dt。解决办法是统一使用第一个查询的别名或者干脆每一条子查询都使用相同别名这样最省心。3. 从执行计划看性能差异union 为什么比 union all 慢3.1 去重背后的代价到底有多大很多刚接触union的开发者不理解“不就是多去个重吗能慢到哪里去”实际上union的去重操作在底层远比想象中复杂。数据库为了去重通常需要对结果集进行排序sort或哈希hash操作以便检测重复行。这意味着它必须把参与合并的所有数据都放进内存或临时表里进行比较。用一个简单的对比来说明假如orders_2024有 100 万行orders_2025有 100 万行使用union all时数据库只需要把两段结果串联返回基本上没有额外计算而使用union时数据库需要把 200 万行全部放入一个临时结构对每一行的所有字段进行比较、去重最终再返回结果。这个过程的 CPU 消耗、内存消耗和临时 I/O 消耗都是不可忽视的。在真实业务里我曾经遇到过一个统计报表原来的 SQL 写了一大串union每次跑十几分钟。我逐个改成union all并在应用层或外层做去重后总耗时降到了 30 秒以内。这个差距在数据量大、字段多的场景下非常明显。3.2 什么场景该用 union all什么场景该用 union从实践角度讲我的判断标准很简单如果业务上能保证两个结果集在逻辑上不会有重复就用union all。如果业务上确实需要去重优先想办法用distinct或者group by在单个查询内解决。只有当你确实需要把多个select结果集合并并且要去掉跨查询之间的重复数据时才用union。举个例子你在做历史数据归档把 2024 年的订单和 2025 年的订单合并。由于业务上订单号全局唯一两个表里不可能存在相同的订单号那么最适合的就是union all因为去重不仅没有意义还会白白消耗性能。再比如你要统计两个渠道的活跃用户去重后的总人数两个渠道的user_id可能出现重叠这时用union是合理的。不过更高效的写法可能是先把两个渠道的user_id合并到一个临时表或子查询中再做一次select distinct在可读性和执行效率上通常更高。另外提醒一个很多人忽略的点union的去重是针对整行数据的。如果你的select列表里有id这种不可重复的字段那么其他字段即使重复也不会被去重这时用union就完全失去了意义。我看到过不少同学写了union但select的列里带了一个主键导致去重效果完全没生效SQL 却白白慢了一倍。3.3 大数据量下的优化思路如果你的合并查询数据量特别大比如千万级别有几个优化方向值得尝试第一尽量缩小每个子查询返回的字段数量。合并的列越多去重或排序的代价越高。把不需要参与逻辑的字段全部干掉能显著降低临时表和内存的压力。第二考虑用临时表分步执行。先分别查询两个结果集写入临时表然后在临时表上做去重或统计。分步执行的好处是更容易定位性能瓶颈而且在数据量大时可以配合索引来加速。第三善用分区和分桶。如果数据库底层是分布式架构比如大数据的某些引擎union all可能被优化成并行执行。而union的去重阶段往往是单点瓶颈要进行全局洗牌这时候成本会成倍增加。第四如果确定不需要去重千万不要用union。这句话我反复强调过因为在实际项目里我会见到有人单纯为了“更严谨”把所有的合并查询都写成union结果造成无谓的性能损耗。做技术选型最基本的逻辑是做“减法”去掉不需要的功能。4. 进阶玩法union 系列在复杂业务里的实际应用4.1 多表结构不同但需要合并统计在实际业务中两张表的字段往往不是完全一致的。比如一家公司有多条业务线每个业务线都有自己的订单表字段有细微差别。这时候你要汇总所有订单可以先在每条子查询里把字段统一补齐比如缺少的字段用常量填充select order_no, amount, line_a as biz_line from order_line_a union all select order_no, amount, line_b as biz_line from order_line_b union all select order_no, amount, line_c as biz_line from order_line_c;这样做的核心逻辑是“结构对齐”。在每条子查询里把需要的列都查出来不存在的列用null或固定值补位最后合并成一个包含biz_line字段的大表。这种做法在做数据中台、报表汇总的时候特别常见。需要注意字段的顺序也必须对齐。比如第一条查询的第三列是biz_line第二条查询的第三列也必须是业务线标识不能一会儿第三列是金额、一会儿第三列是业务线数据库可不会帮你猜意图。4.2 用 union 实现“补数”和行转列逻辑union系列还有一种高级用法在月份表或日期维度表中补全缺失记录。比如你有一个订单表统计每个月的销售额但某个月没有订单那么这个月在汇总结果里就不会出现。为了让报表连续可以在查询里先查一个月份维度表把每个月的日期都列出来再用union all补上原始统计缺失的月份最后用group by聚合。更常见的场景是行转列pivot。有些数据库支持内置的 pivot 语法但也有很多场景用union all实现。比如你有一张学生各科成绩表想把同一行的语文、数学、英语成绩转成多行一科的明细select student_id, chinese as subject, chinese_score as score from scores union all select student_id, math as subject, math_score as score from scores union all select student_id, english as subject, english_score as score from scores;这个写法本质上是在把横向的宽表拆解成纵向的长表配合其他工具做可视化分析非常方便。同样如果你想反过来把长表变成宽表也可以用union all配合group by和条件聚合实现不过那就不是union的主场了。4.3 在 ORM 与报表工具里合并查询的注意事项到了实际工程层面很多人不是在和原生 SQL 打交道而是在用各种 ORM 框架或者报表工具。比如 MyBatis、JPA、Hibernate以及大数据领域常见的 Flink SQL、Spark SQL它们对union系列支持情况有一定差异。以 MyBatis 为例union直接写在 XML 里的 SQL 语句中即可正常使用但要注意动态 SQL 拼接时各个子查询的列名和类型要保持一致。有些开发同学会在一个 if 判断里只拼其中一段子查询另一个 if 判断里拼其他子查询结果运行时生成的 SQL 列数不一致报错很隐蔽。在 Flink SQL 里处理数据流时union all很常用因为流式计算场景下多张表或数据流合并时你通常不希望去重而是保留所有记录继续往下游计算。不过需要注意Flink SQL 中union的去重逻辑和底层状态管理会带来额外开销如果对实时性要求高能不用union就不用。报表工具比如 FineReport、Crystal Reports 等使用union前一定要先确认工具的取数模式和引擎对集合操作符的支持情况。有些报表工具默认对结果集做分页处理如果你在 SQL 里写了union但工具本身对子查询做了包装最后生成的分页 SQL 可能会导致order by或limit作用域错误查询结果和预期不一致。5. 常见问题速查与排错实录5.1 “列数不匹配”和“类型不兼容”报错怎么排查这是使用union系列操作符最常见的报错。查询语句里提示“列数不匹配”第一件事就是把每个子查询的select列表都列出来对比数量是否一致。我习惯在写复杂合并前先用注释把每个子查询的字段清单备注在 SQL 旁边这样出了错一眼就能定位。如果是类型不兼容报错信息通常会指向某一列。比如 Oracle 报ORA-01790: expression must have same datatype as corresponding expression意思是某列的类型不一致。解决办法是使用cast或convert函数把列统一成相同类型select cast(id as varchar(50)) as id from table_a union all select id from table_b;这里要注意id在table_b里是不是已经是varchar类型如果不是就都需要做转换。5.2 合并后行数比预期少数据去哪了这种情况十有八九是用了union而且结果集中出现了大量重复行。去重逻辑导致重复记录被合并行数自然变少。如果你确定数据不能丢要么改用union all要么用一个可以区分每行来源的字段附加进去从而避免整行重复。举个例子你要合并两个系统里的会员列表两个表都有member_id但可能来自不同渠道这种情况直接用union会把重复会员合并成一行行数变少。如果业务上想知道每个会员在哪个系统出现过应该查询member_id的同时再加一个source_system字段这样每一行都不会完全重复union去重就不会生效或者你直接改用union all更有意义。5.3 order by 和 limit 不生效这是另一个高频踩坑点。很多同学写完这个 SQL 之后发现order by没有作用select name from table_a order by name union all select name from table_b;数据库大概率会报语法错误因为order by的位置不对。正确写法是把order by放到末尾select name from table_a union all select name from table_b order by name;如果是limit不生效最常见的场景是limit写在中间某条子查询里但没加括号导致它被解析成对整个结果集的限制。举例select id from table_a limit 10 union all select id from table_b;在 MySQL 中这条 SQL 的limit会作用于最后的结果集也就是说先合并再取前 10 条而不是分别取两个表的前 10 条再合并。如果你想要“分别取前 10 条”必须加括号写成两个带limit的子查询(select id from table_a order by id limit 10) union all (select id from table_b order by id limit 10);5.4 性能突然变差如何判断是不是 union 的锅当一条 SQL 性能变差可以先看执行计划。绝大多数数据库都会为union增加一个去重步骤比如 MySQL 的Using temporary; Using filesort或者 Oracle 执行计划里的SORT UNIQUE。看到这些关键词基本可以确认union去重是性能瓶颈之一。优化方向很简单用union all替代union如果必须去重尝试在外层使用count、distinct等方式替代把不需要的排序和临时表操作去掉在子查询里提前过滤掉无效数据减少合并时的数据量。6. 几个我踩过无数次的坑给后来人提个醒写到这里我想分享几个我在真实项目中踩过的坑希望能给你省点时间。第一个坑是无条件信任union的“去重”。我接手过一个历史数据迁移任务两个来源表里的记录主键一样但业务字段不一致。我以为用union去重后会保留一份完整的数据没想到它是对整行去重结果主键相同但其他字段不同的两行都被保留了下来。最终我只能改写逻辑先按主键分组再决定取哪条记录。所以当你说“去重”时一定要想清楚是按照哪几个字段去重union的去重粒度是整个结果行不是某一个字段。第二个坑是排序规则不一致导致的“假重复”。一次线上系统出现重复会员数据排查了很久最后发现一张表的字段是CHAR类型另一个是VARCHAR类型排序规则一个忽略了尾随空格一个没有。结果abc 和abc在某些环境里被判重在另一些环境里不判重两边行为不一致。修复方式是把字段统一成VARCHAR并统一排序规则问题才解决。第三个坑是最容易被忽视的union会掩盖类型转换异常。曾有一次我把一个数值字段和一个字符串字段用union合并数据库在非严格模式下直接做了隐式转换值看起来正常但排序、聚合时行为非常诡异。后来我把所有字段都显式cast结果稳定了下来。SQL 这种语言越是依赖隐式行为越容易留隐患。第四个坑是在分页查询里用union导致结果集错乱。如果你做的是一个列表页用union合并两个结果集再整体做分页那么当两个结果集之间存在“此消彼长”的数据变动时页面上很容易出现重复或遗漏。应对办法是尽量避免对动态变化的实时数据直接做union分页而是先把数据快照到临时表再做分页查询。7. 如果你还在纠结选 union 还是 union all我的建议最后回到最初的问题到底选union还是union all我给团队定的规矩很简单默认情况下能写union all就写union all只有当你已经清楚确认业务上需要跨结果集去重并且去重字段明确时才使用union。这不是说union不能碰而是说数据库的很多操作都是“零成本”的假象union的去重逻辑在数据量上来之后代价会呈指数级放大。而且从业务可维护性角度看union all的行为更直观它的结果集就是所有子查询的简单叠加任何人都能轻易理解而union因为涉及全局去重一旦数据质量出问题排查难度会大很多。从 SQL 优化的延伸视角来看理解集合操作的执行原理比背下一堆“语法规则”重要得多。当你明白union为什么慢、union all为什么快、去重和排序的代价来自哪里你在写合并查询时就能自然而然地做出合理判断而不需要靠猜。我个人在做代码评审时最在意的不是 SQL 写得有多炫酷而是它是否足够简单、是否容易排错、是否能支撑未来的数据规模增长。union all恰好是那种朴素但可靠的选择而union更像一把锋利但需要小心的刀用对了地方能帮你省事用错了地方就是在给自己挖坑。