1. 四个表的设计思路:为什么是这四张表
拿到“SchoolDB对应的4个表的DDL语句——仅有结构”这个标题时,我第一反应是:这不就是最经典的选课系统表结构吗?学生、课程、教师、成绩,四张表构成了一个精简但五脏俱全的教学管理数据库。没有数据,只有结构,反而把重点全部压在了“设计是否合理”上——字段选型、约束逻辑、主外键关系、索引设计,这些东西在建表那一刻就决定了这个数据库未来好不好用。
先说清楚四张表各自的定位:
student:学生信息表,存放学生维度的基础属性。course:课程信息表,存放课程维度的基础属性。teacher:教师信息表,存放教师维度的基础属性。score:成绩关系表,记录学生与课程之间的成绩映射,同时通过教师ID关联授课关系。
为什么需要四张表而不是三张或五张?从业务建模的角度看,学生和课程是核心实体,教师是另一个核心实体,而成绩本质上是“学生选课”这个行为产生的结果。如果把成绩字段塞进学生表,一个学生选多门课就会出现大量重复行,直接破坏第一范式;如果把授课教师塞进课程表,一门课换老师就要 UPDATE 课程表,但历史成绩又需要保留原来的授课关系,处理起来非常别扭。所以成绩表单独抽出来,同时承担“选课关系”和“成绩记录”两种职责,一张关系表解决学生-课程-教师三个实体之间的多对多关联,这是最省事也最符合直觉的建模方式。
有人可能会问,教师和学生之间的关系要不要单独建表?比如辅导员、导师这种场景。如果业务需要,确实可以加一张advisor之类的关联表。但在一个基础版的 SchoolDB 里,把授课教师放在score表里作为冗余关联字段,已经能覆盖“某门课由谁教”的查询需求。真要再细化教师和课程的多对多关系,那是后续迭代的事,第一版结构保持克制反而是好事。
建表阶段的克制比过度设计更难。你永远可以后续
ALTER TABLE加字段、加索引,但你很难在结构混乱的初始设计上腾挪出清晰的查询逻辑。
2. 逐表拆解DDL:字段、类型、约束背后的选择逻辑
2.1 学生表(student)
学生表是整个系统的基石,几乎所有业务查询最终都要落到“某个学生是谁”这个问题上。先看完整的建表语句:
CREATE TABLE `student` ( `sno` varchar(20) NOT NULL COMMENT '学号', `sname` varchar(50) NOT NULL COMMENT '学生姓名', `ssex` char(1) NOT NULL DEFAULT '男' COMMENT '性别', `sage` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '年龄', `sdept` varchar(50) DEFAULT NULL COMMENT '院系', `class_no` varchar(20) DEFAULT NULL COMMENT '班级编号', `enroll_date` date DEFAULT NULL COMMENT '入学日期', `status` tinyint NOT NULL DEFAULT 1 COMMENT '学籍状态:1-在读,2-休学,3-毕业,4-退学', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`sno`), KEY `idx_sname` (`sname`), KEY `idx_sdept` (`sdept`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='学生信息表';学号sno选varchar(20)而不是int,这是很多新手最容易踩的坑。学号在真实场景里往往带有前缀,比如年份加流水号(20240001),甚至还有字母(E20240001),如果用int存,不仅存不下字母,而且前导零会被直接吞掉,等到对账或者对接其他系统时才发现学号对不上,非常痛苦。varchar(20)是行业共识,预留位数,别省。
性别ssex用char(1),只存一个汉字。可能有人觉得用tinyint存 0/1 更节省空间,但实际查询时每次都要CASE WHEN转换,纯属给自己找麻烦。业务表核心满足的是"查询简单直观",而不是"省一个字节"。当然,如果你所在团队有硬性的字段规范要求用编码,那就跟团队规范走,但我个人的经验是枚举值较少的场景直接用可读字符更省心。
年龄sage用tinyint unsigned,取值范围 0 到 255,对学生年龄来说绰绰有余。unsigned是为了防止出现负数,虽然代码层可以校验,但数据库加一层约束总没有坏处。这里要说明一下,如果后续需要做年龄段的统计查询,建议在应用层计算出生日期范围而不是直接用年龄字段,毕竟年龄是随时间变化的,存一个固定值很快就会失真。
学籍状态status用tinyint加注释,这属于扩展预留字段。一个学校系统运行三五年后,退学、休学、交换生等状态几乎必然出现。与其到时候改字段,不如一开始就留一个状态位。注释写清楚每个数字代表什么含义,后面无论是同事接手还是自己维护,都不需要翻代码才能看懂。这条经验我反复跟别人强调:建表时宁可多写几行注释,也不要半年后对着裸字段猜含义。
2.2 课程表(course)
课程表描述“有什么课可以被选”,核心字段是课程编号、课程名、学分和前置课程。建表语句如下:
CREATE TABLE `course` ( `cno` varchar(20) NOT NULL COMMENT '课程编号', `cname` varchar(100) NOT NULL COMMENT '课程名称', `cpno` varchar(20) DEFAULT NULL COMMENT '先修课程编号,关联本表cno', `ccredit` decimal(3,1) NOT NULL DEFAULT 0.0 COMMENT '学分', `course_type` tinyint NOT NULL DEFAULT 1 COMMENT '课程类型:1-必修,2-选修,3-通识', `semester` varchar(10) DEFAULT NULL COMMENT '开课学期,如2024-2025-1', `total_hours` smallint unsigned DEFAULT NULL COMMENT '总学时', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`cno`), KEY `idx_cname` (`cname`(32)), KEY `idx_cpno` (`cpno`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='课程信息表';课程编号同样是varchar(20),原因跟学号类似,课程编号通常会带院系缩写或年级前缀。这里有一个容易出现认知偏差的字段:课程类型course_type。有些设计会把课程类型做成独立字典表,再用外键关联。但放在 SchoolDB 这种小规模场景里,独立字典表带来的 Join 成本远高于它节省的那点维护成本,直接一个tinyint加注释就能解决问题。什么时候该拆表,什么时候不该拆,标准只有一个:字段的选择范围是否经常变。课程类型一年可能都不动一次,老老实实做固定枚举就好了。
前置课程cpno关联本表cno,这是一种自引用外键关系,用于表达“学这门课之前需要先学哪门课”。如果业务里有课程依赖链查询,比如“学完这门课需要的前置课程链条”,这个字段就是关键入口。
学分ccredit用decimal(3,1),这又是一个典型的“为什么不用 float”的例子。float是近似值存储,0.1 + 0.2 在二进制里会产生精度误差,而学分这种东西是要算绩点的,2.5 就是 2.5,没有任何近似空间。decimal(3,1)表示总位数 3 位、小数位 1 位,最大支持 99.9 学分,对学生课程已经非常充裕了。我见过有人在学分上踩精度坑,最后平均绩点差了零点几,改起来要重算整个学期的数据,教训惨痛。
2.3 教师表(teacher)
教师表在四个表里经常被忽视,但它是成绩表关联链路里不可缺少的一环。先看 DDL:
CREATE TABLE `teacher` ( `tno` varchar(20) NOT NULL COMMENT '教师工号', `tname` varchar(50) NOT NULL COMMENT '教师姓名', `tsex` char(1) NOT NULL DEFAULT '男' COMMENT '性别', `tage` tinyint unsigned DEFAULT NULL COMMENT '年龄', `title` varchar(20) DEFAULT NULL COMMENT '职称:教授、副教授、讲师等', `tdept` varchar(50) DEFAULT NULL COMMENT '院系', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`tno`), KEY `idx_tname` (`tname`), KEY `idx_tdept` (`tdept`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='教师信息表';教师表的字段设计比较常规,职称title和院系tdept都是典型的“慢变维度”,很少更新,直接冗余在表里完全够用。电话和邮箱字段用varchar,这里要注意一个细节:电话号码不应使用数值类型。国内手机号如果是int类型会溢出,即使改用bigint,前导零或者其他分机号也存不进去,更重要的是你永远不会对手机号做加减乘除。字符串类型才是电话号码的正确归属。
年龄字段tage设为NULL允许为空,这个跟学生表不同。学生的年龄入学时基本能拿到,但教师年龄不一定能采集到,且对教师做年龄统计的需求频率也低得多,没必要NOT NULL强制约束。在真实项目里,我建议年龄这种字段能不加就不加,真要统计分析时可以用身份证号或出生日期推导。但考虑到 SchoolDB 是教学示例,保留tage字段用来演示BETWEEN查询年龄区间也说得过去。
2.4 成绩表(score)
成绩表是整张结构图的核心枢纽,它同时承接学生、课程、教师三个维度的关联关系。表结构如下:
CREATE TABLE `score` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `sno` varchar(20) NOT NULL COMMENT '学号', `cno` varchar(20) NOT NULL COMMENT '课程编号', `tno` varchar(20) NOT NULL COMMENT '授课教师工号', `score` decimal(4,1) DEFAULT NULL COMMENT '百分制成绩,如95.5', `exam_time` datetime DEFAULT NULL COMMENT '考试时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_sno_cno` (`sno`, `cno`), KEY `idx_cno` (`cno`), KEY `idx_tno` (`tno`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='成绩表';这里有个非常值得展开的设计决策:成绩表的主键用不用自增id?很多教科书上的经典设计是不用id,直接用 (sno,cno) 作为联合主键。这种设计的逻辑是:一个学生选修一门课,成绩只记录一条,联合主键天然去重,且不需要额外维护一个自增字段。
但在实际工程中,我强烈建议保留自增id作为主键,同时把 (sno,cno) 设为唯一索引。原因有三个:
第一,业务上虽然“一个学生一门课一条成绩”,但特殊场景下会有补考、重修记录。一旦存在同学生同课程的多条记录(比如补考前后各一条),联合主键就直接崩了,而自增主键配合唯一索引依然可以容纳重修补考的新记录。
第二,外键或者关联查询时,其他表如果要引用成绩,有一个单值主键会非常方便。
第三,InnoDB 的聚簇索引结构对自增主键非常友好,写入时顺序追加,不容易产生页分裂。而用两个 varchar 字段做联合主键,索引体积更大,写入性能也更差。
再说score字段用decimal(4,1)。百分制成绩最大 100.0,decimal(4,1)的精度刚好覆盖,一位小数对大多数学校的学分绩计算也够用。如果成绩要按五级制(优秀、良好、中等、及格、不及格)来统计,我建议直接用varchar存等级文本,或者在应用层做映射,不要在数据库里同时存数字和等级两个字段,那是数据冗余的典型反面教材。
exam_time用datetime而不是date,因为考试时间可能精确到某个上午或下午的场次,datetime保留了更大的弹性。这里补充一点:如果你用的是 MySQL 8.0,建议直接建表时用DATETIME(3)或更高的毫秒精度。为什么?很多应用在并发写入时会依赖数据库时间戳做先后排序,如果精度只到秒,同一秒内的写入顺序就无法判断了。虽然 SchoolDB 规模小,但这个习惯值得从第一张表开始养成。
2.5 为什么外键约束被普遍弃用
看过上面四张表的同学应该注意到,我的 DDL 里没有FOREIGN KEY外键约束。这不是偷懒,而是刻意为之。
教科书教我们用外键保证数据的完整性,比如成绩表里的sno必须存在于学生表中,否则插入失败。但真实项目里,外键约束往往是第一个被 DBA 要求移除的东西。原因很简单:
- 外键约束会带来额外的锁竞争和检查开销,在高并发写入场景下严重拖慢性能。
- 外键会让删除操作变得很危险。比如删除一个学生时,如果他在成绩表里有记录,外键会阻止删除或触发级联删除。级联删除是生产事故的高发源,一个不小心就把关联数据扫了一片。
- 微服务架构下,不同表可能属于不同的数据源,外键根本无从谈起。
所以现代业务系统的普遍实践是:幂等设计在应用层做,关联关系靠索引保证查询性能,数据的逻辑一致性由服务代码控制。你在 DDL 里看不到外键,不代表表之间没有关系,而是关系通过业务逻辑和查询时的 JOIN 来维护。这张成绩表里的sno、cno、tno就是三个逻辑外键,我把它们全部建了普通索引(idx_cno、idx_tno),目的就是为了让后续的成绩按课程查、按教师查都能走索引。
3. 实操全记录:从零建库建表到结构验证
3.1 数据库与字符集的选择
建表之前,先要确定数据库本身的字符集和排序规则。SchoolDB 的表结构全部基于 InnoDB 引擎、utf8mb4字符集。为什么是utf8mb4而不是utf8?因为 MySQL 的utf8实际只支持最多 3 个字节的编码,表情符号这些 4 字节字符存不进去,而utf8mb4是完整的四字节字符集,兼容全部 Unicode 字符。
排序规则utf8mb4_0900_ai_ci是 MySQL 8.0 的默认值,0900代表 Unicode 9.0 标准,_ai_ci表示口音不敏感且大小写不敏感。这意味着查询WHERE sname = 'zhang'时会匹配到Zhang、ZHANG等变体。如果你需要大小写敏感的比较,就要改成utf8mb4_0900_as_cs或utf8mb4_bin。
在 MySQL 5.7 环境里,utf8mb4_0900_ai_ci是不存在的,需要改用utf8mb4_general_ci。这一点写进建表脚本时要注意核对目标环境的版本。我实际操作时用如下语句先创建数据库:
CREATE DATABASE IF NOT EXISTS `school_db` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci;IF NOT EXISTS是一个非常实用的保护句法,脚本重复执行不会报错。在敏感的生产环境里,建议连 DROP DATABASE 这种语句都不要写在初始化脚本里,防止手滑把整个库干掉。
3.2 建表顺序与执行验证
四张表的创建顺序有讲究。虽然我没有使用物理外键,但逻辑上成绩表依赖学生表和课程表,所以先建基础表、再建关系表。执行完建表 SQL 后,用SHOW CREATE TABLE命令逐表核对:
SHOW CREATE TABLE student\G SHOW CREATE TABLE score\G这条命令会返回当前表的完整建表语句,相当于数据库替你校验了一遍 DDL 的合法性。我每次建完新表必然执行一次,主要看三样东西:
- 字符集是否是
utf8mb4,防止建表语句里被会话级变量干扰。 - 注释是否完整保留,方便后面其他人读懂表结构。
- 索引是否符合预期,比如
score表有没有uk_sno_cno这个唯一索引。
建完所有表后,再执行一条全库层面的大检查:
SELECT table_name, table_comment, engine, table_rows FROM information_schema.tables WHERE table_schema = 'school_db';table_rows在 InnoDB 里是估算值,不精确,但可以用来看表是否已经存在。更可靠的方式是用SELECT COUNT(*)逐表验证。
3.3 表结构层面模拟一轮数据写入
DDL 写完不填数据,只能算完成了一半。我习惯立刻用几条 INSERT 语句验证约束是否按预期工作。以下面这组最小数据为例:
INSERT INTO student (sno, sname, ssex, sage, sdept, class_no, enroll_date) VALUES ('20240001', '张三', '男', 18, '计算机学院', 'CS2401', '2024-09-01'); INSERT INTO course (cno, cname, cpno, ccredit, course_type, semester, total_hours) VALUES ('CS101', '程序设计基础', NULL, 3.0, 1, '2024-2025-1', 48), ('CS102', '数据结构', 'CS101', 4.0, 1, '2024-2025-1', 64); INSERT INTO teacher (tno, tname, tsex, tage, title, tdept) VALUES ('T001', '李四', '男', 40, '教授', '计算机学院'); INSERT INTO score (sno, cno, tno, score, exam_time) VALUES ('20240001', 'CS101', 'T001', 88.5, '2025-01-10 09:00:00');全部执行成功后再故意插入一条重复记录:
INSERT INTO score (sno, cno, tno, score, exam_time) VALUES ('20240001', 'CS101', 'T001', 90.0, '2025-01-10 09:00:00');此时应该抛出Duplicate entry '20240001-CS101' for key 'score.uk_sno_cno'的错误。看到这个报错,说明唯一约束生效了。真正可用的 DDL 不是“能建出来”,而是“能按预期拒绝错误数据”。
4. 常见问题与排查技巧实录
4.1 建表报错 ERROR 1064:语法解析失败
这是写 DDL 时最容易遇到的错。最常见的三个原因逐一排查:
- 字段名碰了关键字。比如
desc、order、group这些词是 MySQL 保留字,如果做字段名必须用反引号包裹。我见过有人在课程表里设了一个desc字段存课程描述,建表时直接报 1064,改成course_desc就好。 - 字符集配置写错位置。有些人把
DEFAULT CHARSET=utf8mb4写进了字段定义里,比如:
`sname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci这样写语法上没问题,但如果你紧接着在表级又写了一次 COLLATE,就可能因为排序规则混用而报错。正确做法是在表级统一设置,字段级特殊设置只留给确实需要不同规则的极少数场景。
decimal(3,1)的精度参数写反了。decimal两个参数第一个是总位数,第二个是小数位。写成decimal(1,3)会直接报错,因为小数位不可能超过总位数。
4.2 自增主键的起始值与批量导入问题
成绩表score用了AUTO_INCREMENT,如果后续需要从旧系统迁移大量历史数据,自增 ID 继续保持从 1 开始其实没什么问题,但如果你希望新表和旧表的 ID 段错开,避免对接时产生困惑,可以设置起始值:
ALTER TABLE score AUTO_INCREMENT = 10000;这里要注意:InnoDB 的自增值在 MySQL 8.0 之前是存在内存里的,重启之后会找当前最大 ID 加 1 重新计算,所以如果你手动插入了一个 ID 很大的记录,后续自增可能“跳号”,这是正常行为,不必惊慌。
批量导入数据时另一类高发问题是:往score表插入数据走了唯一索引去重,但业务上又允许一次考试缺考记录(score字段为 NULL)。UNIQUE KEY uk_sno_cno不限制 NULL 值,多个 NULL 是可以同时存在的,所以缺考记录不会触发唯一冲突。但如果后续允许同一个学生对同一门课有补考和正常考试两行记录,那就需要调整唯一索引或者加一个exam_type字段进联合唯一索引,这一点在结构上线前想清楚。
4.3 时间字段的默认值到底该不该用
我的 DDL 里给create_time和update_time都设置了DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP。这是 MySQL 5.6.5 之后支持的特性,非常实用。update_time会在任何一行数据发生 UPDATE 时自动刷新,省去应用层手动维护修改时间的麻烦。
但有一个坑:如果你用的 MySQL 版本太老(5.5 及以下),不支持这么写,只能通过触发器实现或者彻底放弃自动更新。另一个坑是,如果把表的数据迁移到其他数据库(比如从 MySQL 迁移到 PostgreSQL),这种写法就不兼容。如果项目明确使用 MySQL 8.0,大胆用上述默认值;如果数据库是不确定的,建议把时间字段的默认值逻辑放到应用侧。
顺带一提,DEFAULT CURRENT_TIMESTAMP在某些严格 SQL 模式下,如果建表语句里时间字段是NOT NULL且没有默认值,插入时又漏掉该字段,就会直接报错。检查 SQL 模式的命令:
SELECT @@sql_mode;如果看到NO_ZERO_DATE,那么时间字段就不能插入'0000-00-00 00:00:00',否则也会报错。这就是为什么我在四个表里把create_time都设置为自动填充,尽量减少手工干预。
4.4 字符集不一致导致 JOIN 索引失效
表结构本身没问题,但不同表的字段字符集不同,JOIN 时 MySQL 无法使用索引。典型场景是:student表建在utf8mb4,而某个老表还在用latin1,两个表 JOIN 时查询计划变成全表扫描。确认一张表的实际字符集可以用:
SELECT table_collation FROM information_schema.tables WHERE table_schema = 'school_db' AND table_name = 'student';所有四张表都应该保持一致。如果发现有不一致的,用如下语句修改:
ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;CONVERT TO会把表里已有字符串数据一并转换,这在数据量大的表上是 DDL 锁表的危险操作,但 SchoolDB 这种规模完全不用担心。
4.5 删表顺序与误删恢复
虽然没加物理外键,但逻辑关系摆在那里,删除表时应该先删成绩表再删基础表。如果你加了FOREIGN KEY,删表顺序错了会直接报错。在开发环境里我习惯用:
DROP TABLE IF EXISTS score, teacher, course, student;一条语句按照依赖顺序排列,很方便。但生产环境永远不要这么干。
误删表后的恢复,靠的是备份策略。我个人的底线是:任何 DDL 变更之前,先备份一份表结构:
mysqldump -u某用户 -p --no-data school_db > school_db_schema_backup.sql--no-data参数保证只导结构,不导出数据。这个备份文件很小,留着不占多少空间,关键时刻能救命。再进一步,如果表里已经有数据了,全量备份用:
mysqldump -u某用户 -p school_db > school_db_full_backup_$(date +%Y%m%d).sql恢复到本地测试环境验证 DDL 变更是否影响查询,再决定是否在生产环境执行。这是我从某次线上事故中学到的血泪经验,现在固定为流程。
5. 把结构当合同:DDL 之后还应该做的事
四张表的 DDL 建完,业务上的“结构骨架”就立起来了。但我个人在实际操作中的体会是,建表只是开始,真正决定表结构是否可靠的是接下来两步。
第一,把 DDL 文档化。所谓文档化,不是把 SQL 粘贴到某个文档里就完事,而是画一张字段说明表,标注每个字段的业务含义、取值范围、关联关系。尤其是status、course_type这类枚举字段,必须写明每个数字代表什么状态。这份文档建议放在代码仓库里,跟建表脚本放一起,随版本走。时间长了你会发现,真正帮助你快速理解整个库的,不是 DDL 本身,而是这份“翻译”过的字段词典。
第二,把建库建表脚本纳入版本管理。不要只在某个人的电脑里留一份 SQL 文件。每个项目都建一个db/init/目录,放完整的建库建表脚本,再配一个README记录执行顺序和注意事项。新同事入职时,照着这个脚本五分钟就能在本地拉起一套和线上结构一致的开发环境。这一点比大多数优化都更影响协作效率。
另外说一个比较容易被忽略的表设计细节:尽量保留字段注释COMMENT。注释看着不起眼,但在information_schema里可以被检索,很多数据字典工具就是靠解析 COMMENT 字段来自动生成文档的。如果你建表时偷懒不写注释,后面再补的成本远高于建表时多敲几下键盘。
关于这四张表本身,后续还可以这样扩展:给student表加birth_date替代sage,让年龄计算永远实时;给course表加capacity(选课容量)和selected_count(已选人数),用来支撑选课抢课的逻辑;给score表加exam_type区分正常考试与补考。不过这些都是业务需求明确之后再迭代的事情,当前这份结构已经足够支撑一个教学管理系统的核心查询场景了。
还是那句话,DDL 是数据库的根基。根基扎实了,后面怎么加索引、怎么优化查询都顺理成章;根基要是歪了,应用层再努力也是在填坑。这份 SchoolDB 四表结构在校验一张试卷上或许只是练习题,但在真实项目中,它就是整个系统最重要的一张设计图纸。