可直接运行的数据库系统概论课程设计包:MySQL导入建表与事务实验避坑

发布时间:2026/10/27 11:04:17
可直接运行的数据库系统概论课程设计包:MySQL导入建表与事务实验避坑

简介:山东科技大学数据库系统概论课程设计是一份面向高校数据库课程学习者与初学者的实践资料,围绕数据表的创建与修改展开,帮助读者把SQL建表、改表等课堂概念落到可运行的程序中。压缩包共5个文件,以2个txt说明与测试数据、1个cpp源代码、1个o编译中间文件及1个exe可执行文件组成,整体约197KB,体积轻便,下载后可直接对照使用。已有682人学习,具备一定参考热度。资源价值在于将CREATE TABLE、ALTER TABLE等操作逻辑写入源码并编译为可执行程序,配合预设测试数据验证字段增删改、属性调整与字段顺序变化等场景,便于观察SQL命令的实际效果;说明文档则对设计目标、操作步骤与注意事项作出指引,适合课程设计复现、实验调试和数据库基础操作练习。

1. 一套能直接跑的数据库系统概论课程设计包,真正省掉的是什么

数据库系统概论课程设计最耗时间的环节,从来不是写那几十行 SQL,而是把环境、表结构、初始化数据和演示流程对齐——很多人卡在第三天,ER 图上的“选课”多对多联系还画不利索,更别说后面的事务实验和报告。这套资源的思路是反过来的:先给你一份导入就能跑的库结构和范例数据,让你看到数据长什么样,再回头解释为什么这么拆表、为什么这个字段要单独拎出来做联系表。

它适合两类人:正在赶课程设计、需要一周内交出可演示系统的学生,以及想快速复习一遍建表、视图、触发器、存储过程、隔离级别这些内容的从业者,把它当成一个最小可跑的沙盒。要提前说清楚的是,包里给的是骨架和范例数据,业务规则、报告结论得自己补,原样交上去大概率会被追问细节。

2. 从压缩包到可连接实例:版本、字符集与导入顺序

先把最容易返工的两件事定死:数据库大版本和字符集。这两项一旦在导入完成后再改,代价不是改一个配置,而是重新灌一遍数据、重跑一遍对象脚本,中间还得处理外键顺序。

2.1 先扫脚本判断版本,而不是先装数据库

课程设计类的 SQL 脚本往往是从不同年份的资料里拼出来的,有的片段是旧版本语法,有的用了新版本特性。装完数据库再执行,报错信息通常很绕——比如一条授权语句在 8.0 上报 1064 语法错误,新手很容易以为是少了分号。正确做法是解压后先做一次静态扫描。

# 在解压目录下执行,判断这套脚本面向哪个大版本 # -r 递归子目录,-n 显示行号,-i 忽略大小写,-E 扩展正则 grep -rniE "row_number|over *\(|with +recursive" ./sql/ \ && echo ">>> 含窗口函数或 CTE,需要 8.0 及以上" \ || echo ">>> 无窗口函数,5.7 也能跑" grep -rniE "grant .* identified by" ./sql/ \ && echo ">>> 存在旧版授权语法,8.0 需拆成 CREATE USER + GRANT" grep -rniE "utf8mb4_0900|collation *=" ./sql/ | head -20

这段扫描解决三类问题。第一类是窗口函数和公用表表达式,8.0 才支持,5.7 直接报 1064;第二类是GRANT ... IDENTIFIED BY这种把建用户和授权写在一起的旧语法,8.0 已经拆开,必须改成CREATE USER加GRANT两步;第三类是排序规则,utf8mb4_0900_ai_ci是 8.0 默认,5.7 不认识,导入时会报 1273 未知排序规则。三条扫完,装哪个版本就不用猜了。

字符集同理,别等中文变成问号再回头查。库里、表里、连接层三处必须一致,脚本头部通常会有SET NAMES utf8mb4,但命令行导入时如果客户端默认字符集不是它,这条语句也救不回来。我一般会在导入命令里显式带上--default-character-set=utf8mb4,把连接层钉死。

提示:如果机器上已经装了 5.7,而脚本又确实用了窗口函数,别硬改脚本降级语法,直接换 8.0 更省事。降级改写容易在OVER(PARTITION BY ...)这类地方埋下结果不一致的隐患。

2.2 四段式导入顺序,错一步就报 1146

资源包里的 SQL 通常不是一个文件,而是一组按前缀编号的脚本。常见的拆分逻辑是:删库建库、建表与约束、灌初始数据、建视图触发器存储过程,最后是用户与权限。这个顺序不是随便排的,视图依赖表,触发器依赖表,外键依赖被引用表,任何一步提前都会撞上ERROR 1146 Table doesn't exist。

#!/usr/bin/env bash # 批量导入脚本:任一环节失败立即中断,避免留下半截状态 set -euo pipefail DB="course_design" CLI="mysql -uroot -p --default-character-set=utf8mb4" # 第一步:重建库,统一字符集与排序规则 $CLI -e "DROP DATABASE IF EXISTS ${DB}; CREATE DATABASE ${DB} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 第二步:按文件名排序依次执行 01 到 04 for f in sql/0*.sql; do echo ">>> 正在执行 ${f}" $CLI "${DB}" < "${f}" done echo ">>> 导入完成"

set -euo pipefail是这段脚本的核心:-e让任何一条命令返回非零就退出,-u让未定义变量直接报错,-o pipefail让管道中任一环节失败都能被捕获。没有这三个选项,某个脚本中途报错但退出码是 0,后面的脚本继续跑,最后得到一个“表建了一半、数据灌了一半”的库,排查成本翻倍。

--default-character-set=utf8mb4放在客户端命令上而不是脚本里,是因为它作用于连接建立阶段,比脚本内的SET NAMES更早生效。如果包里带了00_drop.sql这类文件,注意它可能包含DROP DATABASE,第一次导入前确认里面写的库名和你准备用的库名一致,别把已有库误删。

2.3 导入完成后先跑三条自检,别急着打开客户端看

导入脚本没有报错,不等于导入成功。表可能建了但数据没进去,字符集可能库对了表不对,时区可能差八小时。三条查询能把大部分问题挡在前面。

-- 1. 基础表数量是否符合预期(对照资源包里的建表脚本数) SELECT COUNT(*) AS base_table_cnt FROM information_schema.tables WHERE table_schema = 'course_design' AND table_type = 'BASE TABLE'; -- 2. 每张表的行数,找出该有数据却为空的表 -- 注意 table_rows 是 InnoDB 的估算值,精确值仍需 COUNT(*) SELECT table_name, table_rows, engine, table_collation FROM information_schema.tables WHERE table_schema = 'course_design' ORDER BY table_name; -- 3. 库级字符集、排序规则与当前会话时区 SELECT @@character_set_database AS db_charset, @@collation_database AS db_collation, @@time_zone AS session_tz, @@system_time_zone AS system_tz;

第一条查询的结果应该和01_schema.sql里CREATE TABLE的条数对得上,少一张就说明某个建表语句被静默跳过了——常见原因是脚本里用了CREATE TABLE IF NOT EXISTS,前一次失败留下的残表让这次直接跳过。第二条重点看table_rows为 0 的表,InnoDB 的估算值虽然不准,但 0 通常意味着真的没数据,或者统计信息还没更新,可以补一条ANALYZE TABLE再看。第三条里time_zone如果是SYSTEM而系统时区不是东八区,后面所有TIMESTAMP字段的显示值都会偏移,这个坑留到第 5 章细说。

三条都过了,再考虑用图形化客户端连上去点点看。反过来先开客户端,很容易被界面上的“表已存在”提示误导,以为导入成功了。

3. 从 ER 图到建表语句:概念设计怎么落成物理结构

课程设计里最容易失分的环节,是 ER 图和建表脚本对不上——图上画了五个实体三个联系,脚本里只有四张表,答辩时被问一句“这个联系去哪了”就很难圆。这一章把转换规则、范式取舍和字段类型三件事拆开说。

3.1 联系类型的转换规则与主键选择

ER 图里的联系转成表,规则是固定的,但每一条都有边界。

联系类型转换方式课程设计里的典型例子
1:1合并到任意一端,外键上加唯一约束学生与学籍档案
1:N外键放在 N 端,不加唯一约束教师与所授课程
M:N单独建联系表,两端主键做联合主键学生与课程的选课
多值属性单独建表,外键指回主体学生的联系电话
弱实体单独建表,主键包含所属实体的主键订单明细依赖订单

选课表是这套资源里最值得琢磨的一张。用(student_id, course_id)做联合主键,天然能防重复选课,插入同一组合直接报主键冲突;但成绩字段需要频繁更新,联合主键的更新代价偏高,而且后续如果要加“重修”场景(同一学生同一课程多条记录),联合主键就撑不住了。我一般的做法是加一个自增代理键做主键,把(student_id, course_id, term)做成联合唯一索引,既保留防重能力,又给业务留了口子。

代理键和业务主键之争在课程设计里没有标准答案,但有判断依据:如果这个实体的业务标识会变(学号可能升位、课程编号可能调整),用代理键;如果永远不变且长度固定,业务主键更直观,调试时看 ID 就知道是谁。

3.2 三范式的判定与反范式的安全边界

判断一张表是否满足第三范式,方法是从主键出发画函数依赖:主键决定所有非主属性,且非主属性之间不能互相决定。选课表里如果塞了course_name,course_id决定course_name,而course_id又不是选课表的主键,这就是传递依赖,违反 3NF。

但实际做课程设计时,全部严格范式化会带来另一个问题:报表查询要 join 四五张表,写起来长、跑起来慢。这时候需要判断哪些冗余是安全的。安全冗余只有两种场景:

第一种是不可变快照。比如成绩单上要记录课程名称和学分,这两项在选课当时就固定了,后续课程改名不应该影响历史成绩单。这种冗余是有业务含义的,不是偷懒。

第二种是可重建的汇总字段。比如班级表里存student_count,真实来源是学生表按class_id分组计数。这种字段只要保证有触发器或定时任务维护,就不会出现数据不一致。风险在于维护逻辑漏了某条更新路径,所以必须配套一个校验查询。

-- 汇总字段一致性校验:找出人数对不上的班级 SELECT c.class_id, c.student_count AS cached_cnt, COUNT(s.student_id) AS real_cnt FROM class c LEFT JOIN student s ON s.class_id = c.class_id GROUP BY c.class_id, c.student_count HAVING cached_cnt <> real_cnt;

这条校验应该写进实验报告的“数据完整性验证”一节,答辩时能直接说明冗余字段是怎么保证一致性的。除此之外的冗余,比如把教师姓名冗余到选课表,都属于给自己挖坑——教师改名时要更新多少行,谁都说不清。

3.3 建表脚本里的类型、约束与默认值

字段类型选错,是课程设计里最隐蔽的扣分点,因为数据量小的时候看不出问题,数据一多或者一算总分就露馅。

CREATE TABLE student ( student_id CHAR(10) NOT NULL COMMENT '学号,定长业务主键', name VARCHAR(30) NOT NULL COMMENT '姓名,留足多字节空间', gender CHAR(1) NOT NULL DEFAULT 'M' COMMENT 'M/F,不用 ENUM 避免扩值要 ALTER', birth_date DATE NULL COMMENT '出生日期,只存日期不存时间', class_id INT NOT NULL COMMENT '所属班级', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', PRIMARY KEY (student_id), KEY idx_class (class_id), CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES class (class_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; CREATE TABLE score ( score_id BIGINT NOT NULL AUTO_INCREMENT, student_id CHAR(10) NOT NULL, course_id CHAR(8) NOT NULL, term CHAR(11) NOT NULL COMMENT '学期,如 2024-2025-1', score DECIMAL(4,1) NULL COMMENT '百分制,保留一位小数', PRIMARY KEY (score_id), UNIQUE KEY uk_stu_course_term (student_id, course_id, term), KEY idx_course_score (course_id, score), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student (student_id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course (course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课与成绩表';

几个参数值得单独说。student_id用CHAR(10)而不是VARCHAR,定长字段在索引里不存长度前缀,等值查询略快,但更重要的是长度约束本身就是一种校验,超长学号插不进去。成绩用DECIMAL(4,1)而不是FLOAT,浮点数在做AVG和求和时会出现59.999999这种值,报告里一打印就尴尬。gender用CHAR(1)加应用层校验,不用ENUM,因为 MySQL 的ENUM增加取值要执行ALTER TABLE重建表结构,数据量大了很慢。

外键约束要显式命名。不命名的外键在删除时会生成student_ibfk_1这种随机名字,后面要DROP FOREIGN KEY时还得先去information_schema里查名字。命名规范建议fk_子表_父表,索引建议idx_字段,联合索引按最左列命名。

还有一个容易忽略的点:CHECK约束在 8.0.16 之前只解析不执行。也就是说,你在 5.7 上写CHECK (score BETWEEN 0 AND 100),插入 150 分照样成功。如果资源包面向 5.7,成绩范围校验必须放在触发器或应用层,不能指望CHECK。

4. 视图、索引、触发器、存储过程:四个加分项怎么落地

课程设计的评分点通常集中在“是否使用了数据库高级对象”。这四个对象的共同特点是:写出来容易,写对不容易,而错误往往在演示时才暴露。

4.1 视图与索引:把高频查询路径固定下来

视图的价值在于把多表 join 的复杂度封装起来,让上层查询只面对一张“宽表”。课程设计里最典型的视图是学生成绩视图,把学生、课程、成绩三张表连起来,再拼上课程学分算加权。

CREATE OR REPLACE VIEW vw_student_score AS SELECT s.student_id, s.name AS student_name, c.course_id, c.course_name, c.credit, sc.term, sc.score, -- 绩点换算:60 分以下记 0,避免负数 CASE WHEN sc.score >= 60 THEN ROUND((sc.score - 50) / 10, 1) ELSE 0 END AS grade_point FROM student s JOIN score sc ON sc.student_id = s.student_id JOIN course c ON c.course_id = sc.course_id;

视图定义里没有WHERE,意味着它会把全量数据都算出来。数据量小无所谓,但如果后续要做分页查询,SELECT * FROM vw_student_score WHERE student_id = ?在 8.0 上可能触发条件合并下推,在 5.7 上则可能先物化整个视图再过滤。判断方法很简单,EXPLAIN一下看select_type是不是DERIVED,是的话说明视图被物化了。

索引要和视图配套建。上面这个视图按student_id过滤,成绩表上已经有联合唯一索引(student_id, course_id, term),最左列能用上,够了。但课程维度统计(某门课的平均分、及格率)走的是course_id,所以我在成绩表上单独加了idx_course_score (course_id, score),这个索引同时能覆盖“按课程查分数区间”的查询,属于覆盖索引,回表都省了。

联合索引的核心规则是最左前缀:(course_id, score)能支持WHERE course_id = ?和WHERE course_id = ? AND score > ?,但支持不了单独的WHERE score > ?。建索引前先用EXPLAIN看现有查询的key列,别凭感觉加。

EXPLAIN 列需要关注的值含义
typeALL全表扫描,数据量大时要处理
typeref / range / index可接受,分别对应等值、范围、全索引扫描
keyNULL没走索引,检查是否有隐式转换或函数包列
rows远大于结果行数统计信息过期,可执行 ANALYZE TABLE
ExtraUsing filesort排序没走索引,考虑把排序列加入联合索引
ExtraUsing temporary用了临时表,常见于 GROUP BY 没走索引

4.2 触发器:用数据库层兜住业务规则

触发器适合放“无论从哪个入口都必须执行”的规则,课程设计里最典型的是选课人数上限和成绩变更留痕。前者用BEFORE INSERT拦截,后者用AFTER UPDATE记录。

DELIMITER $$ CREATE TRIGGER trg_score_before_insert BEFORE INSERT ON score FOR EACH ROW BEGIN DECLARE cur_cnt INT DEFAULT 0; -- 只对已有成绩的正式选课计数,NULL 表示尚未录入成绩 SELECT COUNT(*) INTO cur_cnt FROM score WHERE course_id = NEW.course_id; IF cur_cnt >= 60 THEN -- 自定义错误码 45000 表示用户定义异常,前端可直接展示 SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该课程选课人数已达上限'; END IF; -- 成绩范围兜底,弥补 5.7 不执行 CHECK 的问题 IF NEW.score IS NOT NULL AND (NEW.score < 0 OR NEW.score > 100) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '成绩必须在 0 到 100 之间'; END IF; END$$ DELIMITER ;

这段触发器有两个技术点。SIGNAL SQLSTATE '45000'是 MySQL 抛自定义错误的固定写法,45000属于用户自定义异常段,应用层捕获后可以直接把MESSAGE_TEXT展示给用户,不用再翻译错误码。第二个点是触发器内部对score表做了COUNT(*)查询——这在BEFORE INSERT里是允许的,因为此时行还没写入;但如果你在AFTER INSERT里对同一张表做查询,就会撞上ERROR 1442 Can't update table in stored function/trigger。

触发器的维护成本经常被低估。每加一条业务规则就加一个触发器,半年后没人说得清一张表上挂了几个。我的习惯是在建表脚本旁边维护一张注释表,记录触发器名、触发时机、对应规则,迁移时一起带走。

4.3 存储过程与 DELIMITER:客户端解析差异是主要坑源

存储过程在课程设计里的作用是展示“过程化 SQL 能力”,典型题目是“输入课程号,输出平均分和及格率”。写法不难,难在导入时的分隔符处理。

DELIMITER $$ CREATE PROCEDURE sp_course_stat( IN p_course_id VARCHAR(8), OUT p_avg_score DECIMAL(5,2), OUT p_pass_rate DECIMAL(5,2) ) BEGIN SELECT ROUND(AVG(score), 2), ROUND(SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) / NULLIF(COUNT(score), 0) * 100, 2) INTO p_avg_score, p_pass_rate FROM score WHERE course_id = p_course_id AND score IS NOT NULL; END$$ DELIMITER ;

DELIMITER $$不是 SQL 语句,是客户端指令,作用是告诉客户端“暂时别用分号切语句”。存储过程体内部有多个分号结尾的语句,不换分隔符的话,客户端读到第一个分号就把半截CREATE PROCEDURE发到服务器,必然报 1064。

这里有个容易被忽略的差异:命令行客户端mysql < file.sql和图形化客户端的“运行 SQL 文件”对DELIMITER的处理不完全一致。有的图形化工具会自己解析分隔符,遇到脚本里的DELIMITER反而报错;有的则原样透传。稳妥做法是存储过程和触发器单独放一个文件,用命令行source或重定向执行,别混在图形化工具的多语句执行里。

调用时注意OUT参数必须用会话变量接:

CALL sp_course_stat('CS101', @avg, @rate); SELECT @avg AS avg_score, @rate AS pass_rate;

如果直接CALL sp_course_stat('CS101', 0, 0),会报参数类型错误。NULLIF(COUNT(score), 0)是为了防止课程没有任何成绩时除零,返回NULL而不是报错,这个细节在演示时能体现考虑周全。

5. 避坑与排查:五条跑不动、查不对的血泪记录

下面五条都是导入和演示阶段真实会撞上的,按“现象、原因、解决”写,能直接对照自己的报错信息定位。

5.1 外键报 1452:初始化数据的插入顺序错了

现象:执行数据脚本时中断,报ERROR 1452 Cannot add or update a child row: a foreign key constraint fails,提示某张子表插入的值在父表里不存在。

原因:数据脚本里的INSERT顺序和建表时的外键依赖相反。比如先插score再插student,或者父表数据用了DELETE后重灌但自增 ID 变了。还有一种隐蔽情况:父表脚本里INSERT被ON DUPLICATE KEY UPDATE跳过,ID 没生成。

解决:按依赖拓扑排序,先父后子。快速定位可以用下面这条查询列出所有外键依赖,再对照脚本里的插入顺序。

SELECT table_name, constraint_name, referenced_table_name FROM information_schema.key_column_usage WHERE table_schema = 'course_design' AND referenced_table_name IS NOT NULL ORDER BY referenced_table_name, table_name;

临时排查时可以SET FOREIGN_KEY_CHECKS = 0先灌进去,但灌完必须改回 1 并跑一遍孤儿数据检查,别把关闭外键检查当成常规操作留在脚本里。

5.2 中文变问号:字符集断在连接层

现象:表里的中文正常,但通过命令行查询或应用写入后显示成???,或者报Incorrect string value: '\xE5\xAD\xA6...'。

原因:库和表是utf8mb4,但连接层是latin1或utf8(三字节)。MySQL 的字符集是分层级的,服务端默认、库、表、列、连接各自独立,任何一层窄了都会截断。utf8在 MySQL 里最大三字节,存不了 emoji 和部分生僻字。

解决:连接串里显式指定characterEncoding=utf8mb4,命令行加--default-character-set=utf8mb4,脚本头部加SET NAMES utf8mb4。三处都写上,别赌哪一层是默认值。检查当前会话用SHOW VARIABLES LIKE 'character_set%';,重点看character_set_client、character_set_connection、character_set_results三个是否都是utf8mb4。

5.3 触发器建不上:DELIMITER 被客户端吃掉了

现象:单独执行CREATE TRIGGER语句成功,但整个脚本文件导入时在触发器处报 1064,错误位置指向触发器体内的第一个分号。

原因:客户端按分号切分语句,DELIMITER指令没生效或被工具忽略。有些图形化工具在执行“运行 SQL 文件”时会自己做语句切分,不认脚本里的DELIMITER。

解决:把触发器和存储过程拆到单独文件,用命令行重定向执行;或者在图形化工具里改为逐条选中执行。判断是否踩了这个坑,看报错行号是不是恰好落在BEGIN之后的第一条语句上。

5.4 时间差八小时:TIMESTAMP 与 DATETIME 的时区行为不同

现象:插入时间明明是下午三点,查出来是早上七点;或者应用和数据库看到的created_at不一致。

原因:TIMESTAMP存储时按 UTC 转换,读取时按会话时区转换;DATETIME原样存储不做转换。如果连接层时区是SYSTEM而服务器系统时区不是东八区,TIMESTAMP就会偏移。另外 JDBC 连接串里serverTimezone配错也会导致同样现象。

解决:业务时间字段统一用DATETIME,避免隐式时区转换;确实需要TIMESTAMP的(比如记录最后更新时间做增量同步),在连接建立后执行SET time_zone = '+08:00'。排查时对比SELECT NOW(), UTC_TIMESTAMP(), @@session.time_zone;三个值就能判断偏移来源。

5.5 索引没走:隐式类型转换和函数包列

现象:student_id上明明有索引,EXPLAIN的type却是ALL,key为NULL。

原因:两种最常见。一是字段是CHAR但查询条件传了数字,MySQL 会把列转成数字比较,索引失效;二是WHERE DATE(created_at) = '2024-09-01'这种函数包列,索引列被函数处理后就无法直接定位。

解决:参数类型和列类型保持一致,字符串就加引号;时间范围查询改写成created_at >= '2024-09-01' AND created_at < '2024-09-02'。改完再EXPLAIN一次确认key有值、rows明显下降。这一步在实验报告的“查询优化”小节里放前后对比,比空谈理论有说服力得多。

6. 把隔离级别跑成可观测实验:一份两会话验证脚本

课程设计里事务隔离级别这一章,光背“脏读、不可重复读、幻读”三个定义,答辩时很容易被追问“你怎么验证的”。最省事的办法是开两个命令行会话,用同一张表把四种隔离级别的行为跑一遍,把结果记成对照表。

先建一张只有两列的小表,避免干扰:

CREATE TABLE t_isolation ( id INT PRIMARY KEY, value INT NOT NULL ) ENGINE=InnoDB; INSERT INTO t_isolation VALUES (1, 100);

会话 A 和会话 B 分别连同一个库。A 执行SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;(或REPEATABLE READ),然后开启事务并更新:

-- 会话 A START TRANSACTION; UPDATE t_isolation SET value = 200 WHERE id = 1; -- 此处不提交,保持事务打开

此时切到会话 B,在READ COMMITTED下查询会读到 100(未提交的修改不可见,不脏读);如果 A 提交后再查,B 能读到 200(同一事务内两次查询结果不同,不可重复读)。把 B 的隔离级别改成REPEATABLE READ再走一遍,A 提交后 B 仍然读到 100,直到 B 自己提交或回滚——这就是快照读的效果。

验证幻读要用范围查询。A 在事务里执行SELECT COUNT(*) FROM t_isolation WHERE value > 50;,B 插入一条value = 300并提交,A 再查一次。REPEATABLE READ下两次计数一致,READ COMMITTED下第二次会多出一条。把这三组结果整理成表格放进报告,比抄定义强得多。

我踩过一次坑:以为改隔离级别只要SET GLOBAL就全局生效,结果新开会话还是旧级别。SET GLOBAL只影响之后建立的连接,当前会话必须用SET SESSION。从那以后我每次做隔离级别实验,都先在两个会话里各执行一次SELECT @@transaction_isolation;确认级别一致再开始,省得跑完一轮发现结论对不上又重来。希望帮到你。

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