资讯详情

资讯详情

schoolDB设计实战:四张表搞定学生选课成绩管理

学校里折腾过数据库的人多少都逃不过课程设计这一关。而课程设计里最经典、也最适合拿来入门练手的就是这种带数据的学校管理库——学生、课程、教师、成绩四张表搞定全部核心业务。我最近刚好整理了一份schoolDB的库四张表全部建好并写入了测试数据就在这儿把整个设计和实操过程拆开讲一遍包括表结构是怎么想出来的、数据为什么这么填、以及后续做查询和排查时我踩过的坑。这套东西不管是交给老师验收还是自己当作练手项目都足够扎实。先说清楚schoolDB到底是个什么定位。它不是一个大型业务系统而是一个典型的、面向教学场景的数据库样例。核心业务就四个管理学生基本信息、维护课程信息、管理教师信息、记录学生选课及成绩。四张表分别对应student学生表、course课程表、teacher教师表、sc选课成绩表。看似简单但关系型数据库里最核心的概念——主键、外键、联合主键、多对多关系、级联操作——全部覆盖了。对于准备数据库课程设计答辩、或者刚学完MySQL想找一套完整样例来练手的人来说这就是一个可以直接拿来当模板的库。1. 为什么是这四张表schoolDB的核心设计思路1.1 四张表的职责划分先回到最基础的问题上一个学校管理系统最少需要几张表很多新手第一次设计数据库时容易犯的毛病是“把所有字段堆在一张表里”。学生信息也放进去、课程信息也放进去、成绩也放进去、老师也放进去——结果就是数据大量冗余改一个老师电话要把这条记录所在的所有行全改一遍数据不一致的风险非常高。这个是没做好职责划分导致的。schoolDB把职责拆成四份是有讲究的student表管学生的静态属性学号、姓名、性别、出生日期、班级等。这些都是不太变化的基础数据。teacher表管教师的属性工号、姓名、职称、系别等。同样属于基础数据。course表管课程属性课程号、课程名、学分、授课教师工号。这里注意授课老师用teacher表的主键来引用而不是直接把老师姓名塞在course表里。这么做的原因很简单老师可能改名但工号不会变如果直接存姓名改一次名要同步修改多张表。sc表管学生和课程之间的联系也就是“谁选了哪门课、考了多少分”。这是整个库中最核心的一张表它同时引用了student表的主键和course表的主键。这四张表合在一起就构成了一个完整的选课成绩管理闭环学生可以选多门课程一门课程可以被多个学生选而成绩作为“选课”这个行为的结果落在sc表里。这个就是典型的多对多关系必须通过中间表来解决sc表就是中间表。1.2 表与表之间的关系是如何确定的设计表结构时我习惯先画一个简单的关系草图不一定要用专业工具手画也行。schoolDB的三条关系主线很清晰学生与成绩一个学生可以拥有多条成绩记录所以student表与sc表是一对多。课程与成绩一门课程可以被多个学生选修产生多条成绩记录所以course表与sc表也是一对多。教师与课程一个老师可以教多门课course表里用teacher_id字段做外键引用所以teacher表与course表是一对多。把关系理清楚之后主键和外键的划分就顺手了。student表的主键是学号s_idteacher表的主键是工号t_idcourse表的主键是课程号c_id。sc表比较特殊它的主键是(s_id, c_id)联合主键因为“一个学生选了一门课”这一条记录用学号和课程号两个字段才能唯一确定。如果只用s_id做主键那这个学生只能选一门课这不合理只用c_id做主键那这门课只能被一个学生选更离谱。联合主键是唯一正确的解法。这里有一个设计细节值得新手注意sc表里不应该再单独设一个自增主键id字段。很多同学习惯给每张表都加一个自增id但sc表作为一个纯关联表联合主键已经足够保证唯一性再额外加一个无意义的id反而会让查询时的索引冗余也在逻辑上混淆了“成绩记录”和“选课事实”之间的绑定关系。这是我在给一批学生项目做代码评审时反复强调过的一个点。2. 四张表的字段设计与数据细节2.1 student表学生主数据的字段取舍student表的字段设计主要考量是“既要覆盖常用查询需求又不要堆砌无用的列”。我最终保留了这几个字段字段名类型约束说明s_idvarchar(10)主键学号如2023001001s_namevarchar(20)非空姓名s_genderchar(2)默认男性别用定长字符串比varchar更省空间s_birthdate可空出生日期s_classvarchar(20)可空班级如“计科2301”有一个容易被忽略的小点学号为什么不直接用整数类型原因有二。首先学号是“编号”而非“数字”对它做加减乘数毫无意义其次学号前缀往往有业务含义比如年份、院系代码用varchar才能方便做模糊查询和分组统计。举个例子统计2023级学生人数直接WHERE s_id LIKE 2023%就行这个写法对整数类型的学号是行不通的。性别字段用char(2)而不是varchar(2)看起来差别不大但char定长在存储时不会产生额外字节来记录长度对这个取值只有“男/女”两种情况的列来说效率更好。这个细节一般在教科书上不太会讲但在实际调优时会体现出价值。2.2 course表与teacher表避免冗余的设计思路course表和teacher表放在一起讲是因为它们之间存在外键引用关系。course表的字段如下字段名类型约束说明c_idvarchar(10)主键课程号如CS101c_namevarchar(50)非空课程名如“数据库原理”c_creditdecimal(3,1)非空大于0学分如3.0t_idvarchar(10)外键引用teacher(t_id)授课教师工号teacher表的字段如下字段名类型约束说明t_idvarchar(10)主键教师工号如TZ001t_namevarchar(20)非空教师姓名t_titlevarchar(20)可空职称如“副教授”t_deptvarchar(30)可空所属系别两个表放在一起能看到一个设计原则course表里只存教师的工号t_id不存教师姓名。这个是从“降低冗余、保证一致性”的角度考虑的。如果你把“张三老师”这个姓名直接写进course表一旦张三调离你要修改所有他授课的课程记录而存t_id的话只需改teacher表里的一行数据。另外course表里大概率会加上一个唯一约束比如(c_name, t_id)不能重复防止同一老师重复录入同一门课。这个约束我当时没有加后来做测试时手动插了两条一模一样的记录虽然没有对查询造成致命影响但明显违反了数据一致性原则——同一老师同一门课出现了两条记录统计学分时会把同一门课的学分重复计算。这个问题在第四章排查部分还会详细讲。2.3 sc表连接学生与课程的桥梁sc表是四张表里我最想讲细的一张因为很多坑都在这里。字段名类型约束说明s_idvarchar(10)联合主键、外键引用student(s_id)学号c_idvarchar(10)联合主键、外键引用course(c_id)课程号scoredecimal(5,2)可空0-100成绩允许空表示未考试三个关键设计点第一联合主键(s_id, c_id)。这个前面已经解释过它是表达“选课事实”唯一性的基础。第二score字段允许NULL。有的同学会问成绩为什么不为空原因是选课记录和成绩记录是两个阶段的事情。学生选了课但还没考试时成绩就是未知用NULL比用0更准确。如果默认成0统计平均分时会把没考试的人按0分算进去数据就不真实了。实际项目中我们可能会用状态字段来区分“已选课/已考试/缓考/缺考”但在教学示例里score允许NULL已经够用。第三级联删除策略。我在创建外键时设置了ON DELETE CASCADE——当student表或course表中的某条记录被删除时sc表中关联的记录也会自动删除。这样做的好处是避免“孤儿数据”。举例来说你看某条sc记录引用的学号在student表中不存在了这就在逻辑上说不通。级联删除能有效降低这种风险。但要注意级联删除要慎用尤其是在真实业务系统里误删一条学生记录可能会导致该学生的所有选课历史被连带删除所以在schoolDB这种课程设计场景中演示级联删除是合适的生产环境往往更常见的是逻辑删除加一个is_deleted字段。3. 从建表到查数据一套可直接抄作业的实操方案3.1 建表SQL的完整写法含约束schoolDB用的数据库默认是MySQL 8.0字符集统一用utf8mb4。这里多提一嘴utf8mb4和utf8的区别很多人搞不清楚简单说utf8在MySQL里最多存3字节字符遇到一些特殊符号或生僻字会直接报错utf8mb4是utf8的超集能存4字节字符表情符号和生僻字都能存。做教学项目最好直接选utf8mb4省得到后期换字符集。建表SQL完整如下CREATE DATABASE IF NOT EXISTS schoolDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE schoolDB; CREATE TABLE student ( s_id VARCHAR(10) PRIMARY KEY, s_name VARCHAR(20) NOT NULL, s_gender CHAR(2) DEFAULT 男, s_birth DATE, s_class VARCHAR(20) ) ENGINEInnoDB; CREATE TABLE teacher ( t_id VARCHAR(10) PRIMARY KEY, t_name VARCHAR(20) NOT NULL, t_title VARCHAR(20), t_dept VARCHAR(30) ) ENGINEInnoDB; CREATE TABLE course ( c_id VARCHAR(10) PRIMARY KEY, c_name VARCHAR(50) NOT NULL, c_credit DECIMAL(3,1) NOT NULL, t_id VARCHAR(10), CONSTRAINT fk_course_teacher FOREIGN KEY (t_id) REFERENCES teacher(t_id) ) ENGINEInnoDB; CREATE TABLE sc ( s_id VARCHAR(10), c_id VARCHAR(10), score DECIMAL(5,2), PRIMARY KEY (s_id, c_id), CONSTRAINT fk_sc_student FOREIGN KEY (s_id) REFERENCES student(s_id) ON DELETE CASCADE, CONSTRAINT fk_sc_course FOREIGN KEY (c_id) REFERENCES course(c_id) ON DELETE CASCADE ) ENGINEInnoDB;注意几点所有表都显式指定ENGINEInnoDB。InnoDB是支持外键约束和事务的存储引擎MyISAM虽快但不支持外键教学场景选InnoDB是标准做法。外键约束名我给显式起了名字比如fk_course_teacher。这样后续要删掉某个约束时可以通过约束名精准操作而不是让系统自动生成一个随机名字到时候还得去information_schema里查。course表里的t_id没有加ON DELETE级联策略默认是NO ACTION等同于RESTRICT。这样设计意图是如果一个老师名下已经分配了课程你直接删掉teacher表里这个老师的记录数据库会拒绝执行强制你先处理课程归属。这个“拦一下”的动作其实是一种保护机制防止意外删除导致课程变成“无主状态”。3.2 有数据的表才有灵魂插入样例数据的技巧表建好了接下来就要造数据。schoolDB现在的测试数据规模是student表20条、teacher表5条、course表8条、sc表42条。这个数据量级对课程设计来说刚好——不多不少既能验证查询逻辑又不会在调试时因为数据太多而眼花缭乱。插入数据时我用了几个技巧值得分享一下。第一个是控制插入顺序。因为有外键约束插入顺序必须严格遵守依赖关系先teacher再course再student最后sc。如果你先插入course表数据而它引用的teacher记录还没插入MySQL就会直接报外键错误。我见过太多新手卡在这一步用INSERT INTO course VALUES(...)系统报错怎么找都找不到原因其实就是teacher表里没这条记录。第二个是造数据时故意保留“多样性”。student表里男女都有出生日期覆盖1998年到2005年班级分了四五个course表里学分有1.5、2.0、3.0、4.0等不同值sc表里成绩有高分、低分、不及格、NULL代表未考试甚至有几条重复选修记录同一学生选了同一课程两次分别对应补考和重修联合主键虽然不允许重复但我通过设置(s_id, c_id)联合主键之外的id字段解决了这个问题——不过为了教学清晰最终版本直接沿用联合主键不允许重复选课。这个过程其实就是在为后续的各种查询需求做铺垫。如果所有成绩都是80分那你就没法验证COUNT、AVG、MAX、MIN、GROUP BY这些聚合函数的效果。第三个是中文数据的字符集问题。很多同学从Navicat图形界面导入数据时一切正常但用命令行source导入SQL文件时就乱码。这个基本上都是因为SQL文件本身是UTF-8编码而客户端连接数据库时用的字符集不对。解决方式是执行SQL前先跑一下SET NAMES utf8mb4;。这里给出一段样例数据的片段方便直接复制验证INSERT INTO teacher (t_id, t_name, t_title, t_dept) VALUES (TZ001, 王建国, 教授, 计算机学院), (TZ002, 李秀荣, 副教授, 计算机学院), (TZ003, 张伟, 讲师, 数学学院); INSERT INTO student (s_id, s_name, s_gender, s_birth, s_class) VALUES (2023001001, 陈晓明, 男, 2005-03-12, 计科2301), (2023001002, 林芳, 女, 2004-11-08, 计科2301), (2023002001, 赵磊, 男, 2005-06-30, 软工2302); INSERT INTO course (c_id, c_name, c_credit, t_id) VALUES (CS101, 数据库原理, 3.0, TZ001), (CS102, 数据结构, 4.0, TZ002), (MA101, 高等数学, 5.0, TZ003); INSERT INTO sc (s_id, c_id, score) VALUES (2023001001, CS101, 88.50), (2023001001, MA101, 91.00), (2023001002, CS101, 76.00), (2023002001, CS102, NULL);3.3 最常写的几条查询SQL有了数据和表结构下面这些查询就是在课程设计报告、期末考试上机题以及真实工作中反复出现的体型。每条我都配上执行后的结果和解读。第一个查询查询每个学生的选课门数和平均分。SELECT s_id, COUNT(*) AS total_courses, AVG(score) AS avg_score FROM sc GROUP BY s_id;这个SQL考察的是分组聚合。COUNT(*)统计每个学生的选课记录数AVG(score)计算平均分。执行结果类似这样s_idtotal_coursesavg_score2023001001289.7500002023001002176.00000020230020011NULL这里有个细节2023002001这个学生的AVG(score)返回NULL不是0。原因就是sc表里有一条score为NULL的记录AVG函数会自动忽略NULL值如果某组所有score都是NULLAVG返回NULL。如果你在应用层做展示时把NULL当成0处理就会出错。实用的解法是包一层IFNULL(AVG(score), 0)这样返回0更符合业务预期。第二个查询学生选课明细要关联三张表。SELECT s.s_name, c.c_name, t.t_name, sc.score FROM sc JOIN student s ON sc.s_id s.s_id JOIN course c ON sc.c_id c.c_id JOIN teacher t ON c.t_id t.t_id ORDER BY s.s_id;这个SQL展示了3表连接的标准写法而且顺便把teacher也关联进来了。实际结果里每个学生选的每门课都能找到对应的授课教师。在实际考试中很多同学会卡在JOIN的顺序上——记住一个原则从中间表sc表出发先连student再连course再连teacher每一步连接的关联字段都是外键关系就不会乱。第三个查询查没及格低于60分的学生名单。SELECT s.s_name, c.c_name, sc.score FROM sc JOIN student s ON sc.s_id s.s_id JOIN course c ON sc.c_id c.c_id WHERE sc.score 60;乍看没什么问题但这个SQL有个隐含陷阱它会把score为NULL的记录自动过滤掉因为NULL 60在SQL中结果是UNKNOWNWHERE条件只保留TRUE的记录。如果业务需求是“查所有没通过考试的人”那缺考NULL的学生也算在内。真正全面的查询要这样写SELECT s.s_name, c.c_name, sc.score FROM sc JOIN student s ON sc.s_id s.s_id JOIN course c ON sc.c_id c.c_id WHERE sc.score 60 OR sc.score IS NULL;这两个查询的差别就是我在课程设计中经常拿来“刁难”学生的点。表面上看第一个查询“没问题”但业务逻辑上漏掉了一种情况。这也是数据建模的意义所在——一个字段的取值范围和默认值设计直接影响后续所有查询逻辑的写法。4. 实操中踩过的坑与排查技巧4.1 外键约束引发的插入失败我在给schoolDB填充数据时第一次插入sc表就报错了错误信息大概是这样ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails当时第一反应是“数据格式不对”检查了好几遍字段类型都没发现问题。后来挨个排查才发现是course表里有一条记录引用的t_id在teacher表中不存在——我在插course数据时写了一个不存在的教师工号。外键约束天生就是干这个的它不允许你在子表里引用一个父表中不存在的记录。这个错误的排查技巧就是按外键关系逐层检查。遇到1452错误优先查你插入的s_id在student表里是否存在、c_id在course表里是否存在、t_id在teacher表里是否存在。很多时候是数据源不干净或者插入顺序不对而不是SQL语法本身有问题。这里补充一个进阶玩法如果你需要快速找出是哪条引用关系出了问题可以用一条LEFT JOIN来查“孤儿记录”SELECT sc.* FROM sc LEFT JOIN student s ON sc.s_id s.s_id WHERE s.s_id IS NULL;理论上这个查询会返回空集因为外键约束已经保证了引用完整性。但如果你手头有历史遗留的脏数据、或者之前使用了不带外键的MyISAM表这个查询就是你排查脏数据的得利工具。我在一些老项目里靠这条SQL找回过大量丢失的关联记录。4.2 数据不一致问题的排查思路schoolDB出现过的另一个状况是两张表的同一条信息对不上号。比如course表里某一门课的授课教师是“TZ002李秀荣”但teacher表里TZ002对应的t_name变成了“李秀荣”没错其他表里引用的t_id却是“TZ002”和“TZ002”编号不统一。这种问题虽然在这套库里很容易被外键约束拦住但我仍然想提一下数据不一致的常见来源因为真实的业务系统里数据不一致几乎无法完全避免。数据不一致的来源主要有这么几类应用层代码里硬编码了冗余字段比如在course表里同时存了t_id和t_name更新时只改了t_name没改t_id关联的teacher表字段。删除了父表记录但没处理子表引用没用级联删除也没做逻辑删除。同一份数据在多个数据库之间同步时发生延迟或失败。解决数据不一致第一条防线就是数据建模时尽可能减少冗余。schoolDB里的设计就是这样——t_name只在teacher表里存在其他表都通过t_id来引用。第二条防线是定期做数据质量检查写几个简单的SQL脚本用LEFT JOIN找孤儿记录用GROUP BY ... HAVING COUNT(*) 1找重复记录。第三条防线才是引入数据库同步工具和专业的比对工具来处理跨库数据同步不过那是另一个话题schoolDB这种单库场景用不到那么重的手段。顺带吐槽一句网上特别流行“数据库同步软件”“同步工具”这类的搜索词。我做项目时会区分场景单库多表不需要同步只用外键约束就能保证一致性多库之间需要同步时优先考虑主从复制或基于binlog的增量同步而不是傻乎乎地拿同步软件整表复制。整表复制在数据量大、表结构频繁变更时非常容易踩坑。4.3 关于表结构同步与ER图的补充说到表结构变更我发现很多人在做完schoolDB之后下一个需求就是“把表结构转成ER图导出”。MySQL里最实用的办法就是借助数据库可视化工具比如Navicat的逆向工程或者是DBeaver的ER图查看功能。操作步骤一般是连接到MySQL实例选中schoolDB数据库右键选择“逆向数据库到模型”工具会自动根据外键关系生成ER图。这个图放到课程设计报告里当插图非常加分。如果不想装重型客户端也可以直接在命令行里查询information_schema数据库来获取所有表结构信息SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA schoolDB ORDER BY TABLE_NAME, ORDINAL_POSITION;information_schema是MySQL自带的元数据仓库里面存着所有数据库、表、列、索引、外键等定义信息。查询它比去看建表SQL更灵活也方便做自动化脚本。说到表结构同步还得提一个概念叫“表结构版本管理”。很多人建完表之后结构说改就改改完就忘了等到最后写报告时根本想不起这个字段是什么时候加的。我的建议是把建表SQL和所有的ALTER语句都放进一个带版本号的SQL脚本目录里比如v1_init.sql、v2_add_index.sql。这个习惯在课程设计里可能看不出多大价值但到真实项目里简直是救命稻草——你不知道哪一天就需要把一个老环境的表结构调整到与最新环境一致没有版本管理的脚本你就只能靠肉眼和运气。最后再分享一个我在这次整理schoolDB时用到的习惯也是很多老DBA都会做的事每次在执行批量插入或结构变更之前先把当前数据导出一份备份。导出命令很简单mysqldump -u root -p schoolDB schoolDB_backup.sql等你要恢复时mysql -u root -p schoolDB schoolDB_backup.sql这个操作给我挽回过很多次误操作。有一次我在调整course表的学分字段时不小心执行了一条不带WHERE条件的UPDATE差点把整个表的所有学分都改成同一个值。因为提前有备份恢复数据只用了几分钟。做数据库工作手要快但备份的屁股要坐稳——先备份再操作这是我从不会跳过的底线。schoolDB这套四张表虽然简单但它把数据建模中的主键与外键、约束设计、级联策略、多对多关系的建模方法、聚合查询、连接查询这些核心知识点都串起来了。对我个人而言最好的学习方法就是去反复折腾一套这样的小库建表、插数据、写查询、故意做错几次、再把数据恢复到正常状态。踩过的坑越多对数据库运行机制的理解就越深。如果你正在做数据库课程设计不妨就把这四张表当成起点在此基础上增加登录用户表、选课时间表、教学评价表慢慢扩展成属于自己的完整系统。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →