LECTURE 23

聚合与数据库:GROUP BY、聚合函数与 HAVING

上一讲的 SQL 只会「一行一行地挑」。这一讲学会把行分堆,再把每一堆压成一行——这是 SQL 里唯一能算出「有多少」「最大是谁」「平均多少」的机制。

教材:Composing Programs §4.3 Declarative Programming 对应作业:HW 06

0. 本讲导读

上一讲把 SQL 的骨架搭起来了:SELECT 造一张新表,FROM 说数据在哪,WHERE 把不想要的行筛掉,JOIN … ON … 把两张表拼起来,ORDER BY 排序,LIMIT 截断。这一整套有一个共同点,值得先停下来看清楚:

它们全都是「逐行」的。 WHERE salary > 40000 判断的是当前这一行;ORDER BY name 比较的是两行之间;连 JOIN 也只是把「A 的某一行」和「B 的某一行」并成更长的一行。输入表有 9 行,输出表最多也就那 9 行的某个子集(或者 join 之后的若干组合)。没有任何一个子句能同时看见多行然后给出一个数。

于是下面这些问题,用上一讲的工具一个都答不出来:

  • 公司一共有几个人?
  • Computer 部门的平均工资是多少?
  • 哪个部门人最多?
  • 哪些主管手底下管着不止一个人?

这些问题的共同结构是:先按某个标准把行分成几堆,然后对每一堆整体算一个数。「几个人」是对全表这一堆数数;「每个部门的平均工资」是先按 division 分堆、再对每堆的 salary 求平均。这个「把多行的信息合成一个值」的动作就叫聚合(aggregation)。

本讲补上的正是这套机制,一共三件东西:

  1. GROUP BY——按指定列把行分堆,每堆最后只剩一行输出;
  2. 聚合函数(aggregation function)——COUNT / MAX / MIN / AVG / SUM,它们吃「一堆行的某一列」,吐一个值;
  3. HAVING——分完堆之后,按「堆的性质」把整堆整堆地筛掉。它对组做的事,正是 WHERE 对行做的事。

加进来之后,SQL 子句的执行顺序也要更新一次。这一讲最容易被忽略、但考试和调试时最要命的一句话是:SQL 不是从上往下执行的。WHERE 在分堆之前跑,HAVING 在分堆之后跑,SELECT 排在两者之后——理解了这个顺序,「为什么 WHERE COUNT(*) > 1 会报错」这类问题就不用背了。

本讲开头还有两段复习:Scheme 的动态作用域(dynamic scope),以及对 Lecture 21 宏(macro)展开规则的一处更正。它们跟 SQL 无关,但都是「名字怎么查」「表达式什么时候被求值」这门课的母题,也都会进期末的考察范围,所以放在第 1、2 节讲完再进 SQL。

核心结论
  • 帧的 parent 由「这一帧是被谁打开的」决定:普通过程(lambda / define)用词法作用域,parent 是定义时所在的帧;mu 过程用动态作用域,parent 是调用它的那一帧。查名字时逐层往 parent 找,跟「是谁触发的查找」无关。
  • 宏的第二步不是裸代入,而是带引号的代入(quoted substitution):未求值的算子数以 quoted 形式传给宏过程。少了这层引号,(list 'quote (list 'failed: expr)) 里的 expr 会先被算成 #f,报错信息就变成了没用的 (failed: #f)。
  • GROUP BY <columns> 把行按这些列的取值分堆,每一堆最终只产出一行。有多少个不同的取值组合,输出就有多少行。
  • 聚合函数只有在「组」这个上下文里才有意义。没有 GROUP BY 时,整张表算作一个组,所以 SELECT COUNT(*) FROM records; 只返回一行。
  • 大多数 SQL 方言要求:SELECT 里的列,要么出现在 GROUP BY 里,要么被包在聚合函数里。SQLite 不强制,它允许「裸列(bare column)」——但那一行的值是从组里任意挑的,不要依赖它。
  • HAVING 筛组,WHERE 筛行。只有写了 GROUP BY 才能写 HAVING;WHERE 里不能出现聚合函数,因为它比分堆早跑。
  • 更新后的执行顺序:FROM/JOIN…ON → WHERE/LIKE → GROUP BY → HAVING → SELECT → AS → ORDER BY → LIMIT。

1. 复习:动态作用域下,parent 到底怎么定

先把这个复习题放在最前面,因为它考的是这门课贯穿始终的那个问题:查一个名字的时候,从哪一帧开始找,找不到往哪儿走。

回忆 Scheme 里两种造过程的方式:

lambda / (define (f …) …)mu
叫什么普通过程(lexically scoped procedure)mu 过程(dynamically scoped procedure)
调用时新帧的 parent过程被定义时所在的那一帧调用它的表达式所在的那一帧
叫什么作用域词法作用域(lexical scope)动态作用域(dynamic scope)
parent 何时确定创建过程的那一刻就定死了每次调用可能不一样

关键的一句话,也是这道复习题唯一想让你记住的:

核心结论

一帧的 parent 只取决于打开这一帧的是普通过程还是 mu 过程,跟「最初是在哪种过程里触发的名字查找」毫无关系。查找本身永远是同一套:当前帧找不到,就去当前帧的 parent 找,一直递归上去。

场景一:g 是普通过程

(define x 1)
(define h (mu () x))
(define (g h)
    (h)
)
(define (f)
    (define x 2)
    (g h)
)

scm> (f)
1

为什么是 1?一步一步走。

逐步推演
1 全局帧里有三个绑定:x → 1、h → mu ()、g → func g(h)、f → func f()。注意 (mu () x) 不会记住定义环境——这正是 mu 和 lambda 的区别。
2 求值 (f)。f 是普通过程,定义在全局帧,所以新帧 f1: f 的 parent = Global。
3 在 f1 里执行 (define x 2),于是 f1 有了自己的 x → 2。全局的 x → 1 没有被改动,只是被遮蔽(shadow)了。
4 执行 (g h):算子 g 在 f1 找不到,去 parent(Global)找到普通过程 g;算子数 h 同理,取到那个 mu 过程。
5 调用 g。g 是普通过程,它定义在全局帧,所以新帧 f2: g 的 parent = Global——不是 f1。f2 里形参 h 绑到那个 mu 过程。
6 在 f2 里执行 (h)。h 是 mu 过程,所以新帧 f3: h 的 parent 是调用它的那一帧,也就是 f2。
7 在 f3 里求值过程体 x。f3 里没有 x → 去 parent f2,f2 里只有 h,没有 x → 去 f2 的 parent,也就是 Global → 找到 x → 1。
8 返回 1。
环境图(场景一)
Global frame
    x  ──→ 1
    h  ──→ mu ()  [无定义环境]
    g  ──→ func g(h)      定义于 Global
    f  ──→ func f()       定义于 Global

f1: f  [parent=Global]          ← f 是普通过程,定义在 Global
    x  ──→ 2                    ← 只遮蔽,不改全局的 x
    正在算:(g h)

f2: g  [parent=Global]          ← g 是普通过程 ⇒ parent = 定义处 Global
    h  ──→ mu ()
    正在算:(h)

f3: h  [parent=f2]              ← h 是 mu 过程 ⇒ parent = 调用处 f2
    正在算:x
    查找路径:f3 ✗ → f2 ✗ → Global ✓ 得到 1

最容易错的一步是第 5 步。很多人看到「是 mu 触发的查找」,就以为整条查找链都会变成动态的,于是沿着 f3 → f2 → f1 走,答出 2。不对。 f2 的 parent 在创建 f2 的那一刻就定死了——因为 g 是普通过程,它的 parent 只能是定义处的 Global。f1 根本不在这条链上。

场景二:把 g 也改成 mu

(define x 1)
(define h (mu () x))
(define g (mu (h)
    (h)
))
(define (f)
    (define x 2)
    (g h)
)

scm> (f)
2

唯一的改动是 g 从普通过程变成 mu 过程。改动很小,链条却整个变了。

环境图(场景二)
Global frame
    x  ──→ 1
    h  ──→ mu ()
    g  ──→ mu (h)
    f  ──→ func f()

f1: f  [parent=Global]
    x  ──→ 2
    正在算:(g h)

f2: g  [parent=f1]              ← g 现在是 mu ⇒ parent = 调用处 f1
    h  ──→ mu ()
    正在算:(h)

f3: h  [parent=f2]              ← h 是 mu ⇒ parent = 调用处 f2
    正在算:x
    查找路径:f3 ✗ → f2 ✗ → f1 ✓ 得到 2

这两个答案我在本仓库的 Scheme 解释器上实际跑过,分别是 1 和 2,与讲义一致。

场景三:自己造一个变式检验理解

讲义只给了两个场景。想确认自己真的懂了,最好的办法是自己改一处,先预测再运行。这次把 h 改回普通过程,g 保持 mu:

(define x 1)
(define (h) x)          ; h 现在是普通过程
(define g (mu (h) (h))) ; g 还是 mu
(define (f)
    (define x 2)
    (g h)
)

scm> (f)
1

预测的依据还是那一句:看开这一帧的是哪种过程。 f1: f 的 parent 是 Global;g 是 mu,所以 f2: g 的 parent 是调用处 f1;但 h 现在是普通过程,它定义在 Global,所以 f3: h 的 parent 是 Global,跟 f2、f1 全都断开了。查 x 直接在 Global 命中,得 1。实际运行确认是 1。

把三个场景并排看,规律就很清楚了——决定答案的只有「最后一环 h 是哪种过程」以及「往上追的那条链上各帧的 parent 是怎么定的」:

ghf3: h 的 parent查 x 的路径结果
场景一普通muf2f3 → f2 → Global1
场景二mumuf2f3 → f2 → f12
场景三mu普通Globalf3 → Global1

场景一和场景三的 f3 parent 完全不同,答案却都是 1——这说明不能靠背结论,只能一帧一帧推。

常见误区

误区一:以为 (define x 2) 改掉了全局的 x。 不是。define 在当前帧创建绑定。f1 里的 x → 2 和 Global 里的 x → 1 是两个独立绑定,只是查找时先撞见 f1 的那个。场景一之所以拿到 1,恰恰是因为查找链绕开了 f1。

误区二:把「谁触发查找」当成「链条怎么走」。 名字查找只有一条规则:当前帧 → parent → parent 的 parent → …。触发查找的是 h 这个 mu 过程,但走到第二步时用的是 f3 的 parent,也就是 f2;f2 的 parent 是谁,早在创建 f2 时就定好了,跟 h 无关。

误区三:忘了 mu 的 parent 是「调用处的帧」,不是「调用者过程的定义处」。 在场景二里 (h) 写在 g 的体内,而 g 的体是在 f2 里执行的,所以 f3 的 parent 是 f2,不是「g 被定义的 Global」。看的是「这个调用表达式当前在哪一帧里被求值」。

直觉

词法作用域看的是代码长什么样(源码里这个函数写在哪个函数里面),光看源码就能画出链条;动态作用域看的是运行时谁调了谁,同一个 mu 过程在不同调用点会看到不同的 x。这就是几乎所有现代语言都选词法作用域的原因:它让「这个名字指的是哪个东西」在读代码时就能确定,而不必先在脑子里模拟一遍程序运行。

2. 更正:宏的第二步是「带引号的代入」

Lecture 21 讲宏的时候,把展开的第二步说成了「代入(substitution)」,暗示可以把算子数原样塞进宏体。这句话不准确,本讲专门更正。先把正式规则抄下来(来自 Scheme Spec):

宏调用的求值三步
  1. 求值算子。如果它不是宏过程,就按普通调用表达式的步骤走。
  2. 把这个宏过程施加到未求值的算子数上。
  3. 宏返回一个值之后,在调用处的环境里求值这个返回值。

问题出在第 2 步的「未求值」到底怎么实现。准确的说法是:它是一次带引号的代入(quoted substitution)。理由不难理解——调用一个宏,等价于「把参数加上引号,调用一个普通过程,然后对结果调 eval」。既然是调普通过程,参数就得是值;一段还没求值的代码要当成值传,唯一的办法就是给它加引号,变成一个数据(list)。

拿 Lecture 21 那个 check 宏来看差别:

(define-macro (check expr)
     (list 'if expr ''passed
          (list 'quote (list 'failed: expr))
     )
)
(define x -2)


scm> (check (> x 0))
(failed: (> x 0))

check 想做的事是:条件成立就返回符号 passed,不成立就返回一段说明「哪个条件挂了」的数据——注意是把那个条件表达式原文打印出来,而不是打印它的值。所以宏体里出现了两层 quote:''passed(两个引号)和 (list 'quote …)。

如果第 2 步是裸代入(错误的理解)

假设把 expr 直接换成 (> x 0)、不加引号,宏体就变成:

; 宏体(原样)
(list 'if expr ''passed
    (list 'quote (list 'failed: expr))
)

; 裸代入 expr,NO quote
(list 'if (> x 0) ''passed
    (list 'quote (list 'failed: (> x 0)))
)

接下来这个宏体本身要被求值(它就是一段普通的 Scheme 代码,用 list 造一棵语法树)。求值 list 调用要先求值所有算子数,于是里面的 (> x 0) 在这里就被算成了 #f:

; 宏返回的是
(quote (failed: #f))

; 第 3 步在调用处求值它,解释器显示
(failed: #f)

(failed: #f) 完全没用——它只告诉你「有个东西是假的」,却没告诉你哪个条件假了。这正是必须加引号的原因。

正确的:带引号的代入

; 宏体(原样)
(list 'if expr ''passed
    (list 'quote (list 'failed: expr))
)

; 代入 expr,WITH quote
(list 'if '(> x 0) ''passed
    (list 'quote (list 'failed: '(> x 0)))
)
逐步推演:这段代码求值成什么
1 最内层:(list 'failed: '(> x 0))。两个算子数都是 quoted 的数据,求值成符号 failed: 和列表 (> x 0)。结果是列表 (failed: (> x 0))。
2 外一层:(list 'quote (failed: (> x 0))) ⇒ 列表 (quote (failed: (> x 0)))。多包的这层 quote 是给第 3 步准备的——不然第 3 步会把 (failed: …) 当成一个函数调用去执行。
3 最外层 list:拼出 (if (> x 0) 'passed (quote (failed: (> x 0))))。这就是宏的返回值——一段代码。
4 第 3 步:在调用处的环境求值这段代码。此处 x 是 -2,(> x 0) 求值为 #f,走 else 分支,得到 (failed: (> x 0))。
5 显示:(failed: (> x 0))。条件的原文被保留下来了。

对照一下 ''passed:外层 quote 让宏返回的代码里出现 'passed,第 3 步求值 'passed 得到符号 passed。要是只写一个 'passed,宏返回的代码里就成了裸的 passed,第 3 步会把它当成一个名字去查——大概率报 Error: unknown identifier: passed。

直觉:quote 的层数怎么数

宏涉及两轮求值:一轮把宏体算成「返回的代码」,一轮把「返回的代码」再算成最终值。所以你要问自己:这个东西希望在第几轮存活下来? 想让它撑过第一轮,加一层 quote;想让它撑过两轮(最终显示为数据而不是被执行),就得加两层。''passed 和 (list 'quote …) 是同一个道理的两种写法。

注意

本仓库 proj/scheme 里的 Scheme 解释器是项目起手代码,define-macro(Problem 18)还没实现。直接跑上面这段会得到:

scm> (define-macro (check expr) ...)
Error: unknown identifier: define-macro

所以本节的展开过程依据的是讲义给出的推导,不是本机实测。等你做完 Scheme 项目的宏那一问,可以回来把这两种版本各跑一遍验证。

3. 热身:把同一张表 join 三次

进入聚合之前,先做完上一讲留下的那道 23.sql Q1。它不涉及任何新语法,纯粹练「自连接(self join)」——同一张表在 FROM 里出现多次,靠别名区分。这个技巧在做 HW 06 时会反复用到。

数据:records 表

本讲用的示例表是 61A 一直在用的 records,五列:name、division、title、salary、supervisor。

namedivisiontitlesalarysupervisor
Alyssa P HackerComputerProgrammer40000Ben Bitdiddle
Ben BitdiddleComputerWizard60000Oliver Warbucks
Cy D FectComputerProgrammer35000Ben Bitdiddle
Eben ScroogeAccountingChief Accountant75000Oliver Warbucks
Lana LambdaAdministrationSecretary25000Lana Lambda
Lem E TweakitComputerTechnician25000Ben Bitdiddle
Louis ReasonerComputerProgrammer Trainee30000Alyssa P Hacker
Oliver WarbucksAdministrationBig Wheel150000Oliver Warbucks
Robert CratchetAccountingScrivener18000Eben Scrooge
关于这张表的来源

name / division 两列直接取自本讲幻灯片;supervisor 一列是从 23-sol.sql 给出的期望输出反推出来的(尤其是 Lana Lambda 的 supervisor 是她自己,这跟教材 §4.3 里的老版本不同);title / salary 取自教材 §4.3 的 records 表。本页所有查询结果都是在本机 SQLite 3.37.2 上用这张表实际跑出来的,并且 Q1 的输出与官方期望输出逐行一致。

题目

要求输出三列:员工 employee、他的主管 middle_manager、他主管的主管 skip_manager。按这三列依次排序。有人是自己的主管,不用特殊处理。

怎么想到的

看到题的第一反应通常是「这得递归吧?沿着 supervisor 一路往上爬」。不用。 题目要的层数是固定的——正好两层。一旦层数固定,就不是递归问题,而是「把表接起来」的问题。

关键的思路转换是这一句:records 这张表既是「员工表」,也是「主管表」——它们是同一张表,只是用途不同。 于是:

逐步推演:拼出这三列
1 需要三份 records:一份当员工(叫它 a),一份当主管(b),一份当主管的主管(c)。必须起别名,否则 name 指哪一份是说不清的。
2 「b 是 a 的主管」怎么写?a 的 supervisor 列存的是名字,而 b 这一行的 name 也是名字。所以条件是 a.supervisor = b.name。
3 同理,「c 是 b 的主管」就是 b.supervisor = c.name。两个条件用 AND 连起来。
4 输出的三列分别是 a.name、b.name、c.name,用 AS 改成题目要的列名。
5 ORDER BY 直接用别名(ORDER BY 排在 AS 之后执行,所以这时别名已经存在了)。
SELECT a.name AS employee, b.name AS middle_manager, c.name AS skip_manager
FROM records AS a
JOIN records AS b
JOIN records AS c
ON a.supervisor = b.name AND b.supervisor = c.name
ORDER BY employee, middle_manager, skip_manager;

实际输出(本机 SQLite):

employee|middle_manager|skip_manager
Alyssa P Hacker|Ben Bitdiddle|Oliver Warbucks
Ben Bitdiddle|Oliver Warbucks|Oliver Warbucks
Cy D Fect|Ben Bitdiddle|Oliver Warbucks
Eben Scrooge|Oliver Warbucks|Oliver Warbucks
Lana Lambda|Lana Lambda|Lana Lambda
Lem E Tweakit|Ben Bitdiddle|Oliver Warbucks
Louis Reasoner|Alyssa P Hacker|Ben Bitdiddle
Oliver Warbucks|Oliver Warbucks|Oliver Warbucks
Robert Cratchet|Eben Scrooge|Oliver Warbucks

验证:追一行

拿 Louis Reasoner 走一遍。三重 join 概念上就是三层嵌套循环:对 a 的每一行,配 b 的每一行,再配 c 的每一行,一共 $9^3 = 729$ 种组合,然后用 ON 的条件筛。

逐步推演:a = Louis Reasoner 这一行
1 a.supervisor 是 'Alyssa P Hacker'。要满足 a.supervisor = b.name,b 只能是 Alyssa P Hacker 那一行——9 个候选里只剩 1 个。
2 此时 b.supervisor 是 'Ben Bitdiddle'。要满足 b.supervisor = c.name,c 只能是 Ben Bitdiddle 那一行。
3 这 729 种组合里,含 a = Louis 的只有一种存活。SELECT 取出三个 name,得到 Louis Reasoner|Alyssa P Hacker|Ben Bitdiddle。✓
4 再看 Lana Lambda:她的 supervisor 是自己,所以 b 也是她那一行;b.supervisor 还是她自己,c 也是她。输出 Lana Lambda|Lana Lambda|Lana Lambda——题目说这样就是对的。✓
常见误区

误区一:条件写反成 a.name = b.supervisor。 这句话的意思变成了「a 是 b 的主管」,输出会整个倒过来。判断方法:supervisor 列存的是上一级的名字,所以「谁的 supervisor」要放在下级那一侧。a 是员工,b 是上级,所以是 a.supervisor = b.name。

误区二:不起别名。 直接写 SELECT name FROM records, records, records;,SQLite 会报(实测):

ambiguous column name: name

三张表都有 name 列,解释器无从判断你指的是哪一张。自连接必须起别名,没有例外。

误区三:以为需要写两个 ON。 写成 JOIN records AS b ON … JOIN records AS c ON … 也可以(这是标准写法),但本讲的写法是把两个 JOIN 连写、条件全放进一个 ON 里用 AND 连接,SQLite 接受这种写法。两种都对,别被形式绕住——JOIN … ON 的本质就是「先做笛卡尔积,再用条件筛」。

4. 为什么需要聚合:逐行的工具答不了整体的问题

现在换个问题:公司一共有几个人?

用上一讲的全部工具试一遍,你会发现每一样都差一点:

你可能想到的写法实际得到什么为什么不行
SELECT * FROM records;9 行完整数据数是要你自己数的,SQL 没告诉你
SELECT name FROM records;9 行名字同上,还是 9 行,不是数字 9
SELECT name FROM records LIMIT 1;1 行LIMIT 是截断,不是计数

症结在于:SELECT 是一个「对每一行做同样的事」的子句。 输入表有几行,它就产出几行(WHERE 只能减少行数,ORDER BY 只能调换行的顺序)。而「有几个人」这个答案,需要同时看到全部 9 行才算得出来。这是一个逐行工具在结构上做不到的事。

再看一个更麻烦的:每个部门的平均工资是多少? 这里有两层需求叠在一起:

  1. 分堆:Computer 的行归一堆、Accounting 的行归一堆、Administration 的行归一堆;
  2. 压缩:每一堆的 salary 列合成一个平均值。

这正好对应本讲的两样东西:GROUP BY 负责第 1 步,聚合函数负责第 2 步。它们几乎总是一起出现,因为分了堆不压缩没意义,不分堆也可以压缩(整张表就是一堆)。

直觉:跟 Python 里的什么对应

GROUP BY ≈ 你在 Python 里写过的「用字典把元素归类」:d[row.division].append(row)。聚合函数 ≈ 对每个 d[key] 这个列表调 len / max / sum / sum(...)/len(...)。SQL 只是把这个「先分组、再规约」的常见套路做成了内建语法,你不用手写循环——这就是声明式编程:你说要什么,数据库自己想办法。

加上这两样之后,一条查询的完整骨架长这样(子句必须按这个顺序书写,虽然不是按这个顺序执行):

SELECT <columns>
FROM <table>
JOIN <table>
ON <condition>
WHERE <condition>
GROUP BY <columns>
ORDER BY <columns>
LIMIT <n>;

5. GROUP BY:把行分成堆

GROUP BY <columns> 做的事只有一件:把 <columns> 上取值相同的行归到同一堆里。 然后——这是最重要的一句——每一堆最终只输出一行。

所以你可以直接算出输出有几行:看 GROUP BY 那些列一共有多少种不同的取值组合。 9 行的 records 按 division 分堆,division 只有 Computer / Accounting / Administration 三种值,输出就是 3 行。

GROUP BY 可视化:按左列颜色把 6 行分成三堆
左边是原表,6 行、2 列。GROUP BY [left-column] 按左列的颜色把行分堆:蓝色一堆(3 行)、橙色一堆(1 行)、紫色一堆(2 行)。分堆只看 GROUP BY 里写的那一列,右列长什么样完全不影响分堆结果。这一步之后,数据库手里握着的不再是「6 行」,而是「3 堆」。
GROUP BY 可视化第二步:每堆用 COUNT(*) 压成一行
第二步把每一堆压成一行:SELECT [left-column], COUNT(*) 里的 [left-column] 取这一堆共有的那个颜色,COUNT(*) 数这一堆有几行——蓝 3、橙 1、紫 2。输出正好 3 行,一堆一行。注意右列消失了:它既不在 GROUP BY 里也不在聚合函数里,没有位置放它。

在 records 上真跑一遍

sqlite> SELECT division, COUNT(*) FROM records GROUP BY division;
division|COUNT(*)
Accounting|2
Administration|2
Computer|5
逐步推演:这 9 行是怎么变成 3 行的
1 FROM records:拿到 9 行。
2 没有 WHERE,9 行全留。
3 GROUP BY division:扫一遍,按 division 的值分堆。
Accounting 堆 = {Eben Scrooge, Robert Cratchet}
Administration 堆 = {Lana Lambda, Oliver Warbucks}
Computer 堆 = {Alyssa P Hacker, Ben Bitdiddle, Cy D Fect, Lem E Tweakit, Louis Reasoner}
4 SELECT division, COUNT(*):对每一堆算一次。division 在堆内是常数(这正是它能出现在 SELECT 里的原因),COUNT(*) 数堆里的行数。
5 3 堆 ⇒ 3 行输出。SQLite 默认按分组列排序输出,所以看起来是字母序,但不要依赖这个——要确定的顺序就写 ORDER BY。

裸列(bare column):SELECT 里放了不该放的东西

现在看讲义里那个专门用来「踩坑」的例子。原表 9 行:

sqlite> SELECT name, division FROM records;
Alyssa P Hacker|Computer
Ben Bitdiddle|Computer
Cy D Fect|Computer
Eben Scrooge|Accounting
Lana Lambda|Administration
Lem E Tweakit|Computer
Louis Reasoner|Computer
Oliver Warbucks|Administration
Robert Cratchet|Accounting

那么下面这条会输出什么?

SELECT name, division
FROM records
GROUP BY division;
Group By (3 of 4):GROUP BY division 之后每组只剩一个 name
答案是 3 行,因为 division 只有 3 个不同的值。division 那一列没问题——同一堆里它本来就只有一个值。有问题的是 name:Computer 堆里有 5 个不同的名字,但输出只有一格,数据库只能随便挑一个。这里挑中了 Alyssa P Hacker,纯粹因为她在表里排第一。

本机实测结果与幻灯片一致:

sqlite> SELECT name, division FROM records GROUP BY division;
name|division
Eben Scrooge|Accounting
Lana Lambda|Administration
Alyssa P Hacker|Computer

像 name 这样既不在 GROUP BY 里、也不被聚合函数包住的列,SQLite 管它叫裸列(bare column)。

核心结论

大多数 SQL 方言(MySQL 严格模式、PostgreSQL 等)会直接拒绝这条查询并报错:有 GROUP BY 时,SELECT 里的列必须要么出现在 GROUP BY 中,要么被包在聚合函数里。

61A 用的 SQLite 不强制这条规则,它会挑一行的值填进去。所以这条查询在 61A 的环境里能跑——但它挑哪一行是没有承诺的,你不能拿它当逻辑依据。

常见误区

误区一:以为 SELECT name, division … GROUP BY division 会返回「每个部门的所有人」。 不会。GROUP BY 的输出一堆只有一行,这是硬规则。想看每个部门的所有人,你根本不需要 GROUP BY,写 SELECT name, division FROM records ORDER BY division; 就够了。

误区二:以为裸列会挑「最大/最小的那一行」。 有一个特别坑的现象:

sqlite> SELECT division, name, MAX(salary) FROM records GROUP BY division;
Accounting|Eben Scrooge|75000
Administration|Oliver Warbucks|150000
Computer|Ben Bitdiddle|60000

看起来 name 正好是每个部门工资最高的人,很诱人。这是 SQLite 的一个实现特性(当聚合函数是 MAX 或 MIN 时,裸列取自那一行),不是 SQL 标准,别的数据库不这么干。而且一旦你把 MAX 换成 COUNT 或 AVG,这个「巧合」立刻消失:

sqlite> SELECT division, name, COUNT(*) FROM records GROUP BY division;
Accounting|Eben Scrooge|2
Administration|Lana Lambda|2
Computer|Alyssa P Hacker|5

Alyssa 既不是 Computer 里工资最高的也不是最低的——它只是第一行。在 61A 里写查询,请当作「裸列的值不可预测」。

误区三:以为 GROUP BY 会排序。 输出恰好按分组列有序,是因为 SQLite 常常用排序来实现分组。这是实现细节。题目要求排序就老老实实写 ORDER BY。

按多列分堆

GROUP BY 后面可以跟多列,这时「同一堆」的意思是这几列的值全都相同:

sqlite> SELECT fur, height FROM dogs GROUP BY fur, height;
curly|31
curly|32
long|26
long|46
long|47
short|28
short|35
short|52

(这里用的是 HW 06 的 dogs 表,8 只狗。因为 8 只狗的 (fur, height) 组合两两不同,所以分出了 8 堆,每堆 1 行——分堆分得太细,等于没分。)

GROUP BY 后面也可以跟表达式,不一定是列名。比如按身高的十位数分档:

sqlite> SELECT height/10, COUNT(*) FROM dogs GROUP BY height/10;
2|2
3|3
4|2
5|1

SQL 里 整数/整数 是整除,所以 26/10 是 2。这条查询说:身高 20 多的有 2 只,30 多的 3 只,40 多的 2 只,50 多的 1 只。

GROUP BY 和 DISTINCT 的关系

不带任何聚合函数的 GROUP BY,效果跟 SELECT DISTINCT 一样——都是「把重复的取值合成一个」:

sqlite> SELECT division FROM records GROUP BY division;
Accounting
Administration
Computer

sqlite> SELECT DISTINCT division FROM records;
Computer
Accounting
Administration

内容一样,顺序不一样(DISTINCT 按遇到的先后,GROUP BY 这里恰好按字母序)。两者的意图不同,选哪个看你想表达什么:

SELECT DISTINCT cGROUP BY c
想表达「去掉重复」「按 c 分堆,然后对每堆算点什么」
能配聚合函数吗不能(没有「组」的概念)能,这才是它存在的理由
能配 HAVING 吗不能能
什么时候用只想看有哪些取值只要出现「每个……的……」这种说法

看到题目里出现「每个」「各自」「分别」,条件反射就该是 GROUP BY。

6. 聚合函数:把一堆行压成一个值

聚合函数是唯一能「同时看见多行」的东西。它们全都是这个形状:吃一组行的某一列,吐一个值。

函数做什么在 records 上的例子结果
COUNT(*)数这一组有几行SELECT COUNT(*) FROM records;9
COUNT(column)数这一列的行数见下方 NULL 的说明—
COUNT(DISTINCT column)数这一列有几个不同的值SELECT COUNT(DISTINCT division) FROM records;3
MAX(column)这一列的最大值SELECT MAX(salary) FROM records;150000
MIN(column)最小值SELECT MIN(salary) FROM records;18000
AVG(column)平均值SELECT AVG(salary) FROM records;50888.88888888889
SUM(column)求和SELECT SUM(salary) FROM records;458000

COUNT(*) 和 COUNT(column) 技术上有区别,但不在 61A 的范围内——讲义明确说了这一点。想知道区别的话:COUNT(*) 数行,COUNT(column) 数这一列非 NULL 的值。造一张有空值的小表就能看出来:

sqlite> CREATE TABLE t (a TEXT, b INTEGER);
sqlite> INSERT INTO t VALUES ('x', 1), ('y', NULL), ('z', 3), ('x', NULL);
sqlite> SELECT COUNT(*), COUNT(b), COUNT(DISTINCT a) FROM t;
4|2|3

4 行,但 b 只有 2 个非空值;a 列有 x, y, z, x 四个值、3 种取值。61A 的作业表基本没有 NULL,所以两种写法结果一样,用 COUNT(*) 就行。

没有 GROUP BY 时,整张表就是一个组

这是理解聚合函数最省事的一条心法。SELECT COUNT(*) FROM records; 之所以返回一行,不是因为有什么特殊规则,而是因为「不分堆」就等于「所有行归成一堆」,一堆一行。

逐步推演:SELECT AVG(salary) FROM records WHERE division = 'Computer';
1 FROM records:9 行。
2 WHERE division = 'Computer':逐行判断,留下 5 行(Alyssa 40000、Ben 60000、Cy 35000、Lem 25000、Louis 30000)。
3 没有 GROUP BY,这 5 行整体算一组。
4 AVG(salary):$(40000+60000+35000+25000+30000)/5 = 190000/5 = 38000$。
5 输出一行:38000.0。注意是浮点数——SQLite 的 AVG 永远返回浮点,即使能整除。

分堆之后,每一堆各算一次

把上一步的 WHERE 换成 GROUP BY,就从「算一个部门」变成「一次算完所有部门」:

sqlite> SELECT division, MAX(salary), MIN(salary), AVG(salary), SUM(salary), COUNT(*)
   ...> FROM records GROUP BY division;
division|MAX(salary)|MIN(salary)|AVG(salary)|SUM(salary)|COUNT(*)
Accounting|75000|18000|46500.0|93000|2
Administration|150000|25000|87500.0|175000|2
Computer|60000|25000|38000.0|190000|5

验证 Computer 那一行:5 个人,工资 40000/60000/35000/25000/30000。最大 60000 ✓,最小 25000 ✓,和 190000 ✓,平均 38000 ✓,人数 5 ✓。同一次分堆可以喂给任意多个聚合函数——数据库只扫一遍,每个函数各自累加自己的东西。

直觉:聚合函数像 reduce

回忆你写过的 reduce(add, lst)。SUM 就是 reduce 配 add,MAX 就是 reduce 配 max,COUNT 就是 reduce 配「每次加 1」。它们的共性是把一个序列折叠成一个值。GROUP BY 则相当于先把序列切成若干子序列,再对每个子序列 reduce 一次。这两个动作合起来在别的语言里叫 map-reduce,SQL 只是给了它一个声明式的写法。

MAX / MIN 对字符串也管用

sqlite> SELECT MAX(name), MIN(name) FROM records;
Robert Cratchet|Alyssa P Hacker

按字典序比较。字符串列上的 MAX 通常没什么实际意义,但知道它不报错就行。

常见误区

误区一:以为 SELECT MAX(salary), name FROM records; 给出「工资最高的人」。 实测:

sqlite> SELECT MAX(salary), name, division FROM records;
150000|Oliver Warbucks|Administration

这次碰巧对了——因为 SQLite 在只有 MAX 一个聚合函数时会让裸列取自那一行。但这仍然是裸列,不是标准行为。想稳妥地拿到「工资最高的人」,应该写成排序加截断:

SELECT name, salary FROM records ORDER BY salary DESC LIMIT 1;

误区二:把聚合函数写在 WHERE 里。 这是本讲第二高频的错误,第 8 节会专门讲执行顺序。先看真实报错:

sqlite> SELECT division, COUNT(*) FROM records WHERE COUNT(*) > 1 GROUP BY division;
misuse of aggregate: COUNT()

误区三:以为空结果集的 SUM 是 0。 不是,是 NULL:

sqlite> SELECT COUNT(*), SUM(salary), MAX(salary) FROM records WHERE salary > 1000000;
0|NULL|NULL

只有 COUNT 在没有行时给 0,其他几个都给 NULL。注意这里仍然输出了一行——因为没有 GROUP BY,「空的一堆」也还是一堆。但一旦加上 GROUP BY,结果就是零行:

sqlite> SELECT division, COUNT(*) FROM records WHERE salary > 1000000 GROUP BY division;
<没有任何输出>

因为一行都没剩,就分不出任何一堆。「一堆一行」这条规则里,零堆对应零行。

COUNT(DISTINCT c) 和 SELECT DISTINCT c

两者容易混。SELECT DISTINCT division FROM records; 给你有哪些取值,SELECT COUNT(DISTINCT division) FROM records; 给你有几种取值:

sqlite> SELECT DISTINCT division FROM records;
Computer
Accounting
Administration

sqlite> SELECT COUNT(DISTINCT division) FROM records;
3

顺带注意 SELECT DISTINCT 的输出没有排序(这里是 Computer 排在最前,因为 Alyssa 是表里第一行)。这也再次说明:要顺序就写 ORDER BY。

7. HAVING:给「组」用的 WHERE

问题:哪些主管手底下管着不止一个人?

第一步好办,按 supervisor 分堆数人数:

sqlite> SELECT supervisor, COUNT(*) FROM records GROUP BY supervisor;
supervisor|COUNT(*)
Alyssa P Hacker|1
Ben Bitdiddle|3
Eben Scrooge|1
Lana Lambda|1
Oliver Warbucks|3

第二步要把 COUNT(*) = 1 的那三行去掉。直觉上该用 WHERE,但 WHERE 在分堆之前跑,那时候还没有「组」这个东西,更没有 COUNT(*) 这个值。所以 SQL 另给了一个子句:

核心结论
  • HAVING <condition> 按条件筛掉整个组,就像 WHERE 筛掉整行一样。
  • HAVING 里可以调用聚合函数——它跑在 GROUP BY 之后,组已经形成了。
  • 只有写了 GROUP BY,才能写 HAVING。
sqlite> SELECT supervisor, COUNT(*) FROM records
   ...> GROUP BY supervisor HAVING COUNT(*) > 1;
supervisor|COUNT(*)
Ben Bitdiddle|3
Oliver Warbucks|3
HAVING 可视化:把 COUNT(*) 不大于 1 的那一组整组划掉
接着前面的 GROUP BY 图往下走。分完堆、算完 COUNT(*) 之后有 3 组:蓝 3、橙 1、紫 2。HAVING COUNT(*) > 1 把橙色那一组整组划掉——注意划掉的是「组」这一整个单位,不是组里的某几行。最后输出蓝和紫两行。
逐步推演:这条查询的五个阶段
1 FROM records:9 行进来。
2 没有 WHERE:9 行全留。
3 GROUP BY supervisor:分成 5 堆。Ben 手下 {Alyssa, Cy, Lem};Oliver 手下 {Ben, Eben, Oliver 自己};Alyssa 手下 {Louis};Eben 手下 {Robert};Lana 手下 {Lana 自己}。
4 HAVING COUNT(*) > 1:对每一堆算 COUNT(*),3、3、1、1、1。留下前两堆,扔掉后三堆。
5 SELECT supervisor, COUNT(*):剩下 2 堆,输出 2 行。

HAVING 里能写什么

HAVING 的条件不一定要含聚合函数,写普通列也合法(虽然那种情况通常该用 WHERE):

sqlite> SELECT division, COUNT(*) FROM records
   ...> GROUP BY division HAVING division LIKE 'A%';
Accounting|2
Administration|2

也可以在 HAVING 里用一个没出现在 SELECT 里的聚合函数——「按什么筛」和「输出什么」是两码事:

sqlite> SELECT division, COUNT(*) FROM records
   ...> GROUP BY division HAVING MAX(salary) > 70000;
Accounting|2
Administration|2

这条查询问的是「哪些部门里有人年薪超过 70000」,输出的却是这些部门的人数。Computer 最高才 60000,所以被筛掉了。

另外,SQLite 允许 HAVING 引用 SELECT 里定义的别名:

sqlite> SELECT division, AVG(salary) AS avg_sal FROM records
   ...> GROUP BY division HAVING avg_sal > 40000;
Accounting|46500.0
Administration|87500.0
注意

上面这个别名写法虽然在 SQLite 里能跑,但严格按执行顺序(HAVING 排在 SELECT/AS 之前)它是不该成立的,别的数据库不一定接受。考试和作业里把聚合函数原样写一遍更稳:HAVING AVG(salary) > 40000。ORDER BY 用别名则完全没问题,因为它确实排在 AS 之后。

常见误区

误区一:没有 GROUP BY 就写 HAVING。 真实报错:

sqlite> SELECT division FROM records HAVING COUNT(*) > 1;
a GROUP BY clause is required before HAVING

报错信息几乎是把讲义那句话直接念了一遍。

误区二:拿 HAVING 当 WHERE 用来筛行。 比如想「只统计 Computer 部门的人」,写成 GROUP BY supervisor HAVING division = 'Computer'。这不会按你想的那样工作:division 在 supervisor 分出来的组里是个裸列,值是随便挑的一行的,于是整组的去留取决于那一行是谁。筛行用 WHERE,筛组用 HAVING,这条界线不要跨。

误区三:以为 HAVING 能替代 WHERE 提升性能。 反了。WHERE 在分堆前把行扔掉,进入分堆的数据更少;HAVING 得等到组建好、聚合值算完才动手。能用 WHERE 表达的条件就用 WHERE。

一句话记住区别

WHERE 的条件里,每个名字指的是某一行的值;HAVING 的条件里,每个名字指的是某一组的性质。所以 WHERE 里不能出现聚合函数(一行谈不上聚合),HAVING 里可以。

把「哪个部门人最多」写完整

导读里那四个问题,现在只剩最后一个没答。「人最多」这种「取第一名」的需求,SQL 里的标准做法不是 HAVING,而是先聚合、再排序、再截断:

SELECT division, COUNT(*) AS n
FROM records
GROUP BY division
ORDER BY n DESC
LIMIT 1;
division|n
Computer|5
逐步推演:为什么这五步能凑出「最多」
1 GROUP BY division(第 3 步)分出 3 堆。
2 SELECT division, COUNT(*)(第 5 步)产出中间表 Accounting|2、Administration|2、Computer|5。
3 AS n(第 6 步)给第二列起名 n。
4 ORDER BY n DESC(第 7 步):这时 n 这个别名已经存在,所以能用。排成 Computer|5、Accounting|2、Administration|2。
5 LIMIT 1(第 8 步)取第一行。

这里 ORDER BY 排的是聚合之后的那张 3 行的表,不是原始的 9 行——这正是执行顺序把 ORDER BY 放在第 7 步的意义。

注意:并列第一会被截掉

LIMIT 1 只管取第一行,不管有没有并列。这里 Accounting 和 Administration 都是 2 人,如果问的是「人最少的部门」,ORDER BY n LIMIT 1 只会给你其中一个,而且是哪一个都不好说。要处理并列,得先把最大值算出来再回去比——那需要子查询,超出本讲范围。做题时看清题目要不要处理并列。

8. 更新后的执行顺序

SQL 是声明式(declarative)语言:你写的是「我要什么」,不是「怎么算」。所以它不是从上往下执行的。加上 GROUP BY 和 HAVING 之后,完整顺序是八步:

更新后的 SQL 执行顺序:FROM/JOIN、WHERE/LIKE、GROUP BY、HAVING、SELECT、AS、ORDER BY、LIMIT
加粗的第 3、4 步是本讲新增的。注意 SELECT 排在第 5 位——它比 WHERE、GROUP BY、HAVING 都晚,尽管它写在最前面。这一条能解释本讲绝大多数「为什么报这个错」。
#子句它拿到的是什么它交出去的是什么
1FROM, JOIN … ON …磁盘上的表一堆行(join 过的话是拼接后的宽行)
2WHERE, LIKE行更少的行
3GROUP BY行组(从这里开始单位变了)
4HAVING组更少的组
5SELECT组(或行)输出表的各列,一组一行
6AS列改了名的列
7ORDER BY输出表排好序的输出表
8LIMIT输出表前 n 行

把这张表读懂,本讲的四个「为什么」全都不用背了:

用执行顺序解释四个报错
1 为什么 WHERE COUNT(*) > 1 报 misuse of aggregate: COUNT()? WHERE 是第 2 步,GROUP BY 是第 3 步。WHERE 跑的时候组还不存在,COUNT(*) 无从算起。
2 为什么没有 GROUP BY 就不能写 HAVING? HAVING 是第 4 步,它的输入必须是第 3 步产出的「组」。没有第 3 步,第 4 步没有输入。
3 为什么 ORDER BY 能用 AS 起的别名,WHERE 不能? AS 是第 6 步,ORDER BY 是第 7 步(在它之后,别名已存在),WHERE 是第 2 步(在它之前,别名还不存在)。
4 为什么 SELECT 里放裸列会出问题? SELECT 是第 5 步,它面对的已经是「组」不是「行」了。要它填一个只有单行才有意义的值,它只能瞎猜。

写查询的固定套路

上一讲给过一个「按问题清单写查询」的方法,现在补上两条,正好和执行顺序一一对应:

问自己写哪个子句
数据在哪张(几张)表里?怎么对应起来?FROM, JOIN … ON …
有哪些行根本不该参与?WHERE, LIKE
要按什么把行分堆?GROUP BY
有哪些堆不该出现在结果里?HAVING
输出要哪几列?哪些需要聚合?SELECT
列名要改吗?AS
要排序吗?ORDER BY
只要前几行吗?LIMIT

注意提问的顺序就是执行的顺序,而不是书写的顺序。按这个顺序想,写出来的时候再按 SQL 要求的语法顺序排列即可。

常见误区

把子句写乱顺序。 SQL 对书写顺序也有硬性要求,虽然它跟执行顺序不一样。GROUP BY 必须在 WHERE 之后、ORDER BY 之前;HAVING 必须紧跟在 GROUP BY 之后。写反了直接是语法错误(实测):

sqlite> SELECT division, COUNT(*) FROM records HAVING COUNT(*) > 1 GROUP BY division;
near "GROUP": syntax error

记法:书写顺序 = 执行顺序,只是把 SELECT(第 5 步)拎到了最前面。

还有一个相关的坑:想给 WHERE 用 SELECT 里的别名,同样不行。SELECT division, COUNT(*) AS n FROM records WHERE n > 1 GROUP BY division; 报的是 misuse of aggregate: COUNT()——SQLite 把 n 展开回它代表的 COUNT(*),然后发现聚合函数出现在了 WHERE 里。

9. 实战:把中文需求翻译成查询

讲义这一节让大家在本地装好 SQLite,用 Spotify 和 IMDb 两个数据集练手(仓库地址 github.com/phrdang/cs61a-su26-lec23,跟着 README 的 Installation 步骤走)。那两份数据本页拿不到,但套路完全一样,所以下面改用 HW 06 里现成的 dogs / parents / sizes 三张表来练——这也正是你接下来要做的作业。

dogs:    name       fur      height
         ace        long     26
         bella      short    52
         charlie    long     47
         daisy      long     46
         ellie      short    35
         finn       curly    32
         ginger     short    28
         hank       curly    31

parents: parent  child        sizes:  size      min  max
         ace     bella                toy       24   28
         ace     charlie              mini      28   35
         daisy   hank                 medium    35   45
         finn    ace                  standard  45   60
         finn    daisy
         finn    ginger
         ellie   finn

需求一:每种毛发的狗有几只、平均多高?只要数量超过 2 的,按平均身高从高到低排

按套路逐条问:

逐步推演:从需求到查询
1 数据在哪? 只用到 fur 和 height,都在 dogs 里。⇒ FROM dogs,不需要 join。
2 要排除哪些行? 没有。⇒ 不写 WHERE。
3 按什么分堆? 「每种毛发」⇒ GROUP BY fur。
4 要排除哪些堆? 「数量超过 2」是对堆的条件 ⇒ HAVING COUNT(*) > 2。
5 输出什么? 毛发、只数、平均身高 ⇒ SELECT fur, COUNT(*) AS n, AVG(height) AS avg_h。
6 排序? 按平均身高降序 ⇒ ORDER BY avg_h DESC(别名在 ORDER BY 里合法)。
SELECT fur, COUNT(*) AS n, AVG(height) AS avg_h
FROM dogs
GROUP BY fur
HAVING COUNT(*) > 2
ORDER BY avg_h DESC;
fur|n|avg_h
long|3|39.666666666666664
short|3|38.333333333333336

验证:curly 只有 finn 和 hank 两只,被 HAVING 筛掉了。long 是 ace 26 / charlie 47 / daisy 46,平均 $(26+47+46)/3 = 119/3 \approx 39.67$ ✓。short 是 bella 52 / ellie 35 / ginger 28,平均 $115/3 \approx 38.33$ ✓。39.67 > 38.33,所以 long 排前面 ✓。

需求二:哪些家长有不止一个孩子?他们最高的孩子多高?

这次要 join:孩子的名字在 parents.child,身高在 dogs.height,两张表靠名字对上。

逐步推演
1 数据在哪? parents(谁是谁的孩子)+ dogs(孩子多高)。连接条件:p.child = d.name。
2 join 之后每一行是「一对(家长, 这个孩子的完整信息)」,一共 7 行(parents 有 7 行,每个 child 都能在 dogs 里找到)。
3 按什么分堆? 「哪些家长」⇒ GROUP BY p.parent。分出 4 堆:ace(2)、daisy(1)、finn(3)、ellie(1)。
4 筛堆:HAVING COUNT(*) > 1,留下 ace 和 finn。
5 输出:家长名、孩子数、孩子里最高的身高 ⇒ MAX(d.height)。
SELECT p.parent, COUNT(*) AS num_kids, MAX(d.height)
FROM parents AS p
JOIN dogs AS d
ON p.child = d.name
GROUP BY p.parent
HAVING COUNT(*) > 1;
parent|num_kids|MAX(d.height)
ace|2|52
finn|3|46

验证:ace 的孩子是 bella(52) 和 charlie(47),最高 52 ✓。finn 的孩子是 ace(26)、daisy(46)、ginger(28),最高 46 ✓。

注意 join 会改变计数

这里 COUNT(*) 数的是 join 之后的行数,不是原表的行数。恰好每个 child 在 dogs 里只匹配一行,所以「行数」正好等于「孩子数」。如果某个 child 在 dogs 里能匹配多行,COUNT(*) 就会被放大。先想清楚「join 完的一行代表什么」,再决定 COUNT(*) 数的是什么。

需求三:WHERE 和 HAVING 同时上场

「只看身高超过 30 的狗,每种毛发各有几只」——「身高超过 30」是行的条件,必须用 WHERE,而且它在分堆之前就把矮狗踢掉了:

sqlite> SELECT fur, COUNT(*) FROM dogs WHERE height > 30 GROUP BY fur;
curly|2
long|2
short|2

对比不加 WHERE 的结果 curly|2 long|3 short|3:long 少了 ace(26),short 少了 ginger(28)。

逐步推演:为什么顺序至关重要
1 第 2 步 WHERE height > 30:8 行剩 6 行(踢掉 ace 26、ginger 28)。
2 第 3 步 GROUP BY fur:对这 6 行分堆 ⇒ curly {finn, hank}、long {charlie, daisy}、short {bella, ellie}。
3 第 5 步 SELECT:三堆各输出一行,COUNT(*) 全是 2。
4 如果把条件挪到 HAVING height > 30:先用全部 8 行分堆,再对每堆看那个裸列 height——值是随便挑的,结果不可预测。这就是两者不能互换的原因。

需求四:结合上一讲的 join 条件

「按体型分类,每一类有几只狗」。体型不是 dogs 表里的列,得跟 sizes 表连接(条件是 min < height <= max,这正是 HW 06 第一题的条件):

SELECT size, COUNT(*) AS n
FROM dogs, sizes
WHERE height > min AND height <= max
GROUP BY size
HAVING n > 2;
size|n
mini|3
standard|3

这条查询把本讲和上一讲的东西全用上了:隐式 join(FROM 里写两张表)、WHERE 里的 join 条件、GROUP BY、聚合函数、HAVING。去掉 HAVING 的完整结果是 mini|3 standard|3 toy|2,medium 一只都没有——ellie 身高 35,按 height > min AND height <= max 落进了 mini(28,35],不是 medium(35,45]。边界值落在哪一侧,取决于你用 > 还是 >=,这是 HW 06 最容易错的地方。

聚合查询写错了怎么调

聚合查询有个讨厌的地方:它经常不报错,只是给你一个错的数。 出错时最有效的办法是把 GROUP BY 那一行连同聚合函数一起删掉,先把「分堆之前长什么样」看清楚。

拿需求二举例。假如结果不对,先看中间表:

sqlite> SELECT p.parent, d.name, d.height
   ...> FROM parents AS p JOIN dogs AS d ON p.child = d.name;
parent|name|height
ace|bella|52
ace|charlie|47
daisy|hank|31
finn|ace|26
finn|daisy|46
finn|ginger|28
ellie|finn|32

7 行,每行是「一个家长 + 他的一个孩子的完整信息」。看到这张表,剩下的事就一目了然了:按 parent 分堆 ⇒ ace 2 行、daisy 1 行、finn 3 行、ellie 1 行;COUNT(*) 就是孩子数,MAX(d.height) 就是最高孩子的身高。

调试聚合查询的三步
  1. 先看 join 之后、分堆之前的那张表(删掉 GROUP BY、HAVING 和所有聚合函数,把参与运算的列都 select 出来)。行数对不对?有没有意外的重复?
  2. 再加 GROUP BY,只 select 分组列和 COUNT(*)。堆数对不对?每堆几行?
  3. 最后才加真正要的聚合函数和 HAVING。

HW 06 提到的 code.cs61a.org 有个 "Step-by-step" 按钮,做的正是这件事——它把 SQL 每一步的中间表画出来给你看。写不出来的时候先去那里把中间表看一眼,比盯着完整查询干想快得多。

本讲小结

概念要点典型陷阱
动态作用域帧的 parent 由「开这一帧的是普通过程还是 mu」决定:普通过程用定义处,mu 用调用处以为「谁触发查找」会改变整条链;define 只在当前帧建绑定,不改外层
宏的第二步是带引号的代入,算子数以 quoted 形式传入少一层 quote,expr 提前被求值,报错信息变成 (failed: #f)
自连接同一张表在 FROM 里出现多次,用 AS 起别名区分不起别名 ⇒ ambiguous column name: name;连接条件写反成 a.name = b.supervisor
GROUP BY c按 c 的取值分堆,一堆输出一行;输出行数 = c 的不同取值个数以为能列出每堆的全部成员;以为它保证排序(那只是实现细节)
裸列 bare column既不在 GROUP BY 也不在聚合函数里的列。SQLite 允许,值任意挑看到 MAX(salary) 旁边的 name 恰好对,就当成规律用
COUNT(*)数组内行数。没有 GROUP BY 时整表算一组,返回一行空结果集时 COUNT 给 0,但 SUM/MAX/AVG/MIN 给 NULL
COUNT(DISTINCT c)数 c 有几种不同取值和 SELECT DISTINCT c 混淆:一个给个数,一个给取值
AVG/SUM/MAX/MIN对组内某一列做规约;AVG 总是返回浮点写进 WHERE ⇒ misuse of aggregate: COUNT()
HAVING筛组,可以调聚合函数;必须先有 GROUP BY没有 GROUP BY ⇒ a GROUP BY clause is required before HAVING;拿它当 WHERE 筛行
执行顺序FROM/JOIN…ON → WHERE/LIKE → GROUP BY → HAVING → SELECT → AS → ORDER BY → LIMIT以为按书写顺序执行;ORDER BY 能用别名而 WHERE 不能,原因就在这
书写顺序= 执行顺序,但把 SELECT 提到最前HAVING 写在 GROUP BY 前 ⇒ near "GROUP": syntax error
一张图记住整条流水线
磁盘上的表
   │ FROM / JOIN … ON …        ← 数据从哪来,怎么对应
   ▼
  行
   │ WHERE / LIKE              ← 逐行筛(此处不能用聚合函数)
   ▼
  更少的行
   │ GROUP BY                  ← 分堆!单位从「行」变成「组」
   ▼
  组
   │ HAVING                    ← 整组整组地筛(此处可以用聚合函数)
   ▼
  更少的组
   │ SELECT / AS               ← 一组输出一行;聚合函数在这里求值
   ▼
  输出表
   │ ORDER BY / LIMIT          ← 排序、截断
   ▼
  最终结果

动手练习

答案都在本机 SQLite 上跑过。用到的表就是本页第 3 节的 records 和第 9 节的 dogs / parents / sizes。

1. 不看答案,说出输出有几行

下面三条查询各输出多少行?

(a) SELECT COUNT(*) FROM records;
(b) SELECT COUNT(*) FROM records GROUP BY division;
(c) SELECT title, COUNT(*) FROM records GROUP BY title;
看答案

(a) 1 行。没有 GROUP BY,整张表算一个组,一组一行,值是 9。

(b) 3 行。division 有 3 种取值,所以 3 堆。输出是 2 / 2 / 5——注意这条查询没有把 division 放进 SELECT,所以你只看得到人数,看不出是哪个部门。这也是为什么写 GROUP BY 时通常要把分组列一起 select 出来。

(c) 8 行。9 个人里只有 Alyssa 和 Cy 的 title 都是 Programmer,其余 7 个 title 各不相同,所以 8 堆:

Big Wheel|1
Chief Accountant|1
Programmer|2
Programmer Trainee|1
Scrivener|1
Secretary|1
Technician|1
Wizard|1

心法:先数「分组列有几种取值」,那就是输出行数。

2. 找出下面这条查询的两处错误

SELECT division, name, AVG(salary) AS avg_sal
FROM records
WHERE COUNT(*) > 1
HAVING avg_sal > 30000
GROUP BY division;
看答案

错误一:COUNT(*) 出现在 WHERE 里。 WHERE 是执行顺序第 2 步,比 GROUP BY(第 3 步)早,那时组还不存在。真实报错:

misuse of aggregate: COUNT()

要按组内行数筛,得挪到 HAVING 里。

错误二:HAVING 写在了 GROUP BY 前面。 书写顺序必须是 GROUP BY 然后 HAVING。真实报错:

near "GROUP": syntax error

另外还有一处不算「错误」但该改:name 是裸列,SQLite 会随便挑一个人的名字填进去,多半不是你想要的。改好之后:

SELECT division, AVG(salary) AS avg_sal
FROM records
GROUP BY division
HAVING COUNT(*) > 1 AND AVG(salary) > 30000;

输出(三个部门人数都 ≥ 2,平均工资都 > 30000,所以全留):

Accounting|46500.0
Administration|87500.0
Computer|38000.0

3. 写一条查询:每种毛发里,最高和最矮的狗差多少?只要差距超过 15 的

看答案
SELECT fur, MAX(height) - MIN(height) AS spread
FROM dogs
GROUP BY fur
HAVING MAX(height) - MIN(height) > 15;

关键点:聚合函数的结果可以参与算术运算,MAX(height) - MIN(height) 是完全合法的表达式。先看不带 HAVING 的全貌:

fur|MAX(height) - MIN(height)
curly|1
long|21
short|24

curly 是 finn 32 和 hank 31,差 1;long 是 26/46/47,差 21;short 是 28/35/52,差 24。所以答案留下 long|21 和 short|24。

顺带:HAVING spread > 15 在 SQLite 里也能跑,但如前所述别名在 HAVING 里不是标准行为,原样写一遍更稳。

4. 只统计 Computer 部门的人,按主管分堆数人数——结果为什么是 3 行而不是 1 行?

SELECT supervisor, COUNT(*) AS n
FROM records
WHERE division = 'Computer'
GROUP BY supervisor;
看答案
supervisor|n
Alyssa P Hacker|1
Ben Bitdiddle|3
Oliver Warbucks|1

因为 WHERE 筛的是员工所在的部门,剩下 5 个 Computer 部门的人:Alyssa(主管 Ben)、Ben(主管 Oliver)、Cy(Ben)、Lem(Ben)、Louis(Alyssa)。这 5 个人的 supervisor 列有三种取值,所以分出 3 堆。

注意 Oliver Warbucks 自己并不在 Computer 部门(他在 Administration),但他作为某个 Computer 员工的主管出现在结果里。WHERE 过滤的永远是行本身的属性,不会顺着 supervisor 这个「引用」传播出去。 想按主管的部门筛,就得 join 一次 records。

5. 判断对错

  1. GROUP BY 会让输出按分组列排好序,所以不用再写 ORDER BY。
  2. 只要 SELECT 里有聚合函数,就必须写 GROUP BY。
  3. SELECT division, COUNT(*) FROM records WHERE salary > 1000000 GROUP BY division; 会输出 3 行,每行 COUNT(*) 是 0。
  4. HAVING 里可以用没出现在 SELECT 里的聚合函数。
看答案

1. 错。 SQLite 常用排序来实现分组,所以看起来有序,但这是实现细节,SQL 标准不保证。需要确定顺序就写 ORDER BY。

2. 错。 不写 GROUP BY 时整张表算作一个组,SELECT COUNT(*) FROM records; 完全合法,返回一行 9。反过来说才对:想写 HAVING,必须先有 GROUP BY。

3. 错。 输出零行。WHERE 在第 2 步就把 9 行全部滤掉了,一行不剩就分不出任何组,「一堆一行」在这里对应「零堆零行」。

对比一下:把 GROUP BY 去掉,SELECT COUNT(*), SUM(salary) FROM records WHERE salary > 1000000; 会输出一行 0|NULL——空的一组仍然是一组。这两种行为的差别是本讲最细的一个点。

4. 对。 例如 SELECT division, COUNT(*) FROM records GROUP BY division HAVING MAX(salary) > 70000; 输出 Accounting|2 和 Administration|2。「按什么筛」和「输出什么」互相独立。