卷 II · 取数CH 05深度 05/24

SELECT:投影、去重与集合

SELECT 你写了千百遍。但你想过 DISTINCT 为什么是「去重」、UNION 为什么默认也去重、LIMIT 1 拿到的到底是不是「第一条」吗?这些问题的答案,全藏在上一章那句话里:关系是集合。

投影DISTINCT集合运算ORDER BY

SELECT 列表:投影,加上「顺手算一下」

SELECT 的核心是投影(π)——决定结果里有哪些列。但它不止能「选列」,还能在选的同时算表达式

SELECT title,
       price,
       price * 0.9        AS sale_price,   -- 现算一列
       upper(genre)       AS genre_up,     -- 调函数
       price > 40         AS is_pricey     -- 布尔表达式也是一列
FROM books;

结果里的每一列,要么是原表的列,要么是由原表的列算出来的表达式AS 给它起个别名,否则 PostgreSQL 会给它一个诸如 ?column? 的难看默认名。这里已经能看出「当真」:price > 40 的结果是真正的 boolean 类型t / f),不是 MySQL 里那个假装成布尔的 1 / 0

DISTINCT:因为集合不重复

投影常会「制造」重复行。比如只投影 genre 一列:

SELECT genre FROM books;
-- scifi, scifi, scifi, fiction, fiction, fantasy —— 6 行,一堆重复

六本书投影出六个 genre,重复得厉害。如果你想问的其实是「一共有哪几种类型」,就要去重——用 DISTINCT

SELECT DISTINCT genre FROM books;
-- scifi, fiction, fantasy —— 3 行

为什么有 DISTINCT 这个词?因为关系本该是集合、集合不重复(第 3 章)。但 SQL 出于性能考虑,默认去重(去重要排序或哈希,不便宜)。所以 DISTINCT 是你在说:「这一步,我要回到纯粹的集合语义。」

◆ DISTINCT ON:PostgreSQL 的一件趁手兵器

PostgreSQL 有个标准 SQL 没有的扩展 DISTINCT ON,解决一个超常见的需求:每个分组取一行。比如「每种类型里最贵的那本书」:

SELECT DISTINCT ON (genre) genre, title, price
FROM books
ORDER BY genre, price DESC;   -- ON 的列必须排在 ORDER BY 最前

它按 genre 分组,每组只留 ORDER BY 排下来的第一行。在别的数据库里,这通常要写一个带窗口函数或相关子查询的啰嗦查询(第 9 章会讲那种写法)。DISTINCT ON 一行搞定,是 PG 用户的心头好。

集合运算:把两个查询的结果相加减

既然每个查询的结果都是一个集合,那两个结果就能做集合运算——并、交、差:

  • UNION:并集(A 或 B 里有的)。
  • INTERSECT:交集(A 和 B 里都有的)。
  • EXCEPT:差集(A 有但 B 没有的)。

三者都有一个 ALL 变体。默认(不带 ALL)会去重——又是「集合不重复」;带 ALL 则保留重复,也更快(省掉去重那一步)。要求是:两个查询的列数相同、类型兼容

▶ 动手 · 集合相加减

下面这台 demo 里,A = 科幻书名的集合,B = 有库存的书名的集合。挨个点四种运算,看结果怎么变。注意 UNIONUNION ALL 的行数差别——那个差,正是被去掉的重复。

数一下:A 有 3 个书名,B 有 4 个。A UNION B5 行(两个集合里出现过的所有书名,Left Hand 和 Three-Body 在两边都有、只算一次);A UNION ALL B7 行(3+4,不去重);A INTERSECT B2 行(两边都有的 Left Hand、Three-Body);A EXCEPT B1 行(科幻但没库存的 Dispossessed)。这些数字都是 demo 当场算的——你能对着 AB 两个集合亲手验证。

ORDER BY 与 LIMIT:给「无序的集合」强加一个顺序

第 3 章那条纪律,这里要还债了:表里的行本质无序。所以「拿第一条」这个说法,脱离 ORDER BY 就没有意义:

-- ✗ 危险:没有 ORDER BY,「第一条」是哪条全看运气
SELECT * FROM books LIMIT 1;

-- ✓ 当真:先定义顺序,再取第一条
SELECT * FROM books ORDER BY price DESC LIMIT 1;   -- 最贵的一本

没有 ORDER BYLIMIT,返回哪一行是不确定的——它可能受索引、受物理存储顺序、受并发影响,今天返回这条、VACUUM 之后返回另一条(卷 IV 会讲 VACUUM 怎么挪动行)。这是无数「本地好好的、线上偶发」bug 的根源。要顺序,就明确写出来。

分页用 LIMIT n OFFSET m。但要提醒一句:OFFSET 很大时会很慢(数据库得先算出前 m 行再扔掉),大数据集分页更好的办法是「键集分页」(keyset pagination,WHERE id > 上一页最后一个 id),这个卷 V 谈性能时再说。

⚠ NULL 在排序里排哪

NULL 参与排序时,PostgreSQL 默认把它排在最后ASC 时 NULL 最大,DESC 时 NULL 最前)。要控制就显式写 ORDER BY col NULLS FIRST / NULLS LAST。这一点各数据库默认不同(MySQL 把 NULL 当最小值排最前),迁移时容易被这个细节坑到。下一章我们专门收拾 NULL

⇄ 对照 MySQL
  • INTERSECT / EXCEPT:PG 一直有;MySQL 直到 8.0.31 才补上。老 MySQL 里你只能用 JOIN(模拟交集)或 LEFT JOIN ... IS NULL(模拟差集)来绕。
  • DISTINCT ON:PG 独有,MySQL 没有对应语法,得用窗口函数或 GROUP BY + 相关子查询 实现「每组取一行」。
  • LIMIT 语法:PG 是 LIMIT n OFFSET m;MySQL 支持简写 LIMIT m, n(注意顺序是「先偏移后行数」,和直觉相反),PG 不认这种简写。
  • 布尔SELECT price > 40 在 PG 里返回真正的 booleant/f),MySQL 返回 1/0(它没有独立的 boolean 类型,BOOLEAN 只是 TINYINT(1) 的别名)。

这一章的一句话

SELECT 是投影;DISTINCT、UNION 默认去重是因为「关系是集合」;LIMIT 脱离 ORDER BY 就是在赌运气,因为行本无序。

说到 NULL 排哪、NULL 参与运算——这是每个数据库用户都栽过跟头的地方。下一章,我们把 NULL 和它背后的三值逻辑彻底讲清。