简介:本资源是《数据库系统概论》第3章核心实验内容的配套实现代码文档,面向高校计算机专业本科生、数据库初学者及课程实践者,聚焦SQL数据定义语言(DDL)与完整性约束的落地应用。文档以Word格式(.doc)完整呈现全部例题代码,共1个文件,大小1.49MB,涵盖student、course、sc三张典型关系表的创建语句、主码/外码/检查/默认值等多类约束实现细节,以及INSERT批量插入、ALTER TABLE结构修改、索引创建与删除等关键操作,并附带常见报错分析与SQL Server适配说明(如datetime类型修正、CASCADE语法兼容性处理等)。内容严格对应教材例题编号(例3–例15),每段代码均含注释解析,便于理解语法逻辑与工程实践差异。目前已有241人学习下载,是课堂笔记补充、课后实操验证与期末复习的实用参考资料。
1. 这不是一份“抄作业”的 DOC 文件,而是一套能跑通的 SQL 实战沙盒:覆盖建表、约束、插入、修改、索引、查询全链路,专治《数据库系统概论》第3章“看懂了但写不出”的玄学卡点
你是不是也经历过:教材例题背得滚瓜烂熟,一打开 SQL Server Management Studio(SSMS)就手抖?CREATE TABLE 写到一半发现 CHECK 约束括号没闭合,INSERT 多加了个逗号直接报错 102;ALTER COLUMN 改个数据类型,弹出“对象依赖于该列”却不知道该删哪个约束;DROP INDEX 时死活找不到索引名,翻遍 student 表 schema 也没见 stusname ——最后才发现自己建的是 stusno。这不是你菜,是教材例题和真实数据库引擎之间隔着一层没明说的“执行上下文”。这份《数据库系统概论》第3章所有例题实现代码.doc,本质是一份可复现、可调试、带排错注释的 SQL 沙盒脚本集:它把王珊老师教材里分散在页眉页脚、批注框、勘误贴士里的隐性知识,全部翻译成 SSMS 里能一行行执行、能看见错误码、能立刻 rollback 的真实语句。它不教你理论,它替你踩坑——比如 SQL Server 不认cascade,Oracle 和 SQL Server 对 NULL 排序逻辑相反,_通配符在中文字段里必须用N'_'才生效……这些血泪经验,全埋在注释里。适合刚学完关系模型、正对着第3章发懵的大二学生,也适合需要快速搭建教学演示环境的助教——你复制粘贴进 SSMS,按顺序执行,就能看到三张表从无到有、数据逐条落库、索引成功创建、查询结果精准返回的完整闭环。它不是 PDF 笔记,是能呼吸的数据库骨架。
2. 数据定义与约束落地:从 CREATE TABLE 到 FOREIGN KEY,为什么你的建表语句总在第3行报错?
2.1 建表语句的“语法糖”陷阱:列级 vs 表级约束的真实差异
教材里常把 PRIMARY KEY、CHECK、DEFAULT 写在同一行,看起来很清爽。但实际执行时,列级约束和表级约束的解析优先级、错误定位粒度、以及后续 ALTER 的兼容性,完全不同。我们以student表为例,拆解每一条约束的底层含义:
CREATE TABLE student( sno CHAR(9) PRIMARY KEY, -- ✅ 列级主键:SQL Server 自动创建唯一聚集索引(默认) sname CHAR(20) NOT NULL, -- ✅ 列级非空:强制插入时必须提供值 ssex CHAR(2) DEFAULT '男' CHECK(ssex IN ('男','女')), -- ⚠️ 危险组合!DEFAULT 和 CHECK 同属列级,但 CHECK 会校验 DEFAULT 值是否合法 sage SMALLINT CHECK(sage>=15 AND sage<=45), -- ✅ 列级检查:插入/更新时校验数值范围 sdept CHAR(20) -- ❌ 无约束:允许 NULL,且无索引 );关键参数说明:
CHAR(9)是定长字符串,存储'200215121'会占满9字节,比VARCHAR(9)更耗空间但查询略快;SMALLINT取值范围是 -32768 到 32767,完全覆盖 15~45,比INT节省2字节存储;CHECK(ssex IN ('男','女'))中的单引号必须是英文直角引号,中文引号'会导致语法错误 102;DEFAULT '男'的值必须符合CHAR(2)长度,'男'正好2字节(UTF-16),若写'男生'会截断为'男'并静默警告。
为什么强调这个?因为后续ALTER COLUMN sage INT失败的根本原因,就藏在这里:CHECK约束被 SQL Server 视为一个独立对象(如CK__student__sage__1CF15040),它绑定在sage列上。当你试图ALTER COLUMN时,引擎发现该列被 CHECK 约束“持有”,必须先释放。这和教材里“直接改类型”的描述存在执行层偏差。
2.2 外键约束的双向依赖:为什么 SC 表建表失败时,错误指向 course 表?
sc表的外键定义是理解参照完整性核心的关键:
CREATE TABLE sc( sno CHAR(9), cno CHAR(4), grade SMALLINT CHECK((grade IS NULL) OR (grade BETWEEN 0 AND 100)), PRIMARY KEY(sno, cno), -- ✅ 表级联合主键 FOREIGN KEY(sno) REFERENCES student(sno), -- ✅ 外键1:sno 引用 student.sno FOREIGN KEY(cno) REFERENCES course(cno) -- ✅ 外键2:cno 引用 course.cno );执行顺序决定生死:
- 必须先
CREATE TABLE student和CREATE TABLE course,否则REFERENCES student(sno)会报错 3701(对象不存在); student和course表的被引用列(sno,cno)必须已声明为 PRIMARY KEY 或 UNIQUE,否则报错 1776(被引用列未建立唯一约束);sc表建表时,SQL Server 会立即验证student和course表是否存在、被引用列是否唯一——这是 DDL 语句的原子性保障,不是运行时检查。
避坑提示:很多初学者把
sc表建在student和course之前,或漏写PRIMARY KEY(cno)导致course.cno不是唯一键,错误信息会指向FOREIGN KEY行,但根因在上游表。建议建表顺序严格遵循:student→course→sc。
2.3 约束命名的隐形规则:为什么 DROP CONSTRAINT 总找不到名字?
教材例题中大量使用匿名约束(如CHECK(sage>=15)),这在开发中极不友好。SQL Server 会自动生成约束名(如CK__student__sage__1CF15040),但名字含哈希值,无法预测。生产环境必须显式命名约束:
-- ✅ 推荐写法:显式命名,便于后续管理 CREATE TABLE student( sno CHAR(9) CONSTRAINT PK_student_sno PRIMARY KEY, sname CHAR(20) CONSTRAINT NN_student_sname NOT NULL, ssex CHAR(2) CONSTRAINT DF_student_ssex DEFAULT '男', sage SMALLINT CONSTRAINT CK_student_sage CHECK(sage>=15 AND sage<=45), sdept CHAR(20) ); -- ✅ 删除约束时直接引用名字,不再靠猜 ALTER TABLE student DROP CONSTRAINT CK_student_sage;参数价值:CONSTRAINT <name>是标准 SQL 语法,在 SQL Server、MySQL 8.0+、PostgreSQL 中通用。命名规则建议:CK_表名_列名_业务含义(检查约束)、PK_表名_列名(主键)、FK_表名_外键列_被引用表_被引用列(外键)。这样sp_helpconstraint student查看约束时,名字即文档。
3. 数据插入与类型对齐:INSERT INTO VALUES 的 4 个致命细节,90% 的人栽在第2条
3.1 字符串长度与中文编码:为什么插入 '李勇' 报错 “字符串截断”?
student表定义sname CHAR(20),看似足够存 20 个汉字。但在 SQL Server 中,CHAR(n)的n指字符数,不是字节数。'李勇'是2个 Unicode 字符,CHAR(20)可存 20 个字符,没问题。但问题出在客户端连接的默认排序规则和隐式转换:
-- ❌ 危险写法:未指定 Unicode 前缀,可能触发代码页转换 INSERT INTO student VALUES('200215121','李勇','男',20,'CS'); -- ✅ 安全写法:显式 N 前缀,声明 Unicode 字符串 INSERT INTO student VALUES(N'200215121', N'李勇', N'男', 20, N'CS');原理说明:
N'李勇'告诉 SQL Server 这是NVARCHAR字面量,按 UTF-16 编码传输;若省略N,客户端可能用GBK或Latin1解析,导致李被误读为乱码字节,插入时因长度超限(CHAR(20)按字节计)报错 8152。尤其当数据库排序规则为Chinese_PRC_CI_AS时,此问题高频出现。
3.2 NULL 值的显式表达:为什么 INSERT INTO sc VALUES('200215121','1',NULL) 报错?
看sc表定义:grade SMALLINT CHECK((grade IS NULL) OR (grade BETWEEN 0 AND 100))。逻辑上允许NULL,但直接写NULL会触发 CHECK 约束校验。问题在于:SQL Server 在 CHECK 中对 NULL 的处理是三值逻辑(True/False/Unknown),而IS NULL是唯一能安全判断 NULL 的运算符。但插入时,NULL字面量本身是合法的,报错通常源于:
- 列顺序错位:
sc表列为(sno, cno, grade),若写VALUES('200215121', NULL, '1'),则NULL被赋给cno(CHAR(4)),违反非空约束(cno无NOT NULL声明,但CHAR(4)默认允许 NULL,此处不报错); - 更常见原因:客户端工具自动补全或格式化。某些 SSMS 插件会将
NULL替换为'NULL'字符串,导致插入'NULL'到SMALLINT列,报错 245(类型转换失败)。
✅绝对安全的写法:
-- 显式指定列名,杜绝顺序错位 INSERT INTO sc (sno, cno, grade) VALUES (N'200215121', N'1', NULL); -- 或使用 DEFAULT 关键字(若 grade 有 DEFAULT 约束) INSERT INTO sc (sno, cno) VALUES (N'200215121', N'1'); -- grade 自动为 NULL3.3 批量插入的事务边界:为什么 10 条 INSERT 有一条失败,其余全回滚?
教材例题把所有INSERT写在一起,看似方便。但 SQL Server 默认每个语句是独立事务。若想保证“全成功或全失败”,必须显式包裹在事务中:
BEGIN TRANSACTION; INSERT INTO student VALUES(N'200215121', N'李勇', N'男', 20, N'CS'); INSERT INTO student VALUES(N'200215122', N'刘晨', N'女', 19, N'CS'); -- ... 其他8条 IF @@ERROR = 0 COMMIT TRANSACTION; ELSE ROLLBACK TRANSACTION;参数说明:
@@ERROR返回上一条语句的错误号,0 表示成功。COMMIT持久化所有更改,ROLLBACK撤销全部。这是教学演示必备技能——避免因某条INSERT主键冲突(如重复sno)导致部分数据脏写。
3.4 数据验证:INSERT 后如何确认数据真实落库?
别只信命令已成功完成。必须用SELECT验证:
-- ✅ 验证 student 表数据量和关键字段 SELECT COUNT(*) AS total_students FROM student; SELECT sno, sname, ssex, sage, sdept FROM student WHERE sno = N'200215121'; -- ✅ 验证外键引用完整性:sc 表的 sno 是否都在 student 中存在? SELECT sc.sno, sc.cno FROM sc LEFT JOIN student ON sc.sno = student.sno WHERE student.sno IS NULL; -- 若返回记录,说明存在孤儿外键技巧:LEFT JOIN ... WHERE ... IS NULL是检测外键违规的标准方法,比NOT IN更可靠(NOT IN遇 NULL 会返回空集)。
4. 表结构修改实战:ALTER TABLE 的 5 个雷区,第4个让 80% 的人重装 SQL Server
4.1 ADD COLUMN:datetime vs DATE,为什么教材写 datetime 而不是 date?
ALTER TABLE student ADD s_entrance datetime;
教材用datetime是因《数据库系统概论》第六版出版时(2018年),SQL Server 2016 已支持DATE类型,但为兼容旧版本(如 SQL Server 2005),仍推荐datetime。实际选型应基于需求:
| 类型 | 存储大小 | 精度 | 推荐场景 |
|---|---|---|---|
DATE | 3 bytes | 日精度 | 入学日期、生日等无需时间的场景 |
DATETIME | 8 bytes | 3.33ms | 需要记录具体时刻,且兼容老系统 |
DATETIME2(n) | 6-8 bytes | 100ns | 新项目首选,精度高,存储优 |
✅现代写法(SQL Server 2008+):
ALTER TABLE student ADD s_entrance DATE; -- 更精确,更省空间 -- 或 ALTER TABLE student ADD s_entrance DATETIME2(0); -- 秒精度,8字节4.2 ALTER COLUMN:为什么 “ALTER COLUMN sage INT” 必须先删 CHECK 约束?
这是 SQL Server 的硬性限制。ALTER COLUMN修改数据类型时,要求该列不能有任何依赖对象。CHECK约束正是这样的依赖对象。执行流程必须是:
-- 步骤1:查出 sage 列的 CHECK 约束名 SELECT name FROM sys.check_constraints WHERE parent_object_id = OBJECT_ID('student') AND OBJECT_NAME(parent_object_id) = 'student'; -- 步骤2:删除约束(假设名为 CK_student_sage) ALTER TABLE student DROP CONSTRAINT CK_student_sage; -- 步骤3:修改列类型 ALTER TABLE student ALTER COLUMN sage INT; -- 步骤4:重建 CHECK 约束(注意:INT 范围更大,原条件仍适用) ALTER TABLE student ADD CONSTRAINT CK_student_sage CHECK(sage>=15 AND sage<=45);血泪经验:
sys.check_constraints是系统视图,OBJECT_ID('student')获取表ID。不要手动记约束名,用查询动态获取——这是 DBA 的基本功。
4.3 ADD UNIQUE:为什么ALTER TABLE course ADD UNIQUE(cname)能成功,而ADD PRIMARY KEY不行?
UNIQUE约束允许NULL值(每列最多一个NULL),且不改变表的物理结构;而PRIMARY KEY要求非空且唯一,并默认创建聚集索引。course表已有PRIMARY KEY(cno),再加PRIMARY KEY(cname)会违反“一个表只能有一个聚集索引”的规则(除非显式指定NONCLUSTERED)。
✅安全添加唯一约束:
-- ✅ 添加唯一约束(允许 NULL) ALTER TABLE course ADD CONSTRAINT UQ_course_cname UNIQUE(cname); -- ✅ 添加非聚集主键(不破坏现有聚集索引) ALTER TABLE course ADD CONSTRAINT PK_course_cname PRIMARY KEY NONCLUSTERED (cname);4.4 DROP TABLE:为什么没有 CASCADE,且必须先删外键?
DROP TABLE student报错消息 3726,因为sc表的FOREIGN KEY(sno)依赖它。SQL Server绝不允许级联删除(CASCADE是 PostgreSQL/MySQL 的语法),这是数据安全的底线设计。
✅正确删除流程:
-- 步骤1:删除依赖它的外键约束(不是删表!) ALTER TABLE sc DROP CONSTRAINT FK_sc_sno_student; -- 假设外键名为此 -- 步骤2:删除表 DROP TABLE student; -- 步骤3:若需重建,重新创建 student 和 sc避坑 / 常见问题 / 排查
现象1:执行DROP TABLE student报错 “因为一个或多个对象访问此列”
原因:sc表存在外键引用,或student表上有索引、触发器、视图依赖
解决:运行sp_fkeys 'student'查看所有外键引用;sp_help 'student'查看依赖对象现象2:
ALTER TABLE student ALTER COLUMN sage INT报错 “ALTER TABLE ALTER COLUMN sage 失败”
原因:除 CHECK 约束外,该列还可能被计算列、索引、统计信息引用
解决:先运行SELECT * FROM sys.dm_db_index_usage_stats WHERE object_id = OBJECT_ID('student')检查索引;用DBCC SHOW_STATISTICS('student', '_WA_Sys_...')查看统计信息现象3:
CREATE CLUSTERED INDEX stusname ON student(sname)报错 “不能在表 'student' 上创建多个聚集索引”
原因:PRIMARY KEY(sno)默认创建了聚集索引,student表已存在一个
解决:要么DROP CONSTRAINT PK_student_sno先删主键(不推荐),要么创建NONCLUSTERED索引:CREATE NONCLUSTERED INDEX IX_student_sname ON student(sname)现象4:
DROP INDEX student.stusname报错 “索引 'student.stusname' 不存在”
原因:索引名不是stusname,而是stusno(教材例14建的),或索引在student表上但属于其他架构(如dbo.student)
解决:用SELECT name FROM sys.indexes WHERE object_id = OBJECT_ID('student')精确查名;删除时写全名DROP INDEX IX_student_sname ON student现象5:
INSERT INTO course插入cpno为NULL,但查询时cpno显示为空字符串''
原因:cpno CHAR(4)是定长类型,NULL被隐式转换为' '(4个空格),显示为空白
解决:改用VARCHAR(4)或CHAR(4) NULL,并在应用层明确区分NULL和空字符串
5. 索引与查询优化:从 CREATE INDEX 到 LIKE 查询,那些教材没写的性能真相
5.1 聚集索引 vs 非聚集索引:为什么CREATE CLUSTERED INDEX stusname ON student(sname)必须先删主键?
PRIMARY KEY默认创建聚集索引(Clustered Index),它决定了表数据的物理存储顺序。一个表只能有一个聚集索引,因为数据行不能按两种顺序同时存储。student表的sno主键已占用这个位置。
✅正确创建方式:
-- 方案1:删除主键(不推荐,破坏实体完整性) ALTER TABLE student DROP CONSTRAINT PK_student_sno; CREATE CLUSTERED INDEX IX_student_sname ON student(sname); -- 方案2:创建非聚集索引(推荐,保留主键聚集索引) CREATE NONCLUSTERED INDEX IX_student_sname ON student(sname);参数说明:
NONCLUSTERED是显式关键字,可省略(默认即非聚集)。非聚集索引是独立的 B+ 树,叶子节点存sname值 +sno(聚集键),通过sno回表查整行。查询WHERE sname = '李勇'时,先走索引找到sno,再用sno查主键索引取数据。
5.2 复合索引与最左前缀:为什么CREATE INDEX index_sno_cno ON sc(sno,cno)能加速WHERE sno=xxx AND cno=yyy,但对WHERE cno=yyy无效?
复合索引(sno, cno)的 B+ 树按sno排序,sno相同时再按cno排序。查询必须包含最左列sno才能使用该索引:
WHERE sno='200215121' AND cno='1'→ ✅ 精准匹配,索引高效WHERE sno='200215121'→ ✅ 范围扫描sno,再过滤cnoWHERE cno='1'→ ❌ 无法跳过sno直接查cno,索引失效,全表扫描
✅针对cno高频查询,应另建索引:
CREATE NONCLUSTERED INDEX IX_sc_cno ON sc(cno); -- 或更优:覆盖索引,避免回表 CREATE NONCLUSTERED INDEX IX_sc_cno_covering ON sc(cno) INCLUDE (sno, grade);5.3 LIKE 查询的性能陷阱:为什么WHERE sname LIKE '_勇'比WHERE sname LIKE '%勇'快 100 倍?
_是单字符通配符,%是多字符通配符。索引只能用于前导匹配:
sname LIKE '李%'→ ✅ 使用索引,查找sname以李开头的所有行sname LIKE '%勇'→ ❌ 索引失效,必须扫描全表找结尾为勇的行sname LIKE '_勇'→ ⚠️ 理论上可用索引,但实际取决于统计信息和查询优化器选择;_无法利用 B+ 树的有序性,通常仍走全表扫描
✅优化方案:
-- 方案1:用全文索引(适合大文本) CREATE FULLTEXT INDEX ON student(sname) KEY INDEX PK_student_sno; -- 方案2:添加计算列并索引(适合固定模式) ALTER TABLE student ADD sname_last_char AS RIGHT(sname, 1) PERSISTED; CREATE INDEX IX_student_sname_last ON student(sname_last_char); -- 查询:WHERE sname_last_char = N'勇'5.4 NULL 排序的跨平台差异:SQL Server 认为 NULL 最小,Oracle 认为最大,如何写出兼容查询?
ORDER BY sage DESC在 SQL Server 中,NULL排在最前(最小);在 Oracle 中,NULL排在最后(最大)。标准 SQL 用NULLS FIRST/LAST显式控制(SQL Server 2022+ 支持,旧版不支持):
✅SQL Server 兼容写法:
-- 强制 NULL 排最后(模拟 Oracle 行为) SELECT * FROM student ORDER BY CASE WHEN sage IS NULL THEN 1 ELSE 0 END, sage DESC; -- 强制 NULL 排最前(SQL Server 默认) SELECT * FROM student ORDER BY CASE WHEN sage IS NULL THEN 0 ELSE 1 END, sage DESC;原理:
CASE生成排序辅助列,NULL映射为 1 或 0,再按sage排序。这是跨数据库的通用技巧。
6. 查询实战与验证技巧:用 3 个 SELECT 语句,10 分钟内验证整个第3章知识链是否打通
6.1 验证数据完整性:一条 SQL 检查所有约束是否生效
别手动一条条INSERT测试。用以下查询,一次性暴露所有潜在违规:
-- ✅ 检查外键完整性:sc 表中是否存在 student 表没有的 sno? SELECT 'sc.sno 引用异常' AS issue, sc.sno, sc.cno FROM sc LEFT JOIN student ON sc.sno = student.sno WHERE student.sno IS NULL UNION ALL -- ✅ 检查外键完整性:sc 表中是否存在 course 表没有的 cno? SELECT 'sc.cno 引用异常' AS issue, sc.sno, sc.cno FROM sc LEFT JOIN course ON sc.cno = course.cno WHERE course.cno IS NULL UNION ALL -- ✅ 检查 CHECK 约束:student 表中是否存在 age 超出 15-45 的记录? SELECT 'student.sage 范围违规' AS issue, sno, sname, sage FROM student WHERE sage < 15 OR sage > 45 UNION ALL -- ✅ 检查 DEFAULT 约束:student 表中 ssex 是否有非 '男'/'女' 的值? SELECT 'student.ssex 值违规' AS issue, sno, sname, ssex FROM student WHERE ssex NOT IN (N'男', N'女') OR ssex IS NULL;执行后若返回空集,说明所有约束已正确加载并生效。这是比SELECT * FROM student更有力的验证。
6.2 验证索引有效性:用 SET STATISTICS IO ON 看清执行计划真相
CREATE INDEX IX_student_sname ON student(sname)是否真被用了?别信猜测,看真实 I/O:
-- 开启 I/O 统计 SET STATISTICS IO ON; GO -- 执行查询 SELECT * FROM student WHERE sname = N'李勇'; -- 关闭统计 SET STATISTICS IO OFF; GO结果解读:
- 若
Logical Reads为 2~3,说明走了索引(根页+叶子页); - 若
Logical Reads等于student表总页数,说明全表扫描,索引未被使用; - 原因可能是:
sname列重复率高(如大量同名)、统计信息过期、查询条件未匹配索引最左列。
✅强制更新统计信息:
UPDATE STATISTICS student IX_student_sname; -- 或更新全表 UPDATE STATISTICS student;6.3 验证查询逻辑:教材例题的“效果一样”背后,藏着执行效率的天壤之别
教材例16提到“法一、法二结果一样”,但未提性能。以SELECT * FROM sc WHERE sno IN ('200215121','200215122')为例:
- 法一(IN 列表):
WHERE sno IN ('200215121','200215122')→ ✅ 索引查找两次 - 法二(OR):
WHERE sno = '200215121' OR sno = '200215122'→ ✅ 同样高效,优化器会转为索引查找 - 危险法(LIKE):
WHERE sno LIKE '20021512%'→ ❌ 索引失效,因sno是CHAR(9),LIKE前导匹配仍可用,但若写成'%121'则全表扫描
✅终极验证技巧:对比执行计划
在 SSMS 中,按Ctrl+M开启“包含实际执行计划”,执行两条语句,观察:
- 是否都显示
Index Seek(绿色图标); Estimated Subtree Cost数值是否接近(< 10% 差异可接受);Number of Rows是否与预期一致(避免笛卡尔积)。
从那以后我每次教学生建完三张表,第一件事不是写查询,而是跑一遍SET STATISTICS IO ON+SELECT * FROM student WHERE 1=0(空查询看开销),再跑上面那个四合一完整性检查。它像一道安检门,把教材里没写的隐性漏洞全筛出来——不是为了炫技,是让学生亲手摸到数据库的“心跳”。希望帮到你。
本文还有配套的精品资源,点击获取