HW 6:SQLok 4 项通过
四道题,一张狗的家谱。从最基础的两表连接一路做到自连接、字符串拼接和分组过滤——这是整门课里唯一一次「不写函数、不写递归」的作业。
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.
翻成人话,拆成三个独立的要求:
parents 表的 child 列里出现过。ellie 没在 child 列出现过,所以她被排除。期望输出(来自 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 行。每一行现在同时装着「孩子叫什么」和「父母多高」。
想通这一点,剩下的就是填空:
parents 和 dogs。写 FROM parents, dogs。parent 要等于狗的名字 name。写 WHERE parent = name。SELECT child。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 DESC | height 来自 dogs,而 dogs 这一侧被 WHERE 锁定成了「父母那只狗」,所以 height 就是父母的身高。DESC(descending)表示从大到小;不写 DESC 默认是 ASC,结果会正好颠倒过来。 |
验证:手动跑一遍
不摊完 56 行,只把 WHERE parent = name 筛剩的 7 行列出来。对 parents 的每一行,找到 dogs 里名字等于 parent 的那一行,把 height 抄过来:
parent | child | name | fur | height |
|---|---|---|---|---|
| ace | bella | ace | long | 26 |
| ace | charlie | ace | long | 26 |
| daisy | hank | daisy | long | 46 |
| finn | ace | finn | curly | 32 |
| finn | daisy | finn | curly | 32 |
| finn | ginger | finn | curly | 32 |
| ellie | finn | ellie | short | 35 |
七行,正好对应 parents 的七行——因为每只作为父母的狗在 dogs 里只有一行,所以是一对一匹配。ellie 作为孩子从未出现,因此她永远不会进入 child 列,这就自动实现了「只要有父母的狗」。
接着 ORDER BY height DESC,按最后一列从大到小重排:
child 列
| 父母身高 | child | 说明 |
|---|---|---|
| 46 | hank | 父母是 daisy(46),全场最高的父母 |
| 35 | finn | 父母是 ellie(35) |
| 32 | ace | 父母都是 finn(32)。三者并列,sqlite 保持它们在中间结果里的原顺序,即 parents 表的插入顺序 ace → daisy → ginger |
| 32 | daisy | |
| 32 | ginger | |
| 26 | bella | 父母都是 ace(26),同理保持插入顺序 |
| 26 | charlie |
最终输出 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 theminand less than or equal to themaxin height to qualify assize.
这句英文的边界必须一个字一个字抠:
| 英文 | 数学 | 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 表的四行:
size | min | max | 实际区间 |
|---|---|---|---|
| toy | 24 | 28 | \((24, 28]\) |
| mini | 28 | 35 | \((28, 35]\) |
| medium | 35 | 45 | \((35, 45]\) |
| standard | 45 | 60 | \((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, sizes | 32 行候选配对。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] | 结果 |
|---|---|---|---|---|---|---|
| ace | 26 | 26>24 且 26≤28 ✓ | 26>28 ✗ | ✗ | ✗ | toy |
| bella | 52 | ✗ | ✗ | 52≤45 ✗ | 52>45 且 52≤60 ✓ | standard |
| charlie | 47 | ✗ | ✗ | 47≤45 ✗ | 47>45 且 47≤60 ✓ | standard |
| daisy | 46 | ✗ | ✗ | 46≤45 ✗ | 46>45 且 46≤60 ✓ | standard |
| ellie | 35 | ✗ | 35>28 且 35≤35 ✓ | 35>35 ✗ | ✗ | mini |
| finn | 32 | 32≤28 ✗ | 32>28 且 32≤35 ✓ | ✗ | ✗ | mini |
| ginger | 28 | 28>24 且 28≤28 ✓ | 28>28 ✗ | ✗ | ✗ | toy |
| hank | 31 | 31≤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 的条件:
bella 和 bella 显然有同一个父母(都是 ace),但这不算兄弟姐妹。还有一条隐含要求:只保留体型分类相同的那些对。ace(toy) 和 daisy(standard) 虽然是兄妹,但尺寸不同,不进结果。
题面给了一个可选但强烈建议的中间步骤:先造一张 siblings 辅助表,只存名字对,再用它去造 sentences。这个建议应该听——把「配对」和「造句」拆成两步,每一步都简单到能一眼看懂。
怎么想到的(一):兄弟姐妹为什么要自连接
先问最朴素的问题:「有同一个父母」这件事,在哪张表里能看出来?
parents 表长这样:
parent | child |
|---|---|
| ace | bella |
| ace | charlie |
| daisy | hank |
| finn | ace |
| finn | daisy |
| finn | ginger |
| ellie | finn |
「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.child | b.child | 问题 |
|---|---|---|---|
| ace | bella | bella | 自己配自己 ✗ |
| ace | bella | charlie | 想要的 ✓ |
| ace | charlie | bella | 与上一行重复 ✗ |
| ace | charlie | charlie | 自己配自己 ✗ |
| daisy | hank | hank | 自己配自己 ✗ |
| finn | ace | ace | 自己配自己 ✗ |
| finn | ace | daisy | 想要的 ✓ |
| finn | ace | ginger | 想要的 ✓ |
| finn | daisy | ace | 重复 ✗ |
| finn | daisy | daisy | 自己配自己 ✗ |
| finn | daisy | ginger | 想要的 ✓ |
| finn | ginger | ace | 重复 ✗ |
| finn | ginger | daisy | 重复 ✗ |
| finn | ginger | ginger | 自己配自己 ✗ |
| ellie | finn | finn | 自己配自己 ✗ |
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.child | b.child | 字典序判断 | 留否 |
|---|---|---|---|
| bella | bella | bella < bella 假 | 丢 |
| bella | charlie | b<c 真 | 留 |
| charlie | bella | c<b 假 | 丢 |
| charlie | charlie | 假 | 丢 |
| hank | hank | 假 | 丢 |
| ace | ace | 假 | 丢 |
| ace | daisy | a<d 真 | 留 |
| ace | ginger | a<g 真 | 留 |
| daisy | ace | d<a 假 | 丢 |
| daisy | daisy | 假 | 丢 |
| daisy | ginger | d<g 真 | 留 |
| ginger | ace | 假 | 丢 |
| ginger | daisy | 假 | 丢 |
| ginger | ginger | 假 | 丢 |
| finn | finn | 假 | 丢 |
siblings 表最终 4 行:(bella, charlie)、(ace, daisy)、(ace, ginger)、(daisy, ginger)。注意 hank 和 finn 都消失了——他们各自是独生子女,没有兄弟姐妹可配。
再跑 sentences。把 Q2 算出的尺寸填进这 4 行:
first | x.size | second | y.size | x.size = y.size? |
|---|---|---|---|---|
| bella | standard | charlie | standard | ✓ 留下 |
| ace | toy | daisy | standard | ✗ 丢弃 |
| ace | toy | ginger | toy | ✓ 留下 |
| daisy | standard | ginger | toy | ✗ 丢弃 |
剩 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
WHERE | HAVING | |
|---|---|---|
| 执行时机 | 分组之前 | 分组之后 |
| 过滤对象 | 单独的行 | 整个组 |
| 能用聚合函数吗 | 不能 | 能,而且通常必须用 |
| 典型用法 | WHERE height > 30(只看高狗) | HAVING COUNT(*) >= 2(只看至少两只狗的组) |
| 语义差别举例 | 「先扔掉所有矮狗,再按毛发分组求均值」——均值只算高狗 | 「先按毛发分组求均值,再扔掉均值太小的组」——均值算全部狗 |
最后一行是理解两者差异的关键:它们不只是「写在哪」的问题,算出来的数是不一样的。
还有一个弯:「每一只狗都在范围内」怎么写
题目说的是「each dog with that fur type is within 30%」,这是一个「对组内所有元素成立」的全称命题。SQL 里没有 FOR ALL。怎么办?
这里需要一个小小的逻辑转换,而题面 Hint 已经替你做了一半:
MIN(height) >= 0.7 * AVG(height)。MAX(height) <= 1.3 * AVG(height)。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 裁决:
身高 = {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
身高 = {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.」
身高 = {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. 整份作业回顾
四道题的题面看着像四件不同的事,其实练的是同一套动作在不同复杂度下的组合。真正带得走的是下面这些。
心法一:所有查询都是「摊平 → 筛行 → 分组 → 筛组 → 挑列 → 排序」
这条流水线解释了本次每一道题:
| 阶段 | Q1 | Q2 | Q3 (sentences) | Q4 |
|---|---|---|---|---|
摊平 FROM | parents × dogs(56 行) | dogs × sizes(32 行) | siblings × sod × sod(256 行) | dogs(8 行) |
筛行 WHERE | parent = name → 7 | 区间匹配 → 8 | 两次查名 + 尺寸相同 → 2 | 无 |
分组 GROUP BY | 无 | 无 | 无 | 按 fur → 3 组 |
筛组 HAVING | 无 | 无 | 无 | 两条 30% 判据 → 1 组 |
挑列 SELECT | child | name, size | || 拼出的句子 | fur, 极差 |
排序 ORDER BY | height 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_variance | GROUP BY + 聚合 + HAVING | 条件误写进 WHERE;只写一个方向的判据 | 一切「按类别统计并筛选类别」的分析:按学院算平均分、按月算销售额 |
调试 SQL 的实用建议
SQL 最难受的地方在于它很少报错,只是给你错的答案。几条自救办法:
SELECT * 看中间结果。怀疑连接不对时,把 SELECT child 临时改成 SELECT *,看看连接后的宽表长什么样、有几行。行数远超预期 ⟹ 少了连接条件。SELECT COUNT(*) FROM ... 是最快的健全性检查。Q1 应该是 7 行(等于 parents 的行数),Q2 应该是 8 行(等于 dogs 的行数),siblings 应该是 4 行。hw/hw06/ 下运行 python3 sqlite_shell.py --init hw06.sql,可以逐条试查询,比每次跑 ok 快得多。siblings。复杂查询写不出来时,先建一张中间表,把问题砍成两半。这和写 Python 时抽出辅助函数是同一个道理。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 逐字一致。