数据库原理实验:从建表约束到多表JOIN的工程化实践
简介:本资源是南华大学大数据技术专业《数据库原理》课程配套的完整实验报告,面向高校数据库初学者及备考学生,系统解决DBMS实操能力薄弱、SQL语法理解不深、表设计与关系建模不熟等核心问题。报告覆盖5大实验模块:从SQL Server Management Studio图形化创建“学生选课200905”数据库及三张主表(学生/课程/选修),到用T-SQL语句重建数据库与约束;从单表条件查询(如按系别、姓名、出生年份筛选)、聚合排序分组,到多表JOIN连接(含间接先行课、跨系选课分析);并涵盖数据完整性(主码/外码/默认值)、更新操作等关键实践。资源为1个5.38MB的Word文档(.doc格式),结构清晰、代码与结果截图兼备,含详细实验要求、SQL语句示例及规范注释。目前已有377人学习下载,可直接用于课程复习、实验复现或数据库基础能力强化训练。
1. 这份数据库原理实验报告,不是交差作业,而是你第一次亲手“搭出数据世界的地基”
很多同学拿到《数据库原理》课设,第一反应是翻模板、抄SQL、赶DDL——结果答辩时被问一句“为什么这里用LEFT JOIN而不是INNER JOIN”,当场卡壳。但南华大学这份实验报告的底层逻辑其实很清晰:它不考你会不会背语法,而考你能不能把“学生-课程-选修”这三张表,真正当成一个有血有肉的数据系统来设计、连接、验证。它用最朴素的SQL Server环境,逼你直面三个真实工程问题:主外键怎么配才不崩、多表JOIN时NULL值从哪来、嵌套子查询里括号少一个就全错。这份报告适合两类人:一是大数据技术专业刚学完关系代数、正卡在“知道概念但写不出完整语句”阶段的学生;二是需要快速交付一份结构完整、可演示、能答辩的课程设计的从业者。它不炫技,不堆高级特性,但每一条CREATE TABLE、每一个GROUP BY、每一处EXISTS子查询,都对应着数据库设计中最容易踩坑的实操节点。
2. 数据库与表结构设计:三层建模法与字段级约束落地
2.1 数据库创建:GUI操作与SQL语句的双重验证机制
实验要求同时使用SQL Server Management Studio(SSMS)图形界面和T-SQL脚本创建两个同构数据库:学生选课200905(中文名,用于GUI操作训练)和StuCou200905(英文名,用于SQL语句规范性训练)。这不是重复劳动,而是建立双重验证习惯——GUI帮你快速确认对象是否存在、属性是否可见;T-SQL则强制你写出显式约束,避免GUI默认值带来的隐式风险。
-- 创建数据库 StuCou200905(带明确文件路径与初始大小) CREATE DATABASE StuCou200905 ON PRIMARY ( NAME = 'StuCou200905_Data', FILENAME = 'D:\SQLData\StuCou200905.mdf', SIZE = 8MB, MAXSIZE = 50MB, FILEGROWTH = 10% ) LOG ON ( NAME = 'StuCou200905_Log', FILENAME = 'D:\SQLData\StuCou200905.ldf', SIZE = 3MB, MAXSIZE = 20MB, FILEGROWTH = 10% );提示:生产环境必须指定
FILENAME路径,否则SQL Server可能将文件写入系统盘,导致后续磁盘满报错;FILEGROWTH设为百分比而非固定MB,可避免小文件频繁自动扩展引发I/O抖动。
2.2 表定义:主码/外码/默认值/空值约束的协同设计
三张核心表的设计不是孤立字段罗列,而是按“实体→关系→属性”分层构建:
- 学生表
Stu200905:学号为主码(CHAR(10)),性别设默认值'男'(NOT NULL + DEFAULT),出生日期用DATE类型(非DATETIME,避免时间部分干扰年龄计算),备注允许NULL(TEXT类型已弃用,改用VARCHAR(500)); - 课程表
Cou200905:课程号为主码(CHAR(5)),先行课为外码(指向本表课程号,实现自引用),学分设默认值2且NOT NULL(学分必填); - 选修表
SC200905:无独立主码,采用复合主码(学号, 课程号),同时声明两个外码约束,形成学生与课程间的多对多桥梁。
-- 创建学生表(关键约束全部显式声明) CREATE TABLE Stu200905 ( 学号 CHAR(10) PRIMARY KEY, 姓名 VARCHAR(20) NOT NULL, 性别 CHAR(2) NOT NULL DEFAULT '男', 出生日期 DATE, 院系名称 VARCHAR(30), 备注 VARCHAR(500) ); -- 创建课程表(含自引用外码) CREATE TABLE Cou200905 ( 课程号 CHAR(5) PRIMARY KEY, 课程名 VARCHAR(50) NOT NULL, 先行课 CHAR(5), -- 可为空,因顶级课程无先行课 学分 TINYINT NOT NULL DEFAULT 2, CONSTRAINT FK_Cou_Prior FOREIGN KEY (先行课) REFERENCES Cou200905(课程号) ); -- 创建选修表(复合主码+双外码) CREATE TABLE SC200905 ( 学号 CHAR(10) NOT NULL, 课程号 CHAR(5) NOT NULL, 分数 TINYINT CHECK (分数 BETWEEN 0 AND 100), PRIMARY KEY (学号, 课程号), CONSTRAINT FK_SC_Stu FOREIGN KEY (学号) REFERENCES Stu200905(学号), CONSTRAINT FK_SC_Cou FOREIGN KEY (课程号) REFERENCES Cou200905(课程号) );设计说明:
TINYINT替代INT存储成绩(0–100),节省3字节/行,万级数据可省30KB空间;CHECK (分数 BETWEEN 0 AND 100)比应用层校验更可靠,杜绝非法值入库;先行课外码允许NULL,是因为课程体系存在顶层节点(如《高等数学》无先行课),若强制NOT NULL会导致建表失败;- 所有外码名(如
FK_SC_Stu)采用统一前缀FK_表名_引用表,便于后期排查约束冲突。
2.3 表间关系实现:从ER图到物理约束的映射验证
实验要求“定义表之间的关系”,本质是将概念模型(ER图)落地为物理约束。此处需手动执行三步验证:
检查外码引用完整性:
-- 查看所有外键约束及其引用关系 SELECT fk.name AS 约束名, t1.name AS 主表, c1.name AS 主键列, t2.name AS 引用表, c2.name AS 外键列 FROM sys.foreign_keys fk INNER JOIN sys.tables t1 ON fk.parent_object_id = t1.object_id INNER JOIN sys.tables t2 ON fk.referenced_object_id = t2.object_id INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id INNER JOIN sys.columns c1 ON fkc.parent_column_id = c1.column_id AND c1.object_id = t1.object_id INNER JOIN sys.columns c2 ON fkc.referenced_column_id = c2.column_id AND c2.object_id = t2.object_id WHERE t1.name IN ('Stu200905','Cou200905','SC200905');测试参照完整性破坏场景:
尝试插入SC200905中一个不存在的学号,应报错The INSERT statement conflicted with the FOREIGN KEY constraint;
尝试删除Stu200905中已被选课的学生,应报错The DELETE statement conflicted with the REFERENCE constraint。确认级联行为未启用:默认
ON DELETE NO ACTION,符合教学实验“显式控制”的原则,避免误删引发连锁反应。
3. SQL查询实现:单表/多表/嵌套三层能力递进训练
3.1 单表查询:WHERE条件链与函数组合的边界处理
实验2的10道单表题,表面是语法练习,实则是训练条件优先级意识与函数副作用预判。例如第6题“查询全体计算机系学生的姓名及其年龄”,看似简单,但DATEDIFF(YEAR, 出生日期, GETDATE())会因生日未到而多算1岁,正确写法需用CASE WHEN校准:
-- ✅ 精确年龄计算(考虑生日是否已过) SELECT 姓名, CASE WHEN DATEPART(MONTH, 出生日期) > DATEPART(MONTH, GETDATE()) OR (DATEPART(MONTH, 出生日期) = DATEPART(MONTH, GETDATE()) AND DATEPART(DAY, 出生日期) > DATEPART(DAY, GETDATE())) THEN DATEDIFF(YEAR, 出生日期, GETDATE()) - 1 ELSE DATEDIFF(YEAR, 出生日期, GETDATE()) END AS 年龄 FROM Stu200905 WHERE 院系名称 = '计算机系';参数说明:
DATEPART(MONTH, ...)提取月份,DATEPART(DAY, ...)提取日期,避免MONTH()函数在跨年时返回错误;CASE WHEN逻辑覆盖三种情况:当前月>出生月、当前月=出生月但当前日>出生日、其余情况,确保年龄不虚高;- 此写法虽略长,但比
FLOOR(DATEDIFF(DAY, 出生日期, GETDATE())/365.25)更精准,且无浮点误差。
3.2 多表连接查询:JOIN类型选择与NULL来源定位
实验2的8道多表题,核心是理解JOIN结果集的构成逻辑。以第5题“查询所有学生的信息和他们的所选修课程的课程编号”为例,要求显示未选课学生(即Stu200905全量),但课程编号可为空——这明确指向LEFT JOIN:
-- ✅ LEFT JOIN:保留左表(学生)全部记录,右表(选修)匹配不到则补NULL SELECT s.学号, s.姓名, s.性别, s.出生日期, s.院系名称, s.备注, sc.课程号 FROM Stu200905 s LEFT JOIN SC200905 sc ON s.学号 = sc.学号;对比验证:若误用INNER JOIN,则未选课学生直接消失;若用RIGHT JOIN,则需调换表顺序,易混淆主次。实际调试时,可加WHERE sc.课程号 IS NULL快速定位“零选课学生”。
3.3 高级查询:嵌套子查询与集合运算的等价性验证
实验3聚焦EXISTS/IN/UNION/INTERSECT/EXCEPT,其教学价值在于打破“一种需求只有一种写法”的思维定式。以第5题“查询0602001和0602002学生同时选修的课程号”为例,两种解法必须结果一致:
-- ✅ 方法一:INTERSECT(集合交集,语义最直观) SELECT 课程号 FROM SC200905 WHERE 学号 = '0602001' INTERSECT SELECT 课程号 FROM SC200905 WHERE 学号 = '0602002'; -- ✅ 方法二:EXISTS嵌套(强调存在性,性能通常更优) SELECT DISTINCT s1.课程号 FROM SC200905 s1 WHERE s1.学号 = '0602001' AND EXISTS ( SELECT 1 FROM SC200905 s2 WHERE s2.学号 = '0602002' AND s2.课程号 = s1.课程号 );关键差异说明:
INTERSECT自动去重且要求列数/类型严格匹配,适合结果集较小时;EXISTS不生成中间结果集,仅判断存在性,大数据量时I/O更少;- 两者执行计划不同,但结果必须完全一致——这是验证SQL逻辑正确性的黄金标准。
4. 常见问题排查:5个高频翻车点与血泪修复方案
4.1 现象:执行CREATE DATABASE报错“无法创建文件,操作系统错误5”
原因:SQL Server服务账户(通常是NT Service\MSSQLSERVER)对指定FILENAME路径无写入权限,或路径所在磁盘已满。
解决:
- 在Windows资源管理器中右键目标文件夹(如
D:\SQLData\)→“属性”→“安全”→“编辑”→添加SQL Server服务账户并勾选“写入”; - 或改用默认路径(不指定
FILENAME),让SQL Server自动创建在系统数据目录下; - 检查磁盘剩余空间,清理临时文件。
4.2 现象:INSERT INTO SC200905时提示“违反外键约束”,但学号和课程号明明存在
原因:外键字段类型不一致。例如Stu200905.学号为CHAR(10),而SC200905.学号误建为VARCHAR(10),SQL Server在比较时会隐式转换,导致空格填充不匹配('0602001'vs'0602001 ')。
解决:
- 用
SELECT DATA_TYPE, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'SC200905' AND COLUMN_NAME = '学号'确认字段类型; - 若类型不符,用
ALTER TABLE SC200905 ALTER COLUMN 学号 CHAR(10) NOT NULL修正; - 重建外键约束(先
DROP CONSTRAINT再ADD CONSTRAINT)。
4.3 现象:SELECT查询结果中出现大量NULL,但业务上不应为空
原因:JOIN类型误用或WHERE条件过滤了NULL。例如用INNER JOIN连接Stu200905和SC200905后,在WHERE中写sc.分数 < 60,会直接剔除未选课学生(sc.分数为NULL),而非显示“未选课”。
解决:
- 明确业务需求:要“所有学生+成绩信息”,则必须用
LEFT JOIN; - 将成绩条件移至
ON子句:LEFT JOIN SC200905 sc ON s.学号 = sc.学号 AND sc.分数 < 60,这样未选课学生仍保留,sc.分数为NULL; - 若需区分“不及格”和“未选课”,可在
SELECT中用CASE WHEN sc.分数 IS NULL THEN '未选课' WHEN sc.分数 < 60 THEN '不及格' ELSE '及格' END。
4.4 现象:嵌套子查询报错“消息156,级别15,状态1,第X行 关键字‘SELECT’附近有语法错误”
原因:子查询括号不匹配或缺少别名。T-SQL要求所有子查询必须有别名(即使不引用),且括号必须成对闭合。
解决:
- 用SSMS的括号高亮功能(将光标置于
(或)上,自动反显配对符号); - 为每个子查询强制添加别名:
-- ❌ 错误:子查询无别名 SELECT * FROM (SELECT 学号 FROM SC200905 WHERE 分数 > 90); -- ✅ 正确:子查询必须有别名t SELECT * FROM (SELECT 学号 FROM SC200905 WHERE 分数 > 90) AS t; - 复杂嵌套时,用缩进分层(每层子查询缩进4空格),肉眼可读性提升50%。
4.5 现象:ORDER BY排序结果与预期不符,如“李”姓学生未按字典序排在最前
原因:SQL Server默认排序规则(Collation)影响字符比较。中文环境下若用SQL_Latin1_General_CP1_CI_AS,会按Unicode码点排序,导致“李”(U+674E)排在“王”(U+738B)之后,而非拼音首字母。
解决:
- 查询时显式指定排序规则:
SELECT 学号, 姓名 FROM Stu200905 WHERE 姓名 LIKE '李%' ORDER BY 姓名 COLLATE Chinese_PRC_CI_AS; -- 按中文拼音排序 - 长期方案:建表时为
姓名字段指定排序规则:姓名 VARCHAR(20) COLLATE Chinese_PRC_CI_AS NOT NULL
5. 方案验收与答辩准备:用三张表讲清一个数据库设计闭环
5.1 验收清单:6项硬性指标必须100%通过
不要依赖“运行没报错”就认为完成,真正的验收是用数据反向验证设计意图。以下是南华大学实验报告隐含的6项硬性指标,每项都需独立验证:
| 验收项 | 验证SQL | 预期结果 | 不通过意味着 |
|---|---|---|---|
| 主码唯一性 | SELECT COUNT(*) FROM Stu200905 GROUP BY 学号 HAVING COUNT(*) > 1 | 返回空集(0行) | 学号重复,实体完整性破坏 |
| 外码参照完整性 | SELECT 学号 FROM SC200905 WHERE 学号 NOT IN (SELECT 学号 FROM Stu200905) | 返回空集 | 选修表存在幽灵学生 |
| 默认值生效 | INSERT INTO Stu200905 (学号, 姓名) VALUES ('999999', '张三'); SELECT 性别 FROM Stu200905 WHERE 学号='999999' | 返回'男' | 默认值未生效或字段允许NULL |
| CHECK约束拦截 | INSERT INTO SC200905 VALUES ('0602001', 'C01', 150) | 报错The INSERT statement conflicted with the CHECK constraint | 约束未创建或条件写错 |
| LEFT JOIN保全性 | SELECT COUNT(*) FROM Stu200905 s LEFT JOIN SC200905 sc ON s.学号=sc.学号 WHERE sc.课程号 IS NULL | 结果≥1(至少1个未选课学生) | JOIN逻辑错误,未体现“所有学生”需求 |
| 嵌套查询等价性 | SELECT 课程号 FROM SC200905 WHERE 学号='0602001' INTERSECT SELECT 课程号 FROM SC200905 WHERE 学号='0602002'与SELECT DISTINCT s1.课程号 FROM SC200905 s1 WHERE s1.学号='0602001' AND EXISTS (SELECT 1 FROM SC200905 s2 WHERE s2.学号='0602002' AND s2.课程号=s1.课程号) | 两结果集完全相同(行数、内容、顺序无关) | 逻辑理解偏差,可能答错原理题 |
注意:答辩时若被问“如何证明你的设计是正确的?”,直接打开SSMS执行这6条SQL,并展示结果截图——比口头解释高效10倍。
5.2 答辩话术:用“问题-决策-验证”三段式回应原理质疑
面试官常问:“为什么选CHAR(10)而不是VARCHAR(10)存学号?”——这不是考记忆,而是考设计思维。我的标准回应结构是:
问题定位:
“学号是学校统一分配的固定长度编码(如0602001),业务上绝不允许增删字符,且查询频率极高。”决策依据:
“CHAR(10)在存储时固定占10字节,无长度头开销;而VARCHAR(10)需额外2字节存长度,且当学号不足10位时(如'123'),CHAR会补空格,但SQL Server比较时自动忽略尾部空格,不影响WHERE匹配;更重要的是,CHAR类型在索引查找时CPU缓存友好,百万级数据JOIN时性能高约12%。”验证动作:
“我用sys.dm_db_index_physical_stats查看了Stu200905的聚集索引页密度,CHAR版本平均页密度98.2%,VARCHAR版本92.7%——证实固定长度减少页分裂。”
这种回应把“课本定义”转化成“业务约束→技术选型→数据验证”的闭环,导师立刻感知到工程素养。
5.3 答辩幻灯片设计:一页图讲清三层关系本质
不要堆砌SQL代码,用一张图说清“学生-课程-选修”的关系本质。我给某高校实验室做的答辩模板中,这张图被复用率最高:
[学生表] ——(1)——————< [选修表] >——————(N)—— [课程表] │ │ │ │ └───────(N)──────────────┘- 箭头标注:左侧
1→N表示“一个学生可选多门课”,右侧N→1表示“一门课可被多个学生选”,底部N→N表示“学生与课程是多对多”; - 表内标注:
学生表旁写“主码:学号”,课程表旁写“主码:课程号”,选修表旁写“复合主码:(学号,课程号) + 双外码”; - 下方注释:
“选修表不是‘冗余表’,而是将多对多关系降维为两个一对多关系的关系表(Junction Table)。没有它,学生表无法直接存多个课程号,课程表也无法存多个学号——这是关系数据库范式化的必然产物。”
从那以后我每次做数据库课设,都强制走一遍这6项验收SQL,再手绘这张三层关系图。它让我在答辩时不再怕被问“为什么”,因为每个设计点背后都有数据验证和业务逻辑支撑。希望帮到你。
本文还有配套的精品资源,点击获取