UNION 去重、UNION ALL 不去重;实测 5 万行差 2.9 倍、15 万行差 3.0 倍。
UNION 的 ORDER BY 只能写在最后一条 SELECT 后面,作用在整个结果上。
UNION 遇到类型不同会自动提升:数字 + 字符串 → 整列变字符串,排序就变成字典序。
8.1c 提了一句 UNION,这一节专门讲它跟 UNION ALL 的差别——这是性能上最容易忽略的一处。
教材 8.2 分三小节:两者的差别、UNION 里的 ORDER BY、自动数据类型转换。
第一节讲性能,第二三节讲两个坑。
演示里做了一次真测:同样两个查询,UNION 和 UNION ALL 各跑三遍取最快。
第一版测出来的结果跟结论相反——因为把「取数据」的时间也算进去了,
改成只测「算」的时间,差别才显出来。这一段值得细看。
一、两者的差别:一个去重,一个不去
SELECT gname FROM tb_goods WHERE cat = '电脑'
UNION ALL
SELECT gname FROM tb_goods WHERE price > 1000
UNION ALL 就是把两个结果首尾相接,不去重、不排序,所以快。上面这段:左边 4 行、右边 2 行,
UNION ALL 之后 6 行——笔记本和显示器各出现两次。
UNION 会去重,所以只剩 4 行。 代价是要先排序(或者建哈希表)才能知道哪些重复——
这个代价跟数据量成正比。
判断标准很简单:确定两边不会有重复,就用 UNION ALL。比如「按月分表查全年」——
12 个月的表本来就互斥,不可能有重复,用 UNION ALL 就对了。
「不确定会不会重复」的时候怎么办? 先跑一次 UNION ALL,看总数对不对;
如果总数对了,就说明没重复,直接用它。 这比一开始就用 UNION 省事。
二、真测一次:差 3 倍
| 数据量 | UNION ms |
UNION ALL ms |
慢几倍 | 行数 |
|---|---|---|---|---|
| 50000 | 34.1 | 11.6 | 2.9 | 300 / 50000 |
| 150000 | 104.6 | 35.0 | 3.0 | 300 / 150000 |
这个数是真测的:同样两个查询,各跑三遍取最快。UNION 慢 3 倍,
而且数据量越大差得越多——因为去重是排序,复杂度是 n log n。
注意最后那一列「行数」:UNION 去重之后只剩 300 行,UNION ALL 是全部行。
这 300 来自数据的构造方式——v 列的取值只有 300 种。
第一版测出来是反的(UNION 更快),为什么? 因为第一版把结果整批取回来了,
UNION ALL 要传 15 万行、UNION 只传 300 行——传数据的时间盖过了去重的时间。
所以测这类差别,只能测「算」的时间(比如外面套一层 COUNT(*)),
不能把「传」的时间算进去。 这是个很实用的教训——测性能要先想清楚在测什么。
三、ORDER BY 写在哪
SELECT gname FROM A
UNION
SELECT gname FROM B
ORDER BY gname DESC -- 只能写在这里
ORDER BY 只能写在最后一条 SELECT 后面,作用在整个 UNION 结果上。
写成第一条 SELECT 后面会报错——因为 UNION 两边要先各自跑完,才能排序。
想给「某一边」排序,得把它包成子查询:
SELECT * FROM (SELECT TOP 3 gname FROM A ORDER BY gname) t
UNION ALL
SELECT * FROM (SELECT TOP 3 gname FROM B ORDER BY gname) t2
这么写的目的通常是「每边取前 3 条,然后拼起来」。
这个需求用 UNION 做不了,因为 UNION 会重新排序——包成子查询才能保住各自的顺序。
还有一条:ORDER BY 里不能用列序号指定「哪一边的列」。整个 UNION 结果的列以前面那个为准——
所以 ORDER BY 1 排的是第一列。
四、自动类型转换:谁说了算
UNION 要求两边的列类型能对上。对不上怎么办?——按优先级提升。
| 左边 | 右边 | 结果 |
|---|---|---|
int |
varchar |
整列变 varchar |
int |
decimal |
整列变 decimal |
date |
varchar |
整列变 varchar |
varchar(10) |
varchar(50) |
整列变 varchar(50) |
「整列变字符串」这一条最容易出事。 数字 1 和字符串 'abc' 拼在一起,
1 会变成 '1'——看着没变,但类型已经变了。
后果在排序上:字符串排序是字典序,'10' 会排在 '9' 前面。
如果你 UNION 了两张表然后按那一列排序,结果会莫名其妙地不对。
这种问题在做报表时很常见: 某一列在一张表里是数字,在另一张表里是字符串,
UNION 之后整列变成字符串——排序、比较、求和全都变了。
解法是显式转换: 两边都写 CAST(x AS VARCHAR) 或者都写 CAST(x AS INT)——
别让数据库猜。 猜的结果不一定是你想要的,而且它不报错。
五、动手 Demo:UNION 与 UNION ALL 实测差 3 倍
这一段先看两者返回的行数差多少,再真测一次性能(只测「算」的时间),
然后演示 UNION 的类型提升——数字和字符串拼在一起,整列会变成字符串。
import sqlite3, time
c = sqlite3.connect(u":memory:")
c.execute(u"CREATE TABLE big (id INTEGER, v TEXT)")
c.executemany(u"INSERT INTO big VALUES (?,?)",
[(i, 'v%d' % (i % 300)) for i in range(50000)])
q1 = u"SELECT COUNT(*) FROM (SELECT v FROM big WHERE id < ? UNION "
u"SELECT v FROM big WHERE id >= ?)"
q2 = u"SELECT COUNT(*) FROM (SELECT v FROM big WHERE id < ? UNION ALL "
u"SELECT v FROM big WHERE id >= ?)"
for tag, q in (("UNION", q1), ("ALL", q2)):
t = time.perf_counter()
n = c.execute(q, (25000, 25000)).fetchone()[0]
print(tag, n, u"%.1f ms" % ((time.perf_counter() - t) * 1000))
实跑输出:
--------------------------------------------------------------------
UNION ALL:不去重的那个
--------------------------------------------------------------------
① UNION 和 UNION ALL 差在哪
--------------------------------------------------------------------
左 4 行、右 2 行
UNION → 4 行
显示器、笔记本、键盘、鼠标
UNION ALL → 6 行
笔记本、键盘、鼠标、显示器、笔记本、显示器
UNION ALL 把两个结果直接接起来,不去重、不排序,所以快。
UNION 要排序去重,代价跟数据量成正比——十万行可能多花一秒。
判断标准很简单:确定两边不会有重复,就用 UNION ALL。
「不确定会不会重复」的时候,先跑一次 UNION ALL 看总数对不对,再决定要不要去重。
② 性能差多少:真测一次
--------------------------------------------------------------------
| 数据量 | UNION ms | UNION ALL ms | 慢几倍 | 行数 |
|---:|---:|---:|---:|---:|
| 50000 | 33.5 | 11.3 | 3.0 | 300 / 50000 |
| 150000 | 102.9 | 36.1 | 2.9 | 300 / 150000 |
这一次 UNION 明显慢了——倍数也是真测的,数据量越大差得越多。
注意这张表还多了一列行数:UNION 去重之后只剩 300 行,UNION ALL 是全部行。
如果像上一版那样把结果整批取回来,取数据的开销会盖过去重的开销——
所以测这类差别,只能测「算」的时间,不能把「传」的时间算进去。
为什么差这么多?因为 UNION 要先排序(或者建哈希表)才能知道哪些是重复的,
而 UNION ALL 只是把两个结果首尾相接——它压根不关心有没有重复。
③ UNION 里的 ORDER BY 写在哪
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| SELECT gname FROM A
| UNION
| SELECT gname FROM B
| ORDER BY gname DESC
ORDER BY 只能写在最后一条 SELECT 后面,作用在整个 UNION 结果上。
写成第一条 SELECT 后面会报错——因为 UNION 两边要先各自跑完,才能排序。
想给「某一边」排序,得把它包成子查询,外面再 UNION。
④ 自动类型转换:谁说了算
--------------------------------------------------------------------
数字、字符串、小数混着 UNION ALL:
1 类型 int
'abc' 类型 str
3.5 类型 float
UNION 的规则是「按优先级提升」:数字 < 字符串,所以整列变成了字符串。
3.5 变成了 '3.5',1 变成了 '1'——看起来没变,但类型已经变了。
如果只取第一个结果集看,你会以为类型是 int,其实整列都是 text。
这种问题在做报表 UNION 多个来源时很常见:某一列在一处是数字,在另一处是字符串,
结果整列变成字符串,排序就变成「按字典序」——10 会排在 9 前面。
这段只测了「算」的时间——外面套了 COUNT(*),不把 5 万行数据传回来。这是测准的关键。
如果去掉那层 COUNT(*)、直接取 v,结果会反过来——因为 UNION ALL 要传 5 万行、
UNION 只传 300 行,传数据的开销盖过了去重的开销。
所以「测性能」这件事,先要想清楚在测什么。想测去重的代价,就得把网络的、
传输的因素排掉——这是个方法论问题,不只是 SQL 问题。
类型提升那段在正文里讲。 它不报错、不改数据,
只是把整列变成了字符串——然后排序变成字典序,'10' 排在 '9' 前面。
30 秒实验
- 把你手上一个用
UNION的查询改成UNION ALL,先确认结果行数没变。 行数没变,就说明本来就没重复,可以放心用UNION ALL。 - 跑
SELECT 1 UNION ALL SELECT 'abc' UNION ALL SELECT 3.5,看三行的类型。 全是字符串——这就是类型提升。 - 想一想:为什么
ORDER BY只能写在最后一条SELECT后面? 因为UNION要先把两边都跑完、合并成一个结果,才能排序。写在前面对应的是「单边排序」, 语法上不成立。
常见误区
- 「
UNION和UNION ALL差不多,随便用哪个」。实测差 3 倍,数据量越大差得越多。 确定没重复就用UNION ALL。 - 「
UNION结果慢是因为数据多」。恰恰相反——UNION结果少, 但它算得慢(要去重排序)。结果少不等于耗时少。 - 「
UNION会自动处理类型不同的问题」。会, 但方式是「提升成字符串」—— 这会让排序变成字典序,'10'排在'9'前面。要精确控制就显式CAST。
自测三问(点开对答案)
1. `UNION` 和 `UNION ALL` 该怎么选? —— **确定两边不会有重复,就用 `UNION ALL`**——它不去重、不排序,快得多。 **不确定的时候,先跑一次 `UNION ALL` 看总数对不对。** 2. 为什么第一次测性能时结论反了? —— **因为把「传数据」的时间算进去了。** `UNION ALL` 要传全部 15 万行, `UNION` 只传 300 行——**传输时间盖过了去重时间。** 只测「算」的时间才对。 3. `SELECT 1 UNION ALL SELECT 'abc'` 结果是什么类型? —— **字符串。** `UNION` 遇到类型不同会按优先级提升, **数字会变成字符串。**后果是排序变成字典序——`'10'` 排在 `'9'` 前面。下一篇 8.3 讲 CASE 的两个高级用法,也是第 8 章的收尾。
重点是行列转换——把「一行一门课」的长表变成「一行一个学生」的宽表,
靠的就是 CASE 加聚合函数。
- ☐ (本篇完,下一篇 8.3 使用 CASE 函数进行查询)