MYSQL从语义到执行计划:UNION、EXISTS、IN 的深度辨析

我排查过一条统计接口,SQL 没改、索引没动,只是把本该用的 UNION ALL 写成了 UNION,数据量过百万后,响应时间从 200ms 飙到 8 秒。

这篇不背"哪个更快"的死结论,只给你一套按表大小、索引和执行计划现场选型的判断框架,文末附决策速查表。

一、三个关键字,分别在解决什么问题?

UNION、EXISTS、IN 出现频率极高,却分属两个语义家族。先把职责边界钉死,后面才不会混。

语义家族 关键字 回答的问题 结果集变化
集合运算 UNION 多个结果集怎么拼成一个 列数不变,行数增加
存在判断 EXISTS / IN 这一行在另一张表里存在吗 行被保留或过滤

之所以容易混,是因为三个关键字最终都产出"一个结果集",但数据流向完全不同:UNION 改变行数,EXISTS、IN 改变哪些行被保留。

我在代码评审里见过有人用 UNION 把部门名称"加"到员工行上,语句不报错,返回的却是逻辑全错的结果。横向加列是 JOIN 的活,UNION 只会纵向堆行。

注意:UNION 管"行怎么堆叠",EXISTS、IN 管"行要不要保留"。

二、UNION 和 UNION ALL,差的不只是重复行?

知道了 UNION 是堆行,那 UNION 和 UNION ALL 到底差在哪?UNION 合并后去除重复行,UNION ALL 保留全部行,去重从来不是免费动作。

UNION 能跑还依赖两条硬性规则,违反直接报错:

  • 参与合并的每个查询必须返回相同数量的列。
  • 对应列的数据类型必须兼容(允许隐式转换);列名不必一致,结果集采用第一个查询的列名。
对比项 UNION UNION ALL
重复行 去除 全部保留
内部动作 排序或哈希,逐行比较 直接拼接
成本特征 排序 O(n log n),可能溢磁盘 接近线性
适用场景 业务明确要求去重 确定无重复,或重复无害
sql 复制代码
SELECT id, name FROM employee
UNION
SELECT id, name FROM employee_history;

我那次慢查询就踩在这:合并后的数据超过 work_mem,排序溢出到磁盘,这是 UNION 在数据增长后突然变慢的最常见原因。哈希去重对中等数据量通常更快,但哈希表同样要驻留内存。

至于走排序还是走哈希,同样由基数估计决定;行数估算偏差大时,连去重方式都会选错。

注意:确定两个结果集无重复(或重复不影响业务),就用 UNION ALL。

三、EXISTS 和 IN,数据库分别怎么跑?

拼接的坑说完,再看存在判断。EXISTS 和 IN 能回答同一个问题,执行路径却完全不同。

对比项 IN EXISTS
执行方式 先建列表,再逐项比对 逐行探测,命中即停
子查询 一次性物化为集合 关联子查询,逐行执行
优化器实现 可改写为半连接 半连接(semi-join)
NULL 处理 列表含 NULL 会异常放行 只看有无行,绕开 NULL

IN 先把结果物化成一个可比较集合,再对外层每行做成员判断;EXISTS 以半连接(semi-join)实现,外层每行只需在内部找到一个匹配。这个差异在关联子查询里最明显:子查询引用了外层列(如 B.cc = A.cc)时,EXISTS 每行可走一次索引查找、命中即停,IN 必须先收集完整个子查询结果,无法短路。

latex 复制代码
IN    : 子查询物化集合 ──> 外层每行查集合(一次建表,反复查找)
EXISTS: 外层取一行 ──> 关联列索引探测 ──> 命中1条即停(短路)
sql 复制代码
SELECT * FROM A WHERE A.cc IN (SELECT cc FROM B);

SELECT * FROM A
WHERE EXISTS (SELECT 1 FROM B WHERE B.cc = A.cc);

WHERE x IN (1, 2, NULL) 中,x = NULL 的结果是 UNKNOWN 而非 FALSE,整行可能不被过滤,本意是过滤却放行了更多行。EXISTS 只关心有没有行返回,不比较具体值,天然绕开这个坑。

IN 后接字面量列表(如 IN (1,2,3))时只构建常量集合,真正涉及物化的是后接子查询。更狠的是 NOT IN:子查询一旦含 NULL,整个结果直接为空,因为每行判断都退化成 UNKNOWN,而 NOT EXISTS 不受影响。

注意:EXISTS 命中即停,且不与 NULL 比较,涉及可空列时更安全。

四、到底选哪个?让小集合待在外层

执行路径清楚了,落到 A、B 两张表上怎么选?核心就一件事:控制循环的层级。

表大小关系 更优写法 原因
A 远小于 B EXISTS 外层循环少,B 表索引探测
B 远小于 A IN 小结果集一次物化,内存查找
两表相当 / 数据量小 几乎无差异 优化器可能改写成同一计划

A 远小于 B 时,EXISTS 近似"遍历 A、每行去 B 探测",外层次数少;B 远小于 A 时,IN 把子查询结果一次性物化,对 A 的每行做内存集合查找,避免反复深入 B。

量级上最直观:A 一万行、B 一千万行时,EXISTS 只触发一万次索引探测;若用 IN,则要先物化一千万行,内存和耗时都被大结果集拖死。

注意:选型的本质是控制循环层级,让小集合待在外层。

五、为什么这些规则有时不灵?

那为什么按规则选了,查询还是慢?因为性能从不由语法单独决定,优化器会改写查询,也会误判。

现代优化器会把 IN 改写为半连接,甚至把 EXISTS 改写为哈希连接。一旦被改写成同一形态,两种写法耗时几乎没有差别,纠结语法就失去意义。我查到两组对比鲜明的实测:

场景 实测现象 执行计划
PostgreSQL,约 1431 条匹配 IN 约 66ms,显式 JOIN 约 65ms Hash IN Join
SQL Server,9000 万行大表 IN 跑数小时,改 INNER JOIN 约 10 秒 前者误用嵌套循环

真正决定计划走向的,是下面三个因素:

因素 作用 缺失后果
关联列索引 支撑 EXISTS 逐行探测 无索引退化为全表扫描
统计信息 基数估计驱动算法选择 统计过时,选错计划
优化器版本 IN / EXISTS 互相改写 旧版本可能不走半连接

同理,把 EXCEPT 改写为 NOT EXISTS 之前,必须确认两侧比较列都有索引;没有索引,反连接(anti-join)会对每行外层记录做完整扫描,可能是最差的执行计划。

注意:脱离索引、数据量和执行计划谈"谁更快",都是空谈。

六、落到具体 SQL,怎么验证?

与其记"总是用 EXISTS",不如走一套可重复的验证流程。

  1. 先问去重是否必要,不需要就写 UNION ALL。
  2. 把关键查询的 IN / EXISTS / JOIN 等价写法都写出来。
  3. 在生产规模数据上跑执行计划,别用教学小表下结论。
  4. 确认关联列索引与统计信息,再决定用哪个写法。
sql 复制代码
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM A
WHERE EXISTS (SELECT 1 FROM B WHERE B.cc = A.cc);

需要补索引或刷新统计信息时,执行:

sql 复制代码
CREATE INDEX idx_b_cc ON B (cc);
ANALYZE B;

看计划时重点确认三件事:关联探测是否走了索引、有没有 Sort 溢出磁盘或全表扫描、估算行数与实际行数是否接近;行数偏差过大说明统计信息过时,先跑 ANALYZE 再判断。

对比两种写法时,只看同一份数据、同一份统计下的总耗时和扫描行数,别拿一次冷缓存的结果下结论。

七、总结

UNION 管"行怎么堆叠",要警惕不必要的去重开销;EXISTS 和 IN 管"行要不要保留",要理解半连接的短路优势和 NULL 语义陷阱。

术语速查表

术语 英文 含义
集合运算 Set Operation 纵向拼接多个结果集
存在性判断 Existence Check 判断行是否在另一结果集中存在
半连接 Semi-join 外层每行只需在内部找到一个匹配
关联子查询 Correlated Subquery 引用外层表列的子查询
基数估计 Cardinality Estimate 优化器对结果行数的预估
统计信息 Statistics 表数据分布信息,驱动计划选择

最终决策依据,永远是你的数据、你的索引、你的执行计划,而不是某条放之四海皆准的规则。一句话收口:先分清是"拼结果"还是"判存在",再用表大小和索引定写法,用执行计划拍板。

参考链接

相关推荐
Moshow郑锴3 小时前
sql-pipeline - 基于PostgreSQL的SQL健康检查与发布编排平台
java·sql
Elastic 中国社区官方博客10 小时前
将你自己的密钥用于现有 Elastic Cloud 部署
大数据·数据库·elasticsearch·全文检索
红海云11 小时前
Jev:给智能系统做判断的模型
大数据·数据库·人工智能
wjkjpcba11 小时前
PCBA烧录程序是什么:PCBA包工包料厂家解析烧录与测试
linux·数据库·人工智能·smt贴片加工·pcba贴片加工厂
꯭自꯭闭꯭11 小时前
达梦事物特性及MVCC
linux·运维·数据库
小马同学-12 小时前
MySQL主从复制和读写分离
数据库·mysql
谢亮_vipxieliang12 小时前
Spring 事务失效的常见场景
java·开发语言·数据库·spring boot
海绵宝宝转agent12 小时前
MySql高频面试八股开源笔记总结
mysql·面试·开源
geovindu12 小时前
sql: JSON and XML Data Handling in SQL using sql server 2025
大数据·数据库·sqlserver·数据库开发·数据库架构