ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库工程能力压力测试:从ER建模到SQL避坑实战

数据库工程能力压力测试:从ER建模到SQL避坑实战

简介:本资源为中南大学数据库课程期末考试真题试卷及配套复习纲要,面向计算机、软件工程等专业本科生及数据库自学者,助力系统梳理核心考点、查漏补缺与应试冲刺。试卷涵盖选择题、填空题、术语解释与简答题四大题型,内容紧扣数据库原理教学重点,包括三级模式结构、ER模型与关系模型对比、SQL语言分类(DDL/DML)、索引机制、事务ACID特性、并发控制问题(如丢失更新)、规范化理论(1NF/2NF/3NF)及数据库设计六步法等高频考点。资源为单个Word文档(.doc),大小164KB,结构清晰:前半部分为29页完整试题与标准答案,后半部分附带7章详细复习提纲,每章标注重点与作业提示,便于分模块精读与背诵。目前已有355人学习下载,是备考高校数据库课程考试的高价值实战资料。

1. 中南大学数据库试题:不是题库,是关系型数据库工程能力的「压力测试清单」

这不是一份拿来背答案的期末复习卷,而是一套被反复验证过、能真实暴露你数据库工程能力断层的「压力测试清单」。我带过三届数据库课程设计,每次开课前都会把这份材料拆开重跑一遍——不是为了对答案,而是看哪些知识点在实操中会突然“失联”:比如写一个看似简单的SELECT ... WHERE 子查询,学生常卡在NOT EXISTS的双重否定嵌套逻辑里;又比如建索引时,明明写了CREATE CLUSTERED INDEX,执行却报错说“表已存在聚簇索引”,结果发现是主键默认建了聚簇索引,自己又重复建了一次;再比如规范化分析题里,“一个系只有一名主任”这个业务约束,若没意识到它隐含所在系 → 系主任的函数依赖,后续推导 3NF 就全盘崩塌。

它覆盖的不是碎片化考点,而是数据库工程师日常要闭环处理的完整链路:从 ER 图建模(CH2)→ 关系模式转换(CH3)→ SQL DDL/DML 实现(CH4)→ 并发控制与事务隔离(CH5)→ 规范化诊断与重构(CH6)→ 应用系统级设计(CH7)。每道题背后都对应一个真实场景:第 5 题“求选修了全部课程的学生”,本质是生产环境里常见的「用户全量行为匹配」问题;第 7 题“插入/更新学生记录”,直指 OLTP 系统中高频的 CRUD 边界校验;而试卷(2)第 14 题问“实现事务隔离性的子系统”,答案是并发控制子系统——这恰恰是高并发电商下单时库存扣减不超卖的核心防线。

适合谁?正在准备数据库原理考试的学生,但更关键的是:刚接手公司遗留数据库维护任务的初级 DBA、需要写复杂报表 SQL 的 BI 工程师、或是正为微服务间数据一致性头疼的后端开发者。它不教你怎么考高分,而是逼你回答:“当SELECT * FROM student WHERE age > (SELECT MAX(age) FROM student WHERE dept='IS')在千万级表上执行超时,你第一反应是加索引、改写子查询,还是怀疑统计信息过期?”——这种肌肉记忆,才是这份试题真正的价值。


2. 从 ER 模型到关系模式:为什么你的 E-R 图总在转换时“漏掉”关键约束?

ER 模型是数据库设计的起点,但多数人只把它当绘图工具,忽略了它和后续 SQL 实现之间的强耦合。中南大学试题里 CH2 和 CH6 的题目(如试卷(3)第五大题第 2 小题)反复验证一个事实:ER 图里没画出来的约束,SQL 里就永远缺一条 CHECK 或 FOREIGN KEY。下面拆解三个最易翻车的转换环节。

2.1 键约束的 ER 显式化:别让“唯一性”藏在文字描述里

看试卷(1)第 6 题:

借阅(书号、书名,库存数,读者号,借期,还期),假如同一本书允许一个读者多次借阅,但不能同时对一种书借多本。

表面看是业务规则,实则是复合主键 + 唯一性约束的 ER 编码。

  • “同一种书不能同时借多本” →(书号, 读者号)必须唯一,但注意不是主键(因允许同一读者多次借同一本书,只要时间不重叠),而是UNIQUE(书号, 读者号)约束;
  • “不能同时借多本”的“同时”二字,暗示需配合借期/还期字段做范围检查,这已超出基础约束范畴,需触发器或应用层控制。

提示:ER 图中,必须用双线椭圆标出“候选键”,用双线矩形标出“弱实体”,否则转换时必然遗漏。试卷(2)第 2 题问“n:m 联系如何实现”,标准答案是“增加一个关系”,但没说清这个新关系的主键必须是两端实体主键的组合——这就是 ER 图里用菱形连接线标注n:m后,必须在菱形内写明“借阅ID”或直接标(书号, 读者号)作为主键。

2.2 弱实体与标识关系:为什么你的外键总报“违反参照完整性”?

试卷(3)第五大题第 2 小题给出的关系模式:

学号,姓名,所在系,系主任,课程名,成绩

并说明:“一个系只有一名主任”。
这里系主任是弱属性,其存在完全依赖于所在系。若直接建表:

CREATE TABLE student ( sno CHAR(10) PRIMARY KEY, sname VARCHAR(20), dept VARCHAR(20), dept_head VARCHAR(20), -- ❌ 错误:未关联到系表 course_name VARCHAR(50), score DECIMAL(4,1) );

问题来了:dept_head怎么保证不填错?如果某系换主任,要批量 UPDATE 所有学生记录?正确做法是拆成两个表,并用外键强制关联:

-- 先建系表,主键即系名(或独立系ID) CREATE TABLE department ( dept_name VARCHAR(20) PRIMARY KEY, dept_head VARCHAR(20) NOT NULL ); -- 学生表引用系表 CREATE TABLE student ( sno CHAR(10) PRIMARY KEY, sname VARCHAR(20), dept_name VARCHAR(20) NOT NULL, course_name VARCHAR(50), score DECIMAL(4,1), FOREIGN KEY (dept_name) REFERENCES department(dept_name) -- ✅ 强制参照 );

参数说明:FOREIGN KEY后的字段类型、长度、是否允许 NULL 必须与被引用表的主键完全一致。试卷(1)第 10 题选项 D “E-R 模型” 正是强调这种实体-联系抽象,而非直接映射字段。

2.3 多值属性与派生属性:别把“计算字段”当普通列存

试卷(1)第七大题第 6 小题要求:

查询选修了全部课程的学生学号和姓名

其参考答案用NOT EXISTS嵌套,本质是关系代数除法运算的 SQL 实现。但很多学生试图用COUNT(*)统计课程总数再比对,这就踩进了“派生属性陷阱”——学生选课数是派生值,不应作为表字段存储,而应在查询时实时计算。

正确思路是:

  1. 获取所有课程集合C;
  2. 对每个学生S,检查C - S.选课集合是否为空;
  3. 若为空,则S选修了全部课程。

SQL 实现必须用NOT EXISTS(如试卷(1)参考答案所示),因为COUNT方案在并发环境下可能因读取时机不同导致幻读。ER 图中,这类属性应标为虚线椭圆(派生属性),明确告知设计者“此值不可存,需计算”。


3. SQL 实战避坑:DDL/DML 语句里藏着的 5 个“静默失败”陷阱

中南大学试题的 SQL 部分(CH4、试卷六、七)看似简单,但每道题都是生产环境常见故障的微缩版。我曾帮某高校教务系统排查过一次慢查询,根源竟是试卷(1)第六大题第 3 小题的索引写法:CREATE INDEX teach ON teacher(bh);—— 表名写错成teach,索引根本没建上,而开发人员只查了SHOW INDEX却没验证索引是否生效。以下是实测高频的 5 个静默失败点。

3.1 索引创建失败却不报错:大小写、空格与关键字冲突

现象:执行CREATE INDEX teach ON teacher(bh);后SHOW INDEX FROM teacher;查不到索引,但命令返回成功。
原因:

  • MySQL 8.0+ 默认区分大小写,若表名实际为TEACHER,而语句写teacher,索引会建在不存在的表上(MySQL 不报错);
  • teach是部分版本的关键字(如 MariaDB 的TEACH函数),导致解析异常;
  • 字段名bh若在建表时定义为BH(大写),而索引语句用小写,在某些配置下会失败。

解决:

# 1. 先确认表名和字段名的真实大小写 SHOW CREATE TABLE teacher; # 2. 用反引号强制指定名称(推荐) CREATE INDEX `idx_teacher_bh` ON `teacher` (`BH`); # 3. 验证索引是否生效(关键!) EXPLAIN SELECT * FROM teacher WHERE BH = 123; # 查看 key 列是否显示 idx_teacher_bh

3.2 ALTER TABLE ADD COLUMN 语法差异:NULL 默认值的隐形炸弹

现象:试卷(1)第六大题第 2 小题ALTER TABLE teacher ADD COLUMN wcf int;在 MySQL 5.7 上执行后,新列wcf全为NULL,但业务要求“误餐费默认为 0”。
原因:不同数据库对ADD COLUMN的NOT NULL处理不同:

  • MySQL:若未指定NOT NULL,默认允许 NULL,且不设默认值;
  • PostgreSQL:必须显式声明DEFAULT 0,否则报错;
  • SQL Server:可设DEFAULT 0 WITH VALUES强制填充旧记录。

解决:

-- MySQL 安全写法(兼容旧数据) ALTER TABLE teacher ADD COLUMN wcf INT DEFAULT 0 NOT NULL; -- PostgreSQL 必须写 ALTER TABLE teacher ADD COLUMN wcf INTEGER DEFAULT 0; -- SQL Server 强制填充历史数据 ALTER TABLE teacher ADD wcf INT DEFAULT 0 NOT NULL; UPDATE teacher SET wcf = 0 WHERE wcf IS NULL;

3.3 视图 WITH CHECK OPTION 缺失:INSERT 时“合法”却破坏业务规则

现象:试卷(1)第六大题第 4 小题建视图BMV(部门为“经管系”),但未加WITH CHECK OPTION。之后执行INSERT INTO BMV VALUES ('张三', 5000, 8000);成功,但查SELECT * FROM teacher发现该记录部门为NULL或其他值。
原因:视图只是查询封装,INSERT实际写入基表。若视图条件为WHERE bm='经管系',但未加WITH CHECK OPTION,数据库不校验新行是否满足bm='经管系'。

解决:

-- ✅ 必须加上 WITH CHECK OPTION CREATE VIEW BMV AS SELECT XM, JBGZ, SFGZ FROM teacher WHERE bm = '经管系' WITH CHECK OPTION; -- 关键! -- 测试:以下 INSERT 将失败(因 bm 不是 '经管系') INSERT INTO BMV VALUES ('李四', 6000, 9000);

3.4 子查询中的 NULL 处理:IN/NOT IN 与 EXISTS 的语义鸿沟

现象:试卷(1)第七大题第 2 小题SELECT 成绩 FROM 选课 WHERE 学号 IN (SELECT 学号 FROM 学生 WHERE 姓名 LIKE '李%'),当学生表中学号有NULL时,结果为空。
原因:IN子句遇到NULL会返回UNKNOWN,导致整个条件失效(三值逻辑)。而EXISTS只关心是否存在匹配行,不受NULL影响。

解决:

-- ❌ 危险:IN 遇 NULL 失效 SELECT score FROM sc WHERE sno IN (SELECT sno FROM student WHERE sname LIKE '李%'); -- ✅ 安全:用 EXISTS 替代 SELECT score FROM sc s WHERE EXISTS ( SELECT 1 FROM student st WHERE st.sno = s.sno AND st.sname LIKE '李%' ); -- 或显式过滤 NULL SELECT score FROM sc WHERE sno IN ( SELECT sno FROM student WHERE sname LIKE '李%' AND sno IS NOT NULL );

3.5 DROP TABLE 的级联风险:删表前没查外键依赖

现象:试卷(1)第六大题第 5 小题DROP TABLE teacher;在有外键引用的库中执行失败,报错Cannot delete or update a parent row: a foreign key constraint fails。
原因:teacher表被其他表(如salary_log)的外键引用,MySQL 默认禁止删除。

解决:

-- 1. 先查依赖关系(MySQL 8.0+) SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = 'teacher'; -- 2. 临时禁用外键检查(仅开发环境!) SET FOREIGN_KEY_CHECKS = 0; DROP TABLE teacher; SET FOREIGN_KEY_CHECKS = 1; -- 3. 生产环境正确做法:先删子表,再删父表 DROP TABLE salary_log; -- 假设这是引用 teacher 的表 DROP TABLE teacher;

4. 关系代数到 SQL:除法运算的三种落地姿势与性能边界

试卷(1)第五大题第 4、5 小题和试卷(2)第五大题第 4 小题,核心都是关系代数中的除法运算(Division):求“选修了全部课程的学生”。这是数据库理论中最易混淆的操作,也是 SQL 实现时性能差异最大的场景之一。很多人死记硬背NOT EXISTS嵌套,却不知其背后的执行计划代价。下面给出三种可落地的方案,并标注适用规模。

4.1 标准解法:双重 NOT EXISTS(理论严谨,中小表首选)

这是教材和试题的标准答案,逻辑清晰,符合关系代数定义。

-- 求选修了全部课程的学生学号和姓名 SELECT s.sno, s.sname FROM student s WHERE NOT EXISTS ( -- 找出一门课程 c,该学生 s 没选修 SELECT 1 FROM course c WHERE NOT EXISTS ( -- 检查学生 s 是否选修了课程 c SELECT 1 FROM sc WHERE sc.sno = s.sno AND sc.cno = c.cno ) );

逻辑说明:外层NOT EXISTS表示“不存在这样的课程 c”,内层NOT EXISTS表示“学生 s 没选 c”。双重否定即“对所有课程 c,学生 s 都选了”。
参数说明:sc表需在(sno, cno)上有联合索引,否则内层子查询会全表扫描sc表,复杂度 O(n×m×k),n=学生数,m=课程数,k=选课记录数。

4.2 优化解法:GROUP BY + HAVING COUNT(大数据量友好)

当课程数固定(如全校 200 门课),且sc表有(sno, cno)索引时,此法性能远超双重嵌套。

-- 假设 course 表共 200 门课 SELECT s.sno, s.sname FROM student s JOIN sc ON s.sno = sc.sno GROUP BY s.sno, s.sname HAVING COUNT(DISTINCT sc.cno) = 200; -- ✅ 直接比数量

优势:只需一次sc表扫描,GROUP BY在索引支持下是高效聚合。
注意:COUNT(DISTINCT sc.cno)必须与课程总数严格相等,若课程表动态增减,需用子查询获取实时总数:

HAVING COUNT(DISTINCT sc.cno) = (SELECT COUNT(*) FROM course)

4.3 工程解法:预计算选课向量(超大规模场景)

当学生数超百万、课程数超千门时,上述两种方法仍可能慢。此时应跳出 SQL 思维,用应用层预计算。

# Python 伪代码:用位图压缩选课状态 from bitarray import bitarray # 1. 初始化课程ID到bit位置的映射(课程ID需连续或哈希后映射) course_to_bit = {cno: i for i, cno in enumerate(course_ids)} # course_ids 来自 SELECT cno FROM course # 2. 对每个学生,生成选课位图 student_bitmaps = {} for sno, cno_list in student_courses.items(): # student_courses: {sno: [cno1, cno2, ...]} bitmap = bitarray(len(course_ids)) bitmap.setall(0) for cno in cno_list: if cno in course_to_bit: bitmap[course_to_bit[cno]] = 1 student_bitmaps[sno] = bitmap # 3. 全1位图即选修全部课程 full_bitmap = bitarray(len(course_ids)) full_bitmap.setall(1) full_students = [sno for sno, bm in student_bitmaps.items() if bm == full_bitmap]

适用场景:实时性要求不高(如T+1报表)、课程集合稳定。内存占用 = 学生数 × (课程数/8) 字节,100万学生、1000门课仅需 ~125MB 内存。
对比:SQLNOT EXISTS在同样数据量下,执行时间可能达分钟级。


5. 数据库保护实战:ACID 如何在事务中“肉眼可见”地失效?

试卷(1)CH5 和试卷(2)第四大题第 4 小题,都在考 ACID 特性,但真正有价值的不是背诵定义,而是理解它们在事务执行中如何被打破、又如何被修复。我曾在线上环境亲眼见过一个UPDATE语句因隔离级别设置错误,导致财务系统出现“幽灵扣款”——钱扣了两次,日志却只记一次。下面用中南试题的典型场景,还原 ACID 的“肉眼可见”时刻。

5.1 原子性(Atomicity):ROLLBACK 不是魔法,是日志的精确回放

现象:试卷(1)第七大题第 7 小题INSERT INTO 学生 VALUES(99001,'张强','男',22,'经管系');执行后,因磁盘满导致写入失败,但部分字段已落盘。
原因:原子性依赖WAL(Write-Ahead Logging)。InnoDB 中,事务开始时先写 redo log(重做日志),再修改 buffer pool;若崩溃,重启时用 redo log 恢复未刷盘的修改。但若 redo log 本身写失败(如磁盘满),则事务无法保证原子性。

验证方法:

-- 1. 查看当前事务日志状态 SHOW ENGINE INNODB STATUS\G -- 2. 检查 redo log 文件大小(避免写满) SELECT FILE_NAME, FILE_SIZE, MAX_SIZE FROM INFORMATION_SCHEMA.FILES WHERE FILE_TYPE = 'LOGFILE'; -- 3. 关键参数:innodb_log_file_size 应 >= 256M(生产环境) SHOW VARIABLES LIKE 'innodb_log_file_size';

教训:原子性不是绝对的。当innodb_log_file_size过小(如默认 48M),高并发写入时 redo log 循环覆盖过快,可能导致部分事务日志丢失。从那以后我每次部署新库,都强制走一遍innodb_log_file_size = 1G的调优流程。

5.2 一致性(Consistency):约束检查的时机决定数据“真假”

现象:试卷(1)第七大题第 8 小题UPDATE 学生 SET 学号=99089 WHERE 学号=95001;执行后,发现95001记录消失,但99089未出现。
原因:一致性由约束(Constraint)+ 触发器(Trigger)+ 应用逻辑共同保障。若学号是主键,此 UPDATE 本质是 DELETE+INSERT。若sc表有外键ON DELETE CASCADE,则95001的选课记录被级联删除;但若sc表无外键,或ON DELETE RESTRICT,则 UPDATE 因外键冲突失败,事务回滚——但应用层若没捕获异常,就会误以为“没执行”。

验证方法:

-- 1. 查外键定义(确认级联行为) SELECT CONSTRAINT_NAME, UPDATE_RULE, DELETE_RULE FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME WHERE kcu.TABLE_NAME = 'sc' AND kcu.COLUMN_NAME = 'sno'; -- 2. 强制开启外键检查(避免被禁用) SET FOREIGN_KEY_CHECKS = 1;

5.3 隔离性(Isolation):READ COMMITTED 下的“不可重复读”现场复现

试卷(2)第二大题第 7 小题问“DBMS 中实现事务隔离性的子系统”,答案是并发控制子系统。但真正重要的是:不同隔离级别下,同一 SQL 执行两次,结果为何不同?

复现步骤(MySQL):

-- 会话 A(设置 READ COMMITTED) SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; SELECT score FROM sc WHERE sno = '95001' AND cno = '1'; -- 返回 85 -- 会话 B(同时执行) START TRANSACTION; UPDATE sc SET score = 95 WHERE sno = '95001' AND cno = '1'; COMMIT; -- 会话 A 再执行 SELECT score FROM sc WHERE sno = '95001' AND cno = '1'; -- 返回 95!即“不可重复读”

原因:READ COMMITTED 级别下,每次 SELECT 都建立新快照,会看到其他已提交事务的修改。若需“可重复读”,必须用REPEATABLE READ(MySQL 默认),它在事务开始时创建一致性快照。

5.4 持久性(Durability):sync_binlog=1 不是银弹,fsync 才是命门

现象:数据库崩溃后,INSERT语句显示成功,但重启后数据丢失。
原因:持久性依赖binlog + redo log + fsync三重保障。若sync_binlog=0(默认),binlog 可能未刷盘;若innodb_flush_log_at_trx_commit=0,redo log 也未刷盘。

安全配置(生产环境必设):

-- MySQL 配置文件 my.cnf [mysqld] sync_binlog = 1 # 每次事务提交都刷 binlog 到磁盘 innodb_flush_log_at_trx_commit = 1 # 每次事务提交都刷 redo log 到磁盘 innodb_doublewrite = ON # 开启双写缓冲,防页损坏

注意:innodb_flush_log_at_trx_commit=1会降低写入性能,但这是持久性的底线。从那以后我每次上线新服务,都强制用mysqlslap压测写入性能,确保在=1配置下 QPS 仍满足 SLA。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进