简介:数据库系统概论课程的SQL练习表文档,围绕学生表、选课表、课程表三张核心表展开,面向正在学习数据库原理与SQL语法的高校学生,帮助读者通过实际建表与插入数据掌握数据库的基本操作和完整性约束。整个资源包只有1个PDF文档,约48KB,内容包含创建数据库的命令、三张表的建表语句,以及多组示例插入数据;其中学生表以学号为主键并设置姓名唯一约束,课程表通过预修课程号外键关联自身,选课表则用学号和课程号组成复合主键,同时外键关联学生表与课程表。学习者可以直接复制文档中的SQL语句到MySQL运行,快速搭建一个学生选课练习环境,随后针对单表查询、多表连接、成绩统计等典型题目进行实操,并理解外键约束对插入顺序的影响。目前已有1907人学习下载,适合作为数据库系统概论上机练习、期末复习或自学入门的轻量实用参考资料。
1. 数据库系统概论SQL练习的经典三表:student、sc、course
刚学数据库系统概论的人,十有八九都撞见过这套student、sc、course三表练习。它几乎是国内数据库课程里最通用的一套SQL练手数据:student存学生基本信息,course存课程信息,sc存选课和成绩,三张表靠主外键关联,把关系模型最基本的一对多、多对多关系都装进去了。PDF里通常是一张表结构说明加几十道SQL题目,从简单查询一路做到嵌套子查询。这东西能帮你解决的实际问题很直接:把理论课上的关系代数、连接、聚合、分组这些概念,落成一条条能跑的SQL,顺手把考试题里那些套路摸熟。适合正在学数据库理论、准备期末考或面试前想快速找回SQL手感的人。别急着跳过——这套练习看着基础,真正动手建库跑一遍,翻车点比你想象的要多。
2. 先把三张表建起来:从PDF表结构到可运行的MySQL库
2.1 读懂PDF里的表结构:字段、主键、外键关系
任何版本的student、sc、course练习,表结构大同小异,常见的字段定义如下。
student表:
| 字段 | 类型 | 说明 |
|---|---|---|
| Sno | CHAR(9) | 学号,主键 |
| Sname | VARCHAR(20) | 姓名 |
| Ssex | CHAR(2) | 性别 |
| Sage | SMALLINT | 年龄 |
| Sdept | VARCHAR(20) | 所在系,比如CS、IS、MA |
course表:
| 字段 | 类型 | 说明 |
|---|---|---|
| Cno | CHAR(4) | 课程号,主键 |
| Cname | VARCHAR(40) | 课程名 |
| Cpno | CHAR(4) | 先行课课程号,可为空 |
| Ccredit | SMALLINT | 学分 |
sc表:
| 字段 | 类型 | 说明 |
|---|---|---|
| Sno | CHAR(9) | 学号,联合主键一部分 |
| Cno | CHAR(4) | 课程号,联合主键一部分 |
| Grade | DECIMAL(4,1) | 成绩,可为空 |
三张表的关系一定要先看明白再动手:student和course相互独立,sc通过Sno、Cno分别引用student和course,形成两个一对多关系,最终组合成多对多关系。换句话说,sc表的主键是(Sno, Cno),这意味着同一个学生选同一门课只能有一条记录,这是后面所有练习题的前提。很多题目问“没有选课的学生”“没人选的课程”,本质都是在利用sc表的引用完整性做差集运算。
2.2 建表DDL:主键、外键、默认值一次写全
我一般会按下面这套DDL在MySQL 8.0里建表。不要删掉外键约束,练习阶段留着它反而能帮你发现数据错误。
CREATE DATABASE IF NOT EXISTS school; USE school; CREATE TABLE student ( Sno CHAR(9) PRIMARY KEY, Sname VARCHAR(20) NOT NULL, Ssex CHAR(2) DEFAULT '男', Sage SMALLINT, Sdept VARCHAR(20) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( Cno CHAR(4) PRIMARY KEY, Cname VARCHAR(40) NOT NULL, Cpno CHAR(4), Ccredit SMALLINT, FOREIGN KEY (Cpno) REFERENCES course(Cno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sc ( Sno CHAR(9), Cno CHAR(4), Grade DECIMAL(4,1), PRIMARY KEY (Sno, Cno), FOREIGN KEY (Sno) REFERENCES student(Sno), FOREIGN KEY (Cno) REFERENCES course(Cno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:student表的Sname设为NOT NULL,因为后续题目经常会按姓名做条件过滤,空姓名会导致查询结果里出现不明不白的行。course表的Cpno是自引用外键,指向course表自身的Cno,这对应题目里那种“查询每一门课的间接先行课”的自连接需求。sc表的Grade用DECIMAL(4,1),能存0到999.9的成绩,实际练习也用不到这么大的范围,但两位小数和一位小数在排序、比较时表现不同,统一用一位小数可以避开浮点比较的玄学问题。
参数说明:如果你用的是SQL Server或者PostgreSQL,把CHAR(9)换成CHAR(9)或VARCHAR(9)都能跑通,DECIMAL(4,1)也是标准语法。MySQL下建表建议显式指定ENGINE=InnoDB,保证外键约束生效——MyISAM引擎不检查外键,插入脏数据不会报错,练习时容易产生误导。utf8mb4是必须的,否则中文系名、中文课程名在部分MySQL版本上会乱码。
2.3 插入样例数据:数据量要小,但覆盖所有边界情况
建完表就该灌数据。我会故意让数据覆盖几类边界情况:有人没选课,有课程没人选,有成绩为空,有同名同姓,有先行课缺失。这样才能把后面题目里“查没选课的学生”“查成绩为空的学生”这类需求跑出有意义的返回值。
INSERT INTO student (Sno, Sname, Ssex, Sage, Sdept) VALUES ('201215121', '李勇', '男', 20, 'CS'), ('201215122', '刘晨', '女', 19, 'CS'), ('201215123', '王敏', '女', 18, 'MA'), ('201215124', '张立', '男', 19, 'IS'), ('201215125', '刘晨', '女', 20, 'CS'), ('201215126', '赵磊', '男', 21, 'MA'); INSERT INTO course (Cno, Cname, Cpno, Ccredit) VALUES ('1', '数据库', NULL, 4), ('2', '数学', NULL, 2), ('3', '信息系统', '1', 3), ('4', '操作系统', '6', 3), ('5', '数据结构', '7', 4), ('6', '数据处理', NULL, 2), ('7', 'PASCAL语言', '6', 4); INSERT INTO sc (Sno, Cno, Grade) VALUES ('201215121', '1', 92.0), ('201215121', '2', 85.0), ('201215121', '3', 88.0), ('201215122', '2', 90.0), ('201215122', '3', 80.0), ('201215123', '1', NULL), ('201215124', '1', 70.0), ('201215124', '2', 75.0), ('201215124', '5', 60.0), ('201215125', '1', 95.0);逻辑说明:第5条和第2条学生同名“刘晨”,这是故意造的,为了让练习“查询所有姓刘的学生”这类题目时能看到结果集里出现多行,避免你以为姓名唯一。course表里课程1的先行课为空,课程3的先行课是课程1,课程4的先行课是课程6,但实际插入顺序是先插课程6再插课程4,否则自引用外键会因为引用的行还没存在而报错。sc表里学生201215123选了课程1但成绩为NULL,学生201215126完全没选课,课程4、6、7暂时没人选——这几种情况是后面差集、空值判断题的题眼。
参数说明:插入顺序不是随意的。MySQL外键检查是逐行校验的,插入course表时要保证被引用的Cno在表里已存在,所以先插没有先行课的行,再插有先行课的行。如果你不想费心排顺序,可以在插入前执行SET FOREIGN_KEY_CHECKS=0,插完再SET FOREIGN_KEY_CHECKS=1,但建议练习时别这么做,因为实际业务里关闭外键检查写数据是很危险的操作。
3. 从单表到多表:把PDF里的查询题拆成四层难度
3.1 第一层:单表查询,练WHERE和ORDER BY
PDF前面的题目基本是单表查询,比如“查询全体学生的姓名、学号和所在系”“查询年龄在20岁以下的学生”。这类题不涉及连接,主要练条件表达式的写法。
SELECT Sname, Sno, Sdept FROM student WHERE Sage < 20 ORDER BY Sno;逻辑说明:WHERE后面是筛选条件,ORDER BY在结果集上做排序。执行顺序上,数据库先扫student表,逐行判断Sage是否小于20,满足条件的行进入结果集,最后按Sno排序。这里有个新手常犯的错误——把列别名放在WHERE里用,比如WHERE Sage < 20想写成WHERE s_age < 20,但s_age这个别名在SELECT阶段才生成,WHERE阶段根本看不到,会直接报错。
参数说明:ORDER BY默认升序,想倒序加DESC。如果排序字段上有索引,数据库可能不走显式排序,直接按索引顺序返回,这在后面用EXPLAIN分析执行计划时会看到。练习时不用关心这个,但要明白排序结果在不同数据库版本间可能不稳定——如果排序字段存在重复值,MySQL不保证重复值之间的相对顺序,需要加第二个排序字段消除随机性。
3.2 第二层:三表连接,把多对多关系打通
“查询每个学生的学号、姓名、选修的课程名及成绩”是这套练习最核心的题。它要同时用student、sc、course三张表,因为学生姓名在student里,课程名在course里,成绩在sc里,而sc正是连接两者的桥梁。
SELECT student.Sno, student.Sname, course.Cname, sc.Grade FROM student JOIN sc ON student.Sno = sc.Sno JOIN course ON sc.Cno = course.Cno ORDER BY student.Sno, course.Cno;逻辑说明:这个查询先让student和sc按Sno做等值连接,得到每一行的学生信息和他们的选课记录,再和course按Cno连接,把课程号翻译成课程名。用的是INNER JOIN,所以只返回在sc里有记录的学生——完全没选课的学生不会出现在结果里。如果题目要求“查询所有学生的选课情况,包括没选课的学生”,就必须把第一个JOIN改成LEFT JOIN,写成FROM student LEFT JOIN sc ON ...,这样没选课的学生会保留一行,课程名和成绩显示为NULL。
参数说明:连接条件里的字段名如果带表名前缀,就不容易产生歧义,因为三张表里确实存在同名风险——student和sc都有Sno,sc和course都有Cno。这里的NULL值也有讲究:如果学生的选课记录里Grade为空,连接后该行Grade也是NULL,排序时NULL会排在最前面还是最后面,取决于数据库实现,MySQL里NULL默认升序排最前,实际练习看到和自己预期不符时先想到这一点。
3.3 第三层:聚合和分组,统计类题的万能框架
统计类题长这样:“查询每门课的选课人数”“查询每个学生的平均成绩”“查询选修课程超过2门的学生学号”。它们统一用GROUP BY加聚合函数解决。
SELECT sc.Cno, COUNT(*) AS cnt, AVG(sc.Grade) AS avg_grade FROM sc GROUP BY sc.Cno HAVING COUNT(*) >= 2 ORDER BY cnt DESC;逻辑说明:GROUP BY sc.Cno把sc表按课程号分组,每组是一门课的所有选课记录。COUNT()统计每组行数,AVG只对非NULL的Grade求平均值——注意AVG会忽略NULL,所以如果一门课有一个学生成绩为空,分母是总选课数减一,不是总选课数。HAVING是在分组之后过滤组,和WHERE在分组之前过滤行完全是两个阶段。这里HAVING COUNT() >= 2筛掉只有一个人选的课,结果只显示选课人数大于等于2的课程。
参数说明:COUNT()和COUNT(Grade)有本质区别。COUNT()数的是行数,即使Grade为NULL也算;COUNT(Grade)只数Grade不为NULL的行数。PDF里很多题的答案在这两个写法上是有讲究的——“查询每门课的考试人数”应该用COUNT(Grade),因为没成绩的不算考了;“查询每门课的选课人数”用COUNT()更合理,选了课就算。做练习时别图省事统一用COUNT(),先明确题目问的是选课人次还是有效成绩人次。
MySQL还有个默认坑:SELECT子句里出现的非聚合列必须出现在GROUP BY里,否则报错或随机取一个值。上面的写法SELECT了sc.Cno和聚合结果,而GROUP BY正好是sc.Cno,所以没问题。如果题目要求按系统计平均年龄,写成SELECT Sdept, AVG(Sage) FROM student GROUP BY Sdept就成立。
3.4 第四层:子查询和自连接,嵌套题的两种解法
PDF后半段基本全是子查询题:“查询成绩高于所有课程平均成绩的学生”“查询没有选任何课的学生”“查询每门课成绩高于该课程平均分的学生”。这类题的核心思路是:先写一个内层查询得出一个集合,外层查询再基于这个集合做判断。
SELECT student.Sno, student.Sname FROM student WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.Sno = student.Sno );逻辑说明:这是典型的关联子查询。内层查询中,sc表的Sno被student表的外层行绑定,每扫描一行学生,就查一次这个学生是否在sc里有记录。NOT EXISTS表示不存在任何一条选课记录,所以返回的是没选任何课的学生。这里用EXISTS而不是IN,是因为当子查询结果集包含NULL时,IN的判断会变得不可靠——IN本质上做等值比较,而NULL既不等于任何值也不等于NULL,结果集里一旦混入NULL,NOT IN会直接返回空集,这套练习里sc表恰好有Grade为NULL的行,能让你真实撞上这个坑。
参数说明:子查询里SELECT 1而不是SELECT *,只是习惯写法,EXISTS只关心是否有行返回,不关心SELECT的列。有些教材喜欢用NOT IN + 子查询写这道题,写法是WHERE Sno NOT IN (SELECT Sno FROM sc),但前提是子查询的Sno列没有NULL——如果sc表的Sno列本身是主键一部分,非空约束保证不会出现NULL,但你自己建表时如果没加主键,这里就可能翻车。所以练习时,除非你完全确定子查询结果集不含NULL,否则优先用NOT EXISTS。
自连接也是这层难度里的高频题。“查询每一门课的间接先行课”,课程表里课程3的先行课是课程1,而课程1的先行课为空,那么课程3的间接先行课还是课程1——准确说,是查出每门课的先行课的先行课。
SELECT c1.Cno, c1.Cname, c2.Cpno FROM course c1 JOIN course c2 ON c1.Cpno = c2.Cno WHERE c2.Cpno IS NOT NULL;逻辑说明:course表在这里被起了两个别名c1和c2,本质上是把同一张表看成两张独立副本,c1代表课程本身,c2代表它的直接先行课。连接条件是c1.Cpno = c2.Cno,也就是把每门课的先行课信息接到这行上。WHERE c2.Cpno IS NOT NULL排除掉那些先行课本身没有先行课的课程。如果去掉这个条件,结果里会出现一堆Cpno为NULL的行,代表这些课程没有间接先行课。
参数说明:自连接不是MySQL专有,SQL标准通用。它的特点是性能上要扫两次表,但逻辑非常直观。如果课程表有上千行,用自连接做这种题没问题;如果上万行,就要考虑用窗口函数或者递归CTE了,但那是后话,练习阶段先用自连接把语义搞明白。
4. 更新和删除练习:INSERT、UPDATE、DELETE的边界条件
4.1 INSERT:外键约束让插入选课记录不再自由
PDF里更新的题量不大,最常见的是“插入一条选课记录”和“修改某个学生的成绩”。这类题看表面简单,实际上手全是约束问题。
INSERT INTO sc (Sno, Cno, Grade) SELECT Sno, '1', 85.0 FROM student WHERE Sdept = 'CS';逻辑说明:这个插入不是硬编码一个学号,而是从student表里动态查出所有CS系学生的学号,给每人insert一条选了课程1的记录,成绩85。这样做的好处是,无论student表里CS系有多少人,都能一次插完。坏处是如果有些CS系学生已经选过课程1,会触发主键冲突——sc表主键是(Sno, Cno),同一个学生重复选同一门课就违反了主键唯一性,整个INSERT语句全部回滚,一条都插不进去。
参数说明:操作外部数据时,如果你的PDF练习里给了具体学号和课程号,直接INSERT INTO sc VALUES ('201215121', '4', 90.0)就能跑通,前提是201215121在student表里存在、课程4在course表里存在。如果某一边不存在,外键约束直接报错。这是正常的——数据库在阻止你制造孤儿数据。我见过有人为了省事关掉外键检查插入,之后再查数据发现对不上,这就是拿练习数据养成了坏习惯。
4.2 UPDATE:先SELECT确认范围,再UPDATE
修改成绩的题,比如“把所有学生的年龄增加1岁”或者“把某门课成绩低于60分的改成60分”,注意这种题在MySQL Workbench里容易直接撞上安全模式。
UPDATE sc SET Grade = 60.0 WHERE Grade < 60 AND Cno = '5';逻辑说明:UPDATE的执行逻辑是逐行扫描sc,对满足WHERE条件的行执行SET赋值。这里把课程5低于60分的成绩改到60分。如果WHERE条件写错,比如漏了AND Cno='5',就会把所有课程里低于60分的成绩全部改成60分,而且这个操作无法撤销——除非你先做了备份或者提前用SELECT查一遍结果。
参数说明:MySQL Workbench默认开启了safe update mode,语义是:UPDATE或DELETE时,WHERE条件必须用到索引列或主键,否则拒绝执行。上面的查询条件只有Grade和Cno,如果Cno上有索引就能跑,没有索引就报错“You are using safe update mode”。常见处理办法是先执行SET SQL_SAFE_UPDATES=0再跑UPDATE,或者给WHERE补上主键范围条件。我一般推荐后者——练习归练习,养成了关安全模式的手感,到了生产环境早晚出事。
4.3 DELETE:删除父表数据前,先想清楚子表怎么办
“删除学号为201215125的学生记录”这类题,难点不在DELETE语法本身,而在于这个学生在sc表里有没有选课记录。如果有,直接删student表会触发外键约束。
DELETE FROM sc WHERE Sno = '201215125'; DELETE FROM student WHERE Sno = '201215125';逻辑说明:先删子表sc里的相关行,再删父表student里的行,顺序不能反。反过来的话,MySQL会因为sc表里存在引用201215125的外键而拒绝删除。这个行为由建表时的外键定义的ON DELETE子句决定——如果你建表时写了ON DELETE CASCADE,那么删除student时MySQL会自动删掉sc表里对应的行;没写,就报错。我建表时故意没写,就是为了练习时能看到这个报错,理解先删子表后删父表的顺序。
参数说明:有些练习题的PDF会要求“删除所有没选课的学生”,这就要先用NOT EXISTS查出目标再删,写法是把第3.4节那个查询改成DELETE FROM student WHERE NOT EXISTS (...)。执行之前,强烈建议先把SELECT那段跑一遍,确认要删的人数和预期一致,再切换成DELETE。这是所有DML操作里最值得养成的一个习惯——先查后删,给自己留后悔药。
5. 三表练习避坑指南:5个最常见的翻车现场
5.1 三表连接丢条件,结果多出大量重复行
现象:student、sc、course三表连完,结果比预期多出几倍甚至几十倍的行数,看起来每行数据都像重复了。
原因:连接条件漏写了一部分,比如FROM student JOIN sc ON student.Sno = sc.Sno JOIN course,后面的JOIN course没写ON,MySQL会把course表每一行都和前面结果做笛卡尔积,行数暴涨。常见于漏了course的ON,或者把student和course直接做连接没走sc。
解决:写多表连接时先数清楚ON的数量,三张表需要两个ON。如果你在连接结果里看到某门课的课程名和成绩完全对不上,先检查每个JOIN的ON字段名和关联方向,再用COUNT(DISTINCT student.Sno)验证结果里的学生数是否和预期一致。
5.2 分组查询SELECT了非聚合列,MySQL直接报错
现象:执行GROUP BY查询,MySQL报错“Expression #1 of SELECT list is not in GROUP BY clause”,或者结果里某列的值看起来是随机取的。
原因:SQL标准要求SELECT里的非聚合列也必须出现在GROUP BY里。MySQL 5.7.5之后默认开启ONLY_FULL_GROUP_BY模式,不再允许以前那种宽松写法。比如SELECT Sname, AVG(Grade) FROM sc JOIN student ON ... GROUP BY sc.Cno,Sname不属于分组列,报错是正常的。
解决:要么把Sname加进GROUP BY,要么改成只查询分组的维度列。如果确实想在分组结果里带上不属于分组条件的信息,用子查询或者窗口函数解决,不要靠关闭ONLY_FULL_GROUP_BY来硬跑——关掉的结果是同一组内随机取一个非聚合列的值,这个“随机”在不同版本里表现还不一样,很容易误导练习判断。
5.3 LEFT JOIN的过滤条件写在WHERE里,保住左表全量的目的落空
现象:写了LEFT JOIN,想保留左表所有行,最后结果里左表某些行还是消失了。
原因:过滤条件写错了位置。典型例子是FROM student LEFT JOIN sc ON student.Sno = sc.Sno WHERE sc.Cno = '1',WHERE阶段会把左表里没选课程1的学生行过滤掉,因为那一行的sc.Cno是NULL,NULL不等于'1'。LEFT JOIN只保证JOIN阶段不丢行,WHERE阶段会重新丢行。
解决:把过滤条件放在ON里,写成LEFT JOIN sc ON student.Sno = sc.Sno AND sc.Cno = '1'。这样左表所有行保留,匹配不到课程1的学生显示NULL课程号和NULL成绩。记住一个判断标准:只影响匹配结果的过滤放ON,要影响最终输出行集合的过滤放WHERE。放在ON后的条件,LEFT JOIN才会有保留左表全量的意义。
5.4 IN子查询结果集里出现NULL,NOT IN直接翻车
现象:用NOT IN子查询查没选课的学生,返回结果为空,但表里明明有没选课的人。
原因:子查询的Sno列没有非空约束,或者查询结果里包含了NULL。NOT IN的语义是“不等于子查询结果里的任何一个值”,而SQL里NULL参与比较,结果既不是真也不是假,是UNKNOWN。整个WHERE条件对每行都变成UNKNOWN,过滤掉所有行。
解决:优先改用NOT EXISTS,或者确保子查询结果集排除了NULL,写法是WHERE Sno NOT IN (SELECT Sno FROM sc WHERE Sno IS NOT NULL)。练习中更推荐直接用NOT EXISTS,因为它的语义就是“不存在”,不受NULL干扰,执行计划优化器处理EXISTS一般也更有优势。
5.5 外键约束让DELETE、UPDATE寸步难行
现象:删除一条student记录,报错“Cannot delete or update a parent row: a foreign key constraint fails”,明明这条语句语法没错。
原因:sc表有外键引用student表,被删除的学号在sc里存在选课记录,数据库拒绝产生引用失效的数据。
解决:先查这个学生在sc里的选课记录,DELETE FROM sc WHERE Sno = ?,再删除student表的记录。或者在建表时给外键加ON DELETE CASCADE,但练习时不建议这么干,因为会掩盖掉“先删子表再删父表”这个顺序意识。遇到这类报错不要违规关外键,按正常业务顺序处理,你会少踩很多隐形的数据一致性问题。
6. 把练习当作评测工具:验证SQL写对了的三个习惯
很多人的练习方式是“写完SQL看结果不报错就觉得对了”,但SQL不报错不等于答案对。我常用的验证方式是先手写预期结果再跑SQL,把练习从“做题”变成“评测”。
第一,做任何统计查询前,先通过简单查询估算答案的规模。比如“查询平均成绩大于80的学生”,我会先跑SELECT COUNT(DISTINCT Sno) FROM sc,确认总共有多少人选了课,再跑完整查询,对比返回人数是否在合理范围内。不在范围就直接看是不是WHERE和HAVING用错了——这是这套练习里最容易出问题的地方。
第二,用EXPLAIN看执行计划,确认连接顺序和过滤下推是否合理。执行EXPLAIN SELECT ...,看type列是否有index或ALL,Extra列是否出现Using temporary或Using filesort。练习数据量小,这些标记不会让你感觉到慢,但出现在实际业务里就是性能炸弹。其中Using temporary常见于GROUP BY和DISTINCT混用,Using filesort常见于ORDER BY没走索引,遇到这两种标记就值得停下来想想能不能改写。
第三,给自己造一份“黄金答案集”。把每道题预期返回的关键行数或特征值记下来,比如“查询所有学生及其选课信息”应该返回11行(6个学生里5个有选课记录,加上一个NA),跑出来的行数对不上就说明JOIN或WHERE有问题。这是成本最低的自测方式,因为这套练习的数据量小,每道题的结果都能手工算出来,手算的过程本身就是在巩固连接和分组语义。
我自己的习惯是留一个专门建库的SQL脚本,里面包含建表、插数据、以及我验证过的每道题的参考答案。碰到记不清的SQL语法,直接翻脚本看当时的写法,比翻PDF快得多。这套student、sc、course三表练习,别看结构简单,把它吃透之后,你再看实际业务里那些动辄二十张表的复杂库,至少不会对着JOIN条件心慌。希望帮到你。
本文还有配套的精品资源,点击获取