新闻详情

新闻详情

首页 / 资讯中心 / 详情

UNION与UNION ALL核心区别:去重原理、性能对比与SQL优化实战

发布时间:2026/10/2 3:40:59来源:尧图网络
UNION与UNION ALL核心区别:去重原理、性能对比与SQL优化实战
先放结论UNION 和 UNION ALL 都是把多条 SELECT 语句的结果纵向拼接区别就一句话——UNION 会去掉重复行UNION ALL 全部保留。但这句轻描淡写的话背后藏着排序、哈希、临时表、索引失效、查询计划选择等一系列问题。我在实际项目里见过不少人因为随手写了个 UNION结果分页查询从 50ms 变成 3s也见过有人无脑用 UNION ALL 导致报表数据翻倍最后对不上账。这篇就把两者的核心区别、底层逻辑、使用规则和优化思路一次说透数据库新手和写过几年 SQL 的人都值得花十分钟看完。1. 认识 UNION 和 UNION ALL语法几乎一样做的事完全不同1.1 什么是数据合并操作在说 UNION 之前先明确一个概念SQL 的集合操作Set Operation包括 UNION、INTERSECT、EXCEPT 这三种其中 UNION 家族是日常开发中使用频率最高的。它解决的核心问题是把多个查询结果纵向叠在一起。注意是纵向不是横向。横向拼接是 JOIN 做的事UNION 不管列变多只管行变多。举个最直观的例子。假设订单系统做了分表order_2023和order_2024各存一年的数据结构完全一致。现在老板要拉一份近两年的订单全量清单最自然的写法就是SELECT order_id, user_id, amount, create_time FROM order_2023 UNION ALL SELECT order_id, user_id, amount, create_time FROM order_2024;这就是合并操作。两份互不相干的数据集通过 UNION ALL 拼成一个完整结果集。类似的场景还有把多个子系统的用户表汇总、把线上和线下渠道的订单合并、把日志表按月拆分的分区做全量查询。UNION 能解决的问题UNION ALL 都能解决。两者的语法结构完全一致都是用SELECT ... UNION [ALL] SELECT ...这种形式。真正的分水岭在于结果集的处理方式而这恰恰是最容易被忽视的。1.2 两者的核心语法与初印象光看语法UNION 和 UNION ALL 的区别只在有没有 ALL 这三个字母-- 形式一UNION默认去重 SELECT column1, column2 FROM table_a UNION SELECT column1, column2 FROM table_b; -- 形式二UNION ALL全部保留 SELECT column1, column2 FROM table_a UNION ALL SELECT column1, column2 FROM table_b;我刚开始带新人时经常问一个问题如果两个结果集本来就没有重复行用 UNION 和 UNION ALL 的结果是不是一样的大多数人的回答是一样。结果一样没错但中间过程完全不一样。数据库引擎处理 UNION 时默认会给最终结果做一次 DISTINCT 去重这意味着它必须把数据集完整地物化到临时表里然后进行排序或者哈希去重。而 UNION ALL 不需要这个步骤它只是把两个结果集首尾相连直接抛给上层。打个比方。UNION 像是把两堆货搬进仓库还要按货号核对一遍把编号重复的挑出来只留一件UNION ALL 则是直接把两堆货倒进同一个仓库有多少算多少一件不差。前面那个场景如果两堆货根本没有重复那核对就纯粹是白干活白白消耗搬运时间。这就是性能差异的根源。2. 核心区别去重逻辑、性能差异与选择依据2.1 去重背后的排序与哈希原理先深入说去重。很多资料只说UNION 会去重但没解释怎么去重。数据库实现去重的方式主要有两种排序去重和哈希去重具体用哪种取决于优化器的选择。排序去重的逻辑是先把所有结果行的每一列拼成一个整体然后按这个整体进行排序排完序之后相邻的相同行就暴露了只保留每组的第一行。这个过程不仅消耗 CPU 做排序还需要临时空间存放排序结果。如果数据量大到内存放不下还会触发磁盘临时表那就是灾难级的慢。哈希去重的逻辑是遍历每一行对整行数据计算哈希值把哈希值放进内存中的哈希表。遇到同哈希值再检查是否真的逐列相等相等就丢弃。这个方案在数据量大时通常比排序更快但依然需要额外的内存开销。不管是哪种方式UNION 都逃不掉比较每一行是否重复这个动作。这个动作的时间复杂度大约在 O(n log n) 到 O(n) 之间而 UNION ALL 的时间复杂度是 O(n)——只做拼接不做比较。当单个查询的结果集有几十万上百万行时这个差距会非常直观。2.2 性能对比为什么去掉 ALL 反而更慢我把同一个业务场景分别用 UNION 和 UNION ALL 跑过对比测试。环境是 MySQL 8.0两张表各有 50 万行数据结构相同其中约 20% 的数据在两张表中都存在理论上 UNION 去重能去掉约 10 万行。第一次跑 UNION耗时 2.4 秒。第二次跑 UNION ALL耗时 0.3 秒。相差 8 倍。如果你觉得这个差距还不够扎心再看一个更常见的案例带 ORDER BY 和 LIMIT 的分页查询。-- 慢 SELECT * FROM ( SELECT id, name FROM table_a UNION SELECT id, name FROM table_b ) t ORDER BY id DESC LIMIT 20;这个写法意味着 UNION 必须先把两张表的行全量合并、全量去重生成一个完整的中间结果才能做排序和分页。即使你只需要前 20 条数据库也得把 100 万行全部处理完。而使用 UNION ALL-- 快 SELECT * FROM ( SELECT id, name FROM table_a UNION ALL SELECT id, name FROM table_b ) t ORDER BY id DESC LIMIT 20;当然如果两张表之间存在大量重复数据UNION ALL 的结果集会多出重复行分页可能出现同样的记录出现在不同页码的情况。但性能差异摆在那里你需要根据业务容忍度做取舍。提示UNION 的性能差不是绝对的差。如果业务必须去重那么用 UNION 是合理的去重本身有成本但这是必须付出的代价。怕的是不需要去重却用了 UNION这才是纯浪费。2.3 什么时候选 UNION什么时候选 UNION ALL我自己的判断标准很朴素按优先级排序第一业务上是否需要去重。如果两个数据集逻辑上不可能重复比如一个是当天新增订单一个是当天取消订单这两个集合天然互斥直接用 UNION ALL 没有悬念。如果两个数据集可能重复但重复不影响统计结果比如各渠道曝光量求和重复的曝光在业务上允许也优先 UNION ALL并在外层加一个精确的去重逻辑如 COUNT(DISTINCT 主键)。第二数据量大小。数据量小比如千行以内UNION 和 UNION ALL 的差异基本可以忽略选哪个都行哪怕用 UNION 也没关系可读性甚至更好——它表达了我要一份不重复的汇总的语义。数据量到十万行以上强烈建议先确认业务能否接受重复行能接受就无脑 UNION ALL。第三排序和分页的场景。如果外层有 ORDER BY LIMIT且内层不需要去重一定用 UNION ALL。这是我在生产环境踩过坑总结出来的后面专门说。第四ETL 和报表任务。这类任务通常要求数据准确无重复宁可花时间也要保证结果干净。这时候优先 UNION但可以换个思路在合并前先对每张表去重再用 UNION ALL 拼接有时候比直接 UNION 更快。因为 UNION 的去重是全量行之间的两两比较而先在各表内部去重能减少参与比较的行数。3. 使用规则与常见踩坑不是简单把两张表拼起来3.1 列数和数据类型的匹配要求UNION 家族的操作要求所有 SELECT 子句返回的列数必须一致这一点是硬性规定写错了直接报错。比如第一个 SELECT 返回 3 列第二个返回 2 列数据库会提示列数量不匹配。列数相同之后数据类型不要求完全一致但必须兼容。MySQL 相对宽松很多类型会自动隐式转换SQL Server 和 PostgreSQL 更严格INT 和 VARCHAR 放在一起可能直接报错或者按隐式转换规则把一边转成另一边。这里有个非常常见的坑排序字段的类型。假设第一个查询的排序列是 INT第二个是 VARCHAR合并后数据库会统一类型但具体统一成哪个类型各数据库的规则不完全一样。你最好不要依赖这种隐式转换而是在写 SELECT 时就显式 CAST明确告诉数据库你要什么类型。另外列名以第一个 SELECT 子句定义的别名为准。也就是说第二个 SELECT 的列名写法随便起都行最终结果集使用的列名来自第一段。我见过有人在第二个查询里写上很清晰的别名想着结果集显示那个名字结果发现根本没生效疑惑半天。SELECT id AS order_id, amount FROM order_2023 UNION ALL SELECT id AS user_id, total_price FROM order_2024;最终结果集的列名是order_id和amount不是user_id和total_price。这不算 bug而是 SQL 标准行为但刚接触的人容易记混。3.2 ORDER BY 与 LIMIT 的正确用法如果你需要对整个 UNION 结果排序ORDER BY 必须放在最后一个 SELECT 语句之后且不能带表名限定。这是另一个高频报错点。-- 正确 SELECT name FROM table_a UNION ALL SELECT name FROM table_b ORDER BY name DESC; -- 错误第一个 SELECT 里带 ORDER BY SELECT name FROM table_a ORDER BY name UNION ALL SELECT name FROM table_b;为什么第一个写法不对因为 UNION 的结果集已经是一个整体逻辑集合对其中某一个子查询单独排序没有意义。绝大部分数据库对子查询内 ORDER BY UNION要么直接报错要么忽略排序。还有一点如果子查询里有 LIMIT那么 LIMIT 必须在合并之前就生效这时候你需要把带 LIMIT 的查询包成子查询再用 UNION 拼外层SELECT * FROM ( SELECT id, name FROM table_a ORDER BY create_time DESC LIMIT 100 ) a UNION ALL SELECT * FROM ( SELECT id, name FROM table_b ORDER BY create_time DESC LIMIT 100 ) b;这个写法的含义是每张表先取最新的 100 条再合并和合并所有行再取前 200 条语义完全不同看业务怎么想。3.3 括号与执行顺序的细节在标准 SQL 中可以使用括号来控制多个 UNION 的组合顺序。MySQL 和 PostgreSQL 支持这种写法但 SQL Server 的语法解析对括号的支持比较微妙我个人不建议在跨数据库兼容场景过度依赖括号。一个常见的组合问题三个查询前两个需要 UNION 去重再和第三个 UNION ALL 拼接。直观想法是(SELECT id FROM table_a UNION SELECT id FROM table_b) UNION ALL SELECT id FROM table_c;在 MySQL 里最外层不加括号也可以因为 UNION 的默认解析顺序是左结合。但为了防止歧义加括号确实更清晰。另一种更稳妥的做法是先用 UNION 得到去重结果作为子查询再 UNION ALLSELECT id FROM ( SELECT id FROM table_a UNION SELECT id FROM table_b ) t UNION ALL SELECT id FROM table_c;这样的可读性更好也更容易维护。4. 实操案例多表合并的典型场景与完整 SQL 示例4.1 场景一分表数据的汇总统计回到开头的分表场景。订单表按年份拆分后要计算所有订单的总金额。最直接的方式SELECT SUM(total_amount) FROM ( SELECT order_id, total_amount FROM order_2023 UNION ALL SELECT order_id, total_amount FROM order_2024 ) t;这个场景下订单号理论上不会跨年重复所以 UNION ALL 是正确答案。如果误用 UNION由于每个订单的金额有概率相同比如都是 99 元去重会把相同金额的行合并导致 SUM 少算。这种错误特别隐蔽——SQL 不报错结果也看起来合理但对不上账。所以涉及汇总统计时要不要去重千万不能想当然要先想清楚业务上什么字段是唯一键。还有另一个经验分表查询后聚合优先在子查询内先做聚合再 UNION ALL 汇总。比如按年份统计订单数和金额SELECT 2023 AS year, COUNT(*) AS cnt, SUM(total_amount) AS amount FROM order_2023 UNION ALL SELECT 2024 AS year, COUNT(*) AS cnt, SUM(total_amount) AS amount FROM order_2024;如果你先 UNION ALL 所有明细行再 GROUP BY数据量会大很多聚合效率明显变低。能用先聚合、后合并的绝不先合并、后聚合。4.2 场景二不同维度数据的横向补全UNION 也可以做行变列的辅助手段。比如系统里有用户表和订单表想生成一张用户维度的运营日报表表中一列是用户 ID一列是指标类型一列是指标值。用户表提供注册数订单表提供下单数SELECT user_id, register_count AS metric_type, 1 AS metric_value FROM users UNION ALL SELECT user_id, order_count AS metric_type, COUNT(*) FROM orders GROUP BY user_id;这种宽表转长表的场景把多列指标变成多行指标用 UNION ALL 特别顺手。如果指标之间本身可能重复同一用户注册数和下单数不同不会重复UNION ALL 就足够而且比各种 CASE WHEN 嵌套要清晰得多。4.3 场景三UNION 在子查询、视图与 CTE 中的使用UNION 经常在视图和 CTE 内部使用把多个来源的数据合并为一张逻辑表方便后续 JOIN。WITH all_users AS ( SELECT id, name, app AS source FROM user_app UNION ALL SELECT id, name, web AS source FROM user_web ) SELECT source, COUNT(*) FROM all_users GROUP BY source;用 CTE 包裹 UNION ALL 的写法可以把哪些表参与合并这个复杂度封装起来外层业务逻辑变得干净。但我有一个提醒视图内部使用 UNION 时如果多个底层表都有索引合并后视图本身是没有索引可言的外层再对视图做 WHERE 过滤往往无法利用底层索引可能全表扫描。这是很多慢查询的根源排查时一定要把视图的定义展开看。5. 经验与实测性能对比、优化思路与避坑指南5.1 一次生产环境的慢查询排查实录去年我处理过一个线上问题。某个列表接口在数据量上来之后突然变慢从 200ms 涨到 4s。抓出 SQL 一看核心逻辑是SELECT id, title, create_time FROM article WHERE type 1 UNION SELECT id, title, create_time FROM article WHERE type 2 ORDER BY create_time DESC LIMIT 20;粗看没什么问题但实际执行计划显示优化器对两张结果集做了一次全量去重。问题在于type字段本身就把数据分开了type1和type2的两组数据在逻辑上完全不可能重复UNION 的去重完全是多此一举。改成 UNION ALL 之后接口耗时降到 180ms。加了几个索引之后进一步降到 80ms。这个案例特别典型因为它涉及的是同一个表的不同类型数据合并很多人在不假思索的情况下就会写 UNION白白浪费了一次去重。这类同一表、不同取值的合并还有一个替代写法用WHERE type IN (1, 2)配合排序很多时候比 UNION ALL 更快因为只需要一次全表/索引扫描SELECT id, title, create_time FROM article WHERE type IN (1, 2) ORDER BY create_time DESC LIMIT 20;但如果你要的是type1 各取前 10 条type2 各取前 10 条再合并那就必须用 UNION ALL 包子查询的形式。这两种语义是不同的问题别混。5.2 优化思路UNION 之外的合并方案除了把 UNION 换成 UNION ALL还有一些场景可以完全绕开 UNION用更省的方式实现。第一种用 JOIN 替代部分场景。JOIN 是横向拼接真正需要每个数据来源给不同列时通常用 JOIN 聚合。比如展示一个产品同时有线上价格和线下价格线上表有一行线下表有一行想要的结果是一行展示两个价格那就不是 UNION 能力范围内的事要用LEFT JOIN 聚合。第二种用条件聚合。比如统计每个用户的多种行为次数可以用SELECT user_id, COUNT(CASE WHEN action click THEN 1 END) AS click_cnt, COUNT(CASE WHEN action buy THEN 1 END) AS buy_cnt FROM user_actions GROUP BY user_id;这比把行为表拆成多个子查询再 UNION 要高效得多一次扫描搞定。第三种利用窗口函数。某些上下行拼接的需求比如取每个用户最近两笔订单也可以先用窗口函数ROW_NUMBER()对每个用户分区排序再取前两行。这比把两个查询结果 UNION 再排序更利于优化器走索引。实战中我的习惯是先问自己能不能一条 SELECT 写出来写不出来再考虑 UNION。把 UNION 当成最后手段而不是第一反应往往能写出更高效的 SQL。5.3 面试常问与速查表总结几个我面试候选人和自己面试时被问过的高频问题当作速查问题答案要点UNION 和 UNION ALL 的区别UNION 对合并结果去重UNION ALL 不去重直接拼接哪个性能好UNION ALL 更快因为省去了排序/哈希去重的开销UNION 去重的实现方式排序去重或哈希去重需要临时表和额外内存列名以哪个 SELECT 为准以第一个 SELECT 的列名为准列数不一致会怎样直接报错要求各 SELECT 返回列数相同UNION 能替代 JOIN 吗不能。UNION 纵向加行JOIN 横向加列什么时候必须用 UNION需要把多个查询结果纵向合并且要去重时什么时候强烈建议 UNION ALL数据源互斥、无重复、或重复无影响时还有一个经典考点UNION 和 UNION ALL 对索引的影响。UNION ALL 的每一段可以独立走索引比如每个子查询都有自己的 WHERE 和索引使用计划而 UNION 由于要去重即使每个子查询走了索引最终结果集也要做一次去重这步可能绕过索引优势。如果两个子查询都命中索引且结果集本身很小差异不大结果集一大差距立刻显现。还有一个容易被忽略的点UNION 只对最终结果的所有列组合去重不是对其中某一列去重。如果我想按user_id 去重但要显示 user 的 name直接写 UNION 是不管用的——它必须整行完全重复才算重复。正确的做法是SELECT user_id, MAX(name) AS name FROM ( SELECT user_id, name, created_at FROM user_app UNION ALL SELECT user_id, name, created_at FROM user_web ) t GROUP BY user_id;先 UNION ALL 保留所有行再按 user_id 分组去重。这样语义清楚也不会误伤行为。5.4 不同数据库的细节差异虽然 UNION 是 SQL 标准语法但不同数据库的实际行为有一些值得注意的差异。MySQL 对 UNION 的去重是整行比较UNION ALL 则完全不做任何检查。MySQL 8.0 之后UNION 的临时表实现可能使用内存临时表数据量大时自动转为磁盘临时表这会导致临时表空间吃紧。所以 MySQL 上如果两张表都是大表且需要 UNION尽量改成 UNION ALL 应用层去重或者用LEFT JOIN WHERE b.id IS NULL的方式只查差集。PostgreSQL 的 UNION 和 UNION ALL 与标准行为一致。PostgreSQL 的优势是UNION 的去重在很多情况下可以走 hash aggregation性能比排序去重好而且它的临时表管理更稳健。但依然没必要把 UNION ALL 换成 UNION 去无脑增加工作量。SQL Server 的 UNION 去重逻辑基本相同但有一个特性SQL Server 对 UNION 的结果集分配临时表空间时如果涉及 TEXT/NTEXT/IMAGE 或大数据类型也会对性能有影响。SQL Server 中如果子查询带有 TOP 或 OFFSET需要包成子查询再合并否则 ORDER BY 的作用域会很混乱。Oracle 的 UNION 是 SORT排序去重UNION ALL 是直接拼接。Oracle 里 UNION 对排序性能非常敏感如果能够确认数据不重复强烈建议用 UNION ALL。此外Oracle 还支持 MINUS 和 INTERSECT这和其他数据库的 EXCEPT、INTERSECT 是同一类集合操作。不同数据库的细节差异本质上不是语法问题而是优化器行为和临时表实现的问题。平时开发如果不注意换个数据库版本或者迁移上云性能表现可能出现很大变化。这也是为什么我始终建议能用 UNION ALL 表达业务语义的绝不写 UNION。6. 写在最后的几条实践建议把上面的内容收敛成几句可以直接记住的话合并前先想清楚数据是否可能重复可能重复且业务不允许重复才用 UNION否则一律 UNION ALL如果用了 UNION留意大结果集下的去重开销ORDER BY 和 LIMIT 放在整个合并的外层子查询各自的排序用子查询包住多表合并先聚合再合并不要先把明细合在一起再聚合。我个人在实际排查慢查询时把 UNION 改成 UNION ALL是我最高频使用的手段之一。改之前先看执行计划确认去重步骤占了多大比例改之后对比结果集行数确认业务不可重复的假设成立。整个过程算下来真正动手改代码只要一分钟难的是想明白这里为什么不该去重。最后分享一个我在代码 review 时常用的检查习惯看到 UNION 先问一句这个去重是业务需求还是顺手写的如果你答不上来大概率就是顺手写的。改掉这个顺手很多性能问题在源头就避免了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Vibe Coding 实战:从提示词工程到 Agent 模式的工程化落地 2026/10/2 4:28:05

Vibe Coding 实战:从提示词工程到 Agent 模式的工程化落地

1. 从“会写代码”到“会描述意图”:Vibe Coding 到底改了什么“Vibe Coding”这个词最近在开发圈子里出现的频率越来越高,很多人第一次听到会以为是某种新的编程语言或者框架,其实它描述的是一种工作方式的转变:开发者不再逐行敲…

阅读更多 →
LimiX-2:面向表格数据的语义块掩码建模 2026/10/2 4:28:05

LimiX-2:面向表格数据的语义块掩码建模

1. LimiX-2不是“另一个BERT复刻”,而是表格数据专属的预训练范式重构你可能刚在论文列表里扫到“LimiX-2 表格模型的masked modeling”这个标题,下意识点开——结果发现满屏公式、消融实验和Ablation Table,连一句“它到底解决了什么实际问题…

阅读更多 →
Codex CLI接入Jev本地模型:OpenAI兼容协议下的高效AI编程配置实战 2026/10/2 4:28:05

Codex CLI接入Jev本地模型:OpenAI兼容协议下的高效AI编程配置实战

给ChatGPT账号充值像割肉、官方的模型偶尔还闹脾气限流,这是我把Codex CLI用起来之后最真实的感受。Codex本身没得说,面向任务的Agent式工作流,规划、改代码、跑验证一步到位,用顺手了是真的回不去。但默认模式下它非常依赖ChatGP…

阅读更多 →
PSO优化Elman回归预测:从初值寻优到可复现的训练流程 2026/10/2 4:28:04

PSO优化Elman回归预测:从初值寻优到可复现的训练流程

简介:面向多变量回归预测与智能优化建模需求,这份资源提供了一套基于粒子群算法(PSO)优化Elman递归神经网络的完整预测模型,适合机器学习、智能计算及数据预测方向的研究者与工程人员使用。PSO-Elman将粒子群全局寻优能力与Elman网络的动态递…

阅读更多 →
VS2017 64位下OSG+osgworks+Bullet3+osgbullet编译与碰撞检测集成指南 2026/10/2 4:28:04

VS2017 64位下OSG+osgworks+Bullet3+osgbullet编译与碰撞检测集成指南

简介:本资源为VS2017 64位环境下编译生成的osg、osgWorks、Bullet3与osgbullet库集合,面向从事三维游戏开发、物理仿真与可视化应用的C开发者,尤其适合需要在Windows平台快速集成3D渲染与碰撞检测的中高级技术人员。压缩包为rar格式&#xff…

阅读更多 →
Python+Playwright抓取动态页面:从XHR截获到数据入库完整实战 2026/10/2 4:27:58

Python+Playwright抓取动态页面:从XHR截获到数据入库完整实战

做爬虫这些年,被问得最多的问题,不是“哪个库好用”,而是“遇到纯前端渲染的页面到底怎么抓”。很多人用 requests 拿不到数据、用 Selenium 又觉得太重太慢,折腾一圈最后发现,真正顺手的方案其实是把 Playwright 当“…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉