简介:数据库课程设计平行志愿模拟录取系统完整项目,面向计算机相关专业学生及需要完成数据库课设的开发者,重点解决志愿填报业务中数据建模与批量录取规则实现问题。项目按照考生、院校、志愿、录取等核心实体设计数据表,适配MySQL与SQL Server,同时以Vue、TypeScript构建前端界面,用Java实现后端接口,形成从建库到展示的完整闭环。资源包包含969个文件,压缩后仅3.87MB,其中前端相关文件(js、css、scss、vue、ts)数量较多,也包含32个Java源文件、SQL建库脚本、PDF说明文档以及Excel样例数据,目录结构直观,便于检索。目前已有104人浏览学习。通过学习这份项目,可掌握数据库ER图转换关系模型、事务机制、存储过程应用与前后端联调方法,并获取可直接运行的完整源码,为课程答辩、功能扩展或二次开发提供扎实基础。
1. 数据库课程设计里的平行志愿模拟录取系统,先别急着解压 zip
拿到“数据库课程设计-平行志愿模拟录取系统.zip”,大多数人第一反应是解压、找报告模板、改个名字就交。但这个方向的核心其实是两件事:一是把平行志愿的投档规则用关系模型表达清楚,二是把“分数优先、遵循志愿、一轮投档”的逻辑写成能在数据库和代码之间跑通的东西。它适合正在做数据库课程设计、又不想只做增删改查的学生,也适合想拿一个完整业务场景练手的人。我先说个反直觉结论:这个题目里 70% 的分数在数据模型和投档规则上,只有 30% 在代码本身。
2. 数据模型先行:平行志愿系统的表设计与初始化脚本
先建库还是先写逻辑?我的习惯是先把表结构定下来。平行志愿模拟录取系统里,经常出现“考生表、院校表、志愿表”三张表就开干的方案,跑起来才发现:专业和院校没分开,一个院校下多个专业没法存;考生被录取之后,没有地方记录最终落点;调剂标识没字段,规则只能写死在代码里。
2.1 用四张核心表装下整个录取业务
我会用考生表、院校计划表、志愿表、录取结果表四张表起步。考生表存学生基本信息:考生号、姓名、总分和语文、数学、外语三科成绩。三科成绩专门用来做同分排序,和总分不是加和关系,这一点在设计表结构时就要想清楚。院校计划表存每所学校的招生计划和专业信息,注意把专业放在同一张表里按行拆开,这样“计算机科学与技术招 5 人”和“软件工程招 3 人”是两条记录,而不是一个院校一条记录。
志愿表存考生的填报顺序:考生号、院校代码、专业代码、志愿序号,1 表示第一志愿。平行志愿的特点是同一批次可以填多个志愿,录取时先看第一志愿(某校某专业),如果这个专业名额满了就看第二志愿,依次往后。录取结果表存最终落点:考生号、录取院校、录取专业、状态码,这张表在算法跑完后被写满。
建表 SQL 如下,字段注释直接写在 DDL 里,方便答辩时讲清楚每张表的用途:
-- 考生表:核心是总分,同分排序用三科成绩 CREATE TABLE students ( stu_id CHAR(10) PRIMARY KEY COMMENT '考生号', stu_name VARCHAR(50) NOT NULL, total_score INT NOT NULL, chinese_score INT NOT NULL DEFAULT 0, math_score INT NOT NULL DEFAULT 0, english_score INT NOT NULL DEFAULT 0, INDEX idx_total (total_score DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里INDEX idx_total (total_score DESC)是为后续“按总分排序”的查询准备的索引,DESC 让数据库按从高到低的顺序维护索引。模拟录取时直接扫索引就能拿到“分数优先”的遍历顺序,而不是靠临时排序。
-- 院校计划表:同一学校多个专业,按 学校+专业 拆行 CREATE TABLE colleges ( college_id CHAR(6) NOT NULL, major_code CHAR(8) NOT NULL, major_name VARCHAR(50) NOT NULL, plan_count INT NOT NULL COMMENT '招生计划人数', min_score INT DEFAULT NULL COMMENT '录取后回填最低分', PRIMARY KEY (college_id, major_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;复合主键(college_id, major_code)是这个表的关键:它保证同一个学校的同一个专业只有一条计划记录。min_score 字段可以在算法跑完后回填,用来快速查看录取最低分。
-- 志愿表:考生 + 院校 + 专业 + 志愿序号 CREATE TABLE volunteers ( stu_id CHAR(10) NOT NULL, college_id CHAR(6) NOT NULL, major_code CHAR(8) NOT NULL, preference_order TINYINT NOT NULL COMMENT '1=第一志愿, 2=第二志愿...', PRIMARY KEY (stu_id, preference_order), KEY idx_college (college_id, major_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;志愿表主键是(stu_id, preference_order),意思是一个考生在同一个志愿序号只能填一条,数据库层面保证不会出现“第一志愿填了两所学校”的脏数据。这个约束在真实系统里非常重要。
-- 录取结果表:算法跑完后写入最终落点 CREATE TABLE admissions ( stu_id CHAR(10) PRIMARY KEY, college_id CHAR(6) NOT NULL, major_code CHAR(8) NOT NULL, status ENUM('ADMITTED','NOT_ADMITTED') NOT NULL DEFAULT 'NOT_ADMITTED', admitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status 用 ENUM 而不是 TINYINT,因为“已录取/未录取”是固定状态,用枚举可读性好,也省得写一堆 status=2 这种魔法数字。四张表合起来,课程设计报告里的 E-R 图也能直接画:考生与志愿是 1:N,院校专业与志愿是 1:N,考生与录取结果是 1:1。答辩时老师问“你的数据模型怎么设计的”,直接对着这张图讲就行。
2.2 初始化脚本:事务、外键和增删改查的顺序
建完表接下来是初始化数据。我见过很多同学把 INSERT 语句直接堆在一个文件里,跑的时候中途报错,前面插入的数据已经生效,后面没插进去,库里留下一堆半截数据。这就是典型的“初始化脚本没有事务边界”。
正确的做法是把所有 INSERT 包在一个事务里:
START TRANSACTION; -- 清空旧数据,注意先删子表再删父表 DELETE FROM admissions; DELETE FROM volunteers; DELETE FROM colleges; DELETE FROM students; -- 重新插入考生数据 INSERT INTO students (stu_id, stu_name, total_score, chinese_score, math_score, english_score) VALUES ('1001', '张明', 618, 118, 135, 128), ('1002', '李华', 612, 122, 130, 125); -- 插入院校计划 INSERT INTO colleges (college_id, major_code, major_name, plan_count) VALUES ('A001', 'M001', '计算机科学与技术', 2), ('A001', 'M002', '软件工程', 3); -- 插入志愿 INSERT INTO volunteers (stu_id, college_id, major_code, preference_order) VALUES ('1001', 'A001', 'M001', 1); COMMIT;把 DELETE 和 INSERT 放进同一个事务,好处是任何一个语句失败,回滚后数据库还是原来的干净状态,不会出现“删了没插进去”的中间态。课程设计里常被追问的“数据一致性”,就是从这种细节体现的。注意 DELETE 顺序:先删 admissions、volunteers 这些子表,再删 colleges、students 父表。顺序反了,有外键约束时直接报错,这个高频踩坑点第 5 章会专门展开。
实际交作业时,我会把增删改查的示例单独放在一个 queries.sql 文件里:按总分倒序查考生、按院校查录取名单、修改某个考生的分数、删除一条志愿记录。答辩时现场演示一句 SELECT,比翻 PPT 有效得多。
2.3 模拟数据生成:分数分布和志愿填报的随机规则
课程设计的数据量,越大越能看出算法的正确性。最少做 100 个考生、5 所院校、每所院校 2 到 3 个专业,足够跑通逻辑;想拿高分建议做到 300 个考生以上,因为平行志愿的“轮次”效果需要足够多的考生才明显。
模拟数据怎么造?我一般用 Python 一次性生成考生成绩和志愿表:
import random # 生成考生成绩:总分呈正态分布,控制在 400~700 区间 def gen_score(): score = int(random.gauss(560, 60)) return max(400, min(700, score)) # 生成志愿顺序:按“冲稳保”逻辑,前几个志愿填热门院校,后面填保底 students = [] for i in range(300): stu_id = f'{1000 + i:04d}' total = gen_score() chinese = random.randint(90, 140) math = random.randint(90, 140) english = random.randint(90, 140) students.append((stu_id, f'考生{i}', total, chinese, math, english))总分用高斯分布生成,模拟真实考试“中间多、两端少”的分布。三科成绩独立随机,这里只作为同分排序键;如果希望三科之和等于总分,可以先随机生成三科再相加,两种做法都行,课程设计不用太较真。志愿填报用“冲稳保”逻辑:前几个志愿填录取线高的学校,后面填分数线低的保底学校,这样模拟出来的投档结果更接近真实场景。
生成完数据后写入 CSV,顺便演示“从 Excel 导入数据库”这条路:
import csv with open('students.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['stu_id', 'stu_name', 'total_score', 'chinese_score', 'math_score', 'english_score']) writer.writerows(students)然后通过 MySQL 的 LOAD DATA 命令批量导入,几百条数据瞬间完成,比一条条 INSERT 快得多。这一步做完,数据环境就绪,可以开始写核心算法了。
3. 模拟录取核心:把“分数优先、遵循志愿、一轮投档”写进代码
3.1 先搞清楚投档规则,再谈写代码
平行志愿的规则三句话就能说清:分数优先、遵循志愿、一轮投档。但很多课程设计只实现了前两句,漏了“一轮投档”的语义。
分数优先,指所有考生按总分从高到低排序,分高的人先选,这决定了算法的遍历顺序必须按分数,不能用插入顺序。遵循志愿,指轮到某个考生时,按他填的专业志愿顺序依次检查,第一志愿专业没满就投进去,满了就看第二志愿。一轮投档,指每个考生在同一批次里只有一次投档机会,一旦投进某个专业,即使后面发现更好的志愿也不能改;如果所有志愿的专业都满了,只能落榜。
这个规则直接引出一个实现问题:如果两个考生同分,谁先谁后?真实高考规则是看单科成绩,各省标准不同,比较顺序一般是语文、数学、英语。我的排序键定为(total_score DESC, chinese_score DESC, math_score DESC, english_score DESC),后面所有代码都以这个顺序为准。
这里采用的是专业平行志愿模式:每个志愿精确到“某校某专业”,投档直接看专业是否还有名额。如果你想做传统院校平行志愿,把志愿表里的 major_code 去掉,按院校总计划投,代码逻辑改动很小。
3.2 Python 实现:按总分排序,逐志愿检索,按计划数投档
我习惯的做法是“先查数据进内存,再在内存里跑算法,最后把结果写回 MySQL”。为什么不纯用 SQL 一次 UPDATE 解决?因为课程设计的重点是展示你理解规则,纯 SQL 做这种逐行判断非常绕,debug 也困难。内存里跑一遍,每一步能打印日志,遇到结果不对,马上知道是排序错了还是志愿顺序错了。
import pymysql # 连接到本地 MySQL 数据库 conn = pymysql.connect( host='127.0.0.1', user='root', password='你的密码', database='volunteer_system', charset='utf8mb4' ) cursor = conn.cursor() # 1. 读取所有考生,按总分排序,同分按单科成绩排序 cursor.execute(""" SELECT stu_id, total_score, chinese_score, math_score, english_score FROM students ORDER BY total_score DESC, chinese_score DESC, math_score DESC, english_score DESC """) students = cursor.fetchall() # 2. 读取院校招生计划,记录剩余名额 cursor.execute("SELECT college_id, major_code, plan_count FROM colleges") plans = {} for college_id, major_code, plan_count in cursor.fetchall(): plans[(college_id, major_code)] = plan_count # 剩余名额初始化为招生计划 # 3. 读取所有考生的志愿,按考生号和志愿序号分组 cursor.execute(""" SELECT stu_id, college_id, major_code, preference_order FROM volunteers ORDER BY stu_id, preference_order """) volunteers = {} for stu_id, college_id, major_code, order_no in cursor.fetchall(): volunteers.setdefault(stu_id, []).append((college_id, major_code)) # 4. 核心算法:分数优先,遵循志愿,一轮投档 results = {} admitted_count = 0 for stu_id, total, chinese, math, english in students: admitted = False if stu_id not in volunteers: # 异常数据兜底:没填志愿的考生直接标记未录取 results[stu_id] = ('NOT_ADMITTED', None, None) continue for college_id, major_code in volunteers[stu_id]: if plans.get((college_id, major_code), 0) > 0: # 该专业还有名额,投档并占用名额 plans[(college_id, major_code)] -= 1 results[stu_id] = ('ADMITTED', college_id, major_code) admitted_count += 1 admitted = True break if not admitted: results[stu_id] = ('NOT_ADMITTED', None, None) # 5. 写回录取结果表 for stu_id, (status, college_id, major_code) in results.items(): if status == 'ADMITTED': cursor.execute(""" INSERT INTO admissions (stu_id, college_id, major_code, status) VALUES (%s, %s, %s, 'ADMITTED') ON DUPLICATE KEY UPDATE college_id = VALUES(college_id), major_code = VALUES(major_code), status = 'ADMITTED' """, (stu_id, college_id, major_code)) else: cursor.execute(""" INSERT INTO admissions (stu_id, college_id, major_code, status) VALUES (%s, NULL, NULL, 'NOT_ADMITTED') ON DUPLICATE KEY UPDATE status = 'NOT_ADMITTED' """, (stu_id,)) conn.commit() cursor.close() conn.close() print(f'模拟录取完成,共录取 {admitted_count} 人')逻辑说明分四块。第 1 步读取考生时用 ORDER BY 直接实现了“分数优先”,后面循环的遍历顺序就是投档顺序。第 2 步把招生计划放到字典里,key 是(college_id, major_code),对应表结构里“同一学校不同专业是不同行”的设计。第 3 步把志愿表按考生号分组成列表,列表顺序就是志愿优先顺序。第 4 步是核心循环:每个考生按志愿顺序依次检查剩余名额,某个专业看到有名额就投,同时名额减一,并立刻 break 跳出志愿循环,保证“一轮投档”——不会再回头去看后面的志愿。
关于ON DUPLICATE KEY UPDATE:admissions 表主键是 stu_id,同一考生的结果只允许存在一条。算法跑第二遍时不需要先清空 admissions,重复执行会覆盖旧结果,这让系统可以随时“重跑模拟”。课程设计答辩时,老师可能为了验证效果改几个招生计划数让你重跑,这个设计让重跑成本为零。
3.3 三个必调参数:招生计划、是否允许调剂、是否设专业级差
代码里能调的参数有三个,课程设计报告里把这些参数写明白,比贴代码更有说服力。
招生计划数,就是投档的“容量”。真实系统里是每个学校提前公布的,模拟系统里改 colleges 表的 plan_count 就能调节竞争激烈程度。把某所热门大学的计算机专业计划从 2 改成 1,录取最低分立刻上浮。
是否允许调剂,这是平行志愿的高频考点。上面代码里没有调剂逻辑,每个专业独立算名额,投的是“专业志愿”。如果要做服从调剂版本,需要在循环里加一步:当某个考生在某所学校填的所有专业志愿都满时,检查这所学校还有没有其他未满专业,有就投到空缺专业。这一步多数课程设计都没做,做了就是加分项。
是否设专业级差,有的录取规则里第二志愿专业要扣 3 分再参与排序。模拟系统里加级差,等于读取考生分数时把 total_score 临时减去级差分,再重新参与专业竞争。
参数说明整理成表,答辩时可以投影出来:
| 参数 | 位置 | 默认值 | 说明 |
|---|---|---|---|
| 招生计划 | colleges.plan_count | 按实际设定 | 专业课招生人数,直接影响录取最低分 |
| 是否调剂 | 算法代码配置 | False | True 时实现“有空缺专业就投” |
| 专业级差 | 配置项分数值 | 0 | 每降一个专业志愿扣 3 分(示例) |
4. 本地跑通最小系统:导库、改配置、验证录取结果的完整步骤
4.1 准备 MySQL 8.0 与 PyMySQL 环境
这个题目最常见的落地环境是 Windows 本地装 MySQL 8.0,因为课程设计验收一般就在自己笔记本上。装好后先确认服务在跑:Windows 在服务管理器里查 MySQL80,macOS 用 brew services list。Python 侧只需要一个 pymysql 包:
pip install pymysql建议把连接信息单独放到 config.py,避免密码散落在各个脚本里。环境问题最常见的三种报错:2003 连不上,多半是 MySQL 服务没启动;1045 密码错误,检查 root 密码;1049 数据库不存在,建库语句没执行。按这个顺序排查,能解决九成环境问题。
关于“数据库连接池”这个概念,课程设计的单机脚本不需要连接池,因为一次算法运行只有几个查询,连接建立和关闭的开销可以忽略。见过有同学为了显得专业,硬上 SQLAlchemy 连接池,结果事务边界变复杂,回滚都没搞明白。我的建议是单连接跑完,用完 close。如果答辩被问“连接池什么时候用”,回答“高并发 Web 系统里复用连接、避免每请求创建连接”就够了,不要在课程设计里硬上。
4.2 用 source 导入 SQL 文件,注意字符集
zip 包解压后通常有 schema.sql 和 data.sql。用 MySQL 命令行导入:
mysql -uroot -p # 登录后执行 SET NAMES utf8mb4; source D:/volunteer_system/schema.sql; source D:/volunteer_system/data.sql;source 是 mysql 客户端的命令,不是 SQL。路径里用正斜杠,Windows 反斜杠容易转义出问题。先 SET NAMES 再导入,是为了让客户端与服务器之间的字符集一致,避免中文乱码。如果 SQL 文件本身是 GBK 保存的,改成 SET NAMES gbk。如果 zip 包只有代码没有 SQL 文件,就用第 2 章的表结构自己建库,一样能跑通。
注意:先确认 SQL 文件的编码,再决定 SET NAMES 的参数。这一步错了,后面全是乱码。
4.3 用三条核验 SQL 判断录取结果是否正确
模拟录取跑完,别急着看结果文件,先用 SQL 做三件事,判断算法到底跑对没有。
第一条,录取总人数不应该超过所有招生计划的总和。跑完算法后,查 admissions 里 ADMITTED 的数量,再查 colleges 的 plan_count 总和,手动对比。如果录取人数超标,说明代码里名额扣减逻辑有 bug。
-- 核验 1:录取人数与招生计划总量对比 SELECT COUNT(*) AS admitted_count FROM admissions WHERE status = 'ADMITTED'; SELECT SUM(plan_count) AS total_plan FROM colleges;第二条,每个考生只能有一条录取记录。admissions 表主键是 stu_id,按理不会重复,但如果你改过表结构或者用 INSERT 而不是 ON DUPLICATE KEY UPDATE 重跑,就可能出现重复行。这条 SQL 查出来为空才算正常:
-- 核验 2:每个考生只能有一条录取记录 SELECT stu_id, COUNT(*) AS cnt FROM admissions GROUP BY stu_id HAVING cnt > 1;第三条,看各专业的录取最低分是否符合“高分先挑”的预期。热门专业的最低分应该比同校冷门专业高,如果出现反常,多半是 JOIN 产生了重复行,或者排序键没生效:
-- 核验 3:各专业录取最低分,倒序看竞争热度 SELECT a.college_id, a.major_code, c.major_name, MIN(s.total_score) AS lowest_score FROM admissions a JOIN students s ON a.stu_id = s.stu_id JOIN colleges c ON a.college_id = c.college_id AND a.major_code = c.major_code WHERE a.status = 'ADMITTED' GROUP BY a.college_id, a.major_code, c.major_name ORDER BY lowest_score DESC;如果验证结果不对,第一件事是打印中间变量:把排序后的考生列表前 20 名打出来,看是不是真的按分数排的。这类问题九成出在排序键或是志愿分组上,很少是数据库本身的问题。
5. 平行志愿模拟录取的避坑指南:5 个真实踩过的坑
这一章的坑是我做这类课程设计时真实遇到过、以及帮人查过的血泪经验。每一条都是“现象 → 原因 → 解决”的结构,你按顺序对一遍自己的代码,能省下大半天 debug 时间。
5.1 坑一:同一考生被两所学校同时录取,结果表出现逻辑矛盾
现象:跑完算法后,查 admissions 表,发现一个考生出现在 A 大学的录取名单里,也出现在 B 大学的录取名单里。
原因:这是“一轮投档”规则没实现的典型表现。常见写法是:外层循环遍历院校,内层循环遍历考生,先给 A 大学挑满人,再给 B 大学挑人,于是同一个高分考生被 A 大学录取后,又因为分数高被 B 大学“录取”了一次。这个翻车现场在答辩里我见过不止一次。
解决:录取循环只能有一个入口——按分数排序后的考生列表。每一名考生只有一次机会,投进一个专业后立即 break 跳出志愿循环,后面的志愿不再参与。第 3 章代码里就是先 ORDER BY 再外层遍历考生、内层遍历志愿列表,一旦投中立刻 break,从结构上杜绝“一人多投”。
5.2 坑二:数据库并发锁导致的死锁,模拟脚本卡死
现象:算法脚本跑着跑着不动了,报 Lock wait timeout exceeded;或者在事务里执行一条 UPDATE,明明只更新一行,却等到超时。
原因:多数是“为了防重跑,先 DELETE 再 INSERT”造成的。DELETE 全表会锁住整张表,如果此时另一个连接在更新同一张表,就会互相等锁。课程设计虽然是单机,但如果开着 MySQL 客户端手动查数据,相当于两个连接同时操作同一张表,死锁很容易出现。
解决:能不加锁就不加锁。先 DELETE 全表再 INSERT 的做法,改成第 3 章的 ON DUPLICATE KEY UPDATE 方案,用主键覆盖旧数据,每次运行都是幂等重放,不需要锁也不需要清表。如果确实需要清表,把 DELETE 和 INSERT 放进同一个事务,并尽快 COMMIT,缩短锁持有时间。
提示:单机课程设计出现死锁,先想想是不是自己开了多个 MySQL 窗口。关掉多余连接,死锁大概率消失。
5.3 坑三:zip 里解压出来的 SQL 文件全是乱码,导入直接报错
现象:解压后打开 schema.sql,中文字段注释显示成“锟斤拷”之类的内容,source 导入时要么报错,要么数据里的中文变成问号。
原因:SQL 文件保存时用 UTF-8,Windows 下 MySQL 控制台默认按 GBK 显示;或者反过来,文件是 GBK,连接用了 UTF-8。这是编码层面的老问题,不是代码 bug,和用什么数据库软件无关。
解决:分两步。第一步确认文件编码,用编辑器右下角看编码,或者直接看文件开头有没有 UTF-8 BOM;第二步,source 之前先执行 SET NAMES utf8mb4 或者 SET NAMES gbk,让连接字符集和文件一致。如果文件编码已经乱掉,唯一的后悔药是把文件另存为 UTF-8 无 BOM 格式再导入。先改编码再导数据,能少走很多弯路。
5.4 坑四:删除旧数据时外键约束不配合,一执行就报错
现象:想清空数据库重新模拟,执行 DELETE FROM students,结果报 Cannot delete or update a parent row。
原因:表里建了外键,考生表是父表,志愿表或录取结果表是子表。删除父表记录时,数据库会检查子表里有没有引用,有引用就不让删。这是数据库在保护数据完整性。很多课程设计为了“数据严谨”给每张表都加了外键,结果删除数据时忘了先删子表,一头撞在外键墙上。
解决:删除顺序是“先子后父”。先 DELETE admissions、volunteers,再 DELETE students、colleges。或者启动事务后临时关闭外键检查:SET FOREIGN_KEY_CHECKS = 0,删完再 SET FOREIGN_KEY_CHECKS = 1。第二种方式也能交差,但答辩时建议说清楚:平时保留外键保证完整性,清库时临时关闭,说明你懂外键的作用,而不是图省事。
5.5 坑五:统计录取最低分,热门专业最低分反而比冷门专业低
现象:用 MIN(total_score) 统计各专业录取最低分,发现一个冷门专业的最低分比热门专业还高,数据明显不符合“分数优先”的直觉。
原因:通常是聚合查询 JOIN 时出现了重复记录。录取结果表里每个考生只有一条记录,理论上 JOIN 不会产生重复行;但如果你核验时直接 JOIN 了志愿表,一个考生填了多个志愿,就会产生一对多,MIN 聚合时就可能算错。
解决:统计只基于录取结果表 JOIN 考生表,别带志愿表。如果非要带志愿表,先 GROUP BY 志愿表去重。写聚合查询之前,先单独跑一条不带聚合的 SELECT 看行数,确认没有重复行再聚合。这个习惯能帮你避开绝大多数 SQL 统计坑。
6. 答辩前最后一步:用事务隔离级别和索引把系统讲出“数据库味”
课程设计做到这里,功能链路已经完整:建表、初始化、模拟录取、结果核验,全流程能跑通。但答辩和项目验收不一样,老师不会只看结果对不对,他会问“你的系统在并发下会不会出问题”“这条查询能不能走索引”。有两个技巧一定能用上:事务隔离级别和索引利用。
先讲事务隔离级别。模拟脚本一次运行里会读考生表、写录取结果表,直到最后才 COMMIT。COMMIT 之前,另一个连接查 admissions 看到的是旧数据,这是 MySQL 默认的 REPEATABLE READ 隔离级别下的正常行为。答辩时主动说“我把写入放进一个事务里,保证了录取结果的原子性:要么全部写成功,要么全部回滚,不会出现半份录取名单”,这句比“我的数据很准”有说服力得多。
再讲索引利用。第 3 章的核心查询是按 total_score、chinese_score、math_score、english_score 排序。如果建表时只建了单列索引,这条查询会走 filesort,数据量大时性能明显下降。补一个复合索引:
ALTER TABLE students ADD INDEX idx_score_order ( total_score DESC, chinese_score DESC, math_score DESC, english_score DESC );跑一下 EXPLAIN 看是不是还出现 Using filesort,这是数据库优化里最能直接演示的闭环:发现问题、建立索引、验证生效。
最后是我自己的习惯:每次跑完模拟录取,都把结果导出一份带时间戳的 CSV,比如 admissions_20250101_1800.csv。答辩时老师问“你改了一次招生计划,怎么证明结果变化”,直接拿出两份 CSV 对比最低分就行,不用现场重跑。这个习惯帮我少解释很多次。希望帮到你。
本文还有配套的精品资源,点击获取