简介:这份资源是西北工业大学软件学院数据库课程的实验五配套资料,面向正在学习数据库系统、需要完成E-Commerce项目概念设计的本科生与自学者。实验要求依据项目描述文档构建完整的电子商务数据库ER模式,资源包内提供了可直接参考的答案与说明,帮助读者理解实体、联系与属性的建模思路,解决概念设计阶段无从下手的难题。压缩包共20个文件,约282KB,以14个gif操作截图、2个txt说明、2个doc实验文档为主,另含1个cdm概念模型文件与1个htm页面,截图覆盖注册、登录、购物车、结算、下单、订单查询等完整业务流程,doc与txt则给出实验要求与结果说明,cdm文件便于用PowerDesigner直接打开查看ER结构。目前已有911人学习下载,适合需要对照范例梳理ER图绘制方法、验证自身设计是否完整的同学参考使用。
1. 数据库实验五到底在练什么:从一次课程设计翻车说起
很多同学做到数据库实验五的时候,第一反应是“前面增删改查都写完了,还能练什么”。我带过几届课程设计,最常见的翻车场景是这样的:表建好了,数据也插进去了,单表查询跑得飞快,可一旦把三张表 JOIN 起来做统计,结果要么重复计数,要么慢到超时,最后只能靠导出到 Excel 手动算。数据库实验五通常就卡在这个位置——它练的不是语法,而是把业务问题翻译成 SQL、再翻译成执行计划的能力。具体来说,它一般覆盖多表连接、子查询、聚合分组、视图与索引这几块,目标是让你能独立完成一个“有业务含义”的查询,而不是背 SELECT 语法。适合谁?适合已经会写单表 CRUD、但一遇到多表统计就发怵的在校生,也适合工作后发现自己 SQL 全靠 ORM 生成、想补回底层功底的开发者。这一章先把实验五的边界划清楚,后面几章再一步步落到可复现的操作上。
2. 实验五的四个核心考点与选型逻辑
2.1 多表连接:为什么 INNER JOIN 不是万能答案
实验五里最常见的业务场景是“查每个学生的选课数和平均分”。新手第一反应往往是先查学生表,再循环查选课表,这就是典型的 N+1 问题。正确做法是用 JOIN 一次拉出来。但 JOIN 也不是随便选:INNER JOIN 会丢掉没选课的学生,LEFT JOIN 才能保留。很多同学在这里翻车,是因为没想清楚“业务上要不要保留空记录”。
-- 统计每个学生的选课门数和平均分,保留没选课的学生 SELECT s.student_id, s.student_name, COUNT(sc.course_id) AS course_count, -- COUNT 列名而非 *,避免空记录被算成 1 ROUND(AVG(sc.score), 2) AS avg_score -- 保留两位小数,避免长尾浮点 FROM student s LEFT JOIN score sc ON s.student_id = sc.student_id GROUP BY s.student_id, s.student_name ORDER BY avg_score DESC;这段代码的关键在 COUNT(sc.course_id) 而不是 COUNT()。用 COUNT() 时,没选课的学生因为 LEFT JOIN 产生一行 NULL,会被算成 1 门课,这是实验报告里最常见的错误之一。GROUP BY 必须带上 student_name,否则在严格模式下会报错。参数上,ROUND 的第二位按业务定,成绩一般两位够用。
2.2 子查询与 EXISTS:什么时候该换写法
实验五常有一道“查没选过某门课的学生”。用 NOT IN 写起来最直观,但子查询结果里一旦有 NULL,整个条件就恒为假,这是血泪经验。更稳的写法是 NOT EXISTS 或 LEFT JOIN ... IS NULL。
-- 查没选过课程号为 C001 的学生,用 NOT EXISTS 避免 NULL 陷阱 SELECT s.student_id, s.student_name FROM student s WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.student_id = s.student_id AND sc.course_id = 'C001' );NOT EXISTS 是相关子查询,对外层每一行执行一次,语义清晰且不受 NULL 影响。如果数据量大,可以改写成 LEFT JOIN 加 IS NULL,让优化器有更多选择。这里 SELECT 1 是习惯写法,不关心返回列,只关心有没有行。
2.3 聚合与 HAVING:WHERE 和 HAVING 的分工
很多同学把过滤条件全塞进 WHERE,结果聚合条件报错。记住一条:WHERE 在分组前过滤行,HAVING 在分组后过滤组。实验五里“查平均分大于 80 的学生”必须用 HAVING。
-- 查平均分大于 80 且选课不少于 2 门的学生 SELECT s.student_id, s.student_name, COUNT(*) AS course_count, AVG(sc.score) AS avg_score FROM student s JOIN score sc ON s.student_id = sc.student_id GROUP BY s.student_id, s.student_name HAVING AVG(sc.score) > 80 AND COUNT(*) >= 2;HAVING 里可以直接用聚合函数,也可以引用 SELECT 里的别名(部分数据库支持)。如果发现 HAVING 写法和 WHERE 混用导致结果不对,先检查过滤的是行还是组。
2.4 视图与索引:实验五里最容易被忽略的落地环节
实验五通常要求“把复杂查询封装成视图”。视图不是性能银弹,它只是存了 SQL 定义,查询时照样展开。但视图能简化上层调用,也方便权限控制。
-- 创建学生成绩汇总视图 CREATE VIEW v_student_score_summary AS SELECT s.student_id, s.student_name, COUNT(sc.course_id) AS course_count, ROUND(AVG(sc.score), 2) AS avg_score FROM student s LEFT JOIN score sc ON s.student_id = sc.student_id GROUP BY s.student_id, s.student_name; -- 查询视图 SELECT * FROM v_student_score_summary WHERE avg_score > 80;索引方面,实验五的数据量通常不大,但养成习惯:在 JOIN 的关联列和 WHERE 的过滤列上建索引。比如 score 表的 student_id 和 course_id 建联合索引,能明显改善多表连接。
-- 在成绩表的关联列上建索引 CREATE INDEX idx_score_student_course ON score(student_id, course_id);注意索引不是越多越好,写入频繁的表要控制数量。实验环境里可以先看执行计划再决定。
3. 从建表到出结果:实验五的完整复现步骤
3.1 建表与初始化数据
实验五一般会给定表结构,但很多同学直接复制粘贴,没注意字段类型和约束。下面是一套常见的三表结构,学生、课程、成绩。
-- 学生表 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, student_name VARCHAR(50) NOT NULL, class_name VARCHAR(50) ); -- 课程表 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) ); -- 成绩表,联合主键防止同一学生同一课程重复录入 CREATE TABLE score ( student_id VARCHAR(20), course_id VARCHAR(20), score DECIMAL(5,2), PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) );成绩表用联合主键是实验五的常见要求,能避免重复选课记录。外键约束在实验环境里建议打开,能帮你提前发现数据不一致。
3.2 插入测试数据并验证
数据不用多,但要覆盖边界:有学生没选课、有课程没人选、有成绩为 NULL。
INSERT INTO student VALUES ('S001', '张三', '软工1班'), ('S002', '李四', '软工1班'), ('S003', '王五', '软工2班'); INSERT INTO course VALUES ('C001', '数据库原理', 3.0), ('C002', '操作系统', 4.0), ('C003', '计算机网络', 3.0); INSERT INTO score VALUES ('S001', 'C001', 85.5), ('S001', 'C002', 78.0), ('S002', 'C001', 92.0), ('S002', 'C003', NULL), -- 缺考,成绩为 NULL ('S003', 'C002', 88.0);插入后先跑一遍全表查询,确认数据进去了。NULL 那条是故意留的,后面验证聚合函数时用得上。
3.3 逐条跑通实验五的典型查询
按考点顺序来:先多表连接,再子查询,再聚合,最后视图。
-- 1. 查每个学生的选课数和平均分 SELECT s.student_id, s.student_name, COUNT(sc.course_id) AS cnt, ROUND(AVG(sc.score), 2) AS avg_score FROM student s LEFT JOIN score sc ON s.student_id = sc.student_id GROUP BY s.student_id, s.student_name; -- 2. 查没选过 C001 的学生 SELECT s.student_id, s.student_name FROM student s WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.student_id = s.student_id AND sc.course_id = 'C001' ); -- 3. 查平均分大于 80 的学生 SELECT s.student_id, s.student_name, AVG(sc.score) AS avg_score FROM student s JOIN score sc ON s.student_id = sc.student_id GROUP BY s.student_id, s.student_name HAVING AVG(sc.score) > 80;每跑一条,对照预期结果检查。比如第 1 条里 S003 没选 C001,但选了 C002,应该出现且 cnt 为 1。S002 的 C003 成绩是 NULL,AVG 会自动忽略 NULL,这是正确行为,但 COUNT 要区分 COUNT(*) 和 COUNT(列)。
3.4 用执行计划验证索引是否生效
实验五不要求调优,但学会看执行计划是加分项。不同数据库命令不同,常见的是 EXPLAIN。
-- 查看查询是否走了索引 EXPLAIN SELECT s.student_id, s.student_name, AVG(sc.score) FROM student s JOIN score sc ON s.student_id = sc.student_id GROUP BY s.student_id, s.student_name;重点看 type 列和 key 列。如果 key 是 NULL,说明没走索引,数据量大时会全表扫描。实验数据少可能看不出来,但习惯要养好。
4. 实验五避坑指南:五条血泪经验
4.1 COUNT(*) 和 COUNT(列) 混用导致统计翻倍
现象:统计选课门数时,没选课的学生显示 1 门。原因:LEFT JOIN 产生 NULL 行,COUNT() 把 NULL 行也算进去。解决:统计关联表字段用 COUNT(关联列),统计主表行数才用 COUNT()。
4.2 NOT IN 遇到 NULL 结果全空
现象:查没选某课的学生,结果一条都没有。原因:子查询返回了 NULL,NOT IN 对 NULL 的比较结果是 UNKNOWN,整个条件不成立。解决:改用 NOT EXISTS 或 LEFT JOIN ... IS NULL。
4.3 GROUP BY 漏字段导致严格模式报错
现象:查询在本地能跑,换到实验平台报错。原因:SELECT 里的非聚合列没全放进 GROUP BY,严格模式不允许。解决:把 SELECT 里所有非聚合列都写进 GROUP BY,或者确认数据库的 sql_mode 设置。
4.4 视图里用 ORDER BY 被忽略
现象:视图定义里写了 ORDER BY,查询时顺序不对。原因:视图本质是子查询,外层查询会重新决定顺序,ORDER BY 在视图里通常无效。解决:排序放在外层 SELECT,视图只负责逻辑封装。
4.5 外键约束导致插入顺序错误
现象:先插成绩表报外键错误。原因:成绩表引用了学生表和课程表,被引用的数据必须先存在。解决:按学生、课程、成绩的顺序插入;删除时反过来,先删成绩再删主表。
5. 把实验五变成可复用能力:三个进阶技巧
5.1 用窗口函数替代自连接做排名
实验五如果要求“查每门课成绩排名”,新手容易用自连接,写起来复杂还慢。窗口函数一行搞定。
-- 每门课内按成绩排名 SELECT course_id, student_id, score, RANK() OVER (PARTITION BY course_id ORDER BY score DESC) AS rank_in_course FROM score WHERE score IS NOT NULL;PARTITION BY 按课程分组,ORDER BY 决定排名顺序。RANK 遇到并列会跳号,DENSE_RANK 不跳,按业务选。这个技巧在实验报告里写出来,能明显拉开差距。
5.2 用 CTE 把复杂查询拆成可读的步骤
实验五的查询一复杂,嵌套子查询就变成“黑匣子”。CTE(WITH 子句)能把中间结果命名,可读性提升明显。
-- 用 CTE 分步计算:先算每人平均分,再筛出大于 80 的 WITH avg_score_cte AS ( SELECT student_id, AVG(score) AS avg_score FROM score WHERE score IS NOT NULL GROUP BY student_id ) SELECT s.student_id, s.student_name, a.avg_score FROM student s JOIN avg_score_cte a ON s.student_id = a.student_id WHERE a.avg_score > 80;CTE 的好处是每一步都能单独跑、单独验证,排错时不用一层层剥嵌套。注意 CTE 默认只在当前语句有效,不能跨语句复用。
5.3 用事务保证批量操作的原子性
实验五如果涉及批量插入或更新,一定要用事务。否则中途报错会留下半截数据,后悔药都没得吃。
-- 批量插入成绩,要么全成功,要么全回滚 BEGIN; INSERT INTO score VALUES ('S001', 'C003', 90.0); INSERT INTO score VALUES ('S002', 'C002', 85.0); -- 如果这里报错,执行 ROLLBACK 而不是 COMMIT COMMIT;BEGIN 开启事务,COMMIT 提交,ROLLBACK 回滚。实验环境里可以先故意制造一个错误,验证回滚是否生效。这个习惯在工作里能救命。
我自己的习惯是:每写完一个复杂查询,先拿小数据集验证逻辑,再上全量数据跑性能。实验五的价值不在那几条 SQL,而在于让你养成“先想清楚业务语义,再动手写”的节奏。希望帮到你。
本文还有配套的精品资源,点击获取