最近课程正好告一段落MySQL作业也交到了第二次。说实话第一次作业大多数人还能靠“照着课件把建表语句抄一遍”混过去到了第二次就明显分层了有人半小时写完有人对着报错刷了一晚上网页。这份作业日期是2025年12月14日题目量不算大但把建表、约束、查询、事务、索引这些基础考点串了个遍做一遍下来基本能把MySQL的常见用法过手一遍。这篇文章我就按当时实际完成作业的思路把环境准备、建表、查询、事务索引和提交前的自查一条条捋清楚适合刚学完SQL语法、准备系统做一次MySQL练习的同学参考也是给我自己留的一份复盘笔记。1. 环境准备不只是装好MySQL把练习数据跑起来1.1 版本选不对后面全是坑很多同学在这栽过跟头作业要求用的5.7电脑上装的8.0结果运行窗口函数、CTE、某些排序规则时结果跟课件讲的不一样还以为是写错了。我自己这次就专门确认了课程要求的版本直接对齐到同版本后面所有SQL才能稳定复现。如果你用的是8.0建表时默认字符集是utf8mb4排序规则是utf8mb4_0900_ai_ci如果你用的是5.7默认可能是utf8mb4_general_ci。这种细节平时不在意等作业里出现中文排序或表情符号存储时结果会让你懵半天。所以第一个建议是看明白作业标注的版本尽量装同一个大版本不要为了新鲜装个最新版给自己添乱。1.2 导入练习数据时的两个“老熟人”报错作业一般会给一个建库建表的SQL文件也可能让你自己手动敲。我这次第一件事就是用source命令把基础数据导进来数据库马上就有了练习环境。在命令行里执行的时候Windows用户最容易遇到路径分隔符问题。比如你把文件放在D盘某个目录下面直接在MySQL命令行敲source D:\mysql_homework\init.sql这条路大概率报错因为反斜杠会被转义理解。正确写法是用正斜杠source D:/mysql_homework/init.sql第二个高频报错是ERROR 1366 (HY000): Incorrect string value多半是SQL文件里的中文在导入时字符集对不上。解决办法是执行前先设置好客户端的字符集SET NAMES utf8mb4; source D:/mysql_homework/init.sql;如果SQL文件本身是GBK编码保存的导入前最好先用文本编辑器把它另存为UTF-8编码再执行source否则后面查询中文时隐患很大。1.3 给自己搭一套能反复折腾的调试数据作业给的数据通常只有十几行做一些简单查询够用但遇到分组取最值、多表关联这种题最好自己在本地扩一套测试数据把边界情况都覆盖到。我这次用的是一套模拟选课系统的数据后面所有示例都基于它逻辑简单又刚好把作业考点串起来学生表studentid、name、class_id。课程表coursecid、name、credit。选课表scid、sid、cid、score。DROP DATABASE IF EXISTS homework_demo; CREATE DATABASE homework_demo DEFAULT CHARACTER SET utf8mb4; USE homework_demo; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, class_id VARCHAR(10) NOT NULL ) ENGINEInnoDB; CREATE TABLE course ( cid INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, credit DECIMAL(3,1) NOT NULL ) ENGINEInnoDB; CREATE TABLE sc ( id INT PRIMARY KEY AUTO_INCREMENT, sid INT NOT NULL, cid INT NOT NULL, score DECIMAL(5,1) DEFAULT NULL, CONSTRAINT fk_sc_student FOREIGN KEY (sid) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (cid) REFERENCES course(cid) ) ENGINEInnoDB;这个脚本做完至少说明你掌握了最基本的建库、建表和约束写法后面所有查询题都是在这个基础上展开的。2. 建表题类型、约束、字符集一起考才见真章2.1 建表不是写完CREATE TABLE就完事很多同学第一次作业里建表能拿满分第二次作业却开始丢分原因很简单第二次作业不再让你照着课件抄而是给一段业务描述让你自己决定字段类型和约束。这等于把“会不会用”和“知不知道为什么这么用”一起考了。我的经验是先拆业务语义再定表和字段最后补约束。举个例子课程表的学分字段新手容易写成FLOAT(3,1)但涉及金额、分数、学分这类需要精确计算的字段优先用DECIMAL。FLOAT和DOUBLE是浮点数二进制表示会产生误差像1.1这种数字存进去再读出来后面可能变成1.0999999…。如果作业里要求“统计平均分”算出来的结果和预期差一丁点扣分挨得冤枉。DECIMAL(M,D)里M是总位数D是小数位数学分的写法DECIMAL(3,1)表示最多三位数字、保留一位小数最大99.9足够用。2.2 字符串长度、默认值、字符集这些“小字段”是大坑VARCHAR(20)里的20指的是20个字符不是20个字节。一个中文字符在utf8mb4下占4字节但VARCHAR的长度按字符计算所以VARCHAR(20)能存20个汉字。如果写错成VARCHAR(20)本来没问题问题是很多人把长度写太小比如VARCHAR(4)存课程名四个字以内的课程能存但“数据结构导论”这种六个字的就报错。数据一多就暴露了。还有一个常被漏掉的点建表时如果不显式指定字符集它会沿用数据库级别的设置。我这次建库时统一指定了utf8mb4所以表也都跟着正确。如果你是直接CREATE TABLE没有指定字符集而数据库默认是latin1那么存中文直接报错或出现乱码。2.3 外键约束失败别把锅甩给“数据库太难”作业里涉及到外键的题最常见的报错是这个ERROR 1452: Cannot add or update a child row: a foreign key constraint fails这行报错在初学者眼里像天书其实排查起来就三个方向。第一个方向两个表的关联字段类型不一致。比如student.id是INTsc.sid你写成了BIGINT或者VARCHAR外键就建不上或者插入时数据库压根不认。第二个方向插入数据时外键值在父表中不存在。往sc表插入一条sid999的记录而student表里根本没有id999就会触发上面的1452。我调试时经常先查一下父表数据再插入这也是最容易被忽略的。第三个方向字段名和引用名写反了比如FOREIGN KEY (sid) REFERENCES student(id)有人会写成FOREIGN KEY (id) REFERENCES student(id)导致语义完全跑偏。在写建表题时养成一个习惯建完表马上用SHOW CREATE TABLE验证结果确认主键、外键、字符集、引擎都和你预想一致。这个习惯比反复用眼睛检查靠谱得多别问我怎么知道的。SHOW CREATE TABLE sc\G能看到完整的建表语句外键约束名、字段类型一目了然。这也是作业提交前最有性价比的检查动作之一。3. 查询题怎么拆从读懂题意到写出能跑的SQL3.1 先画表关系再拆题最后写SQL第二次作业里查询题占比最大也是最容易在“题意理解”上翻车的部分。很多同学拿着题就写SQL写完发现查出来的结果比预期多很多行原因多半是没先想清楚表之间怎么关联。我的办法是拿到一道查询题先在草稿纸上画出涉及哪几张表、通过哪个字段连接、需要返回哪些字段。比如“查每门课成绩最高的学生的姓名和分数”涉及course、sc、student三张表关联路径是course.cid sc.cid再通过sc.sid student.id拿姓名。不画出来很容易在JOIN条件里漏条件产生笛卡尔积。3.2 单表查询排序、限制、日期函数是送分题先看最简单的单表题。查“年龄最大的学生”本质是先按生日排序再取一条SELECT * FROM student ORDER BY birth_date ASC LIMIT 1;这里要提醒一句SQL里取“前N条”不是用TOP而是LIMIT N想取第N到第M条用LIMIT N-1, M-N1或者LIMIT offset, count。作业里经常出现“第2到第5名”这种表述用LIMIT 1,4就行。日期处理是单表题的另一个高频点。比如“查询2025年选课的学生”可以用YEAR函数SELECT DISTINCT sid FROM sc WHERE YEAR(create_time) 2025;如果题目要求“按年月展示”通常用DATE_FORMATSELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) FROM sc GROUP BY month;没接触过格式化字符串的同学容易卡在使用%Y还是%y上%Y是四位年份%y是两位年份。这个坑我踩过一次后现在写日期格式化一定先看官方文档再动手。3.3 JOIN题ON和WHERE的过滤时机这一题决定你是60分还是90分多表连接是查询题的核心最经典的一道题是“统计每个分类的商品数量没有商品的分类也要显示为0”。新手通常先查有商品的分类发现有些分类根本没出现就以为数据有问题。实际上应该用LEFT JOIN把左表分类表的所有记录都保留右表商品表没有匹配到时商品数量显示为NULL再用COUNT(商品字段)统计时NULL不计入得到0。这道题有个进阶陷阱如果把过滤条件写在WHERE里LEFT JOIN会悄悄退化成INNER JOIN。比如-- 期望没有商品的分类显示0 SELECT c.cid, COUNT(p.pid) FROM category c LEFT JOIN product p ON c.cid p.cid WHERE p.price 100 GROUP BY c.cid;结果不满足条件的分类直接不显示了。正确做法是把价格条件放进JOIN的ON子句SELECT c.cid, COUNT(p.pid) FROM category c LEFT JOIN product p ON c.cid p.cid AND p.price 100 GROUP BY c.cid;这个ON和WHERE的差异作业里不一定会直接考原题但很可能换个马甲出现。想拿高分务必理解这个机制JOIN后的ON决定你关联哪些行WHERE决定最终结果里保留哪些行。放在ON里过滤发生在连接阶段不满足条件的右表行变成NULL左表行仍然保留放在WHERE里过滤发生在连接完成之后NULL行会被直接丢弃。3.4 分组聚合和“组内TopN”这类卡住很多人的题分组题里有一道经典难题查每门课程最高分的学生姓名。思路分两步先用GROUP BY取每门课的最高分SELECT cid, MAX(score) AS max_score FROM sc GROUP BY cid;再把这个结果作为子查询与sc表做关联取出对应的学生id最后再连student表拿姓名。如果题目要求“每组中的前两名”事务上更稳的方式是用窗口函数ROW_NUMBER()SELECT * FROM ( SELECT sc.sid, sc.cid, sc.score, ROW_NUMBER() OVER (PARTITION BY cid ORDER BY score DESC) AS rn FROM sc ) t WHERE t.rn 2;注意窗口函数在MySQL 8.0及以上才支持。如果课程环境是5.7这个写法会直接报语法错误只能用“自连接子查询”的笨办法代替。所以作业要求什么版本真的会影响你选择解法。看几道典型的选课系统作业题可以自己先动手写再看下面的参考实现查询没有选任何课程的学生的姓名。SELECT name FROM student WHERE id NOT IN (SELECT DISTINCT sid FROM sc WHERE sid IS NOT NULL);这里很多人忽略一个细节如果子查询结果中包含NULLNOT IN会返回空结果因为SQL里NULL的布尔判断结果是“未知”。所以务必要把子查询里的sid用WHERE sid IS NOT NULL过滤掉或者改用NOT EXISTS。查询选修课程超过3门的学生学号和选课数。SELECT sid, COUNT(*) AS cnt FROM sc GROUP BY sid HAVING cnt 3;查询每门课程的平均分并按平均分从高到低排序只显示平均分高于80分的课程。SELECT cid, AVG(score) AS avg_score FROM sc GROUP BY cid HAVING avg_score 80 ORDER BY avg_score DESC;注意WHERE和HAVING的分工WHERE过滤的是分组前的原始行HAVING过滤的是分组后的聚合结果。第二轮作业里至少有一道题专门考这个区别别把HAVING写成WHERE。4. 事务与索引作业里的“附加题”其实很值钱4.1 事务操作题一半靠理解一半靠手速第二次作业如果进度快会加入事务题最常见的是“模拟转账”。这种题的考点不是你能不能背出ACID四个字母而是能不能正确使用事务控制语句。一个标准的转账事务流程是这样的START TRANSACTION; UPDATE account SET balance balance - 500 WHERE id 1; UPDATE account SET balance balance 500 WHERE id 2; -- 确认无误后提交 COMMIT; -- 如果执行到一半发现余额不足回滚 ROLLBACK;写事务题有个细节必须先确认第一条UPDATE语句确实影响了一行否则后面提交了也是白提。可以用ROW_COUNT()来检查或者在执行UPDATE后马上SELECT一下余额。我在练习时习惯把两个UPDATE写成两条加一个简单的“余额充足”判断而不是一上来就写COMMIT。事务部分还会追问隔离级别比如REPEATABLE READ和READ COMMITTED的区别。作业一般不要求深究但要理解一个核心直觉隔离级别越低并发越高读到“脏数据”的风险也越大隔离级别越高数据越安全并发越差。MySQL 8.0默认是REPEATABLE READ这跟Oracle默认的READ COMMITTED不一样作业里如果提到“MySQL默认隔离级别”答案别写错。4.2 索引题EXPLAIN是你最好的老师索引题在第二次作业里通常以“优化”的形式出现给你一条大表查询语句让你提效。很多同学第一反应是“给查询的字段加索引”但加在哪个字段上、用没用上心里没底。教你一个万能办法用EXPLAIN看执行计划。EXPLAIN SELECT * FROM sc WHERE sid 100;执行结果里有个type字段从好到坏大概是const、ref、range、index、ALL。如果看到ALL说明是全表扫描查询效率最差如果看到ref或range说明索引生效了。我在练习时建了一个测试大表插入几万行数据没加索引时查一个非主键字段耗时几十毫秒加上索引后瞬间变成几毫秒。把这两次EXPLAIN结果、耗时对比写进作业说明里这部分就比别人多了一个层次。但索引不是加得越多越好。每个索引都会占用空间并拖慢INSERT、UPDATE和DELETE的速度。作业里如果让你设计索引先问自己三个问题这个查询的WHERE条件用到了哪个字段有几条SQL共用同一个字段前缀有没有冗余索引如果同一张表上已经有了一个复合索引(a,b)再单独建一个(a)的索引就是冗余的可以直接删掉。5. 提交作业前我建议你多做的几件事5.1 用一套自查清单兜底第二次作业完成后我一般不会立刻提交而是在命令行里重新登录一遍数据库从头执行一次所有题目对应的SQL。执行过程中重点检查三类问题第一每条SQL结尾是不是都有分号。MySQL命令行里分号是语句结束标志缺了它整条语句不会执行你会看到个“-”继续等待输入这时候不是报错是你忘了分号。第二表名和字段名的拼写是否一致。建表时叫sc查询时写成SC没问题因为Windows下MySQL对表名大小写不敏感但如果你部署在Linux服务器上表名大小写是敏感的sc和SC会被当成两张表。作业提交到服务器上判分时这是非常常见的报错来源。哪怕你在本地跑通了也要检查一遍大小写习惯。第三结果集的字段顺序是不是题目要的。很多时候题目说“显示学生姓名和课程名”你先写了课程名和学生姓名内容对得上但顺序反了人工批改容易扣分。5.2 调试代码少用主库多开一个临时库练习过程中反复插数据、删数据、建索引很容易把作业要的原数据改乱。我这次的做法是单独建一个临时库比如homework_debug所有练手语句都在里面跑跑通了再回到作业库里执行一次。这样即便把临时库炸了原库一点事没有。调试多表查询时如果结果集莫名其妙多出很多行先用SELECT COUNT(*)确认数据量再单独查一下连接字段里有没有重复值。连接条件不是唯一字段时左表一行可能匹配右表多行结果集就会翻倍。这也是作业调试时最容易被忽略的情况。5.3 把这次作业变成以后能复用的笔记第二次作业做完最值钱的不是分数而是你留下的一份SQL示例集。我以前总想着“我会了就不用记”结果过两周再遇到类似题目居然连LEFT JOIN和RIGHT JOIN的过滤时机都要重新想一遍。这次我一题一题整理成了自己的笔记每条SQL旁边标注了当时的解题思路和踩过的坑。下次写博客或者帮同学看代码时翻出来直接用省了很多时间。另外一个小技巧是善用SHOW WARNINGS。MySQL里很多查询不会直接报错只是默默返回一个不太对的结果或者某些隐式转换会消耗性能。执行完复杂查询后加上一句SHOW WARNINGS它会提示你哪些字段做了隐式类型转换、哪些索引被忽略了。作业里追求“能跑”只是及格线能在执行计划里挑出潜在毛病才算真正把这轮知识练到自己身上了。我现在做查询题之前还会习惯性地先想一遍“这个需求可不可能在WHERE里过滤掉NULL”“分组后要不要再筛一次”这些全部是这次作业逼出来的条件反射。等这批题目做完你再看那些看起来复杂的SQL题基本都是一层层拆下来的套路不会再有那种“每个单词都认识但整句读不懂”的无力感了。