DHU上机打卡D7,熟悉的朋友一眼就知道,这是东华大学机房自学打卡系列的第7天。说是“上机打卡”,其实就是每天固定时间去机房,对着屏幕把当天的训练任务啃完,然后提交记录。第七天是个挺微妙的节点——新鲜感过了,惰性开始冒头,但一旦撑过去,学习节奏就真正成型了。我这天的任务集中在数据库的复杂查询上,内容不算难,但细节非常多,从早到晚在机器前磨了将近五个小时,踩了不少坑,也整理出一套很实用的排查思路,值得记录下来。
这期的内容适合三类人看:正在学校机房上数据库课、准备期末上机考试的同学;自学SQL但卡在“看得懂、写不出”阶段的初学者;以及想找一个可复现的上机打卡节奏、培养代码手感的人。不管你是哪一类,这篇能帮你少走弯路。
1. D7打卡的主题定在哪:多表连接与统计查询
1.1 为什么第七天要安排这个内容
前六天我的打卡内容分别是环境配置、单表增删改查、基础条件筛选、排序与分页、聚合函数入门、分组与去重。到第七天,单表操作基本已经熟练,自然要进入多表连接和统计查询。数据库这门课,单表查询只是热身,真正体现“上机手感”的,是把两张甚至三张表关联起来之后还能理清思路、写出正确结果。
我选这个主题还有一个原因:它是绝大多数上机考试的必考模块。期末机考里,简答题可以背,但多表查询的SQL必须现场写、现场跑,平时不练,考场上很容易卡在JOIN的语法上,或者逻辑想明白了但字段名对不上,一报错就慌。D7这个位置刚好适合集中攻坚。
1.2 连接类型的选择逻辑
当天我主要用了三种连接:内连接(INNER JOIN)、左连接(LEFT JOIN)、以及一个自连接(SELF JOIN)。很多教材把这个讲得很玄乎,其实用生活场景一解释就通了。
内连接就像一个相亲平台,双方都有资料才给匹配,任何一方缺记录,结果里就不出现。左连接则像你拿着全班名单去核对谁交了作业,交过的显示时间,没交的也要显示名字,只是作业时间那里是NULL。
那天遇到一道题是这样的:有三个表——
- students(学生表):s_id, s_name, class_id
- classes(班级表):class_id, class_name
- scores(成绩表):s_id, course_id, score
题目要求:统计每个班级的平均分,只显示平均分大于80分的班级,并按平均分从高到低排列。这本是一道很常规的题,但考察的点全在细节上。
1.3 执行顺序:为什么老手也要默写
写多表查询之前,我习惯在纸上先写一遍执行顺序:FROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。这一步看起来是基础功,但第七天我做那道统计题时,差点把WHERE和HAVING的过滤时机搞混。
WHERE是在分组之前过滤原始行,HAVING是在分组之后过滤聚合结果。平均分大于80这个条件是聚合之后才能判断的,所以要放进HAVING。如果放进WHERE,SQL会直接报错,因为此时聚合函数还没执行,压根没法判断。
关于表关联,我用了一个在实际开发中很常见的技巧:先写FROM和JOIN,再加WHERE过滤,最后才考虑SELECT返回哪些列。这样每一步的中间结果都清晰,排查问题时也能定位到具体环节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心实操:D7当天的四道训练题全解析
2.1 题目一:内连接查询学生选课信息
第一道题是这样:查询所有选了课程的学生姓名、课程名称和成绩。
sql复制SELECT s.s_name, c.course_name, sc.score
FROM students s
INNER JOIN scores sc ON s.s_id = sc.s_id
INNER JOIN courses c ON sc.course_id = c.course_id
ORDER BY sc.score DESC;
这一题我特意加了个ORDER BY,主要是为了后面检查数据时方便肉眼核对——排名靠前的记录一眼扫过去,能快速判断连接条件有没有错位。
实操时要注意:连接条件必须列在ON子句里,而不是放进WHERE。虽然ANSI SQL里,内连接把条件放WHERE也能得到同样的结果,但放到ON里语义更清晰,可读性更好。左连接或者全外连接时,两者会产生截然不同的结果,尽早养成写ON的习惯,后面才不会踩坑。
2.2 题目二:左连接列出未选课学生
第二题要求:列出所有学生,包括没有选课的学生,显示其选课情况。
sql复制SELECT s.s_id, s.s_name, sc.score
FROM students s
LEFT JOIN scores sc ON s.s_id = sc.s_id
ORDER BY s.s_id;
这个查询的核心在于理解NULL的意义。没选课的学生,scores表里没有对应记录,所以LEFT JOIN之后,右边列是NULL。很多人在看到NULL时习惯用“= NULL”去判断,这是个经典错误。NULL不是值,而是一个“未知”状态,必须用IS NULL或IS NOT NULL来判断。
如果题目改成“只显示没选课的学生”,就简单加一行:
sql复制WHERE sc.s_id IS NULL
第六天打卡时我在这上面栽过跟头,第七天写这题已经顺手。但顺手不代表可以掉以轻心,我还是特意跑了三次结果,确认NULL逻辑没有出错。
2.3 题目三:按班级统计平均分
这是当天最磨人的一题,题目描述是这样的:统计每个班级的平均分,只显示平均分大于80分的班级,按平均分从高到低排列。
我的第一版写法:
sql复制SELECT c.class_name, AVG(sc.score) AS avg_score
FROM students s
INNER JOIN classes c ON s.class_id = c.class_id
LEFT JOIN scores sc ON s.s_id = sc.s_id
GROUP BY c.class_name
HAVING AVG(sc.score) > 80
ORDER BY avg_score DESC;
这版本跑出来结果是对的,但我花了不少时间确认一个小细节:LEFT JOIN和INNER JOIN在这里的差别。如果一个班有学生但全部没成绩,AVG计算时会跳过NULL,结果可能是NULL或者不满足条件,具体取决于数据库实现。为了让统计结果稳定可预测,我后来把LEFT JOIN换成了INNER JOIN,因为统计平均分时,没有成绩的行本来就不该参与分母计算。
这就是典型的上机“打磨”过程:第一版能跑,第二版要问自己对不对,第三版才确认最优。
2.4 题目四:自连接查询同班同学对
第四题是个自连接,场景是:查询所有来自同一班级的学生对(A,B),且A的学号小于B的学号,去掉重复。
sql复制SELECT a.s_name AS student_a, b.s_name AS student_b, a.class_id
FROM students a
INNER JOIN students b ON a.class_id = b.class_id
WHERE a.s_id < b.s_id
ORDER BY a.class_id, a.s_id;
单独看这个SQL不长,但自连接的逻辑很容易绕晕。我第一次写时忘加a.s_id < b.s_id,结果出现了“张三大李”和“李四张三”这种重复组合,而且还有自己跟自己配对的记录。加上条件后结果干净多了,也保证了配对唯一性。
这个练习对我帮助很大,因为它强迫你从“两表思维”跳到“一表两用思维”。第七天之前我从来没想过一张表可以和自己连接,通过这个案例彻底理解了表别名的必要性。不取别名的话,连字段名都会冲突,MySQL会直接报错。
3. 上机过程中的排错与调试实录
3.1 报错一:Unknown column 'student_name'
这个报错看起来很基础,但当时真让我卡了快十五分钟。原因是我在students表里定义的字段是s_name,而不是student_name。写SQL时凭记忆敲了student_name,结果就是报错。
这类问题的排查思路很简单:先检查字段名是否和表结构一致,再看是单表的字段还是连接后两张表的同名字段。后者需要用表别名指定:s.s_name,而不是直接写s_name。我后来统一了规范:多表查询中,所有字段一律加别名前缀,绝不裸写。这个习惯在多表操作时能避免很多混淆。
3.2 报错二:Invalid use of group function
这个报错比字段名错误隐藏得更深。我当时想在WHERE子句里用MAX函数过滤,即查找成绩高于最高平均分的学生,直接写成了:
sql复制WHERE score > MAX(score)
立刻就报了这个错。原因很简单:聚合函数不能用在WHERE里,它只属于HAVING和SELECT的聚合阶段。这是我写SQL半年来遇到频率最高的报错之一。遇到这个报错,第一个反应就该是:是不是把聚合函数写到WHERE了?改为HAVING,问题解掉。
3.3 报错三:结果集为空但逻辑看似正确
这个不报错,但比报错更危险。有一道题是查“没有选任何课程的学生”,我信心满满地写好SQL,一执行,返回一行都没有。
后来排查发现,问题出在我之前往scores表里插入了两条“幽灵数据”——学号根本不存在于students表,但成绩表里有记录。当我用反向排除法时,这俩学生不会被正确匹配,导致结果集错乱。这也是为什么很多实际项目中会设置外键约束,就是为了防止这种数据不一致。
日常练习中,我养成了一个习惯:每次跑多表查询前,先分别SELECT COUNT(*)看一下每个表的行数,然后对关键连接字段做一次快速验证。数据量不对,后面怎么查都白搭。这个习惯在打卡D7当天帮了大忙。
3.4 工具层面的两个小坑
除了SQL本身的坑,我还遇到两个工具层面的问题。
第一个是MySQL Workbench的“Safe Updates”模式。这个模式默认开启,会对没有主键条件的UPDATE或DELETE语句直接拒绝执行,报错信息是“You are using safe update mode”。我那天清理临时数据时,想直接执行DELETE FROM temp_scores,结果被拦住了。解决方式有两个:要么在查询里加上主键条件,要么临时关闭safe update模式。这个改动要谨慎,生产环境不建议关闭。
第二个是连接编码问题。当天导入中文数据后,查询结果出现乱码,我开始以为是数据坏了,后来发现是连接未指定utf8mb4。MySQL连接串在初始化时加上characterEncoding=utf8mb4,中文显示就正常了。这个问题在实验室机房的旧版本客户端上特别常见,需要留意。
4. 打卡记录的结构与复盘方法
4.1 我D7当天记录了什么
第七天结束前,我按固定模板写了打卡记录。模板结构是这样的:目标、代码片段、关键报错、解决思路、用时、心得。这样做的价值在于,半个月后回头看,能快速回忆起当时卡在哪里、怎么解决的,不需要重新推敲一遍。
比如当天的记录里,我特意写下了“自连接查询容易产生重复配对,务必加a.s_id < b.s_id条件”这条心得。这是写代码时根本不会想到的、只有亲手跑过才知道的经验。它比书本上的结论更有价值。
4.2 打卡数据怎么用
六天下来,我已经积累了近百条报错记录。我把它们按类型做了标签,D7当天新增了Group Function和Self Join两个标签。做标签的意义在于下次遇到类似问题,能快速定位到自己的学习盲区。比如我当前标签统计里,JOIN相关占了三成,那就说明这块需要额外加练。
这种记录方式不需要额外工具,Typora或者记事本都行。重要的是坚持用同一个格式,方便后续检索。上机打卡的核心,不是每天做了多少题,而是每天有没有留下可追溯的东西。哪怕当天只跑通一道题,只要把思路写清楚,就是有效打卡。
4.3 一天打下来,实感如何
第七天确实比想象中累,因为多表连接和统计查询是逻辑密集型的训练,不是套模板就能糊弄过去的。每次报错都需要回到字段定义和连接逻辑里重新查,中途有几次真的想直接关电脑走人。
但撑过那阵子之后,手感一下子上来了。到晚上收尾时,我已经能不看参考答案,独立写出包含JOIN、GROUP BY、HAVING、ORDER BY四件套的完整查询。这种从卡壳到流畅的过渡,就是上机打卡最大的意义。
5. 给刚起步的人:怎么把D7复制成你的打卡节奏
5.1 从最简单的环境开始,别贪多
很多朋友问我上机打卡怎么开始,我给出的建议是:只要电脑能装SQLite或者MySQL,就够了。不要一上来就折腾集群、分布式、数据仓库之类的东西。D7当天的所有练习,其实在SQLite里也能完成七成,MySQL只是多支持了一些语法细节。
环境越简单,越容易坚持。我见过太多人因为配置环境花了三天,然后就放弃了。打卡的本质是每天动一下手,不是每天搞一套重型工具链。
5.2 给自己定一个可量化的打卡目标
所谓的“打卡”,不能只是“我今天看了数据库”这种模糊记录。最好具体到:今天完成了几个查询、写出了几个正确的SQL、踩了几个坑、下次如何避免。
我D7的目标是:四道训练题全部独立完成,至少记录两个报错及解决方案。结果实际完成了四道题、记录了三个报错。目标设低一点,完成时带来的成就感反而更强,也更愿意继续下去。
5.3 习惯比时长更重要
第七天打卡给我的最大启示是:真正拉开差距的不是智商,而是你能否在第七天、第十七天、第七十天都坐到那台机器前,把当天的任务完成。很多人卡在第七天放弃,我见过太多人了。但只要撑过这个节点,后面的惯性会自动推着你前进。
如果你也在进行类似的打卡计划,第七天前后是最关键的放弃窗口。这时候不需要加量,稳定住原计划,哪怕只做一道题,也是在积累。每天那一点点重复,最后会变成特别扎实的底子。
上机打卡这件事,说到底就是一件特别笨的事情,笨到只要按时去做,就能超过大多数人。
