简介:《数据库系统概论》第三章围绕关系数据库标准语言SQL展开,对应经典教材中第三章的全部例题代码,面向正在系统学习数据定义、表结构创建与各类完整性约束的数据库初学者。文档以学生表、课程表、成绩表三张母表为主线,完整给出建表语句,覆盖列级约束与表级约束两种写法,包含主码外码关联、检查约束取值限定、默认值设定、数据插入操作、修改表结构的命令,并延伸到聚簇索引与非聚簇索引的建立和删除。代码针对SQL Server环境做了适配,对于标准SQL中CASCADE、CLUSTER等语法差异给出替代写法以及报错原因,便于读者对照教材逐例上机验证,快速定位常见问题。资源包为一个doc文件,大小约一点四九兆字节,结构清晰、注释详实,既适合课堂同步练习,也可作为期末复习或自学SQL的参考材料。目前已有两百四十一人学习该文档,值得本科及高职数据库课程学习者下载使用。
1. 这份《数据库系统概论》第3章例题代码,到底能帮你干什么
学数据库最痛苦的不是搞不懂概念,而是例题全看懂了,一合上书自己敲,SELECT 后面该跟什么就卡壳。《数据库系统概论》第3章讲的是关系数据库标准语言 SQL,从建表、插入到单表查询、连接查询、嵌套查询和数据更新,每一节都有配套例题。这份 doc 把所有例题的实现代码集中成了一个文件,相当于把散在书里的 SQL 片段汇总成一份可照着敲的参考答案。它解决的正是“看明白”与“跑通”之间的鸿沟:不用再到处翻书凑语法,也不用担心教材示例数据在本地跑不出来。适合数据库课程在读学生、准备考研复试的考生,以及刚入职需要快速补 SQL 基本功的开发者。
2. 第3章例题背后:SQL 查询从单表到嵌套连接的代码骨架
打开这份 doc 之前,先要搞明白第3章的例题到底在训练什么。教材的例题不是随机拼凑的,它有一条清晰的递进线:定义数据、插入数据、查数据、改数据。而其中最核心、也最吃基本功的是 SELECT 的六种形态。
2.1 例题类型与 SQL 特性的对应关系
第3章例题一般集中在数据定义、数据查询和数据更新三大块。数据定义主要是 CREATE TABLE / ALTER TABLE,数据更新是 INSERT / UPDATE / DELETE,但真正占篇幅的是数据查询。查询部分从单表选择、条件过滤、排序,到多表连接、嵌套子查询、分组聚合,再到集合操作,一层比一层复杂。
| 例题类型 | 核心 SQL 特性 | 教科书里最常出现的写法 | 需要注意的点 |
|---|---|---|---|
| 库表定义 | CREATE TABLE / ALTER TABLE | 定义 Student、Course、SC 三张表 | 主外键约束、字段默认值 |
| 单表查询 | SELECT / WHERE / ORDER BY | 查询全体学生、按年龄排序 | 列名大小写、字符串比较 |
| 连接查询 | JOIN / 多表 WHERE | 学生表与选课表连接 | 列名歧义,要加表限定 |
| 嵌套查询 | IN / EXISTS / 比较运算符 | 查询选修某课程的学生姓名 | 子查询结果集的语义 |
| 分组统计 | GROUP BY / HAVING | 每门课选课人数、平均分 | HAVING 与 WHERE 的区别 |
| 集合操作 | UNION / INTERSECT / EXCEPT | 计算机系与男生的并集 | 列数匹配、去重行为 |
| 数据更新 | INSERT / UPDATE / DELETE | 插入记录、改系别、删选课 | 外键顺序、事务提交 |
注意,这些类型不是孤立的:后面的连接查询会用到前面的单表查询作子查询;分组统计往往又依赖连接查询的结果。所以从文档里提取代码时,要保持原有顺序。一旦打乱,很容易出现“表不存在”或“列不存在”的报错。
2.2 为什么我建议先用 MySQL 跑这些例题
教材例题绝大多数是标准 SQL,MySQL 8.0 能兼容绝大部分,唯一要改的是个别 SQL Server 方言。用 MySQL 而不是 Oracle,是因为社区版免费、单机安装快、命令行和图形工具都成熟;对于验证例题这种量级的脚本,MySQL 的默认隔离级别和数据类型都够用。如果你手头只有 SQLite,也可以跑,但 ALTER TABLE 和事务行为有差异,需要额外注意。
| 数据库 | 对教材标准 SQL 的兼容性 | 本地安装成本 | 适合场景 |
|---|---|---|---|
| MySQL 8.0 | 高,需替换个别方言 | 低 | 教材例题验证、日常学习 |
| SQLite | 中,ALTER TABLE 和事务行为差异大 | 极低 | 临时验证、嵌入式 |
| SQL Server | 高 | 高(需授权或开发版) | 教材按 SQL Server 编写时 |
| Oracle | 低,大量语法差异 | 很高 | 企业生产环境 |
我一般会在本地装一个 MySQL 8.0,建一个专门的库,比如study,然后让所有例题脚本按顺序执行。这样不管文档里有多少段代码,都能在一个干净环境里复现。选 MySQL 还有一个好处是它的报错信息足够直白,比如外键约束失败会直接告诉你哪一行有问题,比某些数据库“死给一个 ORA-”要友好得多。
2.3 建库建表的最小准备动作
先把环境准备好。用下面的 SQL 创建一个独立数据库,避免污染其他数据。
-- 创建独立数据库,字符集统一为 utf8mb4 CREATE DATABASE IF NOT EXISTS study DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE study;这里的逻辑是:IF NOT EXISTS保证重复执行不报错;utf8mb4是为了让中文注释和字符串正常存储,避免字符集不一致引发的乱码和比较问题。如果你用 MySQL 5.6,同样支持 utf8mb4,只是排序规则名称可能略有差异。注意,字符集不仅要写在建库语句里,客户端连接时也要指定,否则前面建的是 utf8mb4,后面连接用的是 latin1,照样乱码。
准备动作就这么简单,确认USE study;执行后没有报错即可。接下来就可以把文档里的建表、插入、查询脚本一段段粘贴进来。建议在 MySQL 命令行里跑,而不是用图形工具一次执行整个文档——因为例题脚本里可能某一句出错,图形工具会把整批回滚,你就分不清到底是哪一句的问题了。
3. 把例题落到本地库:建表、造数、跑查询的最小可复现脚本
第3章的例题几乎都是从学生选课库开始的。这套表结构很经典,后面的每一道查询题都围绕着它展开。下面这段是例题的根表,所有代码都基于这三张表。
3.1 第3章例题的经典表结构与造数脚本
USE study; CREATE TABLE Student ( Sno CHAR(9) PRIMARY KEY, Sname VARCHAR(20) NOT NULL, Ssex CHAR(2) DEFAULT '男', Sage SMALLINT, Sdept VARCHAR(20) ); CREATE TABLE Course ( Cno CHAR(4) PRIMARY KEY, Cname VARCHAR(40) NOT NULL, Cpno CHAR(4), -- 先修课号 Ccredit SMALLINT ); CREATE TABLE SC ( Sno CHAR(9), Cno CHAR(4), Grade DECIMAL(5,2), PRIMARY KEY (Sno, Cno), FOREIGN KEY (Sno) REFERENCES Student(Sno), FOREIGN KEY (Cno) REFERENCES Course(Cno) );代码说明:Sno用 CHAR(9) 是因为学号固定长度,数字和字母混用时不要用 INT,否则前导零会丢失;Grade用 DECIMAL(5,2) 可以保留两位小数,能存 0 到 999.99,足够描述百分制成绩。外键在 SC 表里定义,保证选课记录必须对应存在的学生和课程,教材里反复强调的“参照完整性”就在这里落地。注意Cpno字段是CHAR(4),但它在 Course 表内部引用的是另一门课的Cno,这叫递归外键,插入时对顺序有要求。
接着造数。我用的是教材最经典的那几条记录,数量少、结果能手算验证。
INSERT INTO Student VALUES ('201215121', '李勇', '男', 20, 'CS'), ('201215122', '刘晨', '女', 19, 'CS'), ('201215123', '王敏', '女', 18, 'MA'), ('201215125', '张立', '男', 19, 'IS'); INSERT INTO Course VALUES ('1', '数据库', NULL, 4), ('2', '数学', NULL, 2), ('3', '信息系统', '1', 4), ('4', '操作系统', '6', 3), ('5', '数据结构', '7', 4), ('6', '数据处理', NULL, 2), ('7', 'PASCAL语言', '6', 4); INSERT INTO SC VALUES ('201215121', '1', 92), ('201215121', '2', 85), ('201215121', '3', 88), ('201215122', '2', 90), ('201215122', '3', 80);这里故意把Course的Cpno设计成有 NULL 有值,就是为了让例题里“查先修课为空”这类题目有得做。插入顺序上,先插 Student,再插 Course,最后插 SC。如果把 SC 提前,MySQL 会立刻报外键错误,因为引用的主表记录还不存在。数据量小有个好处:每跑完一个查询,你可以用纸笔算一遍结果,确认代码真的理解了,而不是碰巧出结果。
3.2 单表查询与排序的实现代码
教材第3章例题从最简单的“查询全体学生的学号和姓名”开始,然后逐步加条件、排序。我把最常出现的四种形态放在一起:
-- 例1:查询全体学生的学号与姓名 SELECT Sno, Sname FROM Student; -- 例2:查询计算机系年龄小于20的男生 SELECT Sname, Sage FROM Student WHERE Sdept = 'CS' AND Sage < 20 AND Ssex = '男'; -- 例3:查询选课成绩大于80的学号,去掉重复 SELECT DISTINCT Sno FROM SC WHERE Grade > 80; -- 例4:按年龄降序输出全体学生 SELECT Sno, Sname, Sage FROM Student ORDER BY Sage DESC;这些代码看着简单,但坑都在细节里。WHERE里字符串要用单引号,MySQL 默认不区分大小写,但教材里的字符串比较是区分大小写的,所以最好统一大小写习惯;DISTINCT作用于整个输出列,不是只作用在第一列,如果同时输出 Sno 和 Sname,重复是指两列的组合重复;ORDER BY默认升序,降序要写DESC,而且它放在最后,如果你在后面又接了LIMIT,顺序是ORDER BY ... LIMIT ...。
还有一类单表查询值得单独列出来:模糊匹配LIKE、空值判断IS NULL、范围判断BETWEEN。这些在例题里出现的频率非常高,而且最容易写错。
-- 例:查询姓“李”且名字为两个字的男生 SELECT Sname FROM Student WHERE Sname LIKE '李_'; -- 例:查询成绩在80到90之间的选课记录 SELECT * FROM SC WHERE Grade BETWEEN 80 AND 90; -- 例:查询没有先修课的课程 SELECT Cno, Cname FROM Course WHERE Cpno IS NULL;LIKE里的下划线表示单个任意字符,百分号表示任意长度;BETWEEN是包含边界的;空值判断必须用IS NULL,写成= NULL永远查不到,这是第3章里反复挖的坑。
3.3 连接、嵌套与分组的实现代码,以及数据更新例题
这组例题是第3章的难点,也是考试重点。我按常用程度排了序。
-- 例5:查询选修了课程的学生姓名(连接查询) SELECT DISTINCT Student.Sname FROM Student, SC WHERE Student.Sno = SC.Sno; -- 等价写法 SELECT DISTINCT Student.Sname FROM Student JOIN SC ON Student.Sno = SC.Sno; -- 例6:查询与“李勇”同一个系的学生(嵌套查询) SELECT Sname, Sdept FROM Student WHERE Sdept IN ( SELECT Sdept FROM Student WHERE Sname = '李勇' ); -- 例7:查询每门课的选课人数与平均分 SELECT Cno, COUNT(*) AS cnt, AVG(Grade) AS avg_grade FROM SC GROUP BY Cno HAVING COUNT(*) > 1; -- 例8:查询计算机系女生(集合操作) SELECT Sname FROM Student WHERE Sdept = 'CS' UNION SELECT Sname FROM Student WHERE Ssex = '女';例5里如果两个表都有Sno,必须加表名限定,否则报列名歧义。这种旧式逗号连接虽然能用,但可读性差,我习惯写成JOIN ... ON。例6的IN子查询最直观,注意子查询结果为空时不会报错,只是返回空结果;如果子查询可能返回多列,IN就不适用了,得用EXISTS。例7的HAVING是在分组之后过滤,WHERE做不到这层;COUNT(*)会统计 NULL 行,而COUNT(列名)不会,这地方在例题里经常玩文字游戏。例8的UNION默认去重,UNION ALL才保留重复行,教材里特意用这个例子讲集合运算。
数据更新部分的例题同样重要,而且和事务绑在一起:
-- 例:将李勇的系别改为 MA UPDATE Student SET Sdept = 'MA' WHERE Sname = '李勇'; -- 例:删除学号为201215125的选课记录 DELETE FROM SC WHERE Sno = '201215125'; -- 提交事务,否则上述 DML 在默认连接下不会持久化 COMMIT;UPDATE和DELETE不带WHERE就是全表操作,这个习惯要养成:写之前先看一眼WHERE,或者在事务里执行、事后确认再提交。第3章例题里基本都带明确的WHERE条件,照抄没问题,但如果你自己扩写,务必小心。
4. 从 .doc 里提取代码并批量验证:转换脚本与执行流程
拿到这份 doc,第一反应是打开复制粘贴。但例题几十条,一条条粘很累,而且容易粘错格式。更靠谱的做法是把代码提取出来,存成 .sql 文件,再批量执行。这里有两个坎:doc 格式转换,以及代码块识别。
4.1 doc/docx 的代码提取思路
这份文档是 .doc 后缀,本质是 Word 老格式。直接读取文本的库大多优先支持 docx,对老版 doc 支持很差。我一般的做法是:先用 LibreOffice 把 .doc 批量转成 .docx,再用 python-docx 读取段落。如果你用的是 Windows 且装了完整版 Office,也可以用 Word 的 COM 接口转换,但命令行下 LibreOffice 更可控,还不用额外付费。
转换命令如下:
soffice --headless --convert-to docx --outdir ./converted 第3章所有例题实现代码.doc--headless表示不弹窗口,--convert-to docx输出新格式,--outdir指定输出目录。命令成功时,./converted下会出现同名 .docx 文件。如果在 Windows 上提示soffice找不到,就把路径换成完整安装路径,比如C:\Program Files\LibreOffice\program\soffice.exe。
4.2 提取脚本:按样式或前缀识别代码块
Word 里的代码通常用等宽字体(Consolas、Courier New)或者单独样式标记。下面的脚本先找到所有段落,再根据内容前缀把代码段筛出来。这样做比人工复制快,而且不容易漏。
from docx import Document doc = Document("converted/第3章所有例题实现代码.docx") code_lines = [] for para in doc.paragraphs: text = para.text.rstrip() if not text.strip(): continue upper = text.strip().upper() # 代码块通常以 SQL 关键字或注释开头 if upper.startswith( ("CREATE", "SELECT", "INSERT", "UPDATE", "DELETE", "ALTER", "DROP", "--") ): code_lines.append(text) print("\n".join(code_lines))这段脚本的逻辑很简单:根据 SQL 关键字判断这段是不是代码,而不是普通解释文字。遇到一条语句被 Word 分成了多段的情况,比如一个 SELECT 跨了三个段落,这个脚本会把后两段漏掉。我在实际处理时会在if里加一个状态变量,一旦当前段以(开头或上一行末尾不是分号,就继续收集下一段。教材例题大多一行一段,直接用上面的脚本也够用,但如果你手里的文档排版复杂,最好先打开 docx 看几段再决定要不要加强处理。
4.3 批量执行提取出的 SQL 并记录报错
提取出来的代码保存为extracted.sql,然后写一个 Python 脚本连接 MySQL 逐条执行,把报错的行号和数据打印出来。
import pymysql conn = pymysql.connect( host="127.0.0.1", user="root", password="你的密码", database="study", charset="utf8mb4", autocommit=True, ) cursor = conn.cursor() with open("extracted.sql", "r", encoding="utf-8") as f: content = f.read() for i, stmt in enumerate(content.split(";")): if not stmt.strip(): continue try: cursor.execute(stmt) except pymysql.MySQLError as e: print(f"第{i+1}条出错: {e}\n语句: {stmt[:80]}") cursor.close() conn.close()把autocommit=True写上去,是血泪经验:例题里有 UPDATE 和 DELETE,如果没提交,后面查询看不到变化,误以为是例子写错了。split(";")对简单例题够用,但文档里如果有存储过程或触发器定义,里面的分号会被错误切开。真遇到这种情况,改用cursor.execute(content)让它一次执行多语句,但代价是报错定位变难。我的建议是:先按分号拆,跑一遍,哪条报错再单独处理。
5. 例题实现避坑:分号、编码、空值与约束的 5 个排查记录
照着文档跑一遍,顺利的话半小时能完事。但绝大多数人会碰到下面几个坎。我梳理了 5 个高频问题,都是自己踩过的,按“现象 → 原因 → 解决”写清楚。
5.1 现象:最后的 UPDATE 执行了,前面的 INSERT 全丢
用图形工具或者 Python 脚本连上 MySQL,跑完整个文档,返回没有报错。关掉连接再重新打开,发现 Student 表还是空的。原因很简单:连接默认开启了事务,你在未提交状态下执行了 INSERT,最后的 UPDATE 也是未提交,如果连接是非自动提交模式,退出时回滚了。解决:在每个 DML 语句后手动加COMMIT;,或者在连接参数里设置autocommit=True。批量执行脚本尤其要检查这一项,文档里的代码是教材样例,不会自动帮你提交事务。
5.2 现象:中文注释变成乱码,导致 SQL 报错
文档里如果代码加了一行中文注释,比如-- 查询计算机系学生,提取出来保存成文件后,用 MySQL 执行报错信息里出现??或汉之类的字符。原因是文件编码和 MySQL 连接编码不一致。解决:保存 .sql 文件时强制用 UTF-8,并确认连接参数里写了charset="utf8mb4"。如果你在 Windows 记事本里另存为,别选“ANSI”,选“UTF-8”。更稳妥的方案是提取脚本里直接以 UTF-8 写入文件,不要再用记事本改。
5.3 现象:选了“1号课程”但嵌套查询返回空
比如例6的变体,查询选了课程 ‘1’ 的学生姓名,子查询SELECT Sno FROM SC WHERE Cno='1'单独跑有结果,外层查出来却是空。多数情况是Cno的数据类型问题:表定义时用了CHAR(4),插入时字符串'1'会被补成'1 '(三个空格)。MySQL 的 CHAR 比较会自动去掉尾部空格,所以Cno='1'能匹配;但如果表定义用的是VARCHAR(4),就不会补空格,查询条件里多一个空格都能导致匹配失败。解决:统一用 CHAR 存储定长代码,连接时也不要在字符串里带多余空格。还有更隐蔽的:从 Word 复制时,'1'变成了全角引号'1',查询条件不认识全角引号,也会空。遇到空结果先把引号换成半角试试。
5.4 现象:外键表插入顺序反了,报 1452 错误
执行建表后,直接粘贴 SC 表的 INSERT,报Cannot add or update a child row: a foreign key constraint fails (1452)。原因很简单:SC 表引用了 Student 和 Course,但这两个主表里还没有数据,外键校验不通过。解决:按依赖顺序插入,先 Student、再 Course、最后 SC。如果文档里的代码顺序被打乱了,不要急着执行,先看 INSERT 出现的先后,把主表数据补上。还有一种情况是主表里有数据,但插入的学号在 Student 里不存在,比如把201215121错抄成201215124,这种 1452 也一样报。
5.5 现象:在 MySQL 里跑了 SQL Server 的 TOP 语法
有些教材版本按 SQL Server 编写,例题里写了SELECT TOP 3 * FROM Student,MySQL 直接语法错误。原因就是方言差异。解决:把TOP n改成LIMIT n,并且放在语句最后;同理,GETDATE()在 MySQL 里是NOW(),ISNULL()是IFNULL(),字符串连接符+要改成CONCAT()。我的习惯是先把文档里的方言关键字全局替换一遍再执行,不要指望 MySQL 能自动兼容。如果手里有 Oracle 教材,还要把ROWNUM换成LIMIT,把SELECT 1 FROM dual改成 MySQL 能认识的写法。
6. 最后一步:用 EXPLAIN 和回归脚本验证例题查询的正确性
例题跑通只是第一步,你得知道这些查询到底是怎么执行的,才算真正掌握第3章。
6.1 用 EXPLAIN 检查查询计划,别只信结果
EXPLAIN能让你看到 MySQL 内部怎么执行这条 SQL。比如嵌套查询:
EXPLAIN SELECT Sname FROM Student WHERE Sno IN (SELECT Sno FROM SC WHERE Cno = '1');重点看type和rows两列。如果type是ALL,说明全表扫描,对教材里的几行数据无所谓,但对真实场景就是灾难。你可以把这条查询改写成 JOIN:
EXPLAIN SELECT Student.Sname FROM Student JOIN SC ON Student.Sno = SC.Sno WHERE SC.Cno = '1';对比两次的rows估算,通常 JOIN 写法能更清晰地看出驱动表和被驱动表。这个对比正是第3章隐含的训练目标:结果一样,执行方式不一样。
6.2 把例题查询变成可重复的回归断言
学习代码时最怕改了一个地方,之前跑通的结果全受影响。我自己的做法是把每个例题的预期结果写成 Python 断言,每次改完脚本重新跑一遍。
import pymysql conn = pymysql.connect( host="127.0.0.1", user="root", password="你的密码", database="study", charset="utf8mb4", autocommit=True, ) def query(sql): with conn.cursor() as cur: cur.execute(sql) return cur.fetchall() # 例1预期:CS 系两个学生 assert query("SELECT Sname FROM Student WHERE Sdept='CS'") == ( ('李勇',), ('刘晨',), ) # 例7预期:课程2有2人选,课程3有2人选 assert query("SELECT Cno, COUNT(*) FROM SC GROUP BY Cno") == ( ('1', 1), ('2', 2), ('3', 2), ) print("全部断言通过")把这段保存成test_examples.py,每次跑完都执行一次。这个习惯帮我在考试前把教材第3章的所有例题变成了自动化测试,比反复抄写有效得多。后来工作中接手别人留下的 SQL 脚本,我第一件事也是写断言把关键查询锁住,再动优化。希望帮到你。
本文还有配套的精品资源,点击获取