
1. 两者到底哪里不一样先别急着背结论很多朋友一开始接触SQL见到UNION和UNION ALL第一反应是这俩不就是把两个查询结果拼在一起吗。这句话不算错但离真正的理解还差着十万八千里。要是你真拿它俩当同一种东西来用迟早会在数据量上来之后被莫名其妙的性能问题折磨到怀疑人生。UNION和UNION ALL在SQL里都属于集合操作家族说人话就是把多个SELECT语句的结果合并成一个结果集返回。但它俩在合并时的处理逻辑有本质差异UNION会对合并后的结果做去重UNION ALL则是完完全全地把所有行堆叠在一起一个字都不多说。这个去重两个字听起来简单背后带来的执行成本却是天壤之别。你可以在脑子里想一下这个场景左手拿一张A4纸上面写了20个名字右手拿另一张A4纸也写了20个名字。UNION ALL的做法是把两张纸上的名字全部照抄到第三张纸上一共40个哪怕有重复的也照写。而UNION的做法是先把两张纸上的名字合并抄下来然后拿起另一张空白纸挨个比对重复的只保留一个最终可能只留下35个甚至更少。这个挨个比对的过程在数据库里的实现方式通常就是排序或哈希去重。数据量小的时候几十行几百行你根本感觉不到差异查询都是毫秒级完成。但一旦数据量冲到几十万、上百万行UNION那个去重动作可能会让查询时间从几毫秒飙到几秒甚至更久。UNION ALL因为不做去重往往能在同样的数据量下保持相对稳定的性能。所以我说句实在话在实际的SQL开发里UNION ALL的使用频率远高于UNION。很多资深开发甚至默认只用UNION ALL除非业务上有非常明确、非做不可的去重需求。这背后的逻辑很简单——数据库每多做一件额外的工作都是在消耗你的查询性能和时间成本。2. UNION和UNION ALL的执行原理为什么UNION慢要真正理解两者区别不能只看它们做了什么还得看数据库引擎怎么做。在绝大多数主流数据库里比如MySQL、PostgreSQL、SQL Server、Oracle执行一条UNION查询的时候数据库内部大致经历了这么几个步骤分别执行UNION两边的SELECT语句各自产生一个临时结果集。把两个结果集合并到一个临时表中。为了去重数据库会对合并后的结果集进行一次排序操作或者构建一个哈希表来检测重复行。把去重后的结果返回给客户端。关键在于第3步。排序操作的时间复杂度大约是O(n log n)哈希去重的空间复杂度也不低。无论是哪种方式都意味着数据库需要额外的CPU计算和内存空间。如果两个结果集各自有10万行合并后是20万行那这20万行里的每一行都要参与去重比对代价可想而知。而UNION ALL的执行路径就简单粗暴得多分别执行两边的SELECT语句。把两个结果集直接首尾相接拼成一个大结果集返回。没有排序没有去重甚至连额外的比对都不需要。数据库只需要把第二个结果集的行追加到第一个结果集后面完事。这也是为什么UNION ALL能比UNION快出一大截的根本原因。给你一个非常直观的生活类比UNION ALL就像是把两条队伍的名单直接拼接在一起人来了就往后站UNION则像是要先让这两队人挨个报名字发现同名同姓的两个人只留一个剩下的人再排成一队。报名字的这个动作人数越多越耗时间。还有个容易被忽略的细节UNION的去重是全字段去重不是只针对某一个字段。也就是说两行数据必须所有列的值都完全相同才会被判定为重复并合并成一行。只要有一列的值不同这两行就都会被保留下来。这一点在业务里非常容易踩坑后面我详细说。3. 什么场景该用UNION什么场景该用UNION ALL光懂原理还不够最关键的还是拿到实际业务里知道怎么选。我根据自己的工作经验总结了几类典型场景你可以直接对号入座。第一个场景两表存在重复数据且业务上确实需要去重后的结果。举例子来说假设你有一张线上订单表和一个线下订单表现在要做一份全渠道订单报表。张三在线上买了一单又在线下买了一单如果目标是统计总共多少个客户下了单那同一个客户只能算一次。这种场景用UNION就很合适数据库自动帮你把重复客户剔除掉。再举个例子从一个用户表里查注册邮箱非空的用户再从另一个临时表里查最近活跃的用户两个结果可能有交集如果报表要的是不重复的活跃用户列表UNION是首选。第二个场景数据分片或者分表存储需要合并全量数据。比如老系统做了按地区分表华北订单表、华东订单表数据之间不可能有交集这时候用UNION ALL就完全没毛病。因为没有重叠去重是多余的只会白白消耗性能。第三个场景日志类数据合并。日志系统最常见的手法就是按月分表比如log_202501、log_202502现在要查询一个季度的日志做分析。日志本身就可能存在同一台机器同一秒内重复记录的情况但分析场景通常不关心是否重复——因为每一条原始日志都代表一次真实事件。你如果用UNION反而会把真实记录给吃掉一部分导致统计失真。这种场景必须用UNION ALL。第四个场景目标表本来就没有唯一约束、不可能产生重复比如按天导出的快照表。很多数据仓库的做法是每天把业务表做一次全量快照存储快照之间天然不重复合并多天快照时直接UNION ALL堆上去就行。这既保留数据完整性性能也最好。说句大实话我在实际项目里见过的最典型的翻车案例就是有人图省事不管三七二十一所有合并查询一律写UNION。结果数据量一大每天晚上跑报表任务别的任务十几分钟就结束了他那个任务要跑一两个小时。最后定位到问题之后改成UNION ALL跑完只要三分钟。就这么简单一个改动效率提升了几十倍。所以说选择的关键就看一条业务逻辑上两个结果集是否允许存在重复行。允许重复就UNION ALL不允许就UNION。如果拿不准优先考虑UNION ALL因为它更安全——它保证了每一行原始数据的完整性去重这件事可以在明确的业务层再做。4. 性能对比与优化策略数据量大的时候差距有多夸张我知道光说UNION比UNION ALL慢你可能感触不深。来点实际的我基于自己做过的一次简单测试给你列一组数据感受一下。测试环境是一台普通的开发服务器上跑的MySQL 8.0两张表每张大约50万行数据字段包括一个自增ID和一个文本字段。测试结果大概如下操作表A行数表B行数耗时约UNION ALL50万50万300msUNION50万50万2.8s看到没有UNION直接比UNION ALL慢了接近一个数量级。而且这只是两张50万行的表如果你的表是上千万行这个差距会进一步拉大。关键是UNION的慢是意料之中的因为排序去重的成本就在那里。那有没有什么优化手段能让UNION在必须去重的场景下跑得更快呢有我的经验是以下几个方向。第一尽量缩小结果集。UNION两边的SELECT条件能限多窄就限多窄把没必要参与合并的数据全部过滤掉。去重数据少了排序和哈希的成本自然就降下来了。第二利用索引优化排序。如果UNION去重时会做排序而排序字段恰好是索引字段数据库可能直接利用索引的有序性跳过排序步骤。实际效果取决于数据库优化器的执行计划但给WHERE条件和排序相关的列建立合适的索引通常没坏处。第三如果业务允许可以改成UNION ALL 分组去重。这个思路很巧妙UNION ALL只负责堆数据然后外层套一个GROUP BY或DISTINCT做去重。在某些数据库上这种方式比直接UNION更可控执行计划更容易优化。比如SELECT id, name FROM ( SELECT id, name FROM table_a UNION ALL SELECT id, name FROM table_b ) t GROUP BY id, name;这个写法把先合再去重的逻辑拆成了两个明确步骤数据库在执行GROUP BY时可以选择更适合的算法。我在几个项目里实测过有些场景下这个写法甚至比直接UNION还快一些。当然这不是绝对标准答案不同数据库、不同数据分布下结果可能不一样你可以在自己的环境里做个对比测试。第四减少UNION使用时的开销可以尝试用临时表替代。先UNION ALL查入临时表再对临时表做DISTINCT或者加唯一索引去重然后再查临时表。这个方案多了一步但分区明确之后往往能通过调整临时表的引擎或索引获得额外收益。另外一个优化相关的重点就是UNION和索引的关系。很多开发有个误解以为UNION两边如果都走索引整体就快。实际上排序去重那一步消耗的往往比索引扫描还高。所以看执行计划不要只盯SELECT部分要去看看有没有Using temporary和Using filesort这两个关键字。一旦看到就基本可以确定UNION的排序步骤没有用好索引这时候调整查询或者索引的策略就有针对性了。5. JOIN也可以合并数据UNION和JOIN到底怎么选说到合并就不得不提另一个容易搞混的兄弟——JOIN。很多入门不久的朋友看到多表查询就纠结这俩都能把表连起来到底该用哪个一句话帮你理清楚UNION是纵向合并JOIN是横向拼接。UNION是把两个结果集的行数相加列数保持不变。JOIN是按照关联关系把两张表的列进行拼接行数不直接相加。举个具体例子订单表和用户表用JOIN可以查出每个订单对应的用户名结果是订单的所有字段 用户的所有字段并排在一起。而UNION则是把两张结构相同的表的行堆到一起是往下加人而不是往右加列。在实际业务里两者的应用场景差异非常大。JOIN用来做关联查询UNION用来做结果合并。JOIN能查出更丰富的横向字段组合UNION则适合把同类数据从多个来源合并统计。如果在业务里需要把两段不同结构的查询结果合并还想着用JOIN去硬凑那大概率会造出很多冗余空列查询性能还有隐患。用一句话概括就是JOIN是工厂流水线把不同零件组装成一件产品UNION是收纳箱把同类的物件归拢在一起。弄混了表达出的业务逻辑就完全变味了。6. 使用UNION时不得不防的语法陷阱UNION虽然好用但在实际开发中有几个非常经典的语法陷阱几乎每个SQL开发者都踩过。我挨个跟你说。第一个陷阱两边SELECT的列数和顺序必须完全一致。这是个硬性规定。UNION要求两边结果集的列数一样而且对应位置的列会自动按位置匹配而不是按列名匹配。如果一边查了三列另一边查了两列数据库直接报错。这算是最容易排查的错误报错信息也很明确往往一看就知道。但真正的坑在下面这个如果两边列数一致但列顺序反了UNION不会报错数据却会悄悄错位。比如左表是(id, name)右表是(name, id)UNION之后的结果集第一列可能一会儿是ID一会儿是名字数据直接混乱。如果你做过跨部门的数据汇总这种两边表结构差不多但字段顺序不一致的情况真的是防不胜防。所以写UNION之前务必确认两侧SELECT的列顺序完全对齐。第二个陷阱数据类型必须兼容。MySQL这类数据库对UNION两侧对应列的数据类型要求是能隐式转换兼容。比如一边是INT一边是VARCHAR数据库可能能自动转换但转换规则有时候会超出你的预期。更危险的是如果一边是整数一边是字符串而且你想让它按整数排序它可能先把字符串转换成数字再排序稍微不注意结果就会不对。更严谨的数据库如Oracle对类型匹配会更严格报错也更直接。第三个陷阱UNION的ORDER BY到底作用于谁。这是个非常经典的操作误区。很多人想对UNION的结果排序随手写成SELECT id FROM table_a ORDER BY id UNION SELECT id FROM table_b;这种写法在很多数据库里直接报语法错误或者ORDER BY只对第一个SELECT生效完全不是预期的效果。正确做法是把ORDER BY放到最后一整个UNION语句的末尾SELECT id FROM table_a UNION SELECT id FROM table_b ORDER BY id;这样才能确保对合并后的完整结果集排序。同理LIMIT子句如果只写在第一段SELECT后面也只会对那一部分生效不会限制最终合并结果。如果想取整个合并结果的前N行必须要把LIMIT放到整个UNION语句的末尾。第四个陷阱如果两边SELECT都包含重复行UNION的语义是最外层的去重。可能有人会有这样的经验两边各自对重复数据做了去重结果UNION之后却出现了重复行等等UNION不是能去重吗这话听起来矛盾其实是因为UNION去重的对象是合并后的完整结果集只有当所有列的值都一样时才算重复。你两边各自查出了相同的一行但这行在两个子查询里出现的位置、关联的其他列不同那就去不掉。这一点在复杂查询里特别容易发生尤其是两边SELECT还带着不同计算字段的时候。第五个陷阱用括号控制UNION的优先级。当UNION和ORDER BY、LIMIT、括号混在一起时执行顺序非常容易弄错。比如你想从两个子查询各取前10条再做合并正确的写法是用括号包住每个子查询(SELECT id FROM table_a ORDER BY create_time DESC LIMIT 10) UNION ALL (SELECT id FROM table_b ORDER BY create_time DESC LIMIT 10);如果你把LIMIT写在括号外面那语义就变成了先把两边全部结果合在一起再取前10条完全不是你本来的意图。这两个写法的结果可能天差地别。7. 慢SQL排查实录因为UNION引发的连锁反应我遇到过最典型的一个生产事故就是一个不起眼的UNION导致的慢SQL。当时是线上一个报表系统每天凌晨跑一次数据统计任务。最初版本的数据量不大一切安好。后来业务扩张表里数据从几十万涨到几千万某个报表任务就突然开始频繁超时有时候凌晨2点开跑到早上7点还没跑完严重影响一天的数据产出。我接手排查第一步是看慢查询日志定位到SQL。那段SQL长这样SELECT user_id, order_amount FROM order_online WHERE order_date 2026-01-01 UNION SELECT user_id, order_amount FROM order_offline WHERE order_date 2026-01-01 ORDER BY user_id;表结构本身没毛病索引也有单看每个SELECT的执行计划都挺快。问题就出在UNION上。两边各自查出几十万行合并后要全部排序去重而ORDER BY user_id又额外增加了一次排序。在千万级数据量下这就是灾难级的执行成本。当时我的处理方案分三步走。第一步先确认业务对去重的需求。和业务方一聊发现这两个表属于线上和线下两个渠道一个用户完全可能在两边都有订单但统计总订单量时两条订单本身是有独立意义的并不需要给用户去重。目标只是合并两个渠道的订单数据因此完全不需要UNION的去重能力。第二步直接把UNION改成UNION ALL顺带把ORDER BY也去掉。因为最终报表会落到一张临时表后面的统计再统一排序。这一改任务时间直接缩短了70%以上再也不用加班等报表了。第三步作为留痕在SQL里加了一行注释说明书写的去重逻辑和排序逻辑放在了哪个环节防止后面接手的人又好心把UNION ALL改回UNION。这次排查给我最大的一个启示是多数UNION导致的慢SQL根子不在UNION本身而是因为业务根本不需要去重开发者却套了一个多余的去重动作。排查的时候第一步不是去优化SQL而是去核对业务语义、确认是否需要去重这个前提。这个前提弄清楚了优化往往就是一行代码的事。还有一个排查技巧也很有用就是看执行计划里输出行数和实际返回行数。如果一个SQL用UNION两边输出都是100万行合并后实际返回也是接近100万行说明根本没有重复数据去重纯属白干。这时候改成UNION ALL是零风险优化。8. 彩蛋UNION和参数校验、防注入的强关联看到这里不知道你注意到没有这篇文章的热词列表里其实还藏着一个凑热闹的家伙——SQL注入。乍一看SQL注入和UNION好像没啥关系但仔细一想你会发现UNION恰恰是SQL注入攻击里最经典的一个利用姿势。很多攻击者会用UNION来拼接自己的查询语句比如在一个登录表单里构造参数把一个正常查询后面接上UNION SELECT强行获取系统表的内容。其根本原理就是利用SQL拼接时对输入校验不严格在原有语句后追加了一段UNION子句。所以这其实也是UNION在实际开发中不得不防的一个负面作用。以防注入的角度来说写UNION相关查询时有一点特别容易被忽略参数校验不能只防拼接还要防类型混淆。比如某个ID字段前端传进来的是字符串你直接用引号拼接进UNION的SELECT里一旦字符串里带上引号和SQL关键字就极有可能被注入。因此无论你是不是用UNION养成参数化查询的习惯都至关重要。尤其UNION这种可以把多个查询结果合并的特性更是注入者最爱的攻击面。我个人的建议是在涉及UNION的SQL中所有外部传入的参数一律走预编译绑定不要做任何字符串拼接。同时对SELECT出来的列做显式类型转换比如用CAST确保数值字段是真整数别让数据库自动判断类型。这些都做对了SQL注入就算真来了也没多少可乘之机。聊聊我自己的体会吧。做SQL开发这么久我越发觉得像UNION和UNION ALL这种看起来最基础的知识点反而是最能拉开开发者水平差距的地方。那些背着UNION慢但能去重UNION ALL快但不去重的口诀就敢上线的同学和有意识地根据业务语义、数据体量、执行计划来做选择的同学最终交付的系统性能完全不在一个量级。最后再分享一个我每次写合并查询前都会过一遍的三连问第一这两边的数据真的需要去重吗第二两边列的顺序和类型真的对齐了吗第三当前的合并结果是要继续加工还是直接输出三个问题想清楚了UNION还是UNION ALL的选择基本就不会出大岔子。希望这篇总结能帮你少走点弯路下次再遇到这个知识点咱们直接动手不用再翻文档了。