简介:《高校成绩管理数据库系统的设计与实现》是2008年国际学院计算机专业“数据库技术课程设计”的完整指南,面向需要完成关系数据库设计实训的高校学生,也可供复习SQL Server数据库开发时参考。文档以某校成绩管理为背景,覆盖了从需求分析、E-R概念模型、关系逻辑模型到SQL建库建表、索引及完整性约束的物理设计全过程,并明确要求构建视图、触发器、存储过程,以及基于VC++或Java/JSP开发B/S或C/S成绩管理系统,还包含数据备份恢复策略和大型实验报告的分步撰写建议。资源共1个doc文件,压缩包大小43KB,虽体量不大但结构清晰,能直接作为课程设计说明书或实验指南使用。目前已有754人学习,对掌握数据库系统设计方法、完成同类课设报告具有参考价值。
1. 高校成绩管理数据库系统:先想清楚再建表
看到“高校成绩管理数据库系统的设计与实现”这个题目,大多数人会以为难点在“成绩表”怎么建。实际上真正让人翻车的,是成绩背后的业务规则:一次成绩到底属于哪个学期、哪次考试,补考重修怎么和原成绩区分,绩点按哪条记录算。光靠“学生—课程—成绩”这三张表,撑不起一个能用的成绩管理系统。
这套系统适合课程设计、毕设,也适合刚接学校信息化项目的新人。它要解决的核心问题是:让成绩数据录入、修改、统计变得可控且可追溯。下面从需求建模、表结构、触发器、并发和常见坑几个方向,讲清楚怎样把“数据库系统概论”课上学的ER图和范式,落地成一份能跑、能改、能答辩的数据库设计。
2. 需求与ER图:把“成绩”拆成能落地的数据模型
2.1 先列实体:学生、教师、课程、考试批次一个都不能少
设计成绩管理系统,第一步不是打开建表工具,而是把能接触到成绩单的业务角色都列出来。学生提供身份,教师给分数,教务员审核发布,辅导员看及格率;另外一个隐藏角色是“考试批次”——同一门课同一学期可以有正常考试、补考、缓考,甚至重修考试。如果只设计 student、course、score 三张表,后面一接真实数据,就会出现“张三这学期的高数成绩到底是正常考还是补考”这种无法回答的问题。
我一般会先画一张ER图,把核心实体定为:学生(学号、姓名、院系、专业、入学年份)、教师(工号、姓名、职称)、课程(课程号、课程名、学分、课程性质)、考试批次(学期、考试类型、起止时间),再画两个关系:学生选课生成“选课记录”,选课记录在某个考试批次下产生“成绩记录”。选课记录是关键,因为成绩不是直接挂在课程上,而是挂在“选课 + 考试批次”上。这一步做好了,后面就算用“数据库系统概论”那套ER图规范去检查,关系模式也不会散架。
2.2 从ER图转关系模式:三张表变六张表的理由
有了实体和关系,转关系模式只需要三条规则。第一,一对多关系下,外键放在多的一端,比如一门课程属于一个院系,课程表里存院系id即可;第二,多对多关系必须用中间表,学生和课程多对多,中间表就是选课表;第三,“成绩”不是学生与课程的直接关系,它建立在一次选课和一个考试批次之上,所以成绩表的外键应指向选课记录和考试批次,而不是同时指向学生和课程。
按这个规则,最终关系模式不是 student、course、score 三件套,而是至少六张:学生表、教师表、课程表、考试批次表、选课表、成绩表。选课表和成绩表分离,好处是同一学生同一课程,可以分别记录“第一次正常考试60分”和“补考58分”,历史记录不互相覆盖。如果再把教师和课程关联,可以加一张授课表,让成绩表的“任课教师”也能追溯。到了这一步,“数据库系统概念”第七版里强调的“最小冗余”才真正落到表结构上。
2.3 建第一版表结构:最小可用建表语句与主键选择
设计好关系模式,就可以写第一版建表语句。常见做法是在 MySQL 8.0 上跑,字符集用 utf8mb4,引擎 InnoDB。下面是一份能直接跑的通表结构,后面章节的触发器和存储过程都基于它。
CREATE DATABASE IF NOT EXISTS score_management DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE score_management; CREATE TABLE student ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, dept VARCHAR(80), major VARCHAR(80), enroll_year SMALLINT, UNIQUE KEY uk_student_no (student_no) ); CREATE TABLE teacher ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, teacher_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, title VARCHAR(30), UNIQUE KEY uk_teacher_no (teacher_no) ); CREATE TABLE course ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) NOT NULL, course_type VARCHAR(20), UNIQUE KEY uk_course_no (course_no) ); CREATE TABLE exam_session ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, term VARCHAR(20) NOT NULL COMMENT '如 2024-2025-1', exam_type VARCHAR(20) NOT NULL COMMENT 'normal/resit/relearn', begin_date DATE, end_date DATE, UNIQUE KEY uk_term_type (term, exam_type) ); CREATE TABLE course_selection ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_id INT UNSIGNED NOT NULL, course_id INT UNSIGNED NOT NULL, teacher_id INT UNSIGNED NOT NULL, semester VARCHAR(20) NOT NULL, UNIQUE KEY uk_selection (student_id, course_id, semester), CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sel_course FOREIGN KEY (course_id) REFERENCES course(id), CONSTRAINT fk_sel_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ); CREATE TABLE score_record ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, selection_id INT UNSIGNED NOT NULL, exam_session_id INT UNSIGNED NOT NULL, score DECIMAL(5,1) NOT NULL, passed TINYINT(1) GENERATED ALWAYS AS (score >= 60) STORED, UNIQUE KEY uk_score_session (selection_id, exam_session_id), CONSTRAINT fk_score_selection FOREIGN KEY (selection_id) REFERENCES course_selection(id), CONSTRAINT fk_score_session FOREIGN KEY (exam_session_id) REFERENCES exam_session(id) );建表语句里,student_no 学号没有直接做学生表主键,而是用自增 id 作为主键,学号用唯一索引保证不重复。原因是学号可能在学籍异动后重新分配,也可能一个学生有多条历史学籍,业务上更适合用代理主键。course_selection 表把 student_id、course_id、semester 三者做唯一键,避免同一学期重复选同一门课;score_record 表只存选课记录和考试批次,不直接存学生和课程,这样补考、重修就能以多行成绩存在,而不是覆盖旧成绩。
提示:这个建表语句是“最小可用版”。真正上线时,course_selection 还需要加 status(选课/退课)字段和选课时间,score_record 要加录入人、复核人、录入时间等审计字段,后面避坑章节会再补。
3. 用触发器与存储过程把“成绩规则”收进数据库
3.1 为什么成绩规则要放数据库而不是应用代码
成绩规则写在应用层,虽然灵活,但实际落地时总会被绕过:教务管理系统前端有输入校验,一接第三方导入、接口批量导入,就有人直接写 SQL 改数据。为了避免这种“黑匣子操作”,常见做法是把关键规则下沉到数据库,用触发器对 insert/update 做最后拦截。比如“成绩必须在0到100之间”“同一选课同一考试批次只能有一条成绩记录”,触发器和唯一索引双保险。
“数据库系统概念”第七版里把这种机制叫主动数据库,实际工程里没有那么多理论,本质就是让数据在入口处保持一致。下面这个触发器用在 score_record 表上,禁止把成绩改成超过范围的值,同时禁止把已“审核发布”的成绩悄悄改掉。发布状态字段需要先加一列,因为前面的基础建表里没有它:
ALTER TABLE score_record ADD COLUMN is_published TINYINT(1) NOT NULL DEFAULT 0; DELIMITER // CREATE TRIGGER trg_score_before_update BEFORE UPDATE ON score_record FOR EACH ROW BEGIN IF NEW.score < 0 OR NEW.score > 100 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'score must be between 0 and 100'; END IF; IF OLD.is_published = 1 AND NEW.score <> OLD.score THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'published score cannot be changed'; END IF; END// DELIMITER ;逻辑说明:SIGNAL SQLSTATE '45000'是 MySQL 自定义异常,触发后事务会回滚。OLD.is_published = 1 AND NEW.score <> OLD.score只拦截已发布成绩的分数变动,不拦截备注字段的修正。如果想保留“教务员纠错”的能力,不能直接开放 update,应该增加一张成绩异动表,走“撤回发布 → 改分 → 重新发布”的流程。
3.2 成绩录入存储过程:一份成绩单自动刷新学生统计
成绩系统一般还会维护每个学生的平均分和绩点。如果每次都用应用层计算并 update 总表,很容易在并发时丢更新。常见做法是提供一个录入成绩的存储过程,在同一个流程里完成:插入成绩、刷新统计表、写入审计表。下面是最小可用版本,我用“按学生重算”而不是“增量累加”,避免重复提交时学分统计翻倍:
DELIMITER // CREATE PROCEDURE sp_submit_score( IN p_selection_id INT UNSIGNED, IN p_exam_session_id INT UNSIGNED, IN p_score DECIMAL(5,1), IN p_operator VARCHAR(20) ) BEGIN DECLARE v_student_id INT UNSIGNED; DECLARE v_old_score DECIMAL(5,1); SELECT student_id INTO v_student_id FROM course_selection WHERE id = p_selection_id; SELECT score INTO v_old_score FROM score_record WHERE selection_id = p_selection_id AND exam_session_id = p_exam_session_id FOR UPDATE; INSERT INTO score_record (selection_id, exam_session_id, score, is_published) VALUES (p_selection_id, p_exam_session_id, p_score, 0) ON DUPLICATE KEY UPDATE score = VALUES(score); -- 重算该生统计,避免累计更新把学分算重复 REPLACE INTO student_gpa_summary (student_id, total_credit, weighted_gpa, updated_at) SELECT cs.student_id, COALESCE(SUM(c.credit * (sr.score >= 60)), 0), COALESCE(SUM(c.credit * (CASE WHEN sr.score >= 90 THEN 4.0 WHEN sr.score >= 80 THEN 3.0 WHEN sr.score >= 70 THEN 2.0 WHEN sr.score >= 60 THEN 1.0 ELSE 0.0 END)), 0) / NULLIF(SUM(c.credit), 0), NOW() FROM course_selection cs JOIN course c ON c.id = cs.course_id JOIN score_record sr ON sr.selection_id = cs.id WHERE cs.student_id = v_student_id GROUP BY cs.student_id; INSERT INTO score_audit(selection_id, exam_session_id, old_score, new_score, operator, op_time) VALUES (p_selection_id, p_exam_session_id, v_old_score, p_score, p_operator, NOW()); END// DELIMITER ;参数说明:p_selection_id 来自选课表主键,p_exam_session_id 表示本次考试批次,p_score 是原始成绩,p_operator 是操作人工号。这里用FOR UPDATE锁住已有的成绩记录,防止两个会话同时把它改成不同分数。ON DUPLICATE KEY UPDATE依赖uk_score_session唯一索引,重复提交时只会覆盖分数,不会新增行。如果学校不允许覆盖既往成绩,应该把这一句改成先判断再SIGNAL,而不是直接更新。
需要注意的是,student_gpa_summary 表要提前建好,字段至少包括 student_id、total_credit、weighted_gpa、updated_at。这里用REPLACE INTO的好处是每次提交都重新计算该生全部已发布成绩,统计结果总是对的;代价是成绩量大时稍慢。对于几千甚至几万学生的成绩系统,这个代价可以接受,比在应用层写一堆加加减减的更新逻辑可靠得多。
3.3 补考、重修与绩点计算:别新建“补考成绩表”
最常见的翻车设计,是给补考和重修单独建表,比如 makeup_score、relearn_score,然后在应用层把三张表的数据先读取再合并。一旦漏判学期或漏判课程,学生成绩单就会出现“两个成绩打架”。更简单可靠的做法是:把 normal、resit、relearn 当作 exam_session 表的 exam_type 字段,所有成绩都落在同一张 score_record 表里。查询“当前生效成绩”时,用窗口函数排序取第一条:
SELECT * FROM ( SELECT s.student_no, c.course_name, e.term, e.exam_type, sr.score, ROW_NUMBER() OVER (PARTITION BY s.id, c.id ORDER BY CASE e.exam_type WHEN 'normal' THEN 0 WHEN 'resit' THEN 1 ELSE 2 END, sr.score DESC) AS rn FROM score_record sr JOIN course_selection cs ON sr.selection_id = cs.id JOIN student s ON cs.student_id = s.id JOIN course c ON cs.course_id = c.id JOIN exam_session e ON sr.exam_session_id = e.id ) t WHERE t.rn = 1;逻辑说明:PARTITION BY s.id, c.id表示按“学生 + 课程”分组,ORDER BY里把 normal 排在最前,分数高的排在前,这样每组只留一条作为展示成绩。补考和重修记录仍然保留在表里,但不会污染成绩单。成绩系统做统计时,也应该以这个窗口查询结果为准,而不是直接SUM(score)所有记录。
4. 事务与并发控制:防止成绩数据被覆盖
4.1 两个教务员同时改一个分数会发生什么
真实教务场景里,A 老师和 B 老师可能同时打开同一选课记录:A 把成绩从 60 改成 90,B 把成绩从 60 改成 85,后提交的覆盖先提交的,整个过程都没有报错。MySQL 的隔离级别只能保证“不脏读”,不能保证“不丢更新”。可重复读只是让一个事务内两次读到的结果一致,解决不了两个事务“先读后写”时丢失对方更新。
验证方式很简单,开两个终端,同时执行下面的语句:
-- 会话1 START TRANSACTION; SELECT score FROM score_record WHERE selection_id = 100 AND exam_session_id = 1 FOR UPDATE; UPDATE score_record SET score = 90, is_published = 1 WHERE selection_id = 100 AND exam_session_id = 1; COMMIT; -- 会话2(在会话1提交前执行,会阻塞在SELECT FOR UPDATE) START TRANSACTION; SELECT score FROM score_record WHERE selection_id = 100 AND exam_session_id = 1 FOR UPDATE; UPDATE score_record SET score = 85, is_published = 1 WHERE selection_id = 100 AND exam_session_id = 1; COMMIT;用FOR UPDATE确实会让会话2的 SELECT 等待会话1提交,但提交之后会话2拿到的锁是新的,它会把分数改成 85。所以锁只是排队,防不了“最后一次覆盖”。要真正可追溯,必须给成绩表增加 version 字段做乐观锁,或者要求所有修改先写入成绩异动审批表,审批通过后再更新正式成绩。对高校成绩这种敏感数据,我建议后者。
4.2 把“成绩录入 + 统计刷新”放进一个事务
成绩录入不能先 insert score 再单独 update 统计表,否则中途断电或者第二步失败,成绩表和统计表就不一致。InnoDB 的 DML 默认是自动提交,但自动提交只保证单条 SQL 的原子性,不保证跨表业务的一致性。需要显式地拉开事务边界:
START TRANSACTION; SELECT id FROM score_record WHERE selection_id = 101 AND exam_session_id = 2 FOR UPDATE; UPDATE score_record SET score = 88, is_published = 1 WHERE selection_id = 101 AND exam_session_id = 2; UPDATE student_gpa_summary SET weighted_gpa = 3.5 WHERE student_id = 20230001; COMMIT;如果第二条 SQL 失败,COMMIT 不会执行,事务仍然持有锁,直到连接关闭或主动 ROLLBACK。在存储过程里可以声明异常处理:DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;。在应用层则建议使用连接池的事务 API,不要手动拼多条 SQL 自己 commit。这里的隔离级别保持 MySQL 默认的 REPEATABLE READ 即可,避免统计报表在同一事务里看到不一致的成绩快照。
4.3 备份与恢复:给误操作留后悔药
成绩系统至少每天要有一个全量备份加 binlog,不然误删选课记录以后只能靠“人工复盘”。对于 50GB 以下的小库,mysqldump 就够用了。备份时要注意导出存储过程和触发器,否则恢复后只有表没有业务规则:
mysqldump -u root -p --single-transaction --routines --triggers score_management > /backup/score_$(date +%F).sql参数说明:--single-transaction对 InnoDB 表用一致性快照备份,不会锁住业务写入;--routines导出存储过程;--triggers导出触发器。恢复时先建空库,再导入:
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS score_management CHARACTER SET utf8mb4;" mysql -u root -p score_management < /backup/score_2025-06-30.sql如果外键表导入顺序不对,会报“Cannot add foreign key constraint”。恢复前先执行SET FOREIGN_KEY_CHECKS=0;,导入完成后再置为 1。这个习惯能省掉很多血泪时间。
5. 成绩系统的避坑与常见问题排查
5.1 成绩重复录入,唯一索引没建全
现象:导入成绩单后发现同一学生同一课程同一学期有两条记录,平均分被拉低,学生到教务处投诉。原因:score_record 表只把自增 id 当了主键,没有建立选课记录 + 考试批次的唯一约束;导入脚本又是先查后插,两个会话同时查到不存在,就都插成两条。解决:像第 2 章建表语句里那样,在 score_record 表上加UNIQUE KEY uk_score_session (selection_id, exam_session_id)。如果数据已经存在重复,先用下面这条 SQL 查出来:
SELECT selection_id, exam_session_id, COUNT(*) FROM score_record GROUP BY selection_id, exam_session_id HAVING COUNT(*) > 1;查到重复后,保留一条is_published=1或 score 非空且录入时间最新的记录,把其他行删除。这种坑在“数据库系统概论”里叫完整性约束,但课堂上只讲概念,建表时很容易漏。实际操作中,我建议在每一张关联表的建表语句后面,单独写一段“业务唯一键清单”,上线前逐条核对。
5.2 查全校成绩越来越慢,索引没跟上查询路径
现象:SELECT * FROM score_record WHERE student_id = 1也能跑,但成绩记录到百万行之后,教务查询接口开始超时。原因:score_record 表只建了外键索引,连接时能用,但独立查询条件可能没有对应索引,或者用了WHERE score LIKE '%9%'这种让索引失效的写法。解决:给所有外键列加普通索引,再给常用查询加组合索引:
ALTER TABLE score_record ADD INDEX idx_score_selection (selection_id); ALTER TABLE score_record ADD INDEX idx_score_exam (exam_session_id); ALTER TABLE exam_session ADD INDEX idx_term (term); EXPLAIN SELECT * FROM score_record WHERE selection_id = 102;用 EXPLAIN 看 type 字段,如果从 ALL 变成 ref 或者 eq_ref,说明索引生效。并不是每个字段都要加索引,成绩表的高频查询条件就那么几个:学生、考试批次、课程。统计平均分和绩点的时候,也不要实时聚合所有成绩,而是像前面那样刷新到 student_gpa_summary 里,再对汇总表做报表查询。
5.3 触发器递归调用把事务打爆
现象:在 score_record 表上建了 UPDATE 触发器,触发器里想联动更新 score_audit,结果报ERROR 1442,批量导入时一组数据失败,整批事务回滚。原因:MySQL 不允许触发器直接修改本表,但可以通过修改另一张表间接触发连锁;存储过程里显式调用自身,也可能超过max_sp_recursion_depth。解决:触发器只做最简单的校验和审计,所有跨表联动都集中到存储过程里。如果触发器触发SIGNAL,会把整个事务回滚,批量导入时最好每条数据一个独立事务,或者先在临时表清洗全部数据,再用一条 SQL 批量写入。
我踩过一次很深的坑:为了记录“改分前旧值”,在 before update 触发器里 insert 到 score_audit,审计表又有一个 after insert 触发器去更新 score_record 的 modified_count 字段,结果一改分就递归回滚。最后把 score_audit 的触发器删掉,改成在存储过程里显式插入审计记录,问题立刻消失。触发器和存储过程要分工,别互相套。
5.4 误删选课记录后发现没有后悔药
现象:管理员清理“异常选课”时按 student_id delete,结果把学生往年已经结课的成绩记录一并删了,成绩单直接空白。原因:外键关系里ON DELETE CASCADE被用在了 course_selection 关联 score_record 上,父表删掉选课记录,子表成绩全部跟随删除。解决:设计时对业务数据表避免级联删除,或者至少把 score_record 的外键设为ON DELETE RESTRICT。更稳妥的方式是软删除,给 course_selection 加deleted_at,查询默认过滤。下面是把已有外键改成限制删除的语句:
ALTER TABLE score_record DROP FOREIGN KEY fk_score_selection; ALTER TABLE score_record ADD CONSTRAINT fk_score_selection FOREIGN KEY (selection_id) REFERENCES course_selection(id) ON DELETE RESTRICT;注意,执行前要先确认没有正在跑的批任务依赖级联删除。如果已经误删,只能从备份和 binlog 恢复,这也是第 4 章强调备份的原因。我一般会给所有业务表加 created_at、updated_at 和一个 is_deleted 标签,不是所有删除都必须物理 delete。
6. 验证数据可靠性的进阶技巧:造数脚本与一致性对账
6.1 用 Python 脚本造成绩数据并验证唯一约束
表结构建好后,不要急着写业务页面,先写一个造数脚本,循环提交成绩,验证唯一索引和触发器是否真的在工作。下面用 pymysql 模拟 100 次重复提交:
import random import pymysql conn = pymysql.connect( host='127.0.0.1', user='root', password='123456', database='score_management' ) cur = conn.cursor() for i in range(1, 101): cur.execute( "INSERT INTO score_record (selection_id, exam_session_id, score, is_published) " "VALUES (%s, 1, %s, 0) " "ON DUPLICATE KEY UPDATE score = VALUES(score)", (i, random.randint(40, 100)) ) conn.commit()参数说明:循环里的 selection_id 从 1 到 100,exam_session_id 固定为 1,每次执行都会撞到uk_score_session唯一键。ON DUPLICATE KEY UPDATE让重复数据不会报错,只更新分数。跑完后执行SELECT COUNT(*) FROM score_record WHERE exam_session_id=1;,如果结果还是 100 行,说明唯一约束有效;如果超过 100 行,就要检查是不是建表时漏了唯一键。
6.2 一致性自检 SQL:把统计表与明细表对账
统计表student_gpa_summary是刷新出来的,可能存在触发器没触发、存储过程被绕过的情况。写一条对账 SQL,每周跑一次,把统计表里的学分和绩点与 score_record 明细重新聚合的结果做对比:
SELECT s.student_id, IFNULL(g.total_credit, 0) AS summary_credit, IFNULL(d.detail_credit, 0) AS detail_credit, IFNULL(g.weighted_gpa, 0) AS summary_gpa, IFNULL(d.detail_gpa, 0) AS detail_gpa FROM (SELECT DISTINCT student_id FROM course_selection) s LEFT JOIN student_gpa_summary g ON g.student_id = s.student_id LEFT JOIN ( SELECT cs.student_id, SUM(c.credit * (sr.score >= 60)) AS detail_credit, ROUND(SUM(c.credit * (CASE WHEN sr.score >= 90 THEN 4.0 WHEN sr.score >= 80 THEN 3.0 WHEN sr.score >= 70 THEN 2.0 WHEN sr.score >= 60 THEN 1.0 ELSE 0.0 END)) / NULLIF(SUM(c.credit), 0), 2) AS detail_gpa FROM score_record sr JOIN course_selection cs ON sr.selection_id = cs.id JOIN course c ON cs.course_id = c.id WHERE sr.is_published = 1 GROUP BY cs.student_id ) d ON d.student_id = s.student_id HAVING summary_credit <> detail_credit OR summary_gpa <> detail_gpa;这个查询只要返回任何一行,就说明统计表和明细表对不上。常见原因有三个:有人绕过存储过程直接 update 了 score_record;触发器写了但没启用;学生选课退课后成绩记录没清理。对账查出来之后,重新执行“刷新所有学生统计”的存储过程,然后人工查数据改动记录。我用这个方案给两个校内项目做过成绩迁移,最大的教训是“先写唯一约束,再写业务逻辑”。成绩系统不怕表多,不怕存储过程长,就怕删除没有边界、更新没有留痕。希望这个造数加对账的思路能帮到你的成绩管理系统落地。
本文还有配套的精品资源,点击获取