十道题覆盖五类写法:
单表筛选、排序、分页、模糊匹配、聚合分组。 每道题都只用一到两张表。
做题的顺序不是「先写 SQL」,是「先问三件事」:要哪张表、要不要排序、结果的粒度是什么。
两个高频错法:count(*) 数的是行数不是人数;
100 * 及格数 / 人数 会先做整数除法,得写 100.0。
这十道题不是新语法,全部能从前十八章找到出处。
它们的价值在于「连起来用」——真实需求不会告诉你「这题考 group by」。
每道题的输出都能和 Demo 里的数据手工核对,这一点很重要:
六个人、三门课、十八条成绩,全部加起来能对得上,说明 SQL 没写错。
一、做题先问三件事
第一件:要哪张表。 单表题直接 where;要显示名字就 join 上去;
要统计就 group by。 这一步想清楚了,SQL 就写对了一半。
第二件:要不要排序。 只要出现 limit、只要出现「前三」「排名」这类词,就必须 order by。
而且排序字段要能唯一定序,不然翻页会漏人或重复。
第三件:结果的粒度是什么。 一行是一个学生,还是一行是一次成绩,还是一行是一门课。
粒度定错,group by 一定写错——而且数字看着还挺像对的,最难查。
这三件事想清楚了再动手写,比写完再调快得多。
二、第一组:筛选和排序
第 1、2 题都是筛选,区别在第 2 题多问了一层「一班是哪一班」。
where cls_id = 1 是硬编码——
真项目里不会这么写,因为班级号会变。 改成 join cls … where c.name = '一班',
多写一句,换来的是「班级改名也不影响」。
这种「用名字查而不是用 id 查」的习惯,在需求变更时能救命。
第 3 题考的是日期怎么存。 birth 存成 TEXT 也能正确排序,
前提是格式必须是 YYYY-MM-DD。 因为这种格式的字典序和时间序是一致的——
2008-01-19 比 2008-07-25 小,字典序也是它小。
换成 2008/1/19 就废了:2008/10/1 的字典序会排在 2008/1/9 前面,
因为第二个字符 0 比 1 小。 这是选日期格式时的一条硬规矩,不是风格问题。
三、第二组:分页和模糊
第 4 题的坑在「以上」两个字:
>= 90 和 > 90 在这批数据上结果一样(因为正好没有 90 分整的),
但这只是凑巧。 需求里「90 分以上」包含不包含 90,一定要问清楚。
第 5 题是分页:
limit 3 offset 0 就是第一页。 但注意排序字段——总分 如果有并列,两页之间会漏人或重复,
生产代码应该写成 order by 总分 desc, s.id。 第 18 章完整讲过这件事。
第 6 题是模糊匹配:
% 匹配任意长度,_ 匹配一个字符。 like '%张%' 用不上索引——
因为前面有通配符,索引没法定位。
几百行的小表无所谓,几十万行就会明显卡,那时候得靠全文索引或搜索引擎。
四、第三组:聚合和分组
第 7 题是每个学生的汇总,
三个聚合函数一起用:count(*)、sum、round(avg(...), 2)。 round 那个 2 不能省——
不加它会打出 72.33333333333333 这种一长串,界面上没法看。
第 8 题有个隐藏的坑:
count(*) 数的是「成绩行数」,不是「学生数」。 现在十八行、三门课各六人,正好齐。
但一旦有人缺考,count(*) 就会少数一个——
因为缺考的人根本没有那一行,不是存了个 0。 要数「应该有多少人」,得从 stu 表数。
第 9 题是及格率,那个 100.0 的小数点不能省。
写成 100 的话,100 * 及格数 / 人数 会先做整数除法,
结果全是 0 或 1。 这正是第 18 章那个「8 / 3 给 2」的坑在实际需求里的样子。
第 10 题是分数段统计,case when 从上往下匹配、命中就停,
所以顺序不能乱写——把 score >= 60 放到最前面,90 分也会被判成「中」。
这题还顺带演示了视图的用处:四五个 when 抄在 select 里又长又容易抄错,
写一次视图,以后随时查 v_seg 就行。 改分段标准时也只改一处——
这才是视图最实在的用处,和「少打字」没关系。
五、十道题跑完,回头看数据
第十题的分段结果是「优 6、中 6、良 5、差 1」,加起来 18,
和总行数对得上。 这是个好习惯:分组统计的结果,各类加起来必须等于总数,
对不上就说明 case 写漏了分支,或者 where 把某些行滤掉了。
七、八、九三题的结果也能互相对:第 7 题六个人的总分加起来是 1462,
除以 18 得 81.22,正是全校平均分——两处数字能对上,说明中间没有漏行或重复。
交叉核对是查 SQL 错误最有效的手段,
比盯着语句看管用得多。
尤其是 join 写错的时候,往往不报错、只是行数变多或变少,只有靠「总数对不对」才能发现。
六、动手 Demo:十道题,一句一道
Demo 按「筛选排序 → 分页模糊 → 聚合分组」三组走,十道题的输出全都打印出来,可以逐条核对。 用的是 19.1 那个库,数据完全一样——所以上面正文里引用到的数字,都能在输出里找到。
# 十道基础题,一句一道;c 是上一个 Demo 建好的库
# 1 列出所有学生
c.execute("SELECT id, name, gender FROM stu ORDER BY id")
# 2 只查一班(不写死班级号)
c.execute("SELECT s.name FROM stu s JOIN cls c ON c.id = s.cls_id "
"WHERE c.name = '一班'")
# 3 女生按出生日期从大到小
c.execute("SELECT name, birth FROM stu WHERE gender = '女' "
"ORDER BY birth DESC")
# 4 成绩 90 分以上,按分数降序
c.execute("SELECT stu_id, course_id, score FROM score "
"WHERE score >= 90 ORDER BY score DESC")
# 5 按总分排名取前三(第一页)
c.execute("SELECT s.name, SUM(sc.score) AS 总分 FROM stu s "
"JOIN score sc ON sc.stu_id = s.id "
"GROUP BY s.id ORDER BY 总分 DESC LIMIT 3 OFFSET 0")
# 6 姓名里带「张」的
c.execute("SELECT name FROM stu WHERE name LIKE '%张%'")
# 7 每个学生的总分和平均分
c.execute("SELECT s.name, SUM(sc.score), ROUND(AVG(sc.score), 2) "
"FROM stu s JOIN score sc ON sc.stu_id = s.id GROUP BY s.id")
# 8 每门课的平均、最高、最低
c.execute("SELECT c.name, ROUND(AVG(sc.score), 2), "
"MAX(sc.score), MIN(sc.score) FROM score sc "
"JOIN course c ON c.id = sc.course_id GROUP BY sc.course_id")
# 9 每门课的及格人数和及格率(100.0 那个小数点不能省)
c.execute("SELECT c.name, COUNT(*), "
"SUM(CASE WHEN sc.score >= 60 THEN 1 ELSE 0 END), "
"ROUND(100.0 * SUM(CASE WHEN sc.score >= 60 THEN 1 ELSE 0 END) "
"/ COUNT(*), 1) FROM score sc JOIN course c "
"ON c.id = sc.course_id GROUP BY sc.course_id")
# 10 按分数段统计(先把 case 做成视图,再分组)
c.execute("CREATE VIEW v_seg AS SELECT stu_id, score, CASE "
"WHEN score >= 90 THEN '优' WHEN score >= 80 THEN '良' "
"WHEN score >= 60 THEN '中' ELSE '差' END AS 段 FROM score")
c.execute("SELECT 段, COUNT(*) FROM v_seg GROUP BY 段 ORDER BY 1 DESC")
实跑输出:
--------------------------------------------------------------------
第八十五课:基础问题——十道题过一遍
--------------------------------------------------------------------
看完题先想「要哪张表」
**基础题的共同点是「一张表就够」**,
**最多连一张附表取名字。** **所以做题的第一步不是写 SQL,是问「这题要哪几张表的哪些列」。**
**下面十道题按难度递增**:
**先单表筛选、再排序分页、再聚合、最后分组统计。**
**每一题的输出都能和 Demo 里的数据对上,可以自己核。**
第一组:筛选和排序
**第 1 题:列出所有学生的姓名和性别,按学号排**
SQL> SELECT id, name, gender FROM stu ORDER BY id
id name gender
-----------------------
1 张伟 男
2 李娜 女
3 王强 男
4 赵敏 女
5 孙悦 女
6 周杰 男
(6 行)
**第 2 题:只查一班的学生**
SQL> SELECT name FROM stu WHERE cls_id = 1
name
-----
张伟
李娜
(2 行)
**`cls_id = 1` 是硬编码。** **真项目里不会这么写**——
**班级号会变,应该先按名字查出 id,或者直接 `join` 上去:**
SQL> SELECT s.name FROM stu s JOIN cls c ON c.id = s.cls_id WHERE c.name = '一班'
name
-----
张伟
李娜
(2 行)
**多写一句 `join`,换来的是「班级改名也不影响」。**
**第 3 题:女生按出生日期从大到小排**
SQL> SELECT name, birth FROM stu WHERE gender = '女' ORDER BY birth DESC
name birth
--------------------
孙悦 2008-09-30
李娜 2008-07-25
赵敏 2008-01-19
(3 行)
**`birth` 存成 `TEXT` 也能正确排序**,
**因为 `YYYY-MM-DD` 这个格式的字典序和时间序是一致的。** **这是选日期格式时的一条硬规矩**:
**别用 `2026/1/9` 这种,`2026/10/1` 会排到 `2026/1/9` 前面。**
第二组:分页和模糊
**第 4 题:成绩 90 分以上的记录,按分数降序**
SQL> SELECT stu_id, course_id, score FROM score WHERE score >= 90 ORDER BY score DESC
stu_id course_id score
-----------------------------------
5 2 96.00
2 1 95.00
5 3 93.00
1 2 92.00
2 3 91.00
5 1 91.00
(6 行)
**`>= 90` 还是 `> 90`,得看需求怎么定义「90 分以上」。**
**Demo 里 90 分整的一条都没有,所以两种写法结果一样**——
**但这只是这批数据凑巧。** **需求里「以上」包含不包含,一定要问清楚。**
**第 5 题:按总分排名,取前三名(第 1 页,每页 3 条)**
SQL> SELECT s.name AS 姓名, SUM(sc.score) AS 总分 FROM stu s JOIN score sc ON sc.stu_id = s.id
GROUP BY s.id ORDER BY 总分 DESC LIMIT 3 OFFSET 0
姓名 总分
----------------
孙悦 280.00
李娜 274.00
张伟 255.00
(3 行)
**`limit 3 offset 0` 就是第一页。** **第 18 章说过:**
**只要用 `limit`,就必须带 `order by`**,
**而且排序字段要能唯一定序。** **这里 `总分` 有并列的话,两页之间会漏人或重复**——
**所以生产代码应该写成 `order by 总分 desc, s.id`。**
**第 6 题:姓名里带「张」的学生**
SQL> SELECT name FROM stu WHERE name LIKE '%张%'
name
-----
张伟
(1 行)
**`%` 匹配任意长度,`_` 匹配一个字符。** **`like '%张%'` 用不上索引**,
**数据量大时要靠全文索引或搜索引擎。** **几百行的小表无所谓,几十万行就会明显卡。**
第三组:聚合和分组
**第 7 题:每个学生的总分和平均分**
SQL> SELECT s.name AS 姓名, COUNT(*) AS 门数, SUM(sc.score) AS 总分, ROUND(AVG(sc.score),
2) AS 平均分 FROM stu s JOIN score sc ON sc.stu_id = s.id GROUP BY s.id
ORDER BY 总分 DESC
姓名 门数 总分 平均分
------------------------------------
孙悦 3 280.00 93.33
李娜 3 274.00 91.33
张伟 3 255.00 85.00
赵敏 3 252.00 84.00
王强 3 217.00 72.33
周杰 3 184.00 61.33
(6 行)
**`round(x, 2)` 是保留两位小数。** **不加它会打出 `72.33333333333333` 这种一长串**,
**界面上没法看。** **本沙箱和 MySQL 的 `round` 都是四舍五入。**
**第 8 题:每门课的平均分、最高分、最低分**
SQL> SELECT c.name AS 课程, COUNT(*) AS 人数, ROUND(AVG(sc.score), 2) AS 平均分,
MAX(sc.score) AS 最高, MIN(sc.score) AS 最低 FROM score sc JOIN course c
ON c.id = sc.course_id GROUP BY sc.course_id
课程 人数 平均分 最高 最低
----------------------------------------------
语文 6 81.83 95.00 60.00
数学 6 79.67 96.00 58.00
英语 6 82.17 93.00 66.00
(3 行)
**`count(*)` 数的是「成绩行数」,不是「学生数」。** **现在 18 条成绩、三门课各 6 人,正好齐。**
**但一旦有人缺考,这两个数就不一样了**——
**缺考的人根本没有那一行,不是存了个 0,`count(*)` 会少数一个。**
**要数「应该有多少人」,得从 `stu` 表数。**
**第 9 题:每门课的及格人数和及格率**
SQL> SELECT c.name AS 课程, COUNT(*) AS 人数,
SUM(CASE WHEN sc.score >= 60 THEN 1 ELSE 0 END) AS 及格数,
ROUND(100.0 * SUM(CASE WHEN sc.score >= 60 THEN 1 ELSE 0 END) / COUNT(*), 1) AS 及格率
FROM score sc JOIN course c ON c.id = sc.course_id GROUP BY sc.course_id
课程 人数 及格数 及格率
------------------------------------
语文 6 6 100.00
数学 6 5 83.30
英语 6 6 100.00
(3 行)
**`sum(case when … then 1 else 0 end)` 是「数满足条件的行」的标准写法。**
**`count(case when … then 1 end)` 也行**,**因为 `count` 不数 `NULL`。**
**注意 `100.0` 那个小数点。** **写成 `100` 的话,`100 * 及格数 / 人数` 会先做整数除法**,
**结果全是 0 或 1。** **第 18 章那个 `8 / 3` 给 `2` 的坑,在这里会咬人。**
**第 10 题:按分数段统计人数——优(≥90)、良(80~89)、中(60~79)、差(<60)**
**这题的 `case` 有四个分支,直接写进 `select` 会把一行撑爆。** **先把它做成视图:**
| CREATE VIEW v_seg AS SELECT stu_id, score, CASE
| WHEN score >= 90 THEN '优' WHEN score >= 80 THEN '良'
| WHEN score >= 60 THEN '中' ELSE '差' END AS 段
| FROM score
**视图建好之后,分组就变成一句话:**
SQL> SELECT 段, COUNT(*) AS 人数 FROM v_seg GROUP BY 段 ORDER BY 人数 DESC
段 人数
------------
优 6
中 6
良 5
差 1
(4 行)
**`case when` 从上往下匹配,命中就停。** **所以顺序不能乱写**:
**把 `score >= 60` 放到最前面,90 分也会被判成「中」。**
**`group by 段` 用的是别名。** **MySQL 允许在 `group by` 里用 `select` 的别名,SQLite 也允许。**
**但标准 SQL 不允许**——**换数据库时可能报错,稳妥的写法是把整个 `case` 再抄一遍。**
**顺便说一句为什么用视图**:**这一串四五个 `when` 抄在 `select` 里,又长又容易抄错**,
**写一次以后随时查 `v_seg` 就行。** **改分段标准时也只改一处**——
**这就是视图最实在的用处,和「少打字」没关系。**
十道题的共同套路
**第一步永远是「要哪张表」**:**单表题直接写 `where`**,**要名字就连 `join`**,
**要统计就 `group by`。**
**第二步是「要不要排序」**:
**只要有 `limit`、只要有「排名」「前三」这类词,就必须 `order by`。**
**第三步是「结果的粒度是什么」**:
**一行是一个学生,还是一行是一次成绩,还是一行是一门课。**
**粒度定错,`group by` 一定写错,而且数字看着还挺像对的。**
第 2 题的两种写法结果一样(张伟、李娜),但 join 那版不依赖班级号,班级改名照样对。
第 3 题按 birth 降序:孙悦(2008-09-30)、李娜(2008-07-25)、赵敏(2008-01-19)。
降序是「大的在前」,所以出生的晚的排前面。
第 4 题六条 90 分以上,最高 96(孙悦数学)。
注意没有 90 分整的记录——所以 > 90 和 >= 90 在这批数据上恰好一样。
第 7 题六个人都是 3 门课,总分从 280 到 184。
六个总分加起来 1462,除以 18 得 81.22——就是这批数据的全校平均分。
第 9 题数学及格率 83.30%(6 人里 5 人及格),语文和英语都是 100%。
那个 100.0 的小数点要是漏了,这里会变成 100、83、100。
第 10 题四段加起来 18:优 6、中 6、良 5、差 1。 对得上总数,说明 case 的分支没有漏。
30 秒实验
- 把第 9 题的
100.0改成100,及格率会变成什么? - 第 7 题加一个
having count(*) = 3,结果会变吗?为什么? - 第 5 题把
order by 总分 desc改成order by 姓名,「前三名」还是不是那三个人?
常见误区
where cls_id = 1硬编码班级号。 班级号会变,应该按名字join查。count(*)当「学生数」用。 它数的是行数,缺考的人没有行,会被少数。- 及格率写成
100 * 及格数 / 人数。 整数相除先砍小数,结果只剩 0 或 1,要写100.0。
自测三问(点开对答案)
1. `birth` 存成 `TEXT` 为什么还能正确排序? —— 因为 `YYYY-MM-DD` 这种定长补零格式的字典序和时间序一致。 换成 `2008/1/9` 这种不补零的写法就会排错。 2. 分组统计的结果要怎么自查? —— 把各类的计数加起来,必须等于总数。对不上说明 `case` 漏了分支,或者 `where` 多滤了行。 3. 为什么 `where cls_id = 1` 不如 `join cls ... where c.name = '一班'`? —— 前者把班级号写死了,班级号一变就查不到;后者按名字查,班级改名、编号重排都不影响。十道基础题过了,下一篇上难度:19.3 高级问题——九道题,题题有坑。
- ☐ (本篇完,下一篇 19.3 高级问题)