🪟 第8章 SQL数据查询基础
📄 一句话概括

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 秒实验

  1. 把你手上一个用 UNION 的查询改成 UNION ALL,先确认结果行数没变。 行数没变,就说明本来就没重复,可以放心用 UNION ALL。
  2. 跑 SELECT 1 UNION ALL SELECT 'abc' UNION ALL SELECT 3.5,看三行的类型。 全是字符串——这就是类型提升。
  3. 想一想:为什么 ORDER BY 只能写在最后一条 SELECT 后面? 因为 UNION 要先把两边都跑完、合并成一个结果,才能排序。写在前面对应的是「单边排序」, 语法上不成立。

常见误区

  1. 「UNION 和 UNION ALL 差不多,随便用哪个」。实测差 3 倍,数据量越大差得越多。 确定没重复就用 UNION ALL。
  2. 「UNION 结果慢是因为数据多」。恰恰相反——UNION 结果少, 但它算得慢(要去重排序)。结果少不等于耗时少。
  3. 「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 加聚合函数。

相关笔记