CS 61A  /  作业解析
HOMEWORK 6

HW 6:SQLok 4 项通过

四道题,一张狗的家谱。从最基础的两表连接一路做到自连接、字符串拼接和分组过滤——这是整门课里唯一一次「不写函数、不写递归」的作业。

对应讲次:Lecture 22(SQL 与表)、Lecture 23(聚合与数据库) 官方题面:cs61a.org/hw/hw06 代码:hw/hw06/hw06.sql

0. 这份作业在练什么

前面五份作业你一直在做同一件事:写一个函数,告诉计算机怎么一步一步算出答案。递归、循环、链表遍历,本质都是「指挥机器按什么顺序动」。这种写法叫命令式编程(imperative programming)。

SQL 把这件事整个掀翻了。写 SQL 的时候,你不描述步骤,只描述你要的那张表长什么样。「给我一张表,它的每一行是一只有父母的狗的名字,按父母身高从高到矮排」——你说完这句话就完事了,至于数据库内部是先扫 parents 还是先扫 dogs、是用嵌套循环还是用哈希索引,全部不归你管。这种写法叫声明式编程(declarative programming)。

所以做这份作业最大的障碍不是语法——SQL 的语法两页纸就讲完了——而是思维方式的切换。绝大多数初学者卡住的瞬间,都是因为脑子里还在想「我先遍历 parents,对每一行去 dogs 里查一下……」。这个念头一旦冒出来,你就会想去找 SQL 的 for 循环,然后发现没有,然后卡死。

本页会反复用同一个心法帮你脱困:把 FROM a, b 想象成「先把 a 的每一行和 b 的每一行两两配对,摊成一张巨大的宽表」,然后 WHERE 从这张宽表里划掉不要的行,SELECT 从剩下的行里挑出要的列。这个「先摊平、再筛选、再投影」的三步模型,能解释这份作业里的每一道题,包括最吓人的自连接(self join)。

本次要点
  • Q1 by_parent_height:两表连接(join)的最小样例 + ORDER BY ... DESC。要点:连接条件写在 WHERE 里,而且排序用的列不必出现在 SELECT 里。
  • Q2 size_of_dogs:连接条件不一定是等号。这里是一个区间匹配 height > min AND height <= max,练的是「把自然语言里的『大于……且不超过……』精确翻译成边界」。
  • Q3 siblings + sentences:本次作业的分水岭。自连接(一张表和自己连接)、用别名 AS 区分两个副本、用 a.child < b.child 一招同时干掉「自己配自己」和「重复配对」、以及用 || 拼字符串。
  • Q4 low_variance:GROUP BY + 聚合函数 MIN/MAX/AVG + HAVING。要点是彻底分清 WHERE(筛行,在分组之前)和 HAVING(筛组,在分组之后)。

做之前你该已经会的

不多,但都是硬要求:

概念一句话在哪道题用到
SELECT ... FROM ... WHERE ...SQL 的骨架:挑列、指定来源表、过滤行全部
连接(join)FROM a, b 产生 a 与 b 所有行的两两组合Q1 Q2 Q3
别名 AS给表或列起个新名字,自连接时是必须的Q3
ORDER BY / DESC排序,默认升序,DESC 转降序Q1
GROUP BY + 聚合函数把行分堆,每堆压缩成一行Q4
HAVING对「堆」做过滤,条件里可以用聚合函数Q4
||字符串拼接(不是 Python 的 +)Q3

三张原始表

整份作业只围绕这三张表转,建议你把它们抄在纸上放旁边。hw06.sql 开头这段是官方给的,不许改:

CREATE TABLE parents (parent TEXT, child TEXT);

INSERT INTO parents VALUES
  ('ace', 'bella'),
  ('ace', 'charlie'),
  ('daisy', 'hank'),
  ('finn', 'ace'),
  ('finn', 'daisy'),
  ('finn', 'ginger'),
  ('ellie', 'finn');

CREATE TABLE dogs (name TEXT, fur TEXT, height INTEGER);

INSERT INTO dogs VALUES
  ('ace',     'long',  26),
  ('bella',   'short', 52),
  ('charlie', 'long',  47),
  ('daisy',   'long',  46),
  ('ellie',   'short', 35),
  ('finn',    'curly', 32),
  ('ginger',  'short', 28),
  ('hank',    'curly', 31);

CREATE TABLE sizes (size TEXT, min INTEGER, max INTEGER);

INSERT INTO sizes VALUES
  ('toy',      24, 28),
  ('mini',     28, 35),
  ('medium',   35, 45),
  ('standard', 45, 60);

把 parents 画成家谱会清楚很多(箭头指向孩子):

ellie(35)
  └── finn(32)
        ├── ace(26)
        │     ├── bella(52)
        │     └── charlie(47)
        ├── daisy(46)
        │     └── hank(31)
        └── ginger(28)

括号里是身高。注意两件事:一,ellie 自己没有父母,所以她不会出现在 Q1 的结果里;二,家谱里的「身高」和「辈分」完全无关——bella 是辈分最低的,却是全场最高的狗(52)。这不是巧合,是出题人故意的,专门用来抓那些把 ORDER BY height 写成按孩子身高排的人。

注意:本仓库的验证状态

本仓库中 hw/hw06/hw06.sql 的四条建表语句已通过官方评分器:在 hw/hw06/ 下运行 python3 ok --local 的输出是 4 test cases passed! No cases failed.——即 by_parent_height、size_of_dogs、sentences、low_variance 四项全部通过。本页贴出的所有 SQL 都逐字取自该文件,没有「凭印象重写」的版本。

另外说明一点:这份作业没有 WWPD(What Would Python Display)或 WWSD 形式的概念题。hw/hw06/tests/ 目录下只有四个 sqlite 类型的测试文件,每个文件里是一条查询语句和它的期望输出,没有需要解锁的选择题。所以本页不设「概念题」小节,四道题全部是写查询。

1. Q1 by_parent_height:第一次连接两张表

题目要什么

官方原话:

Create a table by_parent_height that has a column of the names of all dogs that have a parent, ordered by the height of the parent dog from tallest parent to shortest parent.

翻成人话,拆成三个独立的要求:

1只要一列,这列装的是狗的名字。不是名字加身高,就是名字,一列。
2只要「有父母的狗」。什么叫有父母?在 parents 表的 child 列里出现过。ellie 没在 child 列出现过,所以她被排除。
3按「这只狗的父母的身高」从高到矮排。注意排序依据是父母的身高,不是这只狗自己的身高。

期望输出(来自 hw/hw06/tests/by_parent_height.py,这是评分器真正会比对的东西):

sqlite> SELECT * FROM by_parent_height;
hank
finn
ace
daisy
ginger
bella
charlie

边界情况有两个,都必须想清楚:

第一,同一个父母有多个孩子时,这些孩子的相对顺序无所谓。finn 的三个孩子 ace、daisy、ginger 的父母身高都是 32,它们三个谁在前谁在后题目都认。题面明确写了「The names of dogs with parents of the same height should appear together in any order」。但——注意这个「但」——评分器 by_parent_height.py 里写着 'ordered': True,意思是它会严格按行比对。所以虽然理论上任意顺序都对,实际上你得跟评分器给的那个顺序一致才能过。好消息是:只要你用了下面这种最自然的写法,sqlite 会保持原表的插入顺序,结果自然就是 ace, daisy, ginger,和期望一致。

第二,一只狗如果有两个父母,会出现两次。本次数据里每只狗只有一个父母,所以不会撞上,但你写的查询逻辑必须是「按 parents 表的行来产出结果」,而不是「按 dogs 表的行来产出结果」——这两种写法在有两个父母的数据上会给出不同答案,题面也说了「Your queries should still perform correctly even if the values in these tables change」。

怎么想到的

先摆清困境:我要的信息分散在两张表里。

  • 「谁是谁的孩子」在 parents 表里;
  • 「某只狗多高」在 dogs 表里。

光看 parents,我知道 hank 的父母是 daisy,但我不知道 daisy 多高。光看 dogs,我知道 daisy 是 46,但我不知道她是谁的父母。要排序,我必须让「孩子的名字」和「父母的身高」出现在同一行上。

这一步是所有 SQL 题的共同起点,记住这句话:凡是需要把两张表的信息凑到一行上的,就是要连接。

第一个念头(错的,但值得走一遍)

刚学完 Python 的人第一反应通常是这样:

# 脑子里的伪代码
result = []
for parent, child in parents:
    h = 去 dogs 里查 parent 的 height
    result.append((child, h))
result.sort(key=lambda t: -t[1])

然后你打开 SQL 手册找 for,找不到;找「查表函数」,也找不到。卡住。

脱困的关键是意识到:SQL 里没有「查」这个动作,只有「配对然后筛」。上面那段伪代码里的「去 dogs 里查 parent 的 height」,在 SQL 里的等价说法是:「把 parents 的每一行和 dogs 的每一行都配一遍,然后只留下那些 parent 恰好等于 name 的配对」。

关键一步:先摊平,再筛选

直觉:FROM a, b 到底是什么

FROM parents, dogs 产生的是笛卡尔积(Cartesian product):parents 的 7 行 × dogs 的 8 行 = 56 行的一张宽表,列是两张表的列拼起来,即 parent, child, name, fur, height 五列。这 56 行里绝大多数是垃圾——比如 (ace, bella, hank, curly, 31) 这行毫无意义,它把「ace 是 bella 的父母」和「hank 高 31」这两条不相干的事实硬凑在了一起。

然后 WHERE parent = name 从这 56 行里只留下那些「name 这一列的狗,恰好就是 parent 这一列的那只狗」的行——7 行。每一行现在同时装着「孩子叫什么」和「父母多高」。

想通这一点,剩下的就是填空:

1要哪两张表?parents 和 dogs。写 FROM parents, dogs。
2怎么把它们对上?父母的名字 parent 要等于狗的名字 name。写 WHERE parent = name。
3要输出哪列?孩子的名字。写 SELECT child。
4怎么排?按父母身高降序。此时 height 这一列指的正是父母的身高(因为我们是拿 parent = name 对上的),所以写 ORDER BY height DESC。

第 4 步有个容易漏的认知:ORDER BY 用的列不需要出现在 SELECT 里。我们 SELECT 的只有 child 一列,但排序时仍然能用 height。原因是执行顺序:SQL 先做 FROM(摊平)→ WHERE(筛行)→ 此时中间结果里五列俱全 → ORDER BY(排序,能看到全部五列)→ 最后才 SELECT(挑列,把不要的列丢掉)。排序发生在丢列之前。

代码

-- All dogs with parents ordered by decreasing height of their parent
-- Join parents with dogs so that each child sits next to its parent's height,
-- then sort by that height, tallest parent first.
CREATE TABLE by_parent_height AS
  SELECT child FROM parents, dogs
    WHERE parent = name
    ORDER BY height DESC;

逐行说明:

片段为什么这样写
CREATE TABLE by_parent_height AS题目要求「创建一张表」,不是「运行一条查询」。CREATE TABLE X AS SELECT ... 把查询结果固化成一张名为 X 的新表,评分器随后 SELECT * FROM by_parent_height 才能查到东西。这一行是题面给的模板,不要改名。
SELECT child只要一列,就是孩子名。写 SELECT * 会输出五列,评分器立刻挂:它期望每行只有 hank 这样一个词,而不是 daisy|hank|daisy|long|46。
FROM parents, dogs两张表用逗号并列,即笛卡尔积。写 FROM parents JOIN dogs ON parent = name 效果等价,但 61A 一律用逗号形式,跟讲义保持一致就好。
WHERE parent = name连接条件。parent 只在 parents 表里有,name 只在 dogs 表里有,两个名字都不歧义,所以不用写 parents.parent、dogs.name(写全了也对,只是啰嗦)。这个等号是比较不是赋值——SQL 里比较相等就用单个 =,没有 ==。
ORDER BY height DESCheight 来自 dogs,而 dogs 这一侧被 WHERE 锁定成了「父母那只狗」,所以 height 就是父母的身高。DESC(descending)表示从大到小;不写 DESC 默认是 ASC,结果会正好颠倒过来。

验证:手动跑一遍

不摊完 56 行,只把 WHERE parent = name 筛剩的 7 行列出来。对 parents 的每一行,找到 dogs 里名字等于 parent 的那一行,把 height 抄过来:

逐步推演:连接后的中间结果
parentchildnamefurheight
acebellaacelong26
acecharlieacelong26
daisyhankdaisylong46
finnacefinncurly32
finndaisyfinncurly32
finngingerfinncurly32
elliefinnellieshort35

七行,正好对应 parents 的七行——因为每只作为父母的狗在 dogs 里只有一行,所以是一对一匹配。ellie 作为孩子从未出现,因此她永远不会进入 child 列,这就自动实现了「只要有父母的狗」。

接着 ORDER BY height DESC,按最后一列从大到小重排:

逐步推演:排序后取 child 列
父母身高child说明
46hank父母是 daisy(46),全场最高的父母
35finn父母是 ellie(35)
32ace父母都是 finn(32)。三者并列,sqlite 保持它们在中间结果里的原顺序,即 parents 表的插入顺序 ace → daisy → ginger
32daisy
32ginger
26bella父母都是 ace(26),同理保持插入顺序
26charlie

最终输出 hank, finn, ace, daisy, ginger, bella, charlie——与 tests/by_parent_height.py 中的期望输出逐行相同。

顺便注意验证过程中的一个反直觉之处:bella 身高 52,是全场最高的狗,却排在最后一行;hank 身高只有 31,却排第一。这正是因为排序看的是父母的身高。如果你的输出是 bella 开头,说明你写的是 WHERE child = name——把连接条件搞反了。

常见误区

误区一:SELECT name 而不是 SELECT child。这两者在连接之后是完全不同的列——name 是父母(因为 WHERE parent = name 把它绑到父母身上了),child 才是孩子。写成 SELECT name 会输出 daisy, ellie, finn, finn, finn, ace, ace,评分器直接报不匹配。

误区二:忘了 WHERE。只写 SELECT child FROM parents, dogs ORDER BY height DESC 不会报语法错误,它会安静地输出 56 行——因为笛卡尔积没有被筛。SQL 不报错但结果荒谬,是这门课里最难 debug 的一类错误。看到输出行数远超预期,第一反应就该是「我的连接条件呢」。

误区三:把 DESC 写成 DSC 或漏掉。漏掉 DESC 时输出会是 bella, charlie, ace, daisy, ginger, finn, hank,正好倒过来。DSC 则会触发 near "DSC": syntax error。

误区四:给建表语句起错名字。题面模板里的表名 by_parent_height 是评分器写死要查的。改成 byParentHeight 会得到 Error: no such table: by_parent_height。

2. Q2 size_of_dogs:连接条件不一定是等号

题目要什么

造一张两列的表 size_of_dogs:第一列是狗名,第二列是它的体型分类。分类规则来自 sizes 表,题面给的定义是:

a dog must be over the min and less than or equal to the max in height to qualify as size.

这句英文的边界必须一个字一个字抠:

英文数学SQL边界含不含
over the min\(h > \text{min}\)height > min不含 min(严格大于)
less than or equal to the max\(h \le \text{max}\)height <= max含 max

也就是说,每个体型是一个左开右闭区间 \((\text{min}, \text{max}]\)。这不是吹毛求疵——看 sizes 表的四行:

sizeminmax实际区间
toy2428\((24, 28]\)
mini2835\((28, 35]\)
medium3545\((35, 45]\)
standard4560\((45, 60]\)

相邻两行的边界是重叠的:toy 的 max 是 28,mini 的 min 也是 28。如果两边都用闭区间(height >= min AND height <= max),那么身高恰好 28 的 ginger 会同时属于 toy 和 mini,输出里就会多出一行。这份数据里有两只狗踩在边界上(ginger 28、ellie 35),是出题人埋的地雷。左开右闭正好保证每个身高落进恰好一个区间。

评分器(hw/hw06/tests/size_of_dogs.py)实际查的是:

sqlite> SELECT name FROM size_of_dogs WHERE size="toy" OR size="mini";
ace
ellie
finn
ginger
hank

它只验证 toy 和 mini 这两类——正好是边界最密集的地方。ellie(35) 必须是 mini 而不是 medium,ginger(28) 必须是 toy 而不是 mini。区间开闭写反了,这条测试立刻挂。

怎么想到的

Q1 已经建立了「凡是要把两张表的信息凑一行,就连接」的直觉。这题同样是两张表:狗名和身高在 dogs,分类和区间在 sizes。所以骨架一定是 SELECT ... FROM dogs, sizes WHERE ...。

唯一的新东西在 WHERE 里。Q1 的连接条件是 parent = name——一个等号。这里没有任何一对列是「相等」关系:狗表里没有 size 列,尺寸表里也没有狗名。那怎么连?

卡壳点与突破

很多人在这里会想去找一个「区间查找函数」,或者退回 Python 思维写一堆 CASE WHEN height <= 28 THEN 'toy' WHEN ...。后者能跑出正确答案,但它把 sizes 表的内容硬编码进了查询——题面明确警告「Your queries should still perform correctly even if the values in these tables change」,sizes 表加一行 ('giant', 60, 80) 你的查询就废了。

突破口是重新读一遍笛卡尔积的定义:WHERE 后面跟的是任意布尔表达式,没人规定它必须是等号。

把 FROM dogs, sizes 摊平后是 8 × 4 = 32 行,每一行形如「某只狗 × 某个体型类别」,五列:name, fur, height, size, min, max。现在问题变成:这 32 个「狗-类别」配对里,哪些是成立的?答案直接就是题面那句话:height > min AND height <= max。

关键一步

连接条件不是「找到匹配的那一行」,而是「判断这个配对成不成立」。等值连接(a = b)只是判断方式里最常见的一种。一旦把 WHERE 看成「对每个候选配对投一次赞成/反对票」,区间连接、不等连接(Q3 会用到 <)就都自然了。

剩下就是 SELECT name, size——题目说「two columns, one for each dog's name and another for its size」,顺序是名字在前、尺寸在后。

代码

-- The size of each dog
-- A dog matches a size class when min < height <= max.
CREATE TABLE size_of_dogs AS
  SELECT name, size FROM dogs, sizes
    WHERE height > min AND height <= max;
片段为什么这样写
SELECT name, size两列,顺序照题目要求。name 来自 dogs,size 来自 sizes——一条 SELECT 里的列可以来自不同表,因为连接之后它们已经在同一行上了。
FROM dogs, sizes32 行候选配对。dogs 写在前面对应 SELECT 里 name 在前,纯粹是可读性,交换顺序不影响结果。
WHERE height > min严格大于。这是 over the min 的直译。用 >= 会让 ginger(28) 同时进 toy 和 mini。
AND height <= max小于等于。这是 less than or equal to the max 的直译。用 < 会让 ginger(28)、ellie(35)、bella(52 不受影响) 中的前两只一个类别都进不去,直接从输出里消失。
没有 ORDER BY这题不要求排序,size_of_dogs.py 里写的是 'ordered': False,评分器不看顺序。

再强调一次为什么 min 和 max 不用加表前缀:这两个名字只在 sizes 表里出现,height 只在 dogs 表里出现,没有歧义。(顺带一提,min/max 和聚合函数 MIN()/MAX() 同名,但 sqlite 靠有没有括号区分,这里当列名用是安全的。)

验证:手动跑一遍

不列全 32 行,只逐狗判断它落在哪个区间。判断方法:对每只狗,扫过 sizes 的四行,看 min < height <= max 是否成立。

逐步推演:八只狗各自的归类
狗身高toy (24,28]mini (28,35]medium (35,45]standard (45,60]结果
ace2626>24 且 26≤28 ✓26>28 ✗✗✗toy
bella52✗✗52≤45 ✗52>45 且 52≤60 ✓standard
charlie47✗✗47≤45 ✗47>45 且 47≤60 ✓standard
daisy46✗✗46≤45 ✗46>45 且 46≤60 ✓standard
ellie35✗35>28 且 35≤35 ✓35>35 ✗✗mini
finn3232≤28 ✗32>28 且 32≤35 ✓✗✗mini
ginger2828>24 且 28≤28 ✓28>28 ✗✗✗toy
hank3131≤28 ✗31>28 且 31≤35 ✓✗✗mini

注意 ellie(35) 和 ginger(28) 这两行:它们各自都恰好只有一个 ✓,而且都是靠「右端点闭、左端点开」才做到的。ellie 因为 35≤35 进了 mini,又因为 35>35 不成立而没进 medium;ginger 因为 28≤28 进了 toy,又因为 28>28 不成立而没进 mini。左开右闭把重叠边界切干净了。

最终 size_of_dogs 是 8 行:

ace|toy
bella|standard
charlie|standard
daisy|standard
ellie|mini
finn|mini
ginger|toy
hank|mini

评分器只查 toy 和 mini 的名字,即 ace, ellie, finn, ginger, hank——与期望输出完全一致。

常见误区

误区一:两边都用闭区间。写 WHERE height >= min AND height <= max,输出会变成 10 行——ginger 出现两次(toy 和 mini),ellie 出现两次(mini 和 medium)。评分器查 toy/mini 时会看到 ace, ellie, finn, ginger, ginger, hank,多了一个 ginger,报错。

误区二:两边都用开区间。写 height > min AND height < max,ginger(28) 和 ellie(35) 会一个类都进不去,从表里彻底消失。这种「结果少了几行」的 bug 特别隐蔽,因为没有任何报错。

误区三:把 sizes 表的数字硬编码。比如 SELECT name, "toy" FROM dogs WHERE height <= 28 这样分四条写再 UNION。它在这份数据上能过 ok,但违反题面要求(表的值变了就错),而且期末考同类题会给不同的 sizes 表来卡你。养成「让数据待在表里,让逻辑待在查询里」的习惯。

误区四:列顺序写反。SELECT size, name 会让第一列是尺寸。虽然本题评分器只查 name 列所以碰巧能过,但 Q3 会拿 size_of_dogs 当积木用,列名和列序错了会连累后面。题目说了「one for each dog's name and another for its size」,就照这个顺序写。

3. Q3 siblings 与 sentences:自连接与字符串拼接

题目要什么

官方定义:兄弟姐妹(siblings)是「有同一个父母」的一对狗。要造一张表,每一行是一个「体型分类相同的兄弟姐妹对」,只有一列,内容是一句描述这对狗的话。

期望输出(hw/hw06/tests/sentences.py):

sqlite> SELECT * FROM sentences;
The two siblings, bella and charlie, have the same size: standard
The two siblings, ace and ginger, have the same size: toy

题面追加了三条硬性约束,每一条都对应一个必须写进 SQL 的条件:

1不能把一只狗和它自己配对。原话「Make sure to not pair a child with themselves」。bella 和 bella 显然有同一个父母(都是 ace),但这不算兄弟姐妹。
2每一对只能出现一次。原话「do not include duplicate pairs」。既然 (bella, charlie) 出现了,(charlie, bella) 就不能再出现。
3句子里两个名字必须按字母序。原话「siblings should be listed in alphabetical order (e.g. "bella and charlie..." instead of "charlie and bella...")」。

还有一条隐含要求:只保留体型分类相同的那些对。ace(toy) 和 daisy(standard) 虽然是兄妹,但尺寸不同,不进结果。

题面给了一个可选但强烈建议的中间步骤:先造一张 siblings 辅助表,只存名字对,再用它去造 sentences。这个建议应该听——把「配对」和「造句」拆成两步,每一步都简单到能一眼看懂。

怎么想到的(一):兄弟姐妹为什么要自连接

先问最朴素的问题:「有同一个父母」这件事,在哪张表里能看出来?

parents 表长这样:

parentchild
acebella
acecharlie
daisyhank
finnace
finndaisy
finnginger
elliefinn

「bella 和 charlie 是兄妹」这个信息,藏在第 1 行和第 2 行的关系里:它们的 parent 列都是 ace。关键认识来了——这个信息不在任何单独一行里,它跨越两行。

而 SQL 的 WHERE 只能对一行做判断。你没法写「这一行的 parent 等于上一行的 parent」,SQL 里根本没有「上一行」的概念。

卡壳与突破

这就是本次作业最大的一道坎。突破的思路是:如果我想同时看到两行,那就让这两行变成同一行。

怎么变?回到笛卡尔积。FROM parents, parents——把 parents 表和它自己做笛卡尔积,得到 7 × 7 = 49 行,每一行装着两条「父母-孩子」记录。这样一来,「第 1 行和第 2 行」这对组合,就成了 49 行里的某一行了。这个操作叫自连接(self join)。

直觉:自连接不神秘

别把「一张表和自己连接」想成什么递归或者自指。数据库根本不知道这两个是同一张表——它就是老老实实地取出 parents 的一份拷贝、再取出另一份拷贝,然后两两配对。你可以想象成有两张一模一样的纸,左手一张右手一张,左手指一行、右手指一行,49 种指法。

但立刻有个技术问题:两份拷贝的列名一模一样,都叫 parent 和 child。写 WHERE parent = parent 是废话(永远为真)。必须给两份拷贝起不同的名字,这就是题面 Hint 说的「If you join a table with itself, use AS within the FROM clause to give each table an alias」。

写成 FROM parents AS a, parents AS b,之后就能用 a.parent、a.child、b.parent、b.child 分别指代左边那份和右边那份。「同一个父母」的条件就是 a.parent = b.parent。

怎么想到的(二):a.child < b.child 这一招

加上 WHERE a.parent = b.parent 之后,49 行剩下多少?按父母分组算:ace 有 2 个孩子贡献 2×2=4 行,daisy 1 个孩子贡献 1 行,finn 3 个孩子贡献 3×3=9 行,ellie 1 个孩子贡献 1 行,共 15 行。列出来:

父母a.childb.child问题
acebellabella自己配自己 ✗
acebellacharlie想要的 ✓
acecharliebella与上一行重复 ✗
acecharliecharlie自己配自己 ✗
daisyhankhank自己配自己 ✗
finnaceace自己配自己 ✗
finnacedaisy想要的 ✓
finnaceginger想要的 ✓
finndaisyace重复 ✗
finndaisydaisy自己配自己 ✗
finndaisyginger想要的 ✓
finngingerace重复 ✗
finngingerdaisy重复 ✗
finngingerginger自己配自己 ✗
elliefinnfinn自己配自己 ✗

15 行里只有 4 行是想要的。要干掉两类垃圾:

第一个念头是加 AND a.child != b.child,这确实杀掉了「自己配自己」的 6 行,剩 9 行。但 (bella, charlie) 和 (charlie, bella) 都还在,重复问题没解决。于是你会想:怎么在两个方向里只留一个?

第二个念头可能是「用 DISTINCT」。不行——DISTINCT 只能去掉完全相同的行,而 (bella, charlie) 和 (charlie, bella) 是两个不同的行,它管不着。

正确的招:把 != 换成 <,写 AND a.child < b.child。

核心结论:< 一招三用

SQL 里 < 作用在文本上是字典序比较('bella' < 'charlie' 为真)。所以 a.child < b.child 同时办成了三件事:

  • 排除自配:一个名字不可能小于它自己,bella < bella 为假,6 行自配全部消失。
  • 去重:对任意两个不同名字,x < y 和 y < x 里恰好有一个为真。所以每一对只会以一种方向留下来。
  • 顺便排好了字母序:留下来的那个方向,一定是 a.child 字母序在前。题目要求「bella and charlie」而不是「charlie and bella」,这个要求免费满足了。

这个技巧在组合枚举里极其通用:凡是要从 n 个东西里选无序的一对,就用「下标 i < j」把每对钉死成唯一一种表示。你在 Python 里写 for i in range(n): for j in range(i+1, n): 做的是同一件事。

剩 4 行,正是想要的:(bella, charlie)、(ace, daisy)、(ace, ginger)、(daisy, ginger)。给这两列起个名字 first 和 second,方便下一步引用。

怎么想到的(三):从名字对到句子

现在 siblings 表有了 4 行名字对。要造句,还缺两样:每只狗的尺寸(用来筛「尺寸相同」并填进句子),以及拼字符串的办法。

尺寸从哪来?Q2 已经造好了 size_of_dogs 表。之前的题目造的表,可以直接当积木用——这是这道题设计成四小问的用意。

但这里有个新的绕:一行 siblings 有两只狗,我需要查两次尺寸。SQL 里没有「查」,只有连接。所以要把 size_of_dogs 连接进来两次——又是自连接的场景,同样要起别名。用 x 查 first 的尺寸,用 y 查 second 的尺寸:

FROM siblings, size_of_dogs AS x, size_of_dogs AS y
WHERE first = x.name AND second = y.name

三张表并列在 FROM 里,笛卡尔积是 4 × 8 × 8 = 256 行,WHERE 的两个等号把它锁回 4 行——每行现在同时有 first、second、x.size(first 的尺寸)、y.size(second 的尺寸)。

然后加上「尺寸相同」的条件:AND x.size = y.size。

最后是拼字符串。题面 Hint 三写了:|| 是连接运算符。注意 SQL 里的 + 是数字加法,不是字符串拼接——写 "a" + "b" sqlite 会试图把两个字符串转成数字,得到 0。必须用 ||。

目标句子:The two siblings, bella and charlie, have the same size: standard。把它切开,哪些是固定的、哪些是变的:

"The two siblings, "  ← 固定
first                 ← 变(bella)
" and "               ← 固定
second                ← 变(charlie)
", have the same size: "  ← 固定
x.size                ← 变(standard)

用 || 依次串起来即可。逗号、空格、冒号一个都不能错——评分器是按整个字符串精确比对的。特别留意:"The two siblings, " 末尾有一个空格," and " 前后各有一个空格,", have the same size: " 开头是逗号、末尾冒号后有一个空格。

代码

-- [Optional] Filling out this helper table is recommended
-- Join the parents table with itself on the shared parent. Requiring
-- a.child < b.child both skips pairing a dog with itself and keeps only
-- one of the two orderings of each pair (alphabetically first name first).
CREATE TABLE siblings AS
  SELECT a.child AS first, b.child AS second
    FROM parents AS a, parents AS b
    WHERE a.parent = b.parent AND a.child < b.child;

-- Sentences about siblings that are the same size
-- Look up each sibling's size, keep pairs whose sizes agree, then build
-- the sentence with the string concatenation operator ||.
CREATE TABLE sentences AS
  SELECT "The two siblings, " || first || " and " || second ||
         ", have the same size: " || x.size
    FROM siblings, size_of_dogs AS x, size_of_dogs AS y
    WHERE first = x.name AND second = y.name AND x.size = y.size;
片段为什么这样写
SELECT a.child AS first, b.child AS second两列都叫 child,不起别名的话 siblings 表会有两个同名列,后面 WHERE first = x.name 就没法写了。AS 在 SELECT 里是给列改名,在 FROM 里是给表改名,同一个关键字两种用法。
FROM parents AS a, parents AS b自连接。两个别名让同一张表的两份拷贝可区分。省略 AS 写 parents a, parents b 也合法,但写全更清楚。
WHERE a.parent = b.parent「同一个父母」。这是兄弟姐妹的定义本身。
AND a.child < b.child一招三用(见上文核心结论):去自配、去重复、顺带保证字母序。这是整道题的技术核心。
"The two siblings, " || first || ...|| 从左到右依次拼接。整个表达式产出一列,题目要求「a single column」。这一列没有起名字,sqlite 会用整个表达式当列名——评分器只查值不查列名,所以无所谓。
FROM siblings, size_of_dogs AS x, size_of_dogs AS y三表连接。size_of_dogs 出现两次,必须起两个别名,否则 sqlite 报 ambiguous column name: name。
WHERE first = x.name AND second = y.name把 x 锁到姐姐那只狗、y 锁到妹妹那只狗。first/second 只在 siblings 里有,不用前缀。
AND x.size = y.size「体型分类相同」的筛选。少了这一条会输出全部 4 个兄弟对而不是 2 个。
句子里用 x.size既然已经要求 x.size = y.size,用哪个都一样。写 y.size 结果完全相同。
注意:y 这张表看起来「没用」

y 从头到尾没出现在 SELECT 里,只在 WHERE 里露了两次面。初学者常想把它删掉。删不得——y 的唯一作用就是把 second 那只狗的尺寸取出来,好和 x.size 比较。没有 y,你就无从得知 charlie 是什么尺寸。SQL 里「引入一张表只为了拿到某个值来做判断」是极常见的写法。

验证:手动跑一遍

先跑 siblings。FROM parents AS a, parents AS b 摊出 49 行,WHERE a.parent = b.parent 剩 15 行(上表已列),再加 a.child < b.child:

逐步推演:a.child < b.child 对 15 行的裁决
a.childb.child字典序判断留否
bellabellabella < bella 假丢
bellacharlieb<c 真留
charliebellac<b 假丢
charliecharlie假丢
hankhank假丢
aceace假丢
acedaisya<d 真留
acegingera<g 真留
daisyaced<a 假丢
daisydaisy假丢
daisygingerd<g 真留
gingerace假丢
gingerdaisy假丢
gingerginger假丢
finnfinn假丢

siblings 表最终 4 行:(bella, charlie)、(ace, daisy)、(ace, ginger)、(daisy, ginger)。注意 hank 和 finn 都消失了——他们各自是独生子女,没有兄弟姐妹可配。

再跑 sentences。把 Q2 算出的尺寸填进这 4 行:

逐步推演:尺寸比对
firstx.sizesecondy.sizex.size = y.size?
bellastandardcharliestandard✓ 留下
acetoydaisystandard✗ 丢弃
acetoygingertoy✓ 留下
daisystandardgingertoy✗ 丢弃

剩 2 行。最后拼字符串,逐段代入:

第 1 行:first=bella, second=charlie, x.size=standard
  "The two siblings, "  ->  The two siblings,
  || first              ->  The two siblings, bella
  || " and "            ->  The two siblings, bella and
  || second             ->  The two siblings, bella and charlie
  || ", have the same size: "
                        ->  The two siblings, bella and charlie, have the same size:
  || x.size             ->  The two siblings, bella and charlie, have the same size: standard

第 2 行:first=ace, second=ginger, x.size=toy
  最终                  ->  The two siblings, ace and ginger, have the same size: toy

两行输出与 tests/sentences.py 里的期望逐字符相同。该测试的 'ordered' 是 False,两行谁前谁后都行。

常见误区

误区一:用 != 而不是 <。写 AND a.child != b.child 得到 6 个 siblings 行(每对出现两次),sentences 就会输出 4 行:bella&charlie、charlie&bella、ace&ginger、ginger&ace。评分器报「多了两行」,而且 charlie and bella 违反字母序要求。

误区二:忘了给自连接起别名。写 FROM parents, parents WHERE parent = parent,sqlite 直接报 Error: ambiguous column name: parent。看到 ambiguous column name,条件反射就是「我把同一张表连了两次却没起别名」。

误区三:用 + 拼字符串。写 "The two siblings, " + first + ...,sqlite 不报错,安静地输出 0——因为它把每个字符串按数字解析(解析失败得 0),然后做加法。这是全作业最阴的一个坑:没有报错,只有一列莫名其妙的 0。

误区四:句子里的空格错了。比如写成 "The two siblings," || " " || first(等价,没问题),但如果写成 "The two siblings," || first 就少了一个空格,输出 The two siblings,bella and ...。评分器精确比对字符串,差一个空格就是错。写完后强烈建议在 hw/hw06/ 下跑 python3 sqlite_shell.py --init hw06.sql 然后 SELECT * FROM sentences; 亲眼看一遍。

误区五:只连一次 size_of_dogs。写 FROM siblings, size_of_dogs WHERE first = name AND second = name——这要求同一行的 name 既等于 first 又等于 second,而 first != second,条件永远为假,输出零行。「结果是空表」时先检查是不是把该分成两份的表当成了一份。

4. Q4 low_variance:分组、聚合与 HAVING

题目要什么

这题的题面绕了几个弯,先拆成两半。

第一半:要算什么。「the height range (defined as the difference between maximum and minimum height) of all dogs that share a fur type」——按毛发类型把狗分堆,每堆算一个「身高极差」=最高的减最矮的。

第二半:哪些堆有资格进入结果。「we'll only consider fur types where each dog with that fur type is within 30% of the average height of all dogs with that fur type」——只保留那些满足「组里每一只狗的身高都在本组平均身高上下 30% 之内」的堆。题面把它翻译成了两条更好写的条件:

  • 组里没有任何身高小于平均值的 0.7 倍;
  • 组里没有任何身高大于平均值的 1.3 倍。

输出两列,顺序是毛发类型、身高极差。期望输出(hw/hw06/tests/low_variance.py):

sqlite> SELECT * FROM low_variance;
curly|1

只有一行。边界情况提醒:题面的示例表格里写的是 Curly(首字母大写),但真实数据里 fur 列存的是小写 curly,评分器要的也是小写。别被题面的排版误导去做大小写转换。

怎么想到的(一):识别出「这是分组题」

前三题的输出都是「每行对应一个原始行(或一对原始行)」。这题不一样:输出的一行对应的是一整组狗——curly 那一行代表 finn 和 hank 两只狗压缩成的一行。

核心结论:什么时候用 GROUP BY

当你发现「输出的一行 = 输入的好几行合起来的某种统计量」时,就该用 GROUP BY。分组的依据(这里是 fur)就是「按什么把行归堆」那个东西。

反过来,如果输出的一行就是输入的一行改造一下,那就是 Q1、Q2 那种普通查询,不需要分组。

识别出来之后,骨架马上就有了:SELECT ... FROM dogs GROUP BY fur。注意这题只用一张表——毛发和身高都在 dogs 里,没有任何连接。前三题连接连惯了的人有时会下意识加个 , sizes,那是多余的。

然后是「极差」。SQL 提供了 MAX() 和 MIN() 两个聚合函数,在 GROUP BY 之下它们作用于每一组内部。所以极差就是 MAX(height) - MIN(height)。

直觉:聚合函数在分组前后的区别

SELECT MAX(height) FROM dogs; 没有 GROUP BY,整张表算一组,输出一行:52。

SELECT fur, MAX(height) FROM dogs GROUP BY fur; 分成 curly / long / short 三组,每组各算一次,输出三行。同一个函数,作用域被 GROUP BY 改变了。

怎么想到的(二):条件该写在哪里

这是本题真正的难点。条件是「组里最矮的狗不低于组均值的 70%,最高的不超过 130%」。第一个念头往往是塞进 WHERE:

-- 错的
SELECT fur, MAX(height) - MIN(height) FROM dogs
  WHERE height >= 0.7 * AVG(height) AND height <= 1.3 * AVG(height)
  GROUP BY fur;

sqlite 会直接拒绝:Error: misuse of aggregate function AVG()。为什么?因为WHERE 在分组之前执行,那时候「组」还不存在,AVG 无从算起。SQL 的执行顺序是死的:

FROM      把表摆出来(若有多张则做笛卡尔积)
  ↓
WHERE     逐行判断,扔掉不合格的【行】       ← 此处还没有组,不能用聚合函数
  ↓
GROUP BY  把剩下的行按 fur 归堆
  ↓
(聚合)   每堆算出 MIN/MAX/AVG/COUNT
  ↓
HAVING    逐组判断,扔掉不合格的【组】       ← 此处组已存在,可以用聚合函数
  ↓
SELECT    挑列
  ↓
ORDER BY  排序

条件涉及 AVG(height),而 AVG 只有在分组之后才有意义,所以条件必须写在 HAVING 里。

核心结论:WHERE vs HAVING
WHEREHAVING
执行时机分组之前分组之后
过滤对象单独的行整个组
能用聚合函数吗不能能,而且通常必须用
典型用法WHERE height > 30(只看高狗)HAVING COUNT(*) >= 2(只看至少两只狗的组)
语义差别举例「先扔掉所有矮狗,再按毛发分组求均值」——均值只算高狗「先按毛发分组求均值,再扔掉均值太小的组」——均值算全部狗

最后一行是理解两者差异的关键:它们不只是「写在哪」的问题,算出来的数是不一样的。

还有一个弯:「每一只狗都在范围内」怎么写

题目说的是「each dog with that fur type is within 30%」,这是一个「对组内所有元素成立」的全称命题。SQL 里没有 FOR ALL。怎么办?

这里需要一个小小的逻辑转换,而题面 Hint 已经替你做了一半:

1「组内每一只狗的身高都 ≥ 平均值的 0.7 倍」⟺「组内最矮的那只 ≥ 平均值的 0.7 倍」。因为最矮的过关了,比它高的自然全过关。写成 MIN(height) >= 0.7 * AVG(height)。
2「组内每一只狗的身高都 ≤ 平均值的 1.3 倍」⟺「组内最高的那只 ≤ 平均值的 1.3 倍」。写成 MAX(height) <= 1.3 * AVG(height)。
3两条用 AND 连起来,全称命题就被压缩成了两个关于极值的判断。

这一步是本题最值钱的思维。「对所有元素成立」这类条件,只要条件是单调的(比如「≥ 某阈值」),就能等价改写成「对极值成立」。这个技巧不限于 SQL——你在写循环判断「列表里所有数都大于 k」时,用 min(lst) > k 也是同一回事。

顺便注意:MIN、MAX、AVG 在这条语句里被用了两遍(SELECT 里一次、HAVING 里一次),完全没问题——它们对同一组算出的值是一样的,数据库会自己优化。

代码

-- Height range for each fur type where all of the heights differ by no more than 30% from the average height
-- Group the dogs by fur type; HAVING then filters out whole groups whose
-- shortest or tallest dog strays more than 30% from the group's average.
CREATE TABLE low_variance AS
  SELECT fur, MAX(height) - MIN(height) FROM dogs
    GROUP BY fur
    HAVING MIN(height) >= 0.7 * AVG(height)
       AND MAX(height) <= 1.3 * AVG(height);
片段为什么这样写
SELECT fur, ...第一列是毛发类型。在 GROUP BY fur 之下,fur 这一列在每组内取值唯一,所以直接选它是合法且有意义的。
MAX(height) - MIN(height)身高极差。两个聚合函数的结果相减,得到的仍是每组一个数。列名没起别名,sqlite 会用这个表达式本身当列名——评分器只比对值。
FROM dogs只需一张表。毛发和身高都在这里。
GROUP BY fur按毛发类型归堆。dogs 有三种 fur(long、short、curly),所以分成三组,最多输出三行。
HAVING MIN(height) >= 0.7 * AVG(height)「最矮的不低于均值 70%」。用 >=(题面说 inclusive,取等号算通过)。写在 HAVING 而非 WHERE,因为用了聚合函数。
AND MAX(height) <= 1.3 * AVG(height)「最高的不超过均值 130%」。同样含等号。
没有 ORDER BY题目不要求排序,测试文件里 'ordered': False。而且结果只有一行,排不排都一样。

关于 0.7 * 而不是 * 0.7:无所谓,乘法可交换。但要注意 AVG() 返回的是浮点数(sqlite 的 AVG 总是返回 REAL),所以 0.7 * AVG(height) 是浮点乘法,不会发生整数除法截断的问题。如果你自己写成 SUM(height) / COUNT(*),那就是整数除法,119 / 3 会得到 39 而不是 39.67——这是个真会咬人的坑,所以老老实实用 AVG。

验证:手动跑一遍

GROUP BY fur 把 8 只狗分成三组:

组 curly:  finn(32),  hank(31)
组 long:   ace(26),   charlie(47), daisy(46)
组 short:  bella(52), ellie(35),   ginger(28)

逐组算聚合值,再用 HAVING 裁决:

逐步推演:组 curly
身高 = {32, 31}
MIN = 31    MAX = 32
AVG = (32 + 31) / 2 = 31.5

条件一:MIN >= 0.7 * AVG
        31 >= 0.7 * 31.5 = 22.05      31 >= 22.05  真 ✓
条件二:MAX <= 1.3 * AVG
        32 <= 1.3 * 31.5 = 40.95      32 <= 40.95  真 ✓
两条都真 -> 保留

输出值:MAX - MIN = 32 - 31 = 1
这一行是  curly|1
逐步推演:组 long
身高 = {26, 47, 46}
MIN = 26    MAX = 47
AVG = (26 + 47 + 46) / 3 = 119 / 3 = 39.666666...

条件一:MIN >= 0.7 * AVG
        0.7 * 39.6667 = 27.7667
        26 >= 27.7667  假 ✗   <- ace 太矮了,只有均值的 65.5%
两条不全为真 -> 整组丢弃

题面的解释印证了这一点:「The average height of long-haired dogs is 39.7, so the low variance criterion requires the height of each long-haired dog to be between 27.8 and 51.6. However, ace is a long-haired dog with height 26, which is outside this range.」

逐步推演:组 short
身高 = {52, 35, 28}
MIN = 28    MAX = 52
AVG = (52 + 35 + 28) / 3 = 115 / 3 = 38.333333...

条件一:MIN >= 0.7 * AVG
        0.7 * 38.3333 = 26.8333
        28 >= 26.8333  真 ✓   <- ginger 险险过关
条件二:MAX <= 1.3 * AVG
        1.3 * 38.3333 = 49.8333
        52 <= 49.8333  假 ✗   <- bella 太高了,是均值的 135.7%
条件二失败 -> 整组丢弃

这一组特别值得看:它通过了第一条却栽在第二条。如果你漏写了 MAX <= 1.3 * AVG,short 组会混进输出,结果变成两行,评分器立刻发现。题面也提示了「For short-haired dogs, bella falls outside the valid range (check!)」——现在你验证过了。

三组只剩一组,最终输出:

curly|1

与 tests/low_variance.py 的期望一致。

常见误区

误区一:条件写进 WHERE。报错 Error: misuse of aggregate function AVG()。记住:条件里出现聚合函数 ⟹ 必须用 HAVING。

误区二:只写一个方向的条件。只写 MIN(height) >= 0.7 * AVG(height) 会漏掉 short 组的淘汰,输出两行 curly|1 和 short|24。只写 MAX 那条则会漏掉 long 组的淘汰。两条都要。

误区三:用 SUM(height)/COUNT(*) 代替 AVG(height)。这是整数除法。long 组会算成 119/3 = 39,阈值变成 0.7*39 = 27.3,结论碰巧还是排除 ace(26 < 27.3),但 short 组会算成 115/3 = 38,阈值 1.3*38 = 49.4,52 仍然超标——这次侥幸没错,但换一组数据就会错。别赌。

误区四:把「30%」理解成「与均值的差不超过 30」。题目说的是相对百分比,不是绝对差值。写成 MAX(height) - AVG(height) <= 30 会让三组全部通过。

误区五:给结果列起了名字并期望它叫 height_range。题面表格里的表头写着 height_range,但评分器只比对值不比对列名。起不起 AS height_range 都能过;起了更可读,无害。

误区六:把大小写当回事。题面示例表里的 Curly 是排版结果,真实数据是 curly。加个 UPPER(fur) 反而会挂。

5. 整份作业回顾

四道题的题面看着像四件不同的事,其实练的是同一套动作在不同复杂度下的组合。真正带得走的是下面这些。

心法一:所有查询都是「摊平 → 筛行 → 分组 → 筛组 → 挑列 → 排序」

这条流水线解释了本次每一道题:

阶段Q1Q2Q3 (sentences)Q4
摊平 FROMparents × dogs(56 行)dogs × sizes(32 行)siblings × sod × sod(256 行)dogs(8 行)
筛行 WHEREparent = name → 7区间匹配 → 8两次查名 + 尺寸相同 → 2无
分组 GROUP BY无无无按 fur → 3 组
筛组 HAVING无无无两条 30% 判据 → 1 组
挑列 SELECTchildname, size|| 拼出的句子fur, 极差
排序 ORDER BYheight DESC无无无

下次卡住时,就照这六格逐格问自己「这一格我要填什么」,比盯着题面干想有效得多。

心法二:没有「查找」,只有「配对 + 判断」

从命令式转到声明式,最难的就是戒掉「去另一张表里查一下」的念头。SQL 的世界里,你能做的只有:把可能相关的东西全部配出来,然后写一个布尔表达式说明什么样的配对是合法的。

一旦接受这一点,很多东西就不再需要死记:

  • 要用两张表的信息 → 把两张表并进 FROM;
  • 要用同一张表的两行 → 把这张表并两次,起两个别名(Q3 siblings);
  • 要查两只狗各自的属性 → 把属性表并两次(Q3 sentences 的 x 和 y)。

心法三:用 < 处理「无序对」

a.child < b.child 这一招同时解决自配、去重和排序三件事。它的一般形式是:当你要枚举一个集合里的无序对时,给元素定一个全序,然后只保留「小的在前」的那一半。Python 里的 for i in range(n): for j in range(i+1, n)、SQL 里的 a.x < b.x,是同一个想法的两种写法。这在期末考的 SQL 题和组合枚举题里都会再遇到。

心法四:全称命题化成极值判断

「组里每一只狗都在范围内」在 SQL 里没有直接对应的写法,但它等价于「组里最矮和最高的都在范围内」。凡是「对所有元素都成立」且条件单调,就往极值上转。

各题对照表

题目核心手法最容易栽的地方迁移到哪里
Q1 by_parent_height等值连接 + ORDER BY ... DESC连接条件写反(child = name),排序错拿成孩子身高任何「A 表存关系、B 表存属性」的查询——这是关系数据库最基本的形态
Q2 size_of_dogs区间连接(非等值条件)区间开闭写错,导致边界上的狗重复或消失分档、分级、按范围归类;也提醒你「连接条件可以是任意布尔式」
Q3 siblings自连接 + 别名 + < 去重忘别名(ambiguous column name)、用 != 导致重复对任何「同一张表内两行之间的关系」:同事、同班、互为好友、路径的两端
Q3 sentences多表连接(同一表并两次)+ || 拼串用 + 拼串静默得到 0;只并一次导致空表报表生成、把查询结果格式化成人读的文本
Q4 low_varianceGROUP BY + 聚合 + HAVING条件误写进 WHERE;只写一个方向的判据一切「按类别统计并筛选类别」的分析:按学院算平均分、按月算销售额

调试 SQL 的实用建议

SQL 最难受的地方在于它很少报错,只是给你错的答案。几条自救办法:

1先跑 SELECT * 看中间结果。怀疑连接不对时,把 SELECT child 临时改成 SELECT *,看看连接后的宽表长什么样、有几行。行数远超预期 ⟹ 少了连接条件。
2数行数。SELECT COUNT(*) FROM ... 是最快的健全性检查。Q1 应该是 7 行(等于 parents 的行数),Q2 应该是 8 行(等于 dogs 的行数),siblings 应该是 4 行。
3用交互式 shell。在 hw/hw06/ 下运行 python3 sqlite_shell.py --init hw06.sql,可以逐条试查询,比每次跑 ok 快得多。
4拆成辅助表。Q3 之所以可做,很大程度是因为题面允许先建 siblings。复杂查询写不出来时,先建一张中间表,把问题砍成两半。这和写 Python 时抽出辅助函数是同一个道理。
5单条跑 ok。python3 ok -q sentences 只跑一道题,输出更聚焦。
本仓库的验证结果

hw/hw06/ 下运行 python3 ok --local:

---------------------------------------------------------------------
Test summary
    4 test cases passed! No cases failed.

四道题(by_parent_height、size_of_dogs、sentences、low_variance)全部通过,本页贴出的 SQL 与 hw/hw06/hw06.sql 逐字一致。