ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

数据库课程设计实战:从建表到事务的完整落地指南

2026/9/25 22:34:18 拓冰建站 浏览量
数据库课程设计实战:从建表到事务的完整落地指南 简介这份资源是南京航空航天大学人工智能专业2024年《数据库原理》课程设计的完整项目包面向正在学习数据库课程、需要完成课程设计或希望系统练习数据库建模与开发的学生。内容围绕需求分析、概念与逻辑设计、物理实现、测试维护等环节展开涉及SQL语言、数据表结构设计、触发器与索引、事务管理及性能优化等核心知识点。压缩包共6个文件约1.92MB包含2个SQL脚本、1个Python程序、1份PDF实验报告、1个说明文档及依赖清单覆盖建库建表、数据操作与项目运行所需材料。目前已有211人学习下载适合作为课程设计参考模板帮助读者理解从建模到实现的完整流程并借鉴报告撰写与项目组织方式提升数据库设计与应用开发能力。1. 从一份课程设计压缩包说起数据库原理到底该怎么落地很多人第一次拿到_NUAA_DB2024_Project.zip这种课程设计包第一反应是解压、找main.py、双击运行然后被一堆报错劝退。我当年也一样觉得数据库原理就是背范式、画 ER 图、考试写 SQL真到动手才发现课程设计考的从来不是你会不会写SELECT而是你能不能把「需求 → 表结构 → 约束 → 事务 → 查询接口」这条链路完整跑通。这份包的核心其实就三样东西setup.sql负责建库建表run.sql负责灌数据和验证查询main.py负责把数据库接到一个能交互的程序上。它解决的是「课本知识落不了地」这个痛点适合正在做数据库课设的本科生也适合想补数据库实操的转行者。下面我按自己复现这类项目的顺序把每一步拆开讲清楚。2. 先看懂 setup.sql建库建表里藏着课程设计的评分点2.1 为什么表结构设计决定了后面 80% 的工作量数据库原理课设的评分表结构设计通常占很大比重因为它是后面所有查询、事务、索引的地基。我见过太多人上来就写main.py结果表建得乱七八糟后面改一个字段要动十几个查询。正确的顺序是先读需求文档或者从setup.sql反推需求把实体和关系理清楚再落成CREATE TABLE。课程设计里最常见的实体无非是学生、课程、选课、教师、成绩这几类。关键不是把表建出来而是把约束建对主键用什么、外键怎么连、哪些字段NOT NULL、哪些字段要UNIQUE、成绩这种数值字段要不要加CHECK。这些约束就是数据库原理里「完整性」那一章的实际体现老师看的就是你有没有把理论用上。一个容易被忽略的点是字符集和存储引擎。MySQL 里如果建表时不指定ENGINEInnoDB默认可能是 MyISAM而 MyISAM 不支持事务和外键后面你写事务回滚会发现根本不生效这就是典型的「玄学 bug」。所以建表语句里显式写上引擎和字符集是省后悔药的做法。2.2 一份可直接抄的建表脚本骨架下面是我一般会用的建表骨架以「学生选课成绩」为例字段和约束都按课程设计的常见要求来。你可以直接改字段名套到自己的题目上。-- setup.sql -- 先删后建保证脚本可重复执行避免表已存在报错 DROP DATABASE IF EXISTS nuaa_db; CREATE DATABASE nuaa_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE nuaa_db; -- 学生表学号做主键姓名和班级不允许为空 CREATE TABLE student ( sno VARCHAR(12) NOT NULL COMMENT 学号, sname VARCHAR(30) NOT NULL COMMENT 姓名, class_name VARCHAR(30) NOT NULL COMMENT 班级, gender CHAR(1) DEFAULT M COMMENT 性别, PRIMARY KEY (sno) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表课程号主键学分用 DECIMAL 避免浮点误差 CREATE TABLE course ( cno VARCHAR(10) NOT NULL COMMENT 课程号, cname VARCHAR(40) NOT NULL COMMENT 课程名, credit DECIMAL(3,1) NOT NULL DEFAULT 0.0 COMMENT 学分, teacher VARCHAR(30) DEFAULT NULL COMMENT 任课教师, PRIMARY KEY (cno) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课表复合主键防止同一学生重复选同一门课 CREATE TABLE sc ( sno VARCHAR(12) NOT NULL, cno VARCHAR(10) NOT NULL, score DECIMAL(5,1) DEFAULT NULL COMMENT 成绩允许为空表示未出分, PRIMARY KEY (sno, cno), CONSTRAINT fk_sc_student FOREIGN KEY (sno) REFERENCES student(sno) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_sc_course FOREIGN KEY (cno) REFERENCES course(cno) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT chk_score CHECK (score IS NULL OR (score 0 AND score 100)) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段脚本有几个参数值得说清楚。utf8mb4而不是utf8是因为 MySQL 的utf8其实是阉割版存不了某些生僻字和 emoji课程设计里如果学生姓名有生僻字就会翻车。DECIMAL(5,1)表示总共 5 位、小数点后 1 位成绩用它可以精确表示 95.5 这种值用FLOAT会出现94.99999的精度问题。ON DELETE CASCADE表示删学生时自动删掉他的选课记录避免留下孤儿数据但要注意这在实际生产里是危险操作课设里用没问题。提示脚本开头加DROP DATABASE IF EXISTS是让脚本可重复执行的关键。很多人第一次跑成功第二次跑就报「database exists」然后手动去删库非常浪费时间。2.3 索引不是越多越好什么时候该加课程设计里老师常问「你这里为什么加索引」。索引的本质是用空间换时间加在经常出现在WHERE、JOIN、ORDER BY里的列上。比如你经常按课程名查课那course.cname可以加索引经常按成绩排序sc.score可以加。但索引会拖慢插入和更新因为每次写数据都要维护索引树。我的经验是主键和外键 MySQL 会自动建索引不用手动加真正需要手动加的是那些高频查询的非键列。课设数据量小索引效果看不出来但你要能说清楚「为什么加」这才是原理课要考的。加索引用CREATE INDEX idx_cname ON course(cname);就行。3. run.sql 怎么灌数据从造测试数据到验证查询3.1 造数据要覆盖边界不能只造「正常值」run.sql通常承担两个任务插入测试数据、跑验证查询。很多人造数据只造几条正常记录结果查询一跑全对但一遇到空值、边界值就出问题。我一般会刻意造几类数据正常成绩、NULL 成绩未出分、0 分和 100 分边界、以及一个没选任何课的学生测外连接。-- run.sql USE nuaa_db; -- 学生包含一个没选课的学生 S004用来验证左连接 INSERT INTO student (sno, sname, class_name, gender) VALUES (S001, 张三, 计算机2201, M), (S002, 李四, 计算机2201, F), (S003, 王五, 计算机2202, M), (S004, 赵六, 计算机2202, F); -- 课程 INSERT INTO course (cno, cname, credit, teacher) VALUES (C001, 数据库原理, 3.0, 刘老师), (C002, 操作系统, 3.5, 陈老师), (C003, 计算机网络, 3.0, 孙老师); -- 选课包含 NULL 成绩和边界分数 INSERT INTO sc (sno, cno, score) VALUES (S001, C001, 92.5), (S001, C002, 88.0), (S002, C001, 100.0), (S002, C003, NULL), (S003, C001, 0.0);这里S002选C003但成绩是 NULL用来测AVG时 NULL 会不会被算进去答案是会被忽略AVG只对非 NULL 求平均。S003的 0 分用来测WHERE score 0会不会把它漏掉。S004没选课用来测左连接时他的选课字段是不是 NULL。这些边界就是课设答辩时老师最爱问的点。3.2 验证查询把课本上的连接、分组、子查询都跑一遍灌完数据要跑查询验证这一步是把数据库原理里的关系代数落到 SQL 上。我一般会准备几条覆盖不同知识点的查询跑通了说明表结构和数据都没问题。-- 1. 内连接查每个学生选的课和成绩 SELECT s.sname, c.cname, sc.score FROM student s JOIN sc ON s.sno sc.sno JOIN course c ON sc.cno c.cno; -- 2. 左连接查所有学生没选课的也要显示出来 SELECT s.sno, s.sname, c.cname, sc.score FROM student s LEFT JOIN sc ON s.sno sc.sno LEFT JOIN course c ON sc.cno c.cno; -- 3. 分组聚合每门课的平均分、最高分、选课人数 SELECT c.cno, c.cname, AVG(sc.score) AS avg_score, MAX(sc.score) AS max_score, COUNT(sc.sno) AS stu_count FROM course c LEFT JOIN sc ON c.cno sc.cno GROUP BY c.cno, c.cname; -- 4. 子查询查平均分高于全体平均分的学生 SELECT sno, AVG(score) AS stu_avg FROM sc WHERE score IS NOT NULL GROUP BY sno HAVING AVG(score) (SELECT AVG(score) FROM sc WHERE score IS NOT NULL);第 3 条查询里COUNT(sc.sno)而不是COUNT(*)是因为左连接下没选课的课程会有一行 NULLCOUNT(*)会把它算成 1COUNT(sc.sno)只数非 NULL结果才正确。这个细节很多人踩坑。第 4 条用HAVING而不是WHERE因为聚合结果的过滤必须用HAVINGWHERE在分组前执行用错会直接报错。注意GROUP BY后面要列出所有非聚合的 SELECT 列MySQL 在ONLY_FULL_GROUP_BY模式下会强制检查否则报错。这是 MySQL 5.7 之后默认开启的很多人从老教程抄代码会在这里翻车。4. main.py 接数据库把 SQL 变成能交互的程序4.1 选 pymysql 还是 ORM课设场景的取舍main.py的职责是把数据库接到程序上做一个能增删改查的界面或命令行。这里第一个决策是用原生驱动还是 ORM。常见做法是课设用pymysql直接写 SQL因为老师要看的就是你懂不懂 SQLORM 虽然省事但把 SQL 藏起来了答辩时说不清楚。pymysql的安装很简单pip install pymysql。连接时要注意几个参数host、port、user、password、database、charset。charset一定要写utf8mb4否则中文会乱码。还有一个坑是autocommit默认是False意味着你插入数据后不commit就不会生效程序退出连接一关数据就没了。# main.py import pymysql def get_conn(): 建立数据库连接charset 必须指定 utf8mb4 防止中文乱码 return pymysql.connect( host127.0.0.1, port3306, userroot, passwordyour_password, # 换成你自己的密码 databasenuaa_db, charsetutf8mb4, autocommitFalse # 手动控制事务方便演示回滚 ) def query_student_scores(sno): 查询指定学生的选课和成绩返回列表 conn get_conn() try: with conn.cursor() as cur: sql SELECT c.cname, sc.score FROM sc JOIN course c ON sc.cno c.cno WHERE sc.sno %s cur.execute(sql, (sno,)) # 参数化查询防止 SQL 注入 return cur.fetchall() finally: conn.close() if __name__ __main__: rows query_student_scores(S001) for cname, score in rows: print(f{cname}: {score})这里最关键的是cur.execute(sql, (sno,))这种参数化写法而不是用字符串拼接... WHERE sno sno 。字符串拼接会导致 SQL 注入比如输入S001 OR 11就能查出所有数据。参数化查询是数据库安全的基本功课设里老师很可能会问。4.2 事务演示转账场景怎么保证一致性数据库原理里事务的 ACID 是重点课设里最好有一个能演示事务的例子最经典的就是转账。下面这段代码演示了转账成功和失败回滚两种情况。def transfer(from_sno, to_sno, amount): 从 from_sno 转 amount 分给 to_sno演示事务的原子性 conn get_conn() try: with conn.cursor() as cur: # 扣款 cur.execute( UPDATE account SET balance balance - %s WHERE sno %s, (amount, from_sno) ) # 模拟异常如果余额不足就抛错触发回滚 cur.execute(SELECT balance FROM account WHERE sno %s, (from_sno,)) balance cur.fetchone()[0] if balance 0: raise ValueError(余额不足事务回滚) # 入账 cur.execute( UPDATE account SET balance balance %s WHERE sno %s, (amount, to_sno) ) conn.commit() # 两步都成功才提交 print(转账成功) except Exception as e: conn.rollback() # 任何一步失败全部撤销 print(f转账失败已回滚: {e}) finally: conn.close()commit和rollback是事务的两个出口全部成功才commit中途出错就rollback保证扣款和入账要么都发生要么都不发生。这就是原子性。如果把autocommit设成True每条 SQL 执行完自动提交rollback就失效了转账扣了款没入账的 bug 就出来了。所以演示事务时autocommit必须是False。4.3 用乐观锁和悲观锁处理并发选课热搜里提到乐观锁和悲观锁这在选课场景里很实用同一门课名额有限多个学生同时选怎么防止超选。悲观锁的思路是「先锁再操作」用SELECT ... FOR UPDATE把行锁住别人得等你提交才能读。def enroll_with_pessimistic_lock(sno, cno): 悲观锁选课先锁住课程行再判断名额 conn get_conn() try: with conn.cursor() as cur: # FOR UPDATE 锁住这一行其他事务要等 cur.execute( SELECT capacity, enrolled FROM course WHERE cno %s FOR UPDATE, (cno,) ) capacity, enrolled cur.fetchone() if enrolled capacity: conn.rollback() return 名额已满 cur.execute(UPDATE course SET enrolled enrolled 1 WHERE cno %s, (cno,)) cur.execute(INSERT INTO sc (sno, cno, score) VALUES (%s, %s, NULL), (sno, cno)) conn.commit() return 选课成功 except Exception as e: conn.rollback() return f失败: {e} finally: conn.close()乐观锁的思路相反它假设冲突很少不加锁而是在更新时检查版本号或条件。比如UPDATE course SET enrolled enrolled 1 WHERE cno %s AND enrolled capacity如果影响行数是 0说明被别人抢先了重试即可。悲观锁适合冲突多的场景乐观锁适合冲突少的场景这是选型时要能说清楚的。提示FOR UPDATE必须在事务里用且autocommitFalse否则锁立刻释放等于没锁。这是很多人写了FOR UPDATE却没效果的真正原因。5. 避坑与排查课设里最容易翻车的 5 个点5.1 中文乱码从建库到连接要全链路统一现象插入的中文姓名在程序里显示成???或乱码。原因字符集不统一可能建库用了latin1或者连接没指定utf8mb4。解决建库、建表、连接三处都写utf8mb4连接参数里加charsetutf8mb4缺一处都可能乱码。5.2 外键报错 1452插入顺序和数据不匹配现象插入sc表时报Cannot add or update a child row。原因外键约束要求sc.sno必须在student里存在sc.cno必须在course里存在。解决先插student和course再插sc检查插入的学号课程号有没有拼错。5.3 事务不生效autocommit 没关现象代码里写了rollback但数据还是被改了。原因连接时autocommitTrue每条语句自动提交rollback无从回滚。解决连接参数设autocommitFalse并在成功后显式commit。5.4 查询结果重复连接条件漏写导致笛卡尔积现象查学生选课结果每个学生出现好几遍。原因多表连接时漏了连接条件或者JOIN条件写错导致笛卡尔积。解决检查每个JOIN后面的ON条件确保连接字段对应正确。5.5 脚本重复执行报错没做幂等处理现象第二次跑setup.sql报「database exists」或「table exists」。原因脚本没有先删后建。解决开头加DROP DATABASE IF EXISTS或者用CREATE TABLE IF NOT EXISTS让脚本可以反复跑。6. 进阶技巧用 EXPLAIN 看懂你的查询到底走没走索引课设做完能跑只是及格能说清楚「为什么这么写」才是拿高分的关键。我一般会教人用EXPLAIN看查询执行计划这是把数据库原理从「会写」提升到「懂优化」的捷径。用法很简单在任意查询前加EXPLAIN就行。EXPLAIN SELECT s.sname, c.cname, sc.score FROM student s JOIN sc ON s.sno sc.sno JOIN course c ON sc.cno c.cno WHERE sc.score 90;输出里重点看几列type表示访问类型ALL是全表扫描最差ref、eq_ref、const越来越好key表示实际用了哪个索引如果是NULL说明没走索引rows是预估扫描行数越小越好。如果type是ALL且rows很大说明这个查询可能需要加索引。我踩过的一个坑是给sc.score加了索引但查询里写的是WHERE score 0 90对列做了运算索引直接失效type还是ALL。索引失效的常见原因还有对列用函数、隐式类型转换字符串列传数字、LIKE %xx前置通配符。这些在课设答辩时能说出来老师会觉得你真的理解了索引原理而不是背概念。另一个技巧是用SHOW PROFILE看每个阶段耗时虽然 MySQL 8.0 之后推荐用performance_schema但课设环境一般够用。我的习惯是任何一条自己觉得「可能慢」的查询先EXPLAIN一遍看type和key再决定要不要加索引。这个习惯从课设一直用到工作省了无数次性能排查的时间。希望帮到你。本文还有配套的精品资源点击获取