新闻详情

新闻详情

首页 / 资讯中心 / 详情

SQL UNION联合查询详解:纵向合并结果集的核心用法与性能优化

发布时间:2026/9/17 10:23:09来源:尧图网络
SQL UNION联合查询详解:纵向合并结果集的核心用法与性能优化
SQL入门系列讲到这里单表查询和连接查询的基本功就算打完了。这一讲我们聊UNION联合查询——一个我工作中用得非常频繁、但很多人一开始都会搞混的集合操作。它的使用场景特别直白你手上有好几张结构相同的表按年份拆的、按部门拆的、按区域拆的现在要把这些表的数据合并到一张结果集里统一统计这个时候就需要用到UNION。UNION的中文名叫“联合查询”本质上是把多个SELECT语句的结果纵向拼接成一个完整的结果集。它也是面试里几乎必考的点因为表面上看只是几个关键字的事背地里却牵扯到结果集结构、数据类型、字符集排序规则甚至执行计划稍不留神就会踩坑。这一讲我会从它和JOIN的本质区别讲起重点拆解UNION和UNION ALL的区别再带一个完整的实操案例最后把常见的报错和我自己踩过的坑一并整理出来。不管你是刚学SQL的初学者还是写了一段SQL但一直没系统整理过集合操作的开发同学这篇都适合你。1. 联合查询的本质把查询结果“纵向拼接”1.1 先彻底分清JOIN和UNION我见过太多没弄懂UNION的人第一反应都是问这和JOIN有什么区别每次遇到这个问题我都用一句话回答JOIN是“横向拼接”UNION是“纵向拼接”。JOIN做的事情是根据关联条件把两张表的列拼到一起。拼完之后列数通常会增加行数一般不会减少。比如说订单表和客户表通过客户ID关联把客户姓名、客户等级这些列追加到订单表右边这就是典型的横向拼接。UNION则完全不同它把两个SELECT查出来的行摞在一起。拼完之后列数不变但行数是两个结果集行数的叠加。我打个比方JOIN像做拼图把两块拼图按边缘卡在一起拼成一张更大的图UNION像整理扑克牌把两叠牌直接合到一叠。一个是横向扩展一个是纵向扩展。这两个操作解决的需求完全不同。JOIN解决的是“多张表的信息需要同时展示在一行里”的问题UNION解决的是“多段结果需要放到一个结果集里统一处理”的问题。如果你把UNION当JOIN用或者反过来把JOIN当UNION用最后拿到的结果一定不是你想要的。1.2 UNION的基本语法和三条硬性规则UNION的语法本身简单得不像话SELECT 语句1 UNION [ALL] SELECT 语句2;看起来轻松但实际使用时有三条硬性规则必须守缺一条就报错。第一两个SELECT查出来的列数必须完全一样。这一点新手经常忽略尤其是查询的列特别多时从上面拷贝下来漏写一列数据库会直接给你一个列数不匹配的报错。记住UNION按位置合并列不是按列名合并所以列数不对等于失败。第二列的顺序必须对应。UNION是以位置为准的——第一个SELECT的第三列会和第二个SELECT的第三列拼到同一列里。如果你把两边的列顺序写反了数据库不报错但结果全部错位这种阴间Bug排查起来极其痛苦。第三对应列的数据类型要兼容。比如一边是INT一边是VARCHAR很多数据库会尝试隐式转换如果一边是字符串一边是日期转换不过去就会报类型转换错误。另外还有一点容易被忽略最终结果集的列名以第一个SELECT的列名为准。第二个SELECT里写的别名哪怕再长再规范都不会出现在最终结果集里。所以你想让结果集列名变得友好要在第一个SELECT里起别名。下面给一个非常典型的例子。两张学生表分别存放2023级和2024级的学生数据结构一模一样CREATE TABLE student_2023 ( id INT, name VARCHAR(50), class_name VARCHAR(20) ); INSERT INTO student_2023 VALUES (1, 张三, 一班), (2, 李四, 二班), (3, 王五, 三班); CREATE TABLE student_2024 ( id INT, name VARCHAR(50), class_name VARCHAR(20) ); INSERT INTO student_2024 VALUES (1, 张三, 一班), (4, 赵六, 一班), (5, 钱七, 二班);注意张三在两年的时间段里整行完全一致这一点对理解UNION的去重行为至关重要。现在执行SELECT id, name, class_name FROM student_2023 UNION SELECT id, name, class_name FROM student_2024;结果是5行张三只会出现一次。因为UNION默认会对结果集做去重。而如果换成UNION ALL结果会是6行张三会出现两次。这引出了下一节的内容。2. UNION和UNION ALL少两个字母差别很大2.1 核心区别去重与不去重UNION与UNION ALL最本质的区别就是去重。UNION等价于“先合并、再对整行做一次DISTINCT”所以重复行只会保留一条。UNION ALL则是“直接追加保留所有行”完全不去重。回到上面的例子student_2023和student_2024里都有张三这一条完全相同的数据UNION会把重复的合并成一个最终输出5行UNION ALL不做任何去重张三出现两次最终输出6行。这里要特别强调一个理解误区UNION的去重是针对“整行的所有列”来判定的而不是单独针对某一列。如果两张表里都有“张三”但张三所在班级不同整行数据不相等那UNION不会认为它是重复行也不会把它合并掉。所以如果你期望的是“按姓名这一列去重每个姓名只保留一条”UNION做不到得用GROUP BY或者ROW_NUMBER()开窗函数去实现。这是一个很常见的需求错位我在实际工作中见过不少新同事在这个地方栽跟头。2.2 性能差异背后的原理性能方面UNION ALL几乎总是比UNION快而且数据量大时差距非常明显。为什么UNION要做去重数据库必须在内存或临时表里对合并后的结果集做排序SORT或哈希去重HASH。这个操作需要额外的CPU、内存和磁盘IO。如果参与合并的数据集有几百万上千万行光是去重这一步就能让查询时间从秒级变成十几秒甚至分钟级。UNION ALL就轻松多了它只是把各个子查询的结果一条条顺序追加到一起没有去重开销执行计划也更简单直观。我在一次生产环境调优里遇到过典型情况两张千万级的表做结果合并业务上确认两边数据不可能有重复行但代码里写的却是UNION。那条SQL跑了十几秒才出结果改成UNION ALL之后一秒出头就出来了。没有任何索引和SQL结构上的改动仅仅是换掉一个关键字查询效率直接提升了一个数量级。2.3 实际场景里怎么选我的选择原则从来都是一句话业务上允许重复就选UNION ALL必须消灭重复行才选UNION。绝大多数分表场景都适用UNION ALL。比如订单表按月拆表每个订单ID全局唯一合并时根本不可能出现重复行这时候用UNION纯粹是让数据库白白做一次无用功。只有一种情况我才会毫不犹豫地使用UNION业务上确实需要把多个结果集合并后去掉完全相同的行。比如从不同历史版本的数据源里合并数据两边可能存在同一笔记录的多个副本这时UNION的去重能力就很有价值。有一点需要说明虽然不同数据库对UNION的底层实现有差异但语义完全一致MySQL、SQL Server、PostgreSQL、Oracle里UNION默认去重UNION ALL不去重Hive、Spark SQL里也是一样。你只需要记住这一条到任何数据库环境都通用。3. UNION的高阶写法排序、常量列、聚合齐上阵3.1 对整个结果集排序的正确姿势多个SELECT合并之后经常需要排序。很多新手会习惯性地在每个子查询后面加ORDER BY这是不对的。ORDER BY必须写在最后一个SELECT的后面而且它排序依据的是合并之后的结果集列名不是某个子查询里独有的列。举个例子把两年的订单合并后按年份和金额排序SELECT 2023 AS order_year, order_id, amount FROM orders_2023 UNION ALL SELECT 2024, order_id, amount FROM orders_2024 ORDER BY order_year ASC, amount DESC;这里ORDER BY引用的order_year、amount都是合并后结果集里真实存在的列。如果你在ORDER BY里写了一个只在第一个子查询里存在、但没被SELECT输出到结果集的列比如id很多数据库会直接报错因为它根本不知道这个列是什么。尤其SQL Server对这个限制执行得很严格它会明确提示你ORDER BY项必须出现在SELECT列表中。3.2 用常量列给不同来源打标记合并多张表时我强烈建议加一个常量列作为来源标签。这个习惯帮我避免过很多次数据追溯的麻烦。SELECT 2023 AS order_year, order_id, amount FROM orders_2023 UNION ALL SELECT 2024, order_id, amount FROM orders_2024;这里的2023、2024就是常量列合并之后每行数据从哪来一眼可见。后续需要按来源分组统计时这个字段也能直接用上。常量列还有一个更实用的价值当两张表的列语义不完全对等时可以用常量或NULL来补位保证列数对齐。比如一张表有备注列另一张没有合并时就可以用CAST(NULL AS VARCHAR(50))来占位保持结构一致。3.3 UNION ALL GROUP BY 做多段聚合有一种常见的需求合并后再统一分组统计或者各子查询先聚合再合并。两种写法不能盲目选要看数据量。如果两张表数据量都特别大建议在子查询里先各自GROUP BY做一次预聚合再把聚合结果UNION ALL起来。这样可以提前削减数据量减少合并阶段的开销。如果合并之后需要跨来源统一分组就先把明细UNION ALL成一个大结果集再在外面套GROUP BY。比如统计两年各班级的学生数SELECT class_name, COUNT(*) AS cnt FROM ( SELECT id, name, class_name FROM student_2023 UNION ALL SELECT id, name, class_name FROM student_2024 ) t GROUP BY class_name ORDER BY cnt DESC;这种写法的好处是逻辑清晰先合并再加工。执行效率也比直接对两张表分别统计再加总要稳定因为统一分组避免了两次GROUP BY结果合并时的各种类型和边界问题。3.4 用UNION模拟FULL OUTER JOIN最后这个进阶技巧非常实用尤其适合MySQL这类不支持FULL OUTER JOIN的数据库。FULL OUTER JOIN要的是“左表有就带左表右表有就带右表两边都有就都带出来”。没有这个语法时可以用LEFT JOIN和RIGHT JOIN配合UNION来模拟SELECT a.id, a.name, b.order_id FROM customer a LEFT JOIN orders b ON a.id b.customer_id UNION SELECT a.id, a.name, b.order_id FROM customer a RIGHT JOIN orders b ON a.id b.customer_id;左连接把左表全部记录带出来右连接把右表全部记录带出来UNION再去掉重叠的部分最后的结果就等价于FULL OUTER JOIN。这个方法在数据对比场景里特别经典。比如比对两个系统的用户表找出哪些用户只存在于系统A、哪些只存在于系统B用这个方法可以一次摸清两边的差异。4. 完整实操合并两张结构相同的订单表4.1 场景说明与建表准备我一直觉得学SQL不能只看语法一定要动手建表、插数据、跑查询才能真正理解一个操作符的行为。这里给一个完整案例。场景是这样的公司订单数据按年份拆表orders_2023和orders_2024结构相同包含订单ID、客户ID、金额、下单日期。现在需要统计这两年各月份的销售总额。先建表并插入测试数据以MySQL为例CREATE TABLE orders_2023 ( order_id INT PRIMARY KEY, customer_id INT, amount DECIMAL(10,2), order_date DATE ); INSERT INTO orders_2023 VALUES (1001, 201, 120.50, 2023-01-15), (1002, 202, 88.00, 2023-02-11), (1003, 203, 250.00, 2023-12-03); CREATE TABLE orders_2024 ( order_id INT PRIMARY KEY, customer_id INT, amount DECIMAL(10,2), order_date DATE ); INSERT INTO orders_2024 VALUES (2001, 201, 99.50, 2024-01-20), (2002, 204, 320.00, 2024-02-05), (2003, 202, 150.00, 2024-12-25);4.2 分步合并与按月统计第一步先把两个子查询单独跑一遍确认各自没有问题。这是我一直强调的排查习惯尤其是初学者两个查询都通了再合并能少踩很多坑。SELECT order_id, customer_id, amount, order_date FROM orders_2023; SELECT order_id, customer_id, amount, order_date FROM orders_2024;第二步用UNION ALL合并SELECT order_id, customer_id, amount, order_date FROM orders_2023 UNION ALL SELECT order_id, customer_id, amount, order_date FROM orders_2024;合并后得到6行数据因为两边的order_id都是主键完全不可能重复所以我优先选UNION ALL而不是UNION避免数据库做无意义的去重。第三步按月份统计销售额。最稳的写法是先用UNION ALL合并成子查询再按月分组SELECT DATE_FORMAT(order_date, %Y-%m) AS month, SUM(amount) AS total_amount FROM ( SELECT order_id, customer_id, amount, order_date FROM orders_2023 UNION ALL SELECT order_id, customer_id, amount, order_date FROM orders_2024 ) t GROUP BY DATE_FORMAT(order_date, %Y-%m) ORDER BY month;执行结果可以清楚看到每个月的销售总额。如果你希望每行数据能识别来源年份加一个来源列即可SELECT 2023 AS source_year, order_id, amount, order_date FROM orders_2023 UNION ALL SELECT 2024, order_id, amount, order_date FROM orders_2024;这种写法在报表里非常常用既方便追溯数据也方便按年分组做后续加工。4.3 判断UNION和JOIN的时机实操多了之后你自然会对“UNION还是JOIN”形成肌肉记忆。我这里提供一个判断标准需要增加新行就选UNION需要增加新列就选JOIN。统计两年数据年份与年份之间是上下堆叠的关系用UNION。查询订单对应的客户姓名订单和客户是左右搭配的关系用JOIN。还有一个常见场景值得提一提同一张表上如果两个查询条件很难用一条WHERE写成AND/OR或者OR会导致执行计划选不到索引可以把条件拆成两个SELECT再用UNION ALL合并。我在优化慢SQL时用过好几次这个套路效果非常明显至少比让数据库去执行一个低效的OR条件要快。5. 常见报错与排查经验5.1 报错列数不一致这个报错最典型。比如SELECT id, name FROM student_2023 UNION SELECT class_name FROM student_2024;数据库会直接提示列数不匹配。排查方式很简单把每个SELECT单独跑一遍数一数列数缺什么就补什么。很多新手在这里犯难的原因是不知道补什么我的建议是补常量占位。比如第二个SELECT没有id就写CAST(NULL AS INT)或者直接写0保证两边结构一致。5.2 数据类型不一致与隐式转换UNION要求对应列的数据类型兼容。遇到不兼容的情况数据库会尝试隐式转换转换不了就报错。一边是字符串一边是数字时MySQL会尽量把字符串转成数字。但这种隐式转换经常带来意外结果比如字符串里有非数字字符时转换出来的值可能不是你想要的。更稳妥的做法是显式用CAST统一类型SELECT CAST(id AS CHAR) AS id, name FROM student_2023 UNION ALL SELECT CAST(id AS CHAR) AS id, name FROM student_2024;这样两边类型明确一致执行结果可控不会出现隐式转换带来的意外。5.3 illegal mix of collations字符集排序规则不一致这个报错很多人处理中文数据时都见过完整的报错信息是illegal mix of collations for operation UNION。我第一次遇到时也是一头雾水后来才明白问题出在哪里。原因非常典型参与UNION的两个字段虽然都是字符串类型但它们的字符集排序规则collation不一样。比如一张表用的是utf8mb4_general_ci另一张用的是utf8mb4_unicode_ci两边直接UNION时数据库不知道应该按哪种规则来做排序和比较于是直接拒绝执行。解决方案有三种。第一种在查询里手动指定COLLATE强制两边统一排序规则SELECT name FROM t1 COLLATE utf8mb4_unicode_ci UNION SELECT name FROM t2;第二种修改表或字段的排序规则让两边一致。但这种方案要评估影响范围因为改排序规则可能影响索引和已有数据的行为不能随便动。第三种如果字段本身不需要复杂排序可以在SELECT时用CONVERT或CAST统一字符集类型绕过collation冲突。SQL Server遇到不同排序规则的数据源时报错现象很类似解决办法同样是COLLATE指定统一规则。所以这个思路各数据库通用。5.4 排序和分页的边界问题合并后排序最常见的报错是ORDER BY引用的列不在结果集里。比如SELECT name FROM student_2023 UNION ALL SELECT name FROM student_2024 ORDER BY id;如果id没有出现在SELECT列表中数据库会直接报错因为外层结果集根本没有id这一列。解决办法有两个。一是把id也放进SELECT列表让它存在于结果集里二是把ORDER BY改成基于结果集里真实存在的列比如name。另外要注意如果合并后需要分页LIMIT必须写在整个UNION语句的最后、ORDER BY之后SELECT id, name FROM student_2023 UNION ALL SELECT id, name FROM student_2024 ORDER BY id LIMIT 10;顺序错了分页结果就会不符合预期。5.5 常见问题速查表现象可能原因解决方案报错列数不匹配各SELECT输出列数不一致单独跑子查询核对列数补常量占位结果行数比预期多使用UNION ALL且数据本身有重复确认业务是否允许重复必要时换UNION结果行数比预期少UNION去掉了不应去掉的行确认是否整行重复用UNION ALL保留中文数据报collation错误两边字符串排序规则不一致用COLLATE统一规则或改表结构报错ORDER BY列不存在引用了结果集外的列将该列加入SELECT或改按结果集列排序数据错位但SQL不报错两边SELECT的列顺序不一致逐列比对两边的列顺序确保位置对应6. 性能优化与生产环境避坑建议6.1 默认优先使用UNION ALL这条经验我每次带新人都会强调没有明确去重需求一律用UNION ALL。UNION去重听起来方便但代价是实打实的。数据库需要把多个结果集合并后做排序或者哈希去重数据量一上来执行时间、临时表空间、内存开销全部跟着涨。我见过太多案例SQL本身写得没毛病就是UNION多了导致慢查询。改成UNION ALL后查询速度立竿见影地提升。关键是业务上本来就不需要去重纯属白花钱。6.2 在子查询里先过滤减少合并数据量UNION合并的数据量越小越快所以每个子查询内部尽量把WHERE、GROUP BY、LIMIT这些操作做完再把结果往外抛。一个常见的低效写法是三个子查询各自查全表UNION后再在外面套WHERE过滤。这种写法相当于把大量无用的行都合并了一遍浪费IO和内存。正确做法是把WHERE条件下沉到每个子查询内部只让需要的数据进入合并阶段。6.3 不要混淆“表模型”和“UNION查询”有一次在讨论Flink SQL写入Doris的Unique Key模型的表时团队里有人把“Unique Key模型”里的主键去重和“UNION去重”混在一起讨论。这里必须明确一下Doris的Unique Key模型是表模型层面的主键更新机制决定的是数据写入时按主键如何合并、覆盖SQL的UNION是查询时的集合操作决定的是结果集怎么拼接。两者都有“去重”这个词但发生在完全不同的环节一个是写数据一个是查数据千万不要混为一谈。6.4 排查UNION问题的几个实用习惯第一任何UNION查询先在数据库里单独跑一遍每个SELECT确认没问题再合并。我排查UNION问题90%的时间都花在单独跑子查询上。第二用EXPLAIN看执行计划。EXPLAIN会展示UNION每个子查询是否走索引、是否产生临时表、是否有排序操作。看到Using temporary或者Using filesort时就要有意识地去优化了。第三UNION的各子查询尽量做列裁剪不要直接SELECT *。列越多合并的数据量越大UNION去重时的排序代价也越高。第四如果UNION语句特别复杂可以先把合并结果存到临时表再对临时表做后续处理。虽然多了一步但可读性和可维护性会好很多排查时也更容易定位问题。写到这里UNION的核心知识点基本就过完了。最后分享一个我的个人体会。最早学UNION时我犯过一个低级错误想统计两个部门名单的总人数用了UNION结果人数比预期少了几个查了半天才发现是有重名的人被UNION去重了。从那次之后我养成了一个习惯凡是用到UNION先问自己一句这个场景里允许重复行吗允许就用UNION ALL不允许才考虑UNION而且一定要确认UNION去重的是整行不是某个字段。把这个习惯刻进脑子里能帮你省掉很多莫名奇妙的数据对不上问题。
网站建设高端定制企业官网
RELATED

相关资讯

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

较早相关资讯

最新相关资讯

500+ AI Agent 项目案例库深读:20 个可运行源码,学会编排再挑框架 2026/9/17 11:17:32

500+ AI Agent 项目案例库深读:20 个可运行源码,学会编排再挑框架

500 AI Agent 项目案例库深读:20 个可运行源码,学会编排再挑框架 【免费下载链接】500-AI-Agents-Projects The 500 AI Agents Projects is a curated collection of AI agent use cases across various industries. It showcases practical application…

阅读更多 →
农业大数据知识图谱系统架构:本体设计、Neo4j导入与查询优化 2026/9/17 11:17:32

农业大数据知识图谱系统架构:本体设计、Neo4j导入与查询优化

简介:这是一份面向人工智能、大数据方向学习者及农业信息化从业者的技术分享课件,以知识图谱关键技术为线索,结合华东师范大学农业大数据魔方知识图谱项目,讲解从概念到系统落地的完整思路。资源包含1个pptx文件,约7.0…

阅读更多 →
五子棋机器人实战:从坐标标定到AI博弈全解析 2026/9/17 11:17:32

五子棋机器人实战:从坐标标定到AI博弈全解析

简介:智能人机对弈五子棋机器人设计相关学术论文PDF,内容基于国家自然科学基金项目,面向机器人、嵌入式及AI方向的学习者,提供一套低成本、软硬件一体化的五子棋人机对战实现方案。资源仅含1个PDF文件,压缩包大小2.55M…

阅读更多 →
小团队自动化测试决策指南:Playwright自建vs平台采购 2026/9/17 11:17:32

小团队自动化测试决策指南:Playwright自建vs平台采购

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Epic Stack 移除 CSRF 防护的架构决策:基于 SameSite=Lax 与蜜罐字段的安全简化 2026/9/17 11:17:32

Epic Stack 移除 CSRF 防护的架构决策:基于 SameSite=Lax 与蜜罐字段的安全简化

Epic Stack 移除 CSRF 防护的架构决策:基于 SameSiteLax 与蜜罐字段的安全简化 【免费下载链接】epic-stack This is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea. 项…

阅读更多 →
OpenProject Exports 系统设置配置指南:导出数量限制与 CSV 公式注入防护 2026/9/17 11:14:31

OpenProject Exports 系统设置配置指南:导出数量限制与 CSV 公式注入防护

OpenProject Exports 系统设置配置指南:导出数量限制与 CSV 公式注入防护 【免费下载链接】openproject OpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with a…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

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

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