☰
学生选课系统课程设计:从E-R图到并发控制的完整数据库实现
2026/10/2 6:56:31 网站建设 项目流程

简介:这是一份数据库系统课程设计报告,主题为“学生选课管理信息系统”,完整呈现从需求分析、数据库设计到物理实现、应用开发的标准流程。报告前半部分围绕系统需求展开,涵盖可行性分析、功能需求、组织结构、业务流与数据流分析,并整理出数据项与数据结构字典;中间部分依次完成实体、属性、联系分析,绘制CDM概念模型,再转化为PDM逻辑模型;后半部分则聚焦SQL Server实践,包括表结构设计、完整性约束代码、视图、触发器、存储过程与索引的创建,以及学生信息、课程信息模块的调试过程,最后给出登录模块和成绩管理模块的程序设计说明。整份报告遵循标准课程设计文档格式,结构完整、内容详实,适合计算机专业学生用作课程设计参考或毕业设计文档模板。资源包内含1个docx文档,压缩后大小约3.96MB,现已有851人学习下载,打开即可查看完整可编辑内容,便于替换表名、字段与页面信息后直接复用。

1. 数据库系统课程设计报告:学生选课管理信息系统在解决什么问题

三个学生同时抢最后一个选课名额,系统发出了两张选课成功的回执。这是把“先查余量再插入”写进学生选课管理信息系统后最容易翻车的现场。这个课设题目几乎把数据库系统课程的核心考点串完了:E-R图设计、关系模式规范化、多表连接查询、事务与并发控制。你最后要交的不是一堆建表语句,而是一个能查课、能选课、能退课、能出成绩单,并在多人同时操作时不产生脏数据的小系统。这篇笔记按“表结构→业务SQL→并发排错→测试与备份→答辩自证”的顺序,把可直接复现的写法讲清楚。适合不确定表怎么拆、并发控制无从下手、希望报告里多一些能演示模块的同学。

2. 先拆E-R图再建表:从成绩单反推六张表的理由与建表SQL

2.1 三张表还是六张表:教学班实体为什么不能省

第一版通常只有学生表、课程表、选课表,跑起来很顺畅,直到出现一个需求:同一门数据库系统概论有两个老师各开一个班。课程表里只有一条CS101,选课表关联不到“哪个老师、哪个时段、哪个教室”,成绩单打印出来也写不出任课教师。这类题在《数据库系统概论》的E-R图例题里经常出现,解决方式就是引入“教学班”实体,有的教材叫开课实体。

教学班的属性包括开课编号、课程号、教师号、学期、上课时间、教室、容量、剩余名额。它和课程之间是多对一,同一门课每学期开多个班;和教师之间也是多对一,一个教师可以带多个班。选课关系要跟着改成“学生选的是教学班而不是课程本身”,这样同一个人重复选同一门课不同老师的班就具备了数据基础。

有同学问教师表要不要单独建。建议单独建,教师的工号、职称、院系放在教学班表里会造成冗余,后续加“按教师查授课任务”的功能时还得拆表。课设规模下六张表足够:学生、教师、课程、教学班、选课。若题目包含登录模块,再加一张管理员表,和核心业务分开,不要让管理员账号混进学生表。

一个课设里常见分歧是成绩字段放哪。常见做法是放在选课表里,因为一次选课对应一条成绩记录,成绩生命周期和选课记录一致,单独建成绩表只会让退课多一张表要联删。报告里写清楚“选课表兼做成绩表”即可,评分关注的是逻辑是否自洽。

2.2 字段约束、主键外键与三个必建索引

字段类型有几个点要注意。student_id用CHAR(10)而不是INT,学号以0开头时会丢前导零。gender用CHAR(2)或TINYINT,别用ENUM,ENUM在迁移数据时会带来预料之外的坑。offer_id用INT AUTO_INCREMENT,这是教学班表的主键。remain用INT UNSIGNED,但光约束非负还不够,选课时要配合WHERE remain>0才能挡住并发。semester用VARCHAR(20),存'2024-2025-1'这种格式,不要用DATE,学期不是自然日期,跨年下学期也没法用DATE准确表达。

索引不要只依赖主键。第一,选课表的联合主键是(student_id, offer_id),它同时防止同一个人重复选同一个班,但教务老师查看“某班选了哪些人”时,最左前缀是student_id,用不上offer_id前缀,所以要给选课表的offer_id单独建索引。第二,课程表按course_id和semester查询最多,course_offer表上建(course_id, semester)联合索引。第三,teacher_id也会被频繁关联,补一个普通索引。

外键约束要写明动作。选课记录被大量业务引用,删除班级时选课记录应一并清理,所以选课表外键用ON DELETE CASCADE;课程和教师被教学班引用,误删会破坏成绩单,用ON DELETE RESTRICT拒绝删除。这个细节放进报告的物理设计章节,比空谈“参照完整性”更有说服力。

2.3 六张表的完整建表SQL与参数说明

CREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_select; CREATE TABLE student ( student_id CHAR(10) NOT NULL, student_name VARCHAR(20) NOT NULL, gender CHAR(2) NOT NULL DEFAULT '男', college VARCHAR(50) NOT NULL, enroll_year VARCHAR(9) NOT NULL, PRIMARY KEY (student_id) ) ENGINE=InnoDB; CREATE TABLE teacher ( teacher_id CHAR(6) NOT NULL, teacher_name VARCHAR(20) NOT NULL, title VARCHAR(20), college VARCHAR(50) NOT NULL, PRIMARY KEY (teacher_id) ) ENGINE=InnoDB; CREATE TABLE course ( course_id VARCHAR(10) NOT NULL, course_name VARCHAR(50) NOT NULL, credit DECIMAL(3,1) NOT NULL, hours INT NOT NULL, PRIMARY KEY (course_id) ) ENGINE=InnoDB; CREATE TABLE course_offer ( offer_id INT NOT NULL AUTO_INCREMENT, course_id VARCHAR(10) NOT NULL, teacher_id CHAR(6) NOT NULL, semester VARCHAR(20) NOT NULL, class_time VARCHAR(50), room VARCHAR(30), capacity INT UNSIGNED NOT NULL, remain INT UNSIGNED NOT NULL, PRIMARY KEY (offer_id), KEY idx_offer_course_sem (course_id, semester), KEY idx_offer_teacher (teacher_id), CONSTRAINT fk_offer_course FOREIGN KEY (course_id) REFERENCES course (course_id) ON DELETE RESTRICT, CONSTRAINT fk_offer_teacher FOREIGN KEY (teacher_id) REFERENCES teacher (teacher_id) ON DELETE RESTRICT ) ENGINE=InnoDB; CREATE TABLE enrollment ( student_id CHAR(10) NOT NULL, offer_id INT NOT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, grade DECIMAL(5,1) DEFAULT NULL, PRIMARY KEY (student_id, offer_id), KEY idx_enroll_offer (offer_id), CONSTRAINT fk_enroll_student FOREIGN KEY (student_id) REFERENCES student (student_id) ON DELETE RESTRICT, CONSTRAINT fk_enroll_offer FOREIGN KEY (offer_id) REFERENCES course_offer (offer_id) ON DELETE CASCADE ) ENGINE=InnoDB;

字符集用utf8mb4而不是utf8,MySQL里的utf8最多存3字节,中文没问题,但学生姓名里出现生僻字就可能入库失败,utf8mb4才是完整Unicode。学分和成绩用DECIMAL,不要用FLOAT,浮点精度问题会导致87.5存成87.49999,成绩单打印出来很难看。外键动作按上一小节的原则,RESTRICT挡住误删课程和老师,CASCADE让删除教学班时自动清理选课记录。

注意course_offer表里的remain是统计冗余字段。没有它,选课前要COUNT(*)一遍选课记录,在并发下还要加锁才能保证不超卖。保留remain并配合条件更新,是适合课设规模的方案,这个选择会在第四章展开。

3. 查课、选课、退课、成绩录入:四条核心SQL的可复现写法

3.1 选课:把“先查后写”改成“条件更新”,余量才不会超卖

选课的业务逻辑看起来简单:查余量,余量大于0则插入选课记录,再把余量减一。问题在于“查”和“减”是两条语句,中间隔着一个应用层的判断,并发场景下两个请求都能查到余量1,然后各自扣减,最终库存变成-1。最可靠的做法是让数据库把“检查余量”和“扣减余量”合并成一条原子语句。

DELIMITER // CREATE PROCEDURE sp_enroll( IN p_student_id CHAR(10), IN p_offer_id INT ) BEGIN DECLARE v_rows INT DEFAULT 0; START TRANSACTION; UPDATE course_offer SET remain = remain - 1 WHERE offer_id = p_offer_id AND remain > 0; SELECT ROW_COUNT() INTO v_rows; IF v_rows = 0 THEN ROLLBACK; ELSE INSERT INTO enrollment(student_id, offer_id, enroll_time, grade) VALUES (p_student_id, p_offer_id, NOW(), NULL); COMMIT; END IF; END// DELIMITER ;

调用示例:CALL sp_enroll('2023001001', 2);

选课的扣余量和插入选课记录必须放在同一个事务里,否则会出现余量扣了但记录没插入的情况。ROW_COUNT()返回上一条UPDATE影响的行数,等于1说明扣减成功且这次选课有效,等于0说明名额已满。这个函数必须在UPDATE之后立刻使用,中间不能夹着其他查询。这里传入的p_offer_id是教学班ID,不是课程ID,因为学生选的是“这个学期的这个老师的这个班”。

如果同一个学生重复选同一个班,INSERT会撞上联合主键(student_id, offer_id)并抛出1062错误。应用层要把1062当成“已选过”提示给用户,并且整个事务要回滚,否则会出现UPDATE扣了余量但选课记录没写入的情况。不要用“先SELECT判断是否已选,没有就插入”的方式去避免1062,并发下这个判断同样不可靠,联合主键兜底加异常回滚才是稳妥路径。

3.2 退课与改录成绩:事务边界和禁用条件

退课和选课逻辑对称,但有一些细节。已录入成绩的课程不能退,否则成绩单上凭空少一条记录,成绩数据完整性被破坏。删除选课记录和回补余量同样要在同一事务里完成。

DELIMITER // CREATE PROCEDURE sp_drop_course( IN p_student_id CHAR(10), IN p_offer_id INT ) BEGIN DECLARE v_rows INT DEFAULT 0; START TRANSACTION; DELETE FROM enrollment WHERE student_id = p_student_id AND offer_id = p_offer_id AND grade IS NULL; SELECT ROW_COUNT() INTO v_rows; IF v_rows = 0 THEN ROLLBACK; ELSE UPDATE course_offer SET remain = remain + 1 WHERE offer_id = p_offer_id; COMMIT; END IF; END// DELIMITER ;

这里必须检查DELETE影响的行数。如果学生同时开了两个窗口把同一门课退两次,第一次删除成功,第二次DELETE影响0行,但UPDATE remain=remain+1依然会执行,余量被多加一个,库存就错了。处理方式是v_rows=0时直接ROLLBACK,不执行回补。grade IS NULL条件保证已录成绩的退课请求不会生效。

成绩录入也要限制权限,不然任何教师都能给任何班录成绩。常见写法是通过JOIN把任课教师条件带进UPDATE:

UPDATE enrollment e JOIN course_offer co ON e.offer_id = co.offer_id SET e.grade = 88.5 WHERE e.student_id = '2023001001' AND e.offer_id = 2 AND co.teacher_id = 'T10001';

影响行数为0时,可以统一提示“找不到该学生在该班的选课记录,或你不是任课教师”。这个写法避免先用SELECT查出记录再在应用层判断归属,把权限判断也放进了一条UPDATE里。

3.3 查课表和成绩单:多表连接的JOIN顺序与结果排序

查询本学期可选课程是系统最高频的查询之一,展示给学生的信息包括课程名、学分、教师、时间、教室、余量。

SELECT c.course_id, c.course_name, c.credit, t.teacher_name, co.class_time, co.room, co.remain FROM course_offer co JOIN course c ON co.course_id = c.course_id JOIN teacher t ON co.teacher_id = t.teacher_id WHERE co.semester = '2024-2025-1' AND co.remain > 0 ORDER BY c.course_id, co.class_time;

WHERE里先用semester等值条件过滤,命中course_offer表上的(course_id, semester)联合索引前缀,再JOIN课程表和教师表取展示字段。ORDER BY用course_id,让同一门课的不同班级在界面里排在一起,后面跟class_time是为了同班次按时间稳定排序。

成绩单查询从选课表出发,先用student_id过滤出某个学生的少数记录,再往外JOIN其他表,中间结果集最小。

SELECT c.course_name, c.credit, co.semester, t.teacher_name, e.grade FROM enrollment e JOIN course_offer co ON e.offer_id = co.offer_id JOIN course c ON co.course_id = c.course_id JOIN teacher t ON co.teacher_id = t.teacher_id WHERE e.student_id = '2023001001' ORDER BY co.semester DESC, c.course_id;

JOIN列上都建了索引,执行计划不会出现全表扫描。ORDER BY用学期倒序,让最新学期的成绩出现在最上面。报告里讲“驱动表先过滤小结果集”时,用这个SQL作例子比贴概念好使。别在WHERE条件里对semester做函数处理,比如WHERE YEAR(semester)=2024,一旦写了函数,索引就失效了,这个坑在后面还会再提。

4. 并发选课把余量扣成负数:隔离级别、锁与四条排查记录

4.1 两个事务同时读到“还有1个名额”的翻车现场

用两个MySQL窗口手工模拟并发,比写一堆压测脚本更能说明问题。窗口A执行:

USE course_select; START TRANSACTION; SELECT remain FROM course_offer WHERE offer_id = 2; -- 结果:1 -- 停在这里,不要提交

窗口B执行同样的语句,结果也是1。两个事务各自都认为“还有名额”,然后都做INSERT和UPDATE,最终remain会被扣两次变成-1,而实际只有一个学生选上。这不是SQL语法问题,是业务逻辑没有把“检查余量”和“扣减余量”做成原子操作。

MySQL默认隔离级别REPEATABLE READ也挡不住这个情况,因为快照读只是让事务内多次SELECT结果保持一致,并不会阻止两个事务基于各自的快照同时通过“余量>0”的判断。这就是为什么课设里不做并发控制,答辩时容易被问住的点。

4.2 READ COMMITTED与REPEATABLE READ:隔离级别解决不了超卖

典型的并发问题有脏读、不可重复读、幻读三种。脏读是读到了未提交的数据,靠READ COMMITTED就能解决;不可重复读是同一事务中两次读到不同值,靠REPEATABLE READ解决;幻读是事务中两次查询的记录集合不同,需要SERIALIZABLE或间隙锁解决。很多同学把隔离级别当成超卖的救命稻草,但超卖的本质是“先读后写”存在时间窗口,隔离级别限制的是读的可见性,并不限制“读到的值在判断之后被别人修改”。

在REPEATABLE READ下,两个事务快照里remain都是1,各自判断通过后并行写入,这就超卖了。真正让数据库串行化这个操作的是行锁和条件更新:UPDATE命中一行时会加排他锁,第二个事务的同一条UPDATE会阻塞,直到第一个事务提交后拿到最新余量继续执行。如果余量已经为0,第二个事务的AND remain>0条件不成立,影响行数为0,事务回滚。

答辩被问到“你用的什么隔离级别”,回答默认REPEATABLE READ,然后补充“关键路径用条件更新+事务串行化扣减”,比只会背隔离级别定义深刻得多。

4.3 条件更新是乐观控制,SELECT ... FOR UPDATE是悲观控制

另一种教科书式的写法是悲观锁:

START TRANSACTION; SELECT remain FROM course_offer WHERE offer_id = 2 FOR UPDATE; -- 应用层判断 remain > 0 UPDATE course_offer SET remain = remain - 1 WHERE offer_id = 2; INSERT INTO enrollment(student_id, offer_id, enroll_time, grade) VALUES ('2023001001', 2, NOW(), NULL); COMMIT;

FOR UPDATE给命中的行加排他锁,其他事务的读写都要等锁释放。这个写法的前提是SELECT必须带主键或索引条件,否则InnoDB会锁整张表,压测时一半请求都会锁等待超时。而且锁要一直持有到事务结束,应用层判断期间锁不释放,吞吐量比条件更新方案低一点。

条件更新本质是乐观控制:不先拿锁,而是用UPDATE影响行数来判断是否成功。它不需要应用层做额外判断,失败直接回滚,代码更短。课设规模下两种方案都能跑通,如果老师明确要求“演示锁”,就用FOR UPDATE版本;如果不要求,条件更新更稳妥。报告里可以写“本系统采用条件更新,避免悲观锁在应用层判断期间长时间持有锁”,这话答辩时站得住。

4.4 四条血泪排查记录:现象、原因、解决

先说结论:课设里并发相关的坑大多是同一个根源——把“读取-判断-写入”拆成了三条应用层语句,中间没有事务保护。

第一条。现象:用两个浏览器同时选最后一个名额,数据库里remain变成-1。原因:应用代码先SELECT remain,再判断是否大于0,再INSERT,最后UPDATE,四个动作没有放进同一个事务。两个请求都通过“remain>0”的判断。解决:把选课逻辑改成存储过程,条件更新加事务。压测前造一条余量正好为1的教学班记录。

第二条。现象:存储过程执行后余量少了1,但enrollment表里没有新增记录。原因:UPDATE成功之后INSERT因为重复键1062报错,应用层没有捕获异常也没有回滚,事务就停在那里,UPDATE的提交和INSERT的失败各走各的。解决:INSERT放进同一事务,捕获1062后统一ROLLBACK,再在应用层做“是否已选过”的预检查来降低撞键概率。

第三条。现象:压测一开始就大量锁等待超时。原因:FOR UPDATE语句没有走索引,把整张course_offer锁了,或者事务里在查完余量后又做了耗时操作才提交,锁持有时间太长。解决:先EXPLAIN看执行计划,确认type不是ALL,再检查事务里有没有把网络请求或文件读写塞在查锁和提交之间。

第四条。现象:加了触发器维护余量后,选课反而报错,统计表和业务表数据不一致。原因:业务UPDATE扣了一次余量,触发器里又扣了一次,两条维护路径同时存在导致重复扣减。解决:余量维护只保留一条路径,要么用业务UPDATE,要么用触发器,选一个,别两个都用。这条后面还会展开。

5. 视图、存储过程与测试数据:报告能加分又能被复现的三个模块

5.1 用视图固定两个高频查询:成绩单与选课统计

视图的价值是把复杂的多表JOIN封装成一个逻辑表,应用层查询只面对一张视图,SQL短,维护时也只需要改视图定义。

CREATE VIEW v_grade_report AS SELECT e.student_id, s.student_name, c.course_id, c.course_name, c.credit, co.semester, t.teacher_name, e.grade FROM enrollment e JOIN student s ON e.student_id = s.student_id JOIN course_offer co ON e.offer_id = co.offer_id JOIN course c ON co.course_id = c.course_id JOIN teacher t ON co.teacher_id = t.teacher_id;

查询成绩单时直接:

SELECT * FROM v_grade_report WHERE student_id = '2023001001' ORDER BY semester DESC;

另一个视图做开课统计,给教务老师看每个班的容量、余量、已选人数:

CREATE VIEW v_offer_stats AS SELECT co.offer_id, c.course_name, t.teacher_name, co.semester, co.capacity, co.remain, co.capacity - co.remain AS enrolled_count FROM course_offer co JOIN course c ON co.course_id = c.course_id JOIN teacher t ON co.teacher_id = t.teacher_id;

注意视图里不要写死某个学期,比如WHERE semester='2024-2025-1',否则换学期要改视图定义。视图定义里只保留结构,具体学年学期由外层查询条件传入。《数据库系统概念》里把视图归入数据抽象的范畴,报告里写一句“通过视图对成绩单查询做数据抽象,应用层不直接面对底层五张表的JOIN”,能体现你对数据库系统原理的理解。

5.2 用存储过程批量生成测试数据,别手动一条条INSERT

手工INSERT几十个学生和几百条选课记录太慢,而且容易录错外键。存储过程批量造数的核心思路是:用循环拼接学号,用取模运算分配班级,用条件表达式生成不同属性。

DELIMITER // CREATE PROCEDURE sp_gen_students(IN p_num INT) BEGIN DECLARE i INT DEFAULT 1; DECLARE v_id CHAR(10); WHILE i <= p_num DO SET v_id = CONCAT('2023', LPAD(i, 6, '0')); INSERT INTO student(student_id, student_name, gender, college, enroll_year) VALUES (v_id, CONCAT('学生', i), IF(i % 2 = 0, '男', '女'), '信息工程学院', '2023'); SET i = i + 1; END WHILE; END// DELIMITER ;

调用一次生成30个学生:CALL sp_gen_students(30);

LPAD(i,6,'0')把数字1变成000001,再和'2023'拼接成10位定长学号。IF(i%2=0,'男','女')让性别分布均匀。CONCAT('学生',i)的命名方便后续识别测试数据。课程、教师、教学班可以照同样思路生成,注意外键顺序不能乱:先造教师,再造教学班,最后造选课记录。

批量选课也可以直接用存储过程:

DELIMITER // CREATE PROCEDURE sp_gen_enroll(IN p_student_count INT, IN p_offer_count INT) BEGIN DECLARE i INT DEFAULT 1; WHILE i <= p_student_count DO CALL sp_enroll(CONCAT('2023', LPAD(i, 6, '0')), 1 + (i MOD p_offer_count)); SET i = i + 1; END WHILE; END// DELIMITER ;

这里的MOD把学生轮流分到不同教学班。造数前先清空enrollment表,避免撞联合主键积累一堆1062错误。测试数据不求大,30个学生、10门课、3个学期、每个学期每门课一个班就足够演示所有功能。

5.3 测试用例表、备份恢复命令与报告撰写顺序

课程设计的测试部分别写“系统运行正常”这种空话,用一张功能测试表把每个操作的输入输出列清楚,答辩时老师扫一眼就心里有数。

模块测试输入预期结果
选课学生S001选班1,余量大于0选课表新增记录,余量减1
选课学生S001重复选班1提示已选过,余量不变
退课学生S001退掉未录成绩的班1选课记录删除,余量回补
退课学生S001退掉已录成绩的班2拒绝退课,数据不变
并发选课两个会话同时抢最后一个名额一人成功,一人提示名额不足

备份恢复是报告里必有的模块,命令别写成图形界面的鼠标操作。命令行给两个命令就够了:

mysqldump --single-transaction -u root -p course_select > course_select_backup.sql mysql -u root -p course_select < course_select_backup.sql

--single-transaction参数在InnoDB下可以在不锁表的情况下做一致性备份,报告里写这个参数比裸mysqldump显得懂行。恢复前确认数据库存在,不存在先CREATE DATABASE。恢复后检查关键表行数:

SELECT COUNT(*) FROM student; SELECT COUNT(*) FROM course_offer; SELECT COUNT(*) FROM enrollment;

行数对得上,备份才算真验证过,不是只跑了命令看一眼输出就结束。报告撰写顺序建议按标准课设模板走:需求分析里的用例场景放前面,概念设计给E-R图和关系模式,逻辑设计写范式检查结果,物理设计给表结构、索引和存储引擎选择,实现与测试给存储过程、视图和并发测试记录,这套顺序下来每章都有东西写,不会出现“系统概述”凑字数的情况。

6. 答辩追问:触发器统计、索引失效与并发自证实验

6.1 余量字段到底该不该用触发器维护

老师看到remain这个统计冗余字段,大概率会问为什么不用触发器自动扣减。常见的触发器方案长这样:

CREATE TRIGGER trg_enroll_ai AFTER INSERT ON enrollment FOR EACH ROW UPDATE course_offer SET remain = remain - 1 WHERE offer_id = NEW.offer_id AND remain > 0;

这个写法的坑很具体:如果业务代码里已经有一条UPDATE course_offer SET remain = remain - 1,触发器再扣一次,余量就被重复扣减了。而且AFTER触发器是在INSERT成功后执行,如果余量已经是0,触发器里的UPDATE影响0行但不会报错,INSERT照样成功,超卖问题依然存在。批量导入选课记录时还会逐条触发,1000条记录就执行1000次UPDATE。更可靠的方案是用BEFORE INSERT触发器去检查余量并拒绝插入,但BEFORE触发器只能定义在enrollment表上,要读取course_offer的余量需要额外的SELECT,锁的控制不如条件更新直接。

课设里更稳妥的说法是:余量维护放在选课存储过程的事务里,用条件更新保证检查与扣减的原子性,触发器只用来写日志表,不碰核心库存字段。这个解释答出来,老师通常不再追问。

6.2 EXPLAIN自证:一个导致索引失效的写法

索引失效在答辩现场很好演示。给semester列建了索引,跑下面这条查询:

EXPLAIN SELECT * FROM course_offer WHERE semester LIKE '%2024%';

EXPLAIN结果里type列是ALL,说明走了全表扫描。因为LIKE以%开头,索引树里无法定位前缀,MySQL只能放弃索引。把查询改成前缀匹配:

EXPLAIN SELECT * FROM course_offer WHERE semester LIKE '2024-%';

type列会变成range,说明用上了索引范围扫描。这个对比实验的直接应用场景是课表查询:应用层从下拉框拿到完整学期字符串,查询条件写等值匹配就好,不需要模糊匹配。如果将来要做模糊搜索,绝不能把%直接拼在传入的参数前头。把EXPLAIN前后的截图放进报告的物理设计部分,比大段文字解释有效得多。

6.3 两个终端跑出阻塞截图,比口头解释锁更有效

最后一个技巧是并发自证,不需要JMeter,两个MySQL命令行窗口就能做。窗口A执行:

USE course_select; START TRANSACTION; UPDATE course_offer SET remain = remain - 1 WHERE offer_id = 2 AND remain > 0;

窗口B执行同一条UPDATE,然后就会卡住不动,这是InnoDB行锁在起作用。回窗口A执行COMMIT,窗口B立刻返回“0 rows affected”,说明第二个事务没有拿到可扣减的余量。把这两张截图放进报告的并发测试栏,比写“我们使用了行锁”更有说服力。

这个实验要注意节奏:窗口A不要执行完就关掉,连接断开会导致事务自动回滚;窗口B也不要提前Ctrl+C,那是主动终止等待,看不看到阻塞效果。实验完两边都回滚或提交,把库里数据恢复到初始状态。我每次交课设前都会把系统里三层外三层折腾一遍:造数据造到接近容量,开两个终端抢同一个班,EXPLAIN看关键查询,最后删库再用备份恢复一次。这些步骤不需要多新的工具,但能帮你提前把并发问题从玄学变成确定,至少不会在答辩现场第一次翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询