资讯详情

SQL中UNION与UNION ALL的区别:去重逻辑、性能差异与实战写法

📅 2026/9/13 21:50:26 | 华诺云谱 👁 阅读
SQL中UNION与UNION ALL的区别:去重逻辑、性能差异与实战写法
总有人把 UNION 和 UNION ALL 当同一件事用直到线上数据多了一倍或者 SQL 跑了半天才回过头来查这个最基础的关键字。UNION 和 UNION ALL 在 SQL 里确实都用来合并多个查询结果集但两者的执行逻辑、结果形态、性能开销差得不是一星半点。这篇文章我想结合自己踩过的坑和使用经验把这两个关键字的差别说清楚并给出一套可以直接用的写法参考。适合刚接触 SQL 的新人也适合写过两三年 SQL 但没仔细抠过执行细节的人。1. union和union all核心差异速览1.1 作用合并多个查询结果集的基本规则UNION 和 UNION ALL 做的事情字面上都是“把多个 SELECT 查询的结果纵向拼在一起”。也就是说第一个查询出来的每一行后面接上第二个查询出来的每一行最终对外表现成一个结果集。我打个比方。你有两个筐一个里面装着红苹果一个里面装着青苹果现在要装进一个大筐。UNION ALL 的做法就是直接把两个筐里的苹果全部倒进去有多少倒多少UNION 的做法则是先把两个筐里的苹果倒出来然后仔细检查如果发现有一样的苹果只留一个剩下的扔掉。这个例子的关键在于“重复数据的处理”。UNION 自带去重逻辑UNION ALL 则原样保留。很多开发者在需要合并数据时随手写了 UNION结果导致严重的性能和结果偏差。而另一些人在本该去重的地方用了 UNION ALL又导致数据膨胀。1.2 核心区别是否去重直接看一个最简单的例子。有两张表-- 表 a select 1 as id union all select 1 as id union all select 2 as id; -- 表 b select 2 as id union all select 3 as id;如果执行select id from a union select id from b;返回结果是 1、2、3 三行因为重复的 1 和重复的 2 被合并了。如果执行select id from a union all select id from b;返回结果是 1、1、2、2、3 五行每个结果里的每一行都原样保留。注意UNION 的去重是“对所有查询结果集中出现的完整行”进行去重不是只对某个字段去重。比如一行是(1, 张三)另一行是(1, 李四)对 UNION 来说这两行并不重复因为整行的内容不完全相同。如果只希望针对 id 去重UNION 做不到需要另想办法后面我会讲。1.3 执行过程与资源消耗的底层逻辑要理解性能差异得先知道数据库内部怎么执行这两个操作。UNION ALL 的执行路径非常简单把两个结果集拿到后直接追加到一起返回给客户端。这个过程基本没有额外的过滤、排序和去重逻辑成本主要在两个查询本身的 IO 和网络传输上。UNION 则要复杂一点。数据库会先收集两个子查询的全部结果行放到临时结构里然后去重。去重一般有两种实现方式排序去重Sort Distinct或哈希去重Hash Distinct。排序去重会把所有行先按所有字段排序然后在相邻行之间比较把重复的剔除哈希去重则是用哈希表记录已经见过的行新行来了就查一下哈希表如果存在就丢弃。不管是哪种方式都会带来额外的 CPU 和内存开销。结果集越大去重的代价越明显。尤其是当两个查询分别来自大表并且大表没有用到索引去重时UNION 可能会创建临时表甚至在磁盘上做排序慢是必然的。所以核心区别不只是一个关键字而是整条 SQL 的执行计划都会变。理解了这一点后面很多优化思路就顺理成章了。2. 完整语法与常见用法拆解2.1 基本语法结构列数、列名、ORDER BY位置UNION 和 UNION ALL 的语法形式差不多select 列1, 列2, ... from 表A where 条件A union [all] select 列1, 列2, ... from 表B where 条件B有一个硬性规则两个 SELECT 查询出来的列数必须一致否则直接报错。至于列的数据类型严格来说应该保持一致或者兼容。比如第一个查询第一列是数字第二个查询同一位置是字符串部分数据库会自动转换但转换失败或结果不符合预期是很常见的。还有一个容易忽略的点UNION 结果集的列名默认以第一个 SELECT 语句的列名为准。比如select id as order_id, amount from order_2024_01 union select id, amount from order_2024_02最终结果集的列名是order_id和amount第二个查询里的列名不影响结果集本身的命名。ORDER BY 的位置也很容易踩坑。很多人喜欢在每个子查询里写 ORDER BY比如select id, amount from order_2024_01 order by amount desc union all select id, amount from order_2024_02 order by amount desc这段 SQL 的语义并不是“第一个查询按 amount 降序第二个也按 amount 降序然后整个结果集也按 amount 降序”。实际上UNION 是一个集合运算最终结果的排序必须在整个语句的最后写 ORDER BY 才有效select id, amount from order_2024_01 union all select id, amount from order_2024_02 order by amount desc那么在子查询里写 ORDER BY 到底有没有用在大多数数据库里如果子查询中写了 ORDER BY但没有 LIMIT 之类的限制优化器通常会忽略它因为没有意义。比如 MySQL 中UNION 的子查询里 ORDER BY 可以被解析但除非配合 LIMIT否则不会真正排序。很多人在调试时发现“排序没生效”原因就在这里。2.2 结合别名、表达式的实操写法实际开发里我们经常需要对不同来源的字段做对齐处理。比如一个查询来自用户表另一个来自订单表字段名不同但都代表时间就可以在 SELECT 时用别名统一select user_name as name, register_time as ts from users union all select contact_name as name, create_time as ts from crm_contacts这样 UNION 出来的结果集列名统一为name和ts。有时候某个结果集缺列可以补常量select user_name, phone, user as source from users union all select contact_name, NULL as phone, crm as source from crm_contacts用 NULL 补位是为了满足列数一致的要求。但在实际使用中要小心如果后续对结果集做 WHERE 过滤或 JOINNULL 值可能会导致行被过滤掉或者关联不上。需要时可以用空串、0 等替代关键看业务语义。表达式的使用也很常见。比如两个结果集都有日期字段一个存的是 DATE 类型一个是 DATETIME 类型可以在 SELECT 时统一格式select from_unixtime(create_ts) as event_date, ... from event_log union all select create_date, ... from backup_log这一步的核心思想是在合并前先把两个子查询的字段“洗干净”让它们变成同一套口径。UNION 本身不会帮你修复字段不一致的问题只会“尽力兼容”。2.3 多表合并的细节与列类型兼容问题当合并的表超过两张时可以连续写多个 UNIONselect id, name from t1 union select id, name from t2 union select id, name from t3注意这里的执行顺序是从左到右且每次 UNION 都会去重。如果你只需要 t1、t2、t3 三个结果集的并集去重这样写没问题。但如果中间混着 UNION ALL去重范围就会发生变化需要特别小心select id, name from t1 union all select id, name from t2 union select id, name from t3这段 SQL 的逻辑是先合并 t1 和 t2 的全部行得到临时结果集再把这个临时结果集和 t3 做 UNION 去重。也就是说最终 t1 和 t2 之间如果有重复并不会因为最后有一个 UNION 而消除因为去重是阶段性完成的。所以多表合并时一定要想清楚到底哪些部分允许重复哪些部分需要去重。如果整体都需要去重建议先写 UNION ALL 拼接全部数据然后在结果集外面包一层去重比如合并成一个子查询后再 SELECT DISTINCT或者直接全用 UNION。这是我在实际项目里经常处理的细节。列类型兼容的问题也值得单独说。比如第一个查询的某列是 VARCHAR(10)第二个查询的同一列是 VARCHAR(20)大多数数据库能正常合并但最终结果集的列长度可能取最大长度。如果一个小伙伴在 Oracle 里合并 VARCHAR2 和 CHAR又没注意类型转换可能出现尾部空格被填充或者查询报错。SQL Server 中两个字段一个varchar一个nvarchar排序规则不同也可能导致报错 “Cannot resolve the collation conflict”这时候需要用 COLLATE 统一排序规则。所以多表合并前先把字段类型的兼容性考虑进去特别是跨库、跨系统取数时宁可多加一层显式转换也不要让数据库自作主张。3. 性能对比为什么union all更快、union何时更安全3.1 sort去重流程讲解前面已经提到 UNION 会去重但为了让大家对性能差异有体感这里详细拆解一下排序去重的流程。假设你要对两个各有 100 万行的结果集做 UNION。数据库大概会这样执行分别执行两个 SELECT 子查询拿到两个结果集。把两个结果集写入一个临时表或者内存结构。按所有列的值做排序把相同的行排在一起。遍历排序后的结果将每行和前一行比较如果相同则丢弃。返回过滤后的行。这里面最耗时的步骤往往是第 3 步。100 万行加上 100 万行就是 200 万行参与排序如果结果集宽度很大比如每行十几个字段还有很长的字符串排序的数据量会非常惊人。系统内存不够时临时把结果写到磁盘上再对磁盘上的数据进行排序性能直接崩掉。UNION ALL 完全不碰第 24 步拿到两个结果集直接拼接返回。在很多数据库里UNION ALL 甚至可以做到边取边返回不需要等待所有结果集都算完。3.2 distinct语义与空值处理有人会问UNION 的去重和 SELECT DISTINCT 一样那对 NULL 怎么处理在标准 SQL 语义中NULL 被视为“未知值”但在去重时数据库中通常把两个 NULL 看作相等。也就是说如果一行里某个字段是 NULL另一行同字段也是 NULLUNION 去重时会认为这两行在该字段上相同。举个例子select null as a, 1 as b from t1 union select null as a, 1 as b from t2结果只有一行因为两个 NULL 被合并了。这一点容易让新人感到意外因为普通等值比较中NULL NULL是不成立的。但在去重、GROUP BY、ORDER BY 等场景里NULL 会被归为一组。所以如果用 UNION 做去重要清楚 NULL 会被合并。如果一个结果集中的所有列恰好都是 NULL两行(NULL, NULL)也会被认为重复只保留一行。这可能会让结果集行数少于预期。反过来UNION ALL 就不会有这种“意外”它如实保留所有行。3.3 大数据量下的性能实测与优化建议我以前在一次跑批任务里遇到过一个典型的慢 SQL两个月表各 800 万行需要合并后去重然后算汇总指标。一开始同事写的是select product_id, channel_id, count(1) as cnt from ( select product_id, channel_id from sales_202401 union select product_id, channel_id from sales_202402 ) t group by product_id, channel_id这个 SQL 在测试环境 10 分钟上生产直接跑了 40 多分钟最后超时。我分析了数据发现两个月的销售明细理论上不会存在重复记录因为每笔订单有唯一 id只不过我们只需要按产品渠道汇总。既然业务上能保证不重复直接用 UNION ALL 就能避免去重开销select product_id, channel_id, count(1) as cnt from ( select product_id, channel_id from sales_202401 union all select product_id, channel_id from sales_202402 ) t group by product_id, channel_id改成 UNION ALL 之后跑批时间降到 8 分钟。原因很简单少做了一次 1600 万行的排序去重省下来的时间非常可观。如果业务上确实存在重复需要去重我一般建议先考虑是否真的每条记录都一样。如果只是某个业务主键重复UNION 并不适合因为它按整行去重更合理的是 UNION ALL 后在外层按主键去重比如用 ROW_NUMBER() 或 GROUP BY。这样不仅去重逻辑更明确还可以用更高效的哈希聚合替代排序去重。此外还有一个更细的优化点如果两个子查询都使用了索引UNION ALL 可以直接利用索引做拼接而 UNION 去重时往往需要把数据搬进临时结构再处理索引可能帮不上忙。这也是为什么在“只合并不重复”的场景里强烈优先选 UNION ALL。4. 实战案例用union与union all解决真实业务需求4.1 场景一订单表按月分表后的全局查询很多项目为了减少单表数据量会把订单表按月拆成order_202401、order_202402、order_202403。当需要查询这几张表合并数据时UNION 和 UNION ALL 怎么选一般来说订单数据有全局唯一的订单号物理上分表只是为了存储业务上不会出现重复。此时应使用 UNION ALLselect order_no, user_id, amount, create_time from order_202401 union all select order_no, user_id, amount, create_time from order_202402 union all select order_no, user_id, amount, create_time from order_202403 order by create_time desc这个写法避免了 UNION 去重时的大排序而且由于订单号唯一结果也不会重复既快又准。但也有例外。比如同一个订单因为业务回滚可能同时出现在两张表里或者分表的边界条件有重叠比如 1 月 31 日 23:59:59 的数据写进了 2 月表此时需要用 UNION 去除重复。然而更严谨的做法是先在业务上明确分表键避免重叠而不是依靠 UNION 去兜底。我的经验是能用业务规则保证唯一就不要让 SQL 去重。4.2 场景二多来源数据合并生成报表报表开发中最常见的场景是从多个系统抽取同一类实体数据合在一起输出。比如销售端和售后端的客户名单合并select customer_id, customer_name, sales as dept from sales_customers union all select customer_id, customer_name, after_sale as dept from after_sale_customers这里用了 UNION ALL目的是保留来源标识dept让报表能看出这个客户是从哪个端来的。如果同一个客户在 sales 和 after_sale 都出现最终报表依然会有两行这通常是合理的因为一个客户既能购买也能报修。但如果报表要求“每个客户只出现一行”并且需要优先选择某个来源的信息就不能简单 UNION。这时要先用 UNION ALL 把全部数据拉出来再按 customer_id 排序并取第一条或者用聚合。比如在 SQL Server 里可以这样写select customer_id, customer_name, dept from ( select customer_id, customer_name, sales as dept, row_number() over(partition by customer_id order by create_time desc) as rn from ( select customer_id, customer_name, create_time from sales_customers union all select customer_id, customer_name, create_time from after_sale_customers ) t ) x where rn 1这种写法的好处是去重逻辑完全由自己控制想按哪个字段去重、想保留哪一行都一目了然而不是依赖 UNION 默认的整行去重。实际工作中这种“UNION ALL ROW_NUMBER”的组合非常常用。4.3 场景三利用union all实现分页数据源组合还有一种常见用法是把多个子查询拼在一起后再做分页。假设一个搜索页面要同时展示商品、文章、活动三类内容每类各取前 10 条合并后再分页展示。如果按普通思路先对每类查 10 条再 UNION然后再分页可行。但如果希望整体结果按发布时间排序并且分页能获取到全部记录需要这样写select * from ( select id, title, product as content_type, publish_time from product_list union all select id, title, article as content_type, publish_time from article_list union all select id, title, activity as content_type, publish_time from activity_list ) t order by publish_time desc limit 0, 20;内层用 UNION ALL 保留所有子查询的记录外层再做排序和分页这是最清晰的方式。有人可能会问为什么不直接在内层每个子查询先分页再 UNION ALL如果业务上只需要每类前 N 条合并后的整体顺序只要需求明确“每类各取前几条”那确实可以select id, title, product as content_type, publish_time from product_list order by publish_time desc limit 10 union all select id, title, article as content_type, publish_time from article_list order by publish_time desc limit 10 union all select id, title, activity as content_type, publish_time from activity_list order by publish_time desc limit 10这种写法在 MySQL 里是有效的因为子查询里 ORDER BY 配合 LIMIT 后排序结果会保留。注意如果你用 SQL Server需要使用 TOP 配合 ORDER BY用 PostgreSQL 则是limit或fetch first。不同数据库语法略有不同但思路一致。我的建议是但凡涉及分页和排序尽量把 UNION/UNION ALL 的结果作为子查询外部再做排序和 limit这样逻辑最稳不容易被数据库优化器“篡改”本意。5. 常见问题与排查技巧实录5.1 报错“列数不同”的解决最常见的错误是“using union queries with different number of columns”。比如select id, name, age from user_info union select id, name from user_addr第一个查询三列第二个查询两列数据库直接拒绝执行。解决方法是检查第二个查询把漏掉的列补上可以是实际字段、常量也可以是 NULL 补位。这里有一个实际踩过的坑当两表字段语义不一致时直接补 NULL 会导致后续业务逻辑错误。比如第一个查询的age是年龄第二个查询同一位置补NULL看上去列数一致了但如果后续报表程序默认按列位置取值就会把 NULL 当年龄展示成空。更稳妥的做法是给补位字段起一个明确的别名并且在业务层做一个标识比如select id, name, age, info as source from user_info union all select id, name, NULL as age, addr as source from user_addr这样至少能区分来源避免把 NULL 当成真实数据。5.2 列名不一致导致结果集字段错位UNION 结果的列名默认取第一个查询的列名但这不代表第二个查询的字段顺序不重要。字段顺序完全按照 SELECT 中出现的位置一一对应而不是按字段名匹配。我曾经见过有人这样写select user_id, user_name from table_a union select real_name, user_id from table_b本意是user_name对应real_name但第二个查询的第一列是real_name第二列是user_id所以最终结果集第一列其实是user_id来自第一个查询列名但它包含了第二个查询的real_name值。结果就是数据错位查询结果看起来很奇怪。正确做法是统一字段顺序和语义select user_id, user_name from table_a union select user_id, real_name as user_name from table_b所以写 UNION 之前一定把两个 SELECT 列表从语义上一一对应好别看名字像位置对不上一样出错。5.3 排序失效子查询里的ORDER BY只影响当前段排序失效是我被问得最多的问题。比如select id, amount from order_202401 order by amount desc union all select id, amount from order_202402 order by amount desc执行后发现整个结果集没有按 amount 降序有时第一个段似乎排了第二个段又乱掉。原因就是前面提到的UNION 作为一个整体它的最终结果不会保障某个子查询的排序除非子查询里有 LIMIT/TOP 等限制。在 MySQL 中如果子查询里写ORDER BY ... LIMIT这个排序结果会保留如果没有 LIMIT优化器通常会忽略子查询里的 ORDER BY。在 SQL Server 中子查询里的 ORDER BY 如果不用 TOP会直接报错或无效需要写成select top (10) ... order by ...才是合法且有意义的。一个最稳妥的方式永远在外层写排序不要寄希望于内层排序的“穿透效果”。select id, amount from order_202401 union all select id, amount from order_202402 order by amount desc5.4 慢sql与union all可替代性分析实际排查慢 SQL 时如果发现某个 UNION 语句非常慢我一般会按以下顺序定位先确认是否真的需要去重。如果不需要直接把 UNION 改成 UNION ALL这是最快优化。如果需要去重但只是针对某个业务主键考虑 UNION ALL 后在外层用 GROUP BY 或 ROW_NUMBER。如果必须 UNION 去重看执行计划里是否出现了SORT、DISTINCT、TEMP等标识确认排序消耗。检查两个子查询是否各自能够利用索引尽量避免“两张大表全表扫描再合并”。如果结果集很大还需要考虑临时表空间是否不足导致结果落盘。另外UNION ALL 的结果集不会去重因此同一段数据如果因为条件重叠被查了两次最终行数会翻倍。很多人用 UNION ALL 后看到行数变多第一反应是惊讶但这恰恰说明需要仔细核对业务条件而不是简单改回 UNION 了事。比如你要查两个时间段的数据但第一个时间段用了 2024-01-01 and 2024-03-31第二个时间段用了 2024-03-01 and 2024-05-313 月的数据会被查两遍。这是业务条件重叠导致的问题不是 UNION ALL 的锅。正确的做法是调整条件或者在外层按唯一键去重。6. 经验心得与避坑建议6.1 什么时候用union什么时候优先用union all我个人的判断标准很简单当两个结果集业务上必然没有重叠或者重复无所谓时一律用 UNION ALL。当业务上需要“过滤掉完全一样的行”时才用 UNION。当需要按某个业务键去重而不是整行去重时不直接用 UNION而是 UNION ALL 后显式处理。例如用户从两个渠道注册渠道可能重叠但注册记录里还有渠道标识即使同一个用户如果渠道不同也不算重复。此时用 UNION 会把“同一用户不同渠道”的重复行误删反而不对。所以别把 UNION 当成万能去重工具它去重的维度是“整行”。6.2 关于union all配合group by的优化思路如果你想对多个结果集做去重并统计我一般不建议用 UNION 直接去重再聚合。比如select channel, count(*) from ( select channel, user_id from t1 union select channel, user_id from t2 ) t group by channel这个 SQL 内部会先对(channel, user_id)做一次 UNION 去重最后还要聚合。其实性能可以优化为select channel, count(*) from ( select channel, user_id from t1 union all select channel, user_id from t2 ) t group by channel, user_id如果你只需要按 channel 统计不关心 user_id那么可以这样写select channel, count(distinct user_id) from ( select channel, user_id from t1 union all select channel, user_id from t2 ) t group by channel这里把去重和聚合放到一起让优化器有更多选择往往比先在子查询里 UNION 去重再聚合更快。当然不同数据库对 COUNT(DISTINCT) 的优化能力不同但我在 MySQL 和 SQL Server 实测中这种写法通常不差。6.3 与flinksql等其他场景的关联思考这几年我在实时计算场景里也常看到 UNION 的用法。Flink SQL 同样支持 UNION 和 UNION ALL语义和离线 SQL 基本一致UNION 会去重UNION ALL 不去重。但在流式计算中UNION 的去重往往依赖状态存储如果流数据量非常大状态膨胀会成为一个新的瓶颈。所以在实时场景里除非业务明确要求全局去重否则我建议优先用 UNION ALL把去重逻辑交给下游或者基于 watermark/主键等语义处理。这也再次说明理解 UNION 和 UNION ALL 的差别不只是写对一条 SQL而是能让你在技术和架构层面做出更合理的选择。我自己写 SQL 的习惯是能不用 UNION 就不用能明确业务重复性就写清楚注释绝不把一个基础关键字当成黑盒。最后再分享一个小技巧——每当你准备在两个查询之间写 UNION 时先停下来问自己一句“这两个查询的结果里会不会出现完全相同的两行如果会我应该怎么去重”想清楚这个问题你的 SQL 会少踩很多坑。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。