考过三级的朋友应该都有同感:数据库这门科目,选择题背一背还能对付,真正拉开差距的,是高级数据库查询这一块。尤其是SELECT语句里的多表连接、分组统计、子查询嵌套,考场上思路一乱,整道题就废了。这篇把“高级数据库查询”拆开揉碎讲一遍,从考点分布到SQL写法再到常见丢分点,都是我自己备考时反复踩坑总结出来的经验,适合正在准备计算机三级数据库技术科目的考生,也适合想系统提升SQL查询能力的初学者参考。
1. 考点范围与复习重心
1.1 考纲里的“高级查询”到底指什么
三级数据库技术科目对SQL部分的考查,绝不是让你写一条简单的“SELECT * FROM 表”就完事。考纲里所谓的高级数据库查询,核心锁定在几个方向:连接查询(内连接、外连接)、聚合函数配合分组统计、嵌套子查询(尤其相关子查询和EXISTS)、集合运算(UNION、INTERSECT、EXCEPT),以及把这些能力组合起来完成一个相对复杂的数据分析任务。
从历年真题来看,“给定两张表,查询满足某个业务条件的学生名单”“统计每门课程的平均分并筛选出高于全体平均分的课程”这类题目出现频率极高。它们考查的不是你会不会写某一条语句,而是你会不会根据题目描述,正确选择连接方式、正确使用分组和筛选条件,以及正确区分WHERE和HAVING的作用时机。这块内容在选择题、填空题、设计题里都会出现,设计题更是直接要求手写SQL语句,所以光靠看和背是不够的,必须动笔。
1.2 备考顺序与核心考点分布
我复习时先列了一张考点分布表,按出现频率和难度把内容分成三档,这样心里有底,知道精力该往哪里投。
| 难度档位 | 核心考点 | 常见题型 | 复习优先级 |
|---|---|---|---|
| 基础必得分 | 单表查询、WHERE条件、 ORDER BY排序、聚合函数(COUNT/SUM/AVG/MAX/MIN) | 选择、填空 | 高 |
| 核心得分区 | 内连接查询、外连接查询、GROUP BY分组、HAVING筛选、简单子查询 | 选择、填空、设计 | 高 |
| 拉分重灾区 | 相关子查询、EXISTS/NOT EXISTS、集合运算、视图查询、多条件嵌套 | 填空、设计、分析 | 中高 |
我个人的建议是:先把第二档的“内连接 + 分组 + HAVING”练到闭眼能写,再攻第三档的子查询与EXISTS。因为子查询题往往是在连接查询的基础上叠加条件,如果连接逻辑本身就糊涂,嵌套之后只会更乱。
1.3 复习资料选择与刷题建议
教材方面,官方指定的三级数据库技术教程里SQL章节是基础,但说实话例题量不够,需要配合其他资源补充。我备考时用的组合是“教材 + 历年真题 + 一套在线SQL练习平台”。在线平台的题库不用太复杂,能把基本的建表、插入、查询练熟就行,重点是练习做题速度。
刷题时我给自己定了一个规矩:每道涉及查询的题,不管会不会,先在纸上写出答案,再对着标准答案批改。直接在电脑上敲SQL得到结果,容易让人产生“我会了”的错觉。考场上没有运行环境,一切依赖你对语法的掌握和逻辑的清晰程度,所以手写训练非常重要。
2. 多表连接查询:理清连接逻辑比背语法更重要
2.1 内连接与外连接的实际语义差异
多表连接是高级查询的第一道关卡。内连接(INNER JOIN)只返回两个表中满足连接条件的匹配行,外连接则分为左外连接(LEFT JOIN)、右外连接(RIGHT JOIN)和全外连接(FULL JOIN),它们会保留某一边表中的不匹配行,并用NULL填充另一边。
理解外连接,我有一个生活化的类比:内连接就像核对两份名单,只把两边都有的人挑出来。左外连接则像以左边名单为准,左边每个人都要出现,右边对不上号的就记成“查无此人”(NULL)。
以经典的学生选课场景为例,假设有学生表(student)和选课表(sc),要查询所有学生的选课情况,包括没选课的学生。这时候必须用LEFT JOIN而不是INNER JOIN:
SELECT student.sname, sc.cno FROM student LEFT JOIN sc ON student.sno = sc.sno;这条语句以student表为基准,即使某个学生在sc表中没有匹配记录,他也会出现在结果中,cno字段显示为NULL。如果用INNER JOIN,这个没选课的学生就被过滤掉了,业务结果就错了。
2.2 ON与WHERE的过滤时机差异
连接查询里最隐蔽的丢分点,就是ON子句和WHERE子句的执行顺序。在INNER JOIN中,ON和WHERE哪个写条件,最终结果几乎一样,但在LEFT JOIN中,两者含义截然不同。
以查询“所有选了课程的学生及其成绩,且只要成绩大于80分的记录”为例:
-- 写法一:把成绩条件放在WHERE里 SELECT student.sname, sc.score FROM student LEFT JOIN sc ON student.sno = sc.sno WHERE sc.score > 80;这条语句的执行过程是:先做左连接,得到所有学生及其选课记录,然后用WHERE过滤掉score不大于80的行。问题在于,没选课的学生score为NULL,NULL > 80 的结果既不是TRUE也不是FALSE,而是UNKNOWN,WHERE条件会把这类行过滤掉。最终结果里没有选课的学生就消失了,LEFT JOIN失去了意义,效果等同于INNER JOIN。
-- 写法二:把成绩条件放在ON里 SELECT student.sname, sc.score FROM student LEFT JOIN sc ON student.sno = sc.sno AND sc.score > 80;写法二先对sc表进行条件筛选,再与student表连接,这样没选课的学生依然保留,score为NULL,业务上完全正确。
这个点几乎年年有人错。考试时如果题目说“查询所有学生及其选课成绩,包括未选课的学生,成绩只显示80分以上的”,那么写法一就是标准错误答案。判断依据很简单:一旦出现“所有XX”这种要求保留基准表全部记录的描述,条件要往ON里放,而不是WHERE。
2.3 三表连接与连接顺序经验
三级考试里三表连接考查得也不少,典型场景是“学生—选课—课程”三张表,查询选了某门课程的学生名单。三表连接的书写套路是:先确定表之间的连接关系,再按关系链写JOIN。
SELECT student.sname, course.cname FROM student JOIN sc ON student.sno = sc.sno JOIN course ON sc.cno = course.cno WHERE course.cname = '数据库';写三表连接时,我的经验是先画出表关系图:student与sc通过sno关联,sc与course通过cno关联。然后按“主表 → 中间表 → 附表”的顺序依次连接。中间表通常是关系表,比如sc选课表,它存放两个外键,起到桥梁作用。
另外一个实战心得:查询结果里同名字段很多,考试判卷时列名并不是只要对了就行,必须明确写出“表名.列名”的限定格式。比如sno在student和sc中都存在,如果SELECT后面直接写sno,部分数据库会报“列名不明确”的错误,判卷时也容易被扣分。养成写“表名.列名”的习惯,是连接查询的基本素养。
3. 聚合分组与统计查询:抓住WHERE、GROUP BY、HAVING的先后逻辑
3.1 聚合函数的隐藏细节
聚合函数这块,多数人都知道COUNT、SUM、AVG、MAX、MIN的基本用法,但有几个细节考场上很爱考。第一个是COUNT(*)和COUNT(列名)的区别:COUNT(*)统计的是行数,包括所有行;COUNT(列名)统计的是该列非NULL值的个数。如果某列为NULL,COUNT(列名)不会把它算进去。
比如查询“每个班级的学生人数”,如果学生表的班级编号列存在NULL值,那么COUNT(class_id)统计出来的结果就会少算未分班的学生,而COUNT(*)不会漏。具体选哪个,取决于题目问的是“有多少行记录”还是“该列有多少非NULL值”。
AVG函数也有同样的坑。AVG(score)只对非NULL成绩求平均,如果某学生缺考导致score为NULL,他不会被计入分子,也不会被计入分母。如果业务上想把缺考当0分处理,就得先用COALESCE函数把NULL转成0再求平均:
SELECT AVG(COALESCE(score, 0)) AS avg_score FROM sc;这个细节在填空和设计题中都出现过,属于一眼就会、不点就懵的考点。
3.2 GROUP BY的分组逻辑与常见错误
GROUP BY执行的是“按列值归类”的操作。同一分组内,所有行的分组列值相同,聚合函数对每一组分别统计。
最常见的错误写法,是在SELECT子句中出现了分组列以外的普通列,而该列没有用聚合函数包裹。例如查询每个班级的人数,并显示班级名称和学生姓名:
-- 错误写法 SELECT class_name, student_name, COUNT(*) FROM student GROUP BY class_name;这条语句在多数数据库的默认设置下直接报错,因为student_name没有被聚合也没有出现在GROUP BY中,系统不知道在一个班级分组的多名学生中取哪一个。考试时如果题目问“以下SQL能否正确执行”,这个就是典型错误选项。正确做法是去掉student_name,或者用MAX、MIN这类聚合函数包裹它(虽然后者业务意义有限,但语法合法)。
GROUP BY和WHERE、HAVING的执行顺序,是我认为整个查询部分最重要的逻辑链条。SQL的执行顺序可以简化理解为:先FROM取表,再WHERE过滤行,再GROUP BY分组,然后HAVING过滤分组,最后SELECT取列并排序。因为WHERE是在分组之前执行的,所以WHERE中不能使用聚合函数;而HAVING是在分组之后执行的,专门对聚合结果进行筛选。
3.3 统计查询的标准模板
把连接、分组、筛选组合起来,就是统计查询的标准模板。我复习时总结了一套固定套路:先确定要查哪张表、要与哪张表连接、按什么分组、用什么聚合函数、分组之后用什么条件过滤。五步走完,SQL基本就成形了。
看一道经典真题:查询选修课程超过两门的学生学号和选课门数。拆解过程是:数据来自sc表,按sno分组,用COUNT计数,筛选条件为数量大于2。因为条件是对聚合结果COUNT(*)的筛选,必须放在HAVING中:
SELECT sno, COUNT(*) AS cnt FROM sc GROUP BY sno HAVING COUNT(*) > 2;这里如果把COUNT(*) > 2写进WHERE,执行时分组还没完成,聚合结果根本不存在,语法上就不合法。
再看一道带连接的统计题:查询各课程的平均成绩,并筛选出平均成绩大于等于80分的课程,结果按平均成绩降序排列。这需要先连接sc和course表取课程名,再按cno分组,最终筛选和排序:
SELECT course.cname, AVG(sc.score) AS avg_score FROM sc JOIN course ON sc.cno = course.cno GROUP BY course.cno, course.cname HAVING AVG(sc.score) >= 80 ORDER BY avg_score DESC;这里有一个细节值得注意:我只按cno分组,却把cname也写进了GROUP BY。这是因为在严格模式下,SELECT中出现的非聚合列必须出现在GROUP BY里,而cname依赖于cno,一并写进去既合法又稳妥。
3.4 分组统计的进阶扩展
三级考试偶尔会把题目难度往上提一点,比如“查询每门课程成绩最高的学生信息”这类问题,单纯用GROUP BY往往解决不了。因为GROUP BY按课程分组后,只能查出每组的最高分,却无法直接拿到对应的学生姓名。
应对这类题,思路要转换:先用子查询查出每门课程的最高分,再与原始表做连接,匹配课程和分数:
SELECT sc.cno, sc.sno, student.sname, sc.score FROM sc JOIN student ON sc.sno = student.sno JOIN ( SELECT cno, MAX(score) AS max_score FROM sc GROUP BY cno ) t ON sc.cno = t.cno AND sc.score = t.max_score;这种“先分组得极值,再连接取明细”的模式非常实用,笔试常考,工作中做报表也经常用。建议把它当成模板记熟。
4. 子查询分类突破:非相关子查询与相关子查询
4.1 非相关子查询的执行顺序
子查询从执行逻辑上分两类:非相关子查询和相关子查询。非相关子查询的内层查询不依赖外层查询的任何值,可以独立执行。数据库会先执行内层查询,得到一个结果集,再把这个结果集作为外层查询的输入。
典型例子是查询成绩高于全体学生平均成绩的学生名单。内层子查询先算出平均分,外层查询再拿每个学生的成绩和这个平均分比较:
SELECT sno, score FROM sc WHERE score > ( SELECT AVG(score) FROM sc );这类子查询写起来难度不大,关键是记住一个原则:子查询返回一行一列时使用“>”“<”“=”等比较运算符;返回多行一列时,就不能直接使用等号,而要搭配IN、ANY、ALL等关键字。
以查询成绩不低于“数据库”课程最高成绩的学生为例,内层返回的是“数据库”课程所有成绩,可能有多行,外层就需要用评分比较:
SELECT sno, score FROM sc WHERE score >= ALL ( SELECT score FROM sc JOIN course ON sc.cno = course.cno WHERE course.cname = '数据库' );ALL语义是“大于等于子查询中所有值”,即大于等于最大值。ANY语义是“大于等于子查询中任意一个值”,即大于等于最小值。考场上这两个容易混,我的记法是:ALL更严格,对应所有;ANY更宽松,对应任一。
4.2 相关子查询:内层依赖外层的核心概念
相关子查询是拉分重灾区,也是三级考试里区分度最高的考点。它的特点是内层查询引用了外层查询的列,每处理外层的一行,内层查询都要重新执行一次,逻辑上相当于两层循环嵌套。
经典例题:查询每门课程中成绩高于该课程平均分的学生。注意这里不是“整体平均分”,而是“各自课程的平均分”,所以内层查询需要根据外层当前行的cno,动态计算对应课程的平均分:
SELECT sc1.sno, sc1.cno, sc1.score FROM sc AS sc1 WHERE sc1.score > ( SELECT AVG(sc2.score) FROM sc AS sc2 WHERE sc2.cno = sc1.cno );执行过程是这样的:数据库从外层取出一行记录(比如cno='C001'),然后带着这个cno去执行内层查询,算出C001课程的平均分,再判断外层这行成绩是否高于该平均分;接着取下一行,重复这个过程。对于三张表连接的相关子查询,内外层都需注意别名的作用。
写相关子查询时,给表起别名是必须的。上面的例子中外层表叫sc1,内层表叫sc2,通过别名来区分“当前行”和“目标数据”,否则SQL无法解析到底引用的是哪份sc表。我见过不少考生在这个点上翻车,把别名一省,整个查询语义就乱了。
4.3 EXISTS与NOT EXISTS的使用诀窍
EXISTS用于判断内层查询是否有结果返回,有则返回TRUE,无则返回FALSE,它不关心子查询具体返回什么值,只关心是否存在满足条件的记录。因此子查询中SELECT什么列其实不重要,习惯上写成SELECT 1即可。
EXISTS最经典的应用是NOT EXISTS,用来表达“不存在”的语义。典型题目:查询没有选修任何课程的学生名单。很多考生习惯用NOT IN,但NOT IN在子查询结果中存在NULL时会出问题,整个查询会返回空集。用NOT EXISTS则不会有这个坑:
SELECT student.sname FROM student WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sno = student.sno );我复习时特意验证过NOT IN的NULL陷阱:如果sc表中存在sno为NULL的记录,NOT IN子查询返回结果集中含有NULL,那么外层查询的结果集会被判定为空。这个行为很反直觉,但笔试和实际应用中都有可能遇到。稳妥起见,遇到“不存在”类问题,优先选NOT EXISTS。
EXISTS和IN在能够互相转换的简单场景下,现代数据库优化器处理得都不错,性能差异没那么明显。但考试更看重语义是否准确,以及能否应对NULL边界情况。EXISTS还有一个优势,它是逐行判断存在性,天然适合表达“至少有一门课满足条件”这类相关子查询。
4.4 子查询与JOIN的选择原则
有时候同样一道题,用子查询和用JOIN都能写出来。查询没选课的学生,既可以用NOT EXISTS,也可以用LEFT JOIN然后过滤NULL。以考生经验来说,两种写法都应该会,因为考试的评分标准和运行环境判断不同,容易把JOIN思路下的NULL判断写错。
SELECT student.sname FROM student LEFT JOIN sc ON student.sno = sc.sno WHERE sc.sno IS NULL;这种“外连接 + IS NULL”模式在功能上与NOT EXISTS等价。需要提醒的是,IS NULL只能判断因连接产生的空值,如果sc表的sno本身允许NULL,这种写法可能把业务上的NULL值误判成“无匹配”。因此考试时,凡涉及“不存在”场景,我优先写NOT EXISTS,语义清晰,也规避了NULL干扰。
5. 进阶查询手段与书写规范:CASE、UNION与视图
5.1 CASE WHEN实现查询结果的逻辑分支
CASE WHEN表达式允许在SELECT子句中对查询结果做条件分支,类似编程语言里的if-else。它在考试中常出现在“对成绩分等级”这类题里,例如把sc表中的成绩分成优、良、中、及格、不及格五档:
SELECT sno, cno, score, CASE WHEN score >= 90 THEN '优秀' WHEN score >= 80 THEN '良好' WHEN score >= 70 THEN '中等' WHEN score >= 60 THEN '及格' ELSE '不及格' END AS level FROM sc;这里有几个规则需要记牢:CASE表达式会按顺序从上往下判断,一旦某个WHEN条件成立,就返回对应结果并结束判断,不会继续往下执行。所以分数区间的书写顺序很重要,建议从高分区间到低区间排列,避免出现条件覆盖不完整的情况。END后面必须加列别名,否则结果集列名会很长很乱,判卷也不好看。
CASE WHEN还可以和聚合函数组合,实现条件统计。比如统计每个班级中成绩及格和不及格的人数:
SELECT class_id, SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) AS pass_cnt, SUM(CASE WHEN score < 60 THEN 1 ELSE 0 END) AS fail_cnt FROM student JOIN sc ON student.sno = sc.sno GROUP BY class_id;这种“CASE WHEN嵌套聚合函数”的方式,比多次查询再拼接要简洁得多。考试时如果给出了按条件统计的题目,优先考虑这个写法。
5.2 UNION与UNION ALL的取舍
UNION用于合并多个查询结果集,核心规则是各查询的列数必须相同,对应列的数据类型要兼容。UNION和UNION ALL的区别在于是否去重:UNION会对结果集做去重处理,UNION ALL直接拼接全部记录。
去重听起来是好事,但代价是要对结果排序或哈希,数据量一大性能就会明显下降。三级考试的填空里经常考“UNION与UNION ALL的区别,以及各自适用场景”,答题要点就是:UNION默认去重,UNION ALL不去重、保留重复行且效率更高。如果业务上确定不会产生重复行,用UNION ALL以减少额外开销。
实际写UNION时,我习惯在每个SELECT后加排序条件时特别小心。整个UNION只能有一个ORDER BY,而且必须放在最后一段SELECT的后方。中间段SELECT如果加了ORDER BY,在很多数据库里会直接报语法错误,在部分数据库里虽然不报错但语义会被忽略。考试设计题里如果要求合并结果并排序,标准写法是这样:
SELECT sno, score FROM sc WHERE score < 60 UNION ALL SELECT sno, score FROM sc WHERE score BETWEEN 60 AND 85 ORDER BY sno;5.3 视图的定义与查询限制
视图是三级考试分析题里比较爱考的内容。视图本质是保存下来的SELECT语句,本身不存储数据,查询视图时数据库实时执行定义中的SELECT逻辑。定义视图的语法是CREATE VIEW加上带AS的查询语句。
视图相关题目有两点高频考查:一是视图能否更新。简单视图(基于单表、包含主键、不使用聚合和DISTINCT)通常可以执行INSERT、UPDATE、DELETE操作,复杂视图(多表连接、聚合函数、GROUP BY)大多不允许更新。这个规则在判断“以下操作能否在视图上执行”的题里几乎是必考。
二是WITH CHECK OPTION的作用。创建视图时加上WITH CHECK OPTION,意味着通过视图执行INSERT或UPDATE时,数据库会检查新数据的行是否仍满足视图定义的WHERE条件,如果不满足则拒绝操作。它的意义在于保证“视图能查到的数据才能通过视图修改”,防止用户通过视图修改出自己看不到的数据。这个机制靠理解记忆,别死背概念。
5.4 查询性能优化视角的书写规范
三级考试中关于查询性能的题目多是概念判断和优化分析,这里分享几个考场必会的基础规则。第一个是避免在WHERE条件的列上使用函数或运算,比如WHERE YEAR(birth_date) = 2000,这种写法导致索引失效,全表扫描。应该改写为范围条件:WHERE birth_date >= '2000-01-01' AND birth_date < '2001-01-01'。
第二个是复合索引遵循最左前缀原则。如果表上建有(班级, 学号)的复合索引,那么加速查询的条件必须包含班级列。单独用学号列作为WHERE条件,用不上该索引。这个知识点在分析“给出索引与查询语句,判断哪些查询能利用索引”的题目中是核心判分点。
第三个是避免SELECT *。查询所有列意味着数据库要把每行的所有字段都读出来,无法利用覆盖索引,网络和内存的开销也大。考试中如果问你“这条查询存在什么性能问题”,写“应明确列出所需列,避免返回无用字段”就是标准得分点。
6. 考场实战技巧与常见丢分点速查
6.1 手写SQL的答题规范
三级考试的SQL设计题对手写规范有隐性要求。我总结了几个能减少无谓失分的书写习惯:第一,语句结尾写分号,除非题目明确说不要求;第二,关键字(SELECT、FROM、WHERE、GROUP BY、ORDER BY等)统一大写,列名和表名保持小写,整体风格一致;第三,复杂语句按逻辑分段换行,每段缩进对齐,方便阅卷老师看出你的思路;第四,连接条件写在ON子句中,过滤条件写在WHERE中,不要混在一个位置。
加分项是把题目给的业务条件翻译成注释写在SQL上方。例如题目说“统计各班级学生人数且只显示人数大于30的班级”,我会先在草稿纸上把条件拆解为“分组键是班级,聚合函数是COUNT,筛选是HAVING COUNT>30”,再动笔写SQL。这个翻译步骤看起来多余,但它能有效避免直接上手时漏掉条件。
6.2 高频失分场景与处理策略
我把备考过程中见过的典型错误按失分原因整理成了速查表,考前最后一天过一遍很有效。
| 错误类型 | 错误写法或错误思路 | 正确做法 | 判分点 |
|---|---|---|---|
| 外连接条件误放 | LEFT JOIN+WHERE过滤右表字段 | 需要保留左表全部行时,条件放ON | 是否理解ON/WHERE语义 |
| 分组列遗漏 | SELECT非分组列未聚合 | 确保SELECT列要么在GROUP BY,要么被聚合 | 语法合法性判断 |
| HAVING误用 | 在HAVING中写非聚合列条件 | 普通列条件放WHERE,聚合条件放HAVING | 执行顺序理解 |
| NOT IN含NULL | 子查询结果可能含NULL | 优先使用NOT EXISTS | NULL处理机制 |
| CASE顺序错乱 | 区间条件从低到高写 | 从高区间到低区间依次书写 | 条件覆盖完整性 |
| 列名未限定 | 多表连接时SELECT列名歧义 | 使用“表名.列名”格式 | 健壮性与规范性 |
这六类错误我在刷题时全部踩过,而且不止一次。印象最深的是外连接条件误放那道题,刷了第三遍才真正理解ON和WHERE的差异。不是记不住,而是没形成意识——每次写LEFT JOIN,都要主动问自己:这个条件如果放WHERE,基准表还会不会保留。
6.3 时间分配与做题策略
三级数据库技术科目的考试时间比较紧张,我的经验是给SQL设计题预留足够时间。选择题遇到拿不准的,先标记跳过,不要恋战。填空题中涉及SQL语法填空的,凭第一感觉填写,如果完全没思路就根据前后关键字推断。
设计题则相反,一定要仔细读题。把题目中的表名、字段名、业务条件全部圈出来,确认查询目标是单表还是多表,是查询明细还是统计汇总。很多设计题失分不是不会写SQL,而是题目明明要求显示“所有学生”却用了INNER JOIN,或者要求“各课程平均分”却漏了分组。关键信息都在题目描述里,审题比写代码更重要。
6.4 考前冲刺与随身速记
考前最后三天不建议再刷新题了,我的做法是把之前做错的题统一过一遍,把错误原因写在题目旁边,然后整理一张SQL模板速记卡。卡片内容包括:三表连接模板、分组统计模板、相关子查询模板、EXISTS反查模板、CASE WHEN分支模板。每张模板配一道典型例题,考前一小时只看卡片,不翻书。
再补两个平时容易被忽略的细节。一是别只背SQL语法,要理解关系代数与SQL的对应关系,三级考试的选择题偶尔会从关系代数的角度提问“哪种操作等价于某个SELECT查询”。二是日期函数、字符串函数这类单行函数也要掌握基础用法,虽然不属于“高级查询”的核心,但会作为辅助条件出现在题目中。
我个人复习这套内容最有用的一个方法,是把每个查询场景做成模板卡片,正面写业务场景,背面写SQL。碎片时间拿出来随机抽卡,看到“查询没选课的学生”就立刻默写NOT EXISTS版本和LEFT JOIN版本。反复练到条件反射之后,考场上遇到任何高级查询题目,第一反应不会是慌张,而是自动套用对应的模板和解题套路。数据库查询这门手艺,说到底就是熟练度,多练一道题,考场上就多一分把握。