简介:《数据库系统概论》期末考试试题完整Word版,面向高校计算机及相关专业备考学生与任课教师,用于课程复习、模拟自测、重点梳理和组卷参考。压缩包共1个doc文件,大小约170KB,内容按期末考试常见题型组织,包含选择题、填空题、判断题、简答题和应用题,涉及物理数据独立性、数据库系统特点、关系模型与关系代数、SQL、视图与授权、规范化、数据库设计步骤、事务原子性与隔离性、封锁机制、并发控制及日志恢复等知识点。部分题目配有答案要点、解析或示例,便于对照理解外码、无损连接、可串行化调度等难点。当前已有3503人学习下载,适合按章节查漏补缺、训练综合题答题思路,也可供教师课堂练习与命题时参考,并可作为期末冲刺阶段的系统复盘材料。
1. 从一份 2004 年的 Word 卷子说起:它凭什么还能当训练集
互联网上流传的数据库期末卷子,绝大多数是手机拍的图、只有答案没有题干的截图,或者十几页 PDF 里夹杂着别校的题。这份《数据库系统概论》期末考试试题的 Word 完整版算是少见的例外:2003-2004 学年 A、B、C 三套卷加上 2004-2005 学年 D2002 级《数据库原理与应用》一套,选择题、填空题、判断题、简答题、应用题、综合题六种题型齐全,而且每道大题后面都跟着参考答案和一部分中间推导。它真正值钱的地方不是"押题命中率"这种玄学,而是它把闭包计算、候选码求解、范式分解、无损连接判定、ER 图转关系模型、封锁协议这些高频考点全都留下了带过程的解题痕迹——你能看到出题人怎么从函数依赖一步步推到 BCNF,也能看到 Chase 表格法填到什么程度算完。适合期末复习的学生,也适合要出卷、要布置作业的教学岗,更适合想把关系数据库理论重新过一遍的工程人。
2. 拆文档:把四套卷子切成带知识点标签的题库
2.1 先看清文档的层次结构
打开文档别急着做题,先摸清它的排版骨架。这份 Word 是三层嵌套:最外层是学年块("2003-2004 学年第二学期期末考试""2004-2005 学年第二学期期末考试"),中层是试卷代号(试题 A、试题 B、试题 C、D2002 级),内层是题型块(一、选择题 二、填空题 三、判断题 四、简答题 五、应用题 六、综合题),题号再往下切。
麻烦在于题型块的分值不一致:C 卷是选择题 20 分每小题 2 分、填空题 20 分每小空 2 分、简答题 15 分、综合题 45 分;D2002 卷则是选择题 15 分每空 1 分、填空题 15 分、判断题 10 分每小题 5 分,后面还多出应用题。所以切题绝不能拿分值当锚点,只能靠"题型标题 + 题号正则"两把刀。
| 切分键 | 正则或判定依据 | 备注 |
|---|---|---|
| 学年块 | (20\d{2})\s*[--]\s*(20\d{2})\s*学年 | 决定题目归属年份 |
| 题型块 | ^[一二三四五六]、后跟题型名与分值 | 决定题型标签 |
| 题号 | ^\d{1,2}[..、] | 决定一题一条记录 |
| 选项 | ^[A-D][..] | 并入上一题,不单独成条 |
| 答案段 | 紧跟题干、以"解""答""结论"开头 | 与题干分开存 |
2.2 老 .doc 先转 .docx,不转会直接翻车
血泪经验:python-docx只认 OOXML 格式,拿它去打开.doc会抛PackageNotFoundError。必须先做一次格式转换,命令行一条就够:
# 用 LibreOffice 无头模式把老 doc 转成 docx,输出到 out/ 目录 soffice --headless --convert-to docx --outdir out/ "数据库系统概论期末考试试题.doc" # 转完立刻检查段落数和表格数,表格丢了后面无损连接题就没法看 python -c " from docx import Document d = Document('out/数据库系统概论期末考试试题.docx') print('段落数', len(d.paragraphs), '表格数', len(d.tables)) "转换会保留段落顺序和绝大部分文字,但有两个副作用要提前知道:一是原文档里的上划线、π、σ、∞ 这类数学符号本来就是图片或特殊字体,转完大概率变成空白或方框;二是无损连接题的二维表在 Word 里可能是用空格对齐的伪表格,转换后不会还原成真正的 table 对象。所以转换后必须跑一次段落数和表格数的核对,数字对不上就说明有内容掉了,得回头对照原文档补。
2.3 按题型切题并挂知识点标签
结构清楚了就能写切题脚本。核心思路是逐段扫描,命中学年块就切换当前年份,命中题型块就切换当前题型,命中题号就新建一条记录,其余段落当作续行并入上一条:
import re, json from docx import Document doc = Document("out/数据库系统概论期末考试试题.docx") lines = [p.text.replace("\u3000", " ").strip() for p in doc.paragraphs] lines = [t for t in lines if t] # 丢掉空段,减少噪声 YEAR = re.compile(r"(20\d{2})\s*[--]\s*(20\d{2})\s*学年") SECTION = re.compile(r"^[一二三四五六]、\s*(.+?)((\d+)\s*分") ITEM = re.compile(r"^(\d{1,2})[..、]\s*") # 标签词典:命中多个标签时按最长关键词优先,避免"关系"误吞"关系代数" TAG_RULES = { "函数依赖与范式": ["函数依赖", "候选码", "闭包", "范式", "BCNF", "无损连接"], "关系代数": ["关系代数", "投影", "元组演算", "自然连接", "选取"], "SQL": ["SELECT", "GRANT", "CREATE VIEW", "SQL"], "ER模型": ["E-R", "ER 图", "实体", "联系类型", "主键", "外键"], "事务与并发": ["事务", "封锁", "S 锁", "X 锁", "并发", "可串行化"], "恢复与完整性": ["故障", "恢复", "完整性", "安全性", "授权"], } records, cur = [], {"year": None, "section": None} for t in lines: if m := YEAR.search(t): cur["year"] = f"{m.group(1)}-{m.group(2)}" continue if m := SECTION.match(t): cur["section"] = m.group(1).strip() continue if m := ITEM.match(t): hit = [k for k, kws in TAG_RULES.items() if any(kw in t for kw in kws)] records.append({"year": cur["year"], "section": cur["section"], "no": int(m.group(1)), "stem": ITEM.sub("", t), "tags": hit or ["未分类"]}) elif records: records[-1]["stem"] += " " + t # 续行并入上一题 with open("bank.jsonl", "w", encoding="utf-8") as f: for r in records: f.write(json.dumps(r, ensure_ascii=False) + "\n") print("题量", len(records), "未分类", sum(r["tags"] == ["未分类"] for r in records))参数上有三处可以调:TAG_RULES里关键词越长越好,单字关键词会把选择题选项里的"关系"两字全吸进来;ITEM允许 1 到 2 位题号,是因为综合题的小问会出现"1."这种编号,需要和主问题区分;records[-1]["stem"] +=那行是兜底逻辑,遇到跨段的题干必须靠它,否则你只会拿到半句话。
2.4 抽检:结构化结果必须自己核对一遍
切完不能直接信。抽检四个动作,五分钟能做完:第一看题号连续性,同一题型块内题号应当从 1 开始递增,出现跳号说明切题正则漏了;第二看重灾区"未分类"占比,超过三成基本是标签词典没覆盖;第三看表格数,无损连接、ER 图那几道题的表格如果为 0,说明原文档是伪表格,得手工补录;第四做去重,把题干去掉标点和空格后取 md5,同一年 AB 卷之间常有重复题,重复的只保留一份,否则后面统计薄弱点会被同一道题拉偏。
3. 闭包与候选码:手算套路加一段脚本对拍
3.1 为什么先拿闭包开刀
闭包是函数依赖这一整块题的地基,不是因为它最难,而是因为它被反复调用:判候选码要算属性闭包,判 2NF 要看非主属性对码的部分依赖,判 3NF 要看传递依赖,Chase 算法判无损连接每一步都是在推闭包,连最小函数依赖集都要靠闭包做冗余检查。这一道题的解法没吃透,后面所有大题都是猜。文档里那道F = { D→G,C→A,CD→E,A→B }求D+、(AC)+、(ACD)+就是标准的入门题,答案给的是DG、ABC、ABCDEG,手推一遍能立刻暴露出你是漏了传递还是忘了并集。
3.2 手算闭包的固定五步
我一般固定这么走:第一步把初始集合写下来当X(0);第二步扫一遍 F,找出左部完全落在当前集合里的依赖;第三步把它的右部并进来,记为X(i+1);第四步比较X(i)和X(i+1),有变化就回第二步;第五步直到不再变化,最后那个集合就是闭包。
拿(ACD)+走一遍:X(0)=ACD;D→G左部在,得ACDG;C→A左部在但 A 已在,无变化;CD→E左部 C、D 都在,得ACDEG;A→B左部在,得ABCDEG;再扫一遍没有任何右部能新增,收敛。整个过程写在纸上不超过六行,比在脑子里默算可靠得多。
提示:手算时每轮只写新增的属性,别重抄整个集合,抄写过程本身就是最容易出错的地方。
3.3 候选码:先缩范围,再逐个验最小性
求全部候选码有个省时间的套路,按属性在 F 中的出现位置分四类:只在左部出现的叫 L 类,只在右部出现的叫 R 类,两边都出现的叫 LR 类,两边都不出现的叫 N 类。结论很干脆——L 类和 N 类属性必定出现在每一个候选码里,R 类属性必定不在任何候选码里,LR 类需要枚举验证。
文档里那道U={A,B,C,D,E}、F={A→BC, CD→E, B→D, E→A}的题,五个属性全是 LR 类,缩不了范围,只能枚举。但枚举也不是瞎试,从单属性开始:A+=ABCDE,A 单独成码;B+=BD、C+=C、D+=D、E+=ABCDE,E 单独成码;再看两属性组合,BC+=BCDEA成立且 B、C 单独都不成码,所以 BC 是最小超键;CD+=CDEAB同理成立。最终答案A、BC、CD、E四个。注意"最小性"这一步不能省,AB也能推出全属性,但 A 单独就够了,AB 就不是候选码。
3.4 脚本对拍:把三个例子一次跑完
手算容易自我说服,所以我习惯写个二十行的脚本对拍,答案不一致就以脚本为准再回头查推导:
from itertools import combinations def closure(attrs, fds): """attrs: 初始属性集合;fds: [(左部set, 右部set)],返回闭包""" res = set(attrs) changed = True while changed: # 一轮轮扫,直到不再新增属性 changed = False for left, right in fds: if left <= res and not right <= res: res |= right changed = True return res def candidate_keys(all_attrs, fds): """按属性个数从小到大枚举,命中超键后再验最小性,保证只留候选码""" keys = [] for size in range(1, len(all_attrs) + 1): for comb in combinations(sorted(all_attrs), size): s = set(comb) if closure(s, fds) != set(all_attrs): continue if all(closure(s - {a}, fds) != set(all_attrs) for a in s): keys.append("".join(sorted(s))) return keys F = [({"A"}, {"B", "C"}), ({"C", "D"}, {"E"}), ({"B"}, {"D"}), ({"E"}, {"A"})] U = set("ABCDE") print(closure({"A", "C", "D"}, F)) # 预期 ABCDEG 级的结果,用于核对 (ACD)+ print(candidate_keys(U, F)) # 预期 ['A', 'BC', 'CD', 'E']closure用集合包含判断代替逐属性比对,写法短但语义等价;candidate_keys的关键在最后那行all(...),它把"去掉任一属性后不再是超键"翻译成代码,缺了这一步你会把AB、ACE这类非最小超键一起收进来。换题的时候只改F和U两个变量,五分钟能核完一整张卷子的候选码题。
4. 范式分解与无损连接:从 1NF 走到 BCNF 的判定链
4.1 判范式只看两件事
判范式不需要背定义,只需要盯住两件事:候选码是什么,非主属性对候选码有没有部分依赖和传递依赖。1NF 只要求属性原子,这份卷子里的题基本都默认满足;2NF 的门槛是非主属性完全函数依赖于候选码;3NF 再要求非主属性不传递依赖于候选码;BCNF 最严,要求每一个非平凡函数依赖的决定因素都必须是超键。
拿文档里那道职工日产量题看:R(职工编号,日期,日产量,车间编号,车间主任),基本 FD 是(职工编号,日期)→日产量、职工编号→车间编号、车间编号→车间主任,候选码是(职工编号,日期)。日产量、车间编号、车间主任都是非主属性,而职工编号→车间编号的左部只是候选码的一部分,这就是部分依赖,所以 R 只是 1NF。这个判断链条很短,但前提是候选码必须先求对,很多人卡在范式上其实是卡在候选码。
4.2 分解的固定动作:先消部分依赖,再消传递依赖
分解有固定顺序,跳步会出问题。第一步消部分依赖:把候选码拆开,让每个非主属性都跟着完整码走。上面的例子拆成R1(职工编号,车间编号,车间主任)和R2(职工编号,日期,日产量),R2 里日产量完全依赖于完整码,达标。
第二步查传递依赖:R1 里有职工编号→车间编号和车间编号→车间主任,于是职工编号→车间主任是传递依赖,R1 不是 3NF,继续拆成R11(职工编号,车间编号)和R12(车间编号,车间主任)。R11 的决定因素是候选码职工编号,R12 的决定因素是候选码车间编号,两个都是 BCNF。
文档里另外两道题更能说明"逐步"两个字的含义。STUDENT(S#,SNAME,SDEPT,MNAME,CNAME,GRADE)候选码是(S#,CNAME),但S#→SNAME,SDEPT,MNAME是部分依赖,先拆成R1(S#,SNAME,SDEPT,MNAME)和R2(S#,CNAME,GRADE);R1 里还有SDEPT→MNAME的传递依赖,再拆成R11(S#,SNAME,SDEPT)和R12(SDEPT,MNAME),此时三个模式的决定因素全部是各自候选码,达到 BCNF。R(W,X,Y,Z)、F={X→Z, WX→Y}那道同理,候选码 WX,X→Z是对码的部分依赖,一拆就到位:R1(W,X,Y)和R2(X,Z)。
注意:分解到 BCNF 通常能保证无损连接,但不一定保持函数依赖,卷子里那道 BCNF 题最后两个模式恰好都保持了依赖,属于运气好,别把它当通例。
4.3 无损连接判定:Chase 表格法四步
无损连接的判定用 Chase 表格法,四个动作:第一步画表,行数等于分解后的子模式数,列数等于全部属性数,属性出现在该子模式里就填a加列号,否则填b加行列号;第二步扫 F 里每个依赖,找左部各列取值完全相同的行;第三步把右部那列的取值统一,如果其中一个是a值就全改成a值;第四步重复二三步直到表不再变化,最后看有没有一行全是a值,有就是无损连接。
用卷子里U={A,B,C,D,E}、F={AB→C, C→D, D→E}、ρ={R1(ABC), R2(CD), R3(DE)}走一遍:初始表里 R1 行的 A、B、C 是a值,R2 行的 C、D 是a值,R3 行的 D、E 是a值。先用C→D,C 列上 R1 是a3、R2 是a3,取值相同,把 D 列统一成a4,R1 的 D 变成a4;再用D→E,D 列 R1、R2、R3 都是a4,把 E 列统一成a5;此时 R1 行变成a1 a2 a3 a4 a5,整行全a,判定无损。原文档给的最终表里列值是残缺的,中间填充过程也没写,照着抄容易得出相反结论。
4.4 用脚本验证分解是否无损
Chase 的过程机械,最适合交给代码。下面这个简化实现教学规模够用:
def lossless(attrs, decomp, fds): """attrs: 全部属性列表;decomp: [{'A','B','C'}, ...];fds: [(左部set, 右部set)]""" idx = {a: i for i, a in enumerate(attrs)} rows = [] for r, sub in enumerate(decomp): rows.append({a: (f"a{idx[a]}" if a in sub else f"b{r}{idx[a]}") for a in attrs}) changed = True while changed: changed = False for left, right in fds: for i in range(len(rows)): for j in range(len(rows)): if i == j or not all(rows[i][a] == rows[j][a] for a in left): continue for a in right: # 右部取值统一,优先保留 a 值 keep = rows[i][a] if rows[i][a].startswith("a") else rows[j][a] if rows[i][a] != keep or rows[j][a] != keep: rows[i][a] = rows[j][a] = keep changed = True return any(all(v.startswith("a") for v in row.values()) for row in rows) attrs = list("ABCDE") print(lossless(attrs, [set("ABC"), set("CD"), set("DE")], [({"A", "B"}, {"C"}), ({"C"}, {"D"}), ({"D"}, {"E"})])) # Truedecomp传的是子模式属性集合的列表,fds里的左右部都要用 set,否则包含判断会失效;keep那行实现的是"a 值吸收 b 值"的规则,教学例子都能收敛。想验证保持函数依赖,就再写一小段把每个原始 FD 投影到子模式上取闭包比对,比手推快得多,也更不容易自我欺骗。
5. 避坑排查:答案残缺、符号丢失与全角陷阱
5.1 答案层面的三个坑
坑一:判断题的答案与题干双重否定,抄着抄着就抄反了。现象是原文写"如果属性 X 在函数依赖的左右都不出现,则候选码中必不包含 X",后面跟的结论是"结论错误",紧接着的解释又说"则候选码中必包含 X"。原因在于题干本身用了一个否定表述,答案先判对错再解释,两句话连读极容易让人误记成"候选码不一定含 X"。解决方法是把这类题单独抽成一张"易混淆判断题"表,只保留题干和正确表述,把"结论错误"这三个字删掉,避免复习时被判定语带跑。
坑二:Chase 表格的中间过程缺失。现象是无损连接那道题只给了一张"最终表",行列对不齐,E 列的值整个缺掉。原因是出题人省略了逐步填充过程,只贴结果,而 Word 排版又把表格压变形了。解决办法是拿 4.4 的脚本重算一遍,把每一轮变化的中间表都打印出来,对照着补回文档。凡是文档里只给结果不给过程的推导题,一律当作"待验证"处理,这是我不吃后悔药的基本操作。
坑三:外键计数只给数字不给清单。现象是 ER 转关系模型那题最后写"主键 10 个、外键 13 个",但正文里没有逐个标出哪个属性是外键。原因是答案默认读者能从下划线波浪线里自己数出来,可转换后的纯文本根本没有下划线。解决方式是转成关系模式后单独列一张外键登记表,一个关系一个关系地填,填完总数自然对得上,对不上就说明漏了某个 1:N 联系带来的外键。
5.2 格式与符号层面的两个坑
坑四:关系代数和元组演算符号在转换后集体消失。现象是转完 docx 或复制到 Markdown,π SNAME(σ TEACHER='程军'(C))只剩一堆字母和空白,∧、∀、∃全变方框。原因是原文档这些符号是用特殊字体或域代码插入的,纯文本提取拿不到。解决办法是拿同一道题的 SQL 答案反推符号位置,再用 Unicode 字符π σ ∞ ∧ ∀ ∃重写一遍,重写后放进 Markdown 里肉眼核对一次。
坑五:全角符号混排导致 SQL 直接报错。现象是把卷子里的SELECT SN,AGE,SEX FROM S WHERE AGE>(SELECT AGE FROM S WHERE SN='王华')粘进客户端执行,报语法错误。原因是原文的引号是全角,S#、C#的井号也是全角,GRADE.后面跟的是全角句点。解决办法是在题库里加一道数据清洗:把#换成#、全角引号换半角、全角括号换半角,然后建三张测试表S(S#,SN,SEX,AGE,DEPT)、C(C#,CN)、SC(S#,C#,GRADE)真实跑一遍。凡是要跑的 SQL 题,不跑一遍不算复习过。
提示:建测试表时记得给
S#、C#加主键,SC上给(S#,C#)建联合主键,否则外键约束题没法验证。
6. 进阶用法:用这份题库做薄弱点诊断和二次命题
题库结构化之后,复习方式可以从"刷题"升级成"诊断"。跑一遍标签统计,哪个知识点占的题量最大、你错得最多,薄弱点就浮出来了:
import json from collections import Counter rows = [json.loads(l) for l in open("bank.jsonl", encoding="utf-8")] stat = Counter(tag for r in rows for tag in r["tags"]) for tag, n in stat.most_common(): print(f"{tag}\t{n} 道") # 先看题量分布,再叠加自己的错题数算错误率拿到分布后按这个处置表行动,比漫无目的地刷有用得多:
| 标签 | 题量特征 | 处置动作 |
|---|---|---|
| 函数依赖与范式 | 占比最高,且多集中在综合题 | 用第 3 章的脚本把每道题重算一遍,错一题补一个变式 |
| 关系代数与元组演算 | 符号残缺严重 | 先用 SQL 反推重写符号,再做关系代数到 SQL 的双向翻译练习 |
| ER 模型 | 只有一到两道大题 | 直接做二次命题:把 7 个实体 8 个联系改成 6 个实体 7 个联系,重数主外键 |
| 事务与并发 | 以选择题和简答为主 | 把 S 锁、X 锁的组合画成兼容矩阵,默写一遍 |
二次命题是这份卷子最被低估的用法。它给的原题带答案,改一个参数就能生成一道新题:把F={A→BC, CD→E, B→D, E→A}里的E→A去掉,候选码立刻从四个收缩到两个;把无损连接那道分解从{ABC, CD, DE}改成{AB, CDE},Chase 表跑完大概率不是无损,正好用来验证你是不是真会判。改完用脚本算出答案,再和手推结果对一遍,一致了才算这道题过关。
我踩过最贵的一次坑,是把卷子里的参考答案当标准答案背了一整周,直到上机建表时发现某道 SQL 的字段名在全角半角之间反复横跳,跑一条错一条,连带把依赖它判断的那几个范式结论也带偏了。从那以后我每次拿到这类题库资源,都强制走一遍"转换 → 切题 → 打标签 → 脚本对拍 → 上机跑 SQL"五步,宁可多花两小时,也不拿没验证过的答案当依据。希望帮到你。
本文还有配套的精品资源,点击获取