简介《数据库实验5嵌套查询》是一份面向数据库课程学习者和实验课学生的实验报告类文档围绕SELECT统计查询、聚合函数、连接查询与嵌套查询展开帮助读者掌握多表检索和复杂条件分析的基本方法。压缩包共1个doc文件大小约642KB内容以CPXS数据库为背景涉及COUNT、SUM、MAX、MIN等统计函数、GROUP BY与HAVING分组筛选以及子查询、派生表和比较谓词等嵌套查询形式并配有客户、产品和订购等业务示例文档还对实验目的、实验内容和思考题作了整理。已有931人学习适合实验课对照、课后复盘和SQL语句梳理。文档把统计查询、连接查询和嵌套查询放在同一实验框架中读者可据此理解各类查询的功能、语法格式与适用场景也可作为数据库查询练习时的参考材料用于查漏补缺和巩固查询基础。1. 从一份“全错”的实验报告说起上个月有个学弟把一份 .doc 发给我说数据库实验里嵌套查询那十几条 SQL 他照着敲了一遍交上去只收到三个字全错了。我打开一看语句确实都能执行但返回的结果跟题目问的根本对不上——COUNT 后面跟着 GROUP BY子查询用!去接一个可能返回多行的结果集。这份《数据库实验5嵌套查询》文档本身是一份带答案的实验报告14 条语句横跨统计查询、连接查询、嵌套查询三类真正的难点不在语法熟不熟而在每条语句背后“题目到底问的是什么”。它适合三类人刚学完 SELECT 语法、要交实验报告的学生需要一套能直接跑的练习库来带课的人以及想拿个小库练 SQL 改写的开发者。下面按我拆这份文档的顺序从建表灌数到逐条改错把能抄作业的部分都写出来。2. 先摸清库的结构CPXS 三张表和统计查询的落点2.1 从语句反推表结构CUSTOMER、PRODUCT、SALE 三个角色实验文档里没有给建表语句只有查询语句。但查询语句里出现的字段名足够反推出整个模型CUSTOMER有CNO客户编号、CNAME公司名称、SITE所在城市、TELE联系电话PRODUCT有PCODE产品编号、PNAME产品名称、PRICE单价、STOCKS库存量SALE是订购明细有CNO、PCODE、OQUANTITY订购数量。三个角色的关系很清楚一个客户可以订购多种产品一种产品可以被多个客户订购SALE就是CUSTOMER和PRODUCT之间的多对多桥表。文档第 12 条要从客户表里按城市找同城公司第 13 条要比较产品单价都依赖这条链路。表名字段推断类型角色CUSTOMERCNO / CNAME / SITE / TELEVARCHAR(10) / VARCHAR(50) / VARCHAR(50) / VARCHAR(20)客户主表CNO 主键PRODUCTPCODE / PNAME / PRICE / STOCKSVARCHAR(10) / VARCHAR(50) / DECIMAL(10,2) / INT产品主表PCODE 主键SALECNO / PCODE / OQUANTITYVARCHAR(10) / VARCHAR(10) / INT订购桥表(CNO,PCODE) 联合主键这里有个细节值得留意文档里没有出现订单号字段。这在教学库里很常见——同一客户买同一产品只记一行数量累加在OQUANTITY里。这个假设直接决定了第 2 条“至少订购两种以上产品的客户”能不能用COUNT(PCODE)来数。如果业务上允许同一客户对同一产品分多次下单那就必须加自增主键COUNT(PCODE)会重复计数得改成COUNT(DISTINCT PCODE)。我在别处见过因为这个假设没对齐统计出来的“产品种类数”比实际大一倍的血泪经验。2.2 统计函数和分组COUNT、SUM、MAX、MIN 各管哪一段文档里用到的聚合函数只有四个COUNT、SUM、MAX、MIN。看着简单但每个函数在“有没有 GROUP BY”下的行为完全不同这是这份实验真正的分水岭。不带 GROUP BY 时整张表被压成一行聚合函数吃的是全部行。SELECT COUNT(*) FROM CUSTOMER返回一个标量SELECT MAX(PRICE) FROM PRODUCT也是一样。这类语句几乎不会写错。带上 GROUP BY 之后语义就变了先按分组列把行切成若干桶每个桶内部做聚合最后每个桶输出一行。第 3 条SELECT CNO, SUM(OQUANTITY) FROM SALE GROUP BY CNO就是典型——每个客户一行汇总他买的所有数量。HAVING则是桶级别的过滤它在分组和聚合都算完之后才执行。文档第 2 条GROUP BY CNO HAVING COUNT(PCODE)2就是在问哪些客户的订单里产品种类数达到两个以上。这个顺序理解不了后面第 6、7 条错在哪就看不出来。顺带说COUNT(*)和COUNT(列名)的区别这两个在实际项目里混淆的概率比想象中高。COUNT(*)数行数不关心列里是不是 NULLCOUNT(列名)会跳过该列为 NULL 的行。教学库字段大多 NOT NULL看不出差别一旦上了真实数据一条统计口径差几千条查起来就是黑匣子。2.3 把实验环境跑起来建库、建表、灌样例数据动手之前先把库搭起来。我用 MySQL 8.x字符集给utf8mb4不然第 4 条和第 11 条里那些中文常量比较会出玄学问题。下面这段脚本可以直接存成schema.sql。-- CPXS 教学库建表脚本字段与类型由实验文档中的查询语句反推 CREATE DATABASE IF NOT EXISTS cpxs DEFAULT CHARSET utf8mb4; USE cpxs; CREATE TABLE customer ( cno VARCHAR(10) PRIMARY KEY, -- 客户编号 cname VARCHAR(50) NOT NULL, -- 客户公司名称 site VARCHAR(50), -- 所在城市 tele VARCHAR(20) -- 联系电话 ); CREATE TABLE product ( pcode VARCHAR(10) PRIMARY KEY, -- 产品编号部分以 B 开头 pname VARCHAR(50) NOT NULL, -- 产品名称 price DECIMAL(10,2) NOT NULL, -- 单价 stocks INT DEFAULT 0 -- 库存量 ); CREATE TABLE sale ( cno VARCHAR(10) NOT NULL, -- 客户编号 pcode VARCHAR(10) NOT NULL, -- 产品编号 oquantity INT NOT NULL, -- 订购数量 PRIMARY KEY (cno, pcode), -- 一个客户对一种产品只记一行 FOREIGN KEY (cno) REFERENCES customer(cno), FOREIGN KEY (pcode) REFERENCES product(pcode) );建表顺序不能反sale的外键依赖另外两张表。如果想跳过外键约束快速灌数据可以先SET FOREIGN_KEY_CHECKS0灌完再打开但教学环境不建议这么干外键本身就是理解三表关系的一部分。建完表灌数据手工写十几条 INSERT 也行但要让统计结果有区分度比如第 2 条要能筛出“订购两种以上产品”的客户数据得有讲究。我一般用一个脚本批量生成保证边界值覆盖到# 生成 CPXS 样例数据保证第 2/8/9 条查询能筛出有意义的结果 import random random.seed(42) # 固定种子每次生成的数据一致方便对答案 sites [上海, 北京, 广州, 深圳] customers [ # (cno, cname, site, tele) (C01, 美美, 上海, 021-0001), (C02, 华联, 上海, 021-0002), # 与美美同城用于第 12 条 (C03, 京泰, 北京, 010-0001), (C04, 广发, 广州, 020-0001), ] products [] for i in range(1, 11): prefix B if i 3 else A # A01~A03 前缀为 B供第 9 条 LIKE B% 使用 products.append(( f{prefix}{i:02d}, f产品{i}, round(random.uniform(8, 30), 2), # 单价跨 10~20 区间供第 7 条使用 random.choice([300, 450, 600, 800]) # 库存跨 500 阈值供第 6 条使用 )) sales [] for cno, _, _, _ in customers: picked random.sample(products, krandom.randint(1, 4)) # 有的客户只买 1 种 for pcode, _, _, _ in picked: sales.append((cno, pcode, random.choice([80, 150, 260]))) with open(data.sql, w, encodingutf-8) as f: for c in customers: f.write(INSERT INTO customer VALUES (%s,%s,%s,%s);\n % c) for p in products: f.write(INSERT INTO product VALUES (%s,%s,%.2f,%d);\n % p) for s in sales: f.write(INSERT INTO sale VALUES (%s,%s,%d);\n % s)这段脚本的关键在于刻意把边界值摆进去产品编号前三个以B开头让第 9 条的LIKE B%不至于返回空集单价随机区间覆盖 10~20第 7 条才有得数客户订购的产品种类从 1 到 4 不等第 2 条的HAVING COUNT(PCODE)2才能筛出非全集的结果。random.seed(42)那句别删固定种子是为了每次重建库之后答案对得上不然改完 SQL 你都不知道是语句写错了还是数据变了。生成完之后执行导入# 依次建库建表、灌数据中文常量比较依赖库的字符集设置 mysql -u root -p schema.sql mysql -u root -p --default-character-setutf8mb4 cpxs data.sql # 验证三张表的行数是否符合预期 mysql -u root -p cpxs -e SELECT (SELECT COUNT(*) FROM customer) AS c, (SELECT COUNT(*) FROM product) AS p, (SELECT COUNT(*) FROM sale) AS s;--default-character-setutf8mb4这个参数在导入含中文的 SQL 文件时最好显式带上。客户端字符集和库字符集不一致时中文会变成问号插进去之后第 4 条按SITE上海分组永远返回空排查半天以为是 SQL 写错了其实数据早就烂在库里。3. 统计查询逐条拆分组聚合的执行顺序决定结果对不对3.1 全表聚合和分组聚合是两件事文档前 5 条里第 1、5 条是全表聚合第 2、3、4 条是分组聚合这两类语句的错误模式完全不一样。SELECT COUNT(*) FROM CUSTOMER和SELECT MAX(PRICE) FROM PRODUCT属于“给多少行就吃多少行”不会出错。稍微值得注意的是MAX(PRICE)返回的是单价最大值这个标量不是“最贵的那条产品记录”。想要整条记录得用子查询或者ORDER BY PRICE DESC LIMIT 1。第 3 条SELECT CNO, SUM(OQUANTITY) FROM SALE GROUP BY CNO是标准的分组聚合写法CNO出现在 SELECT 列表里同时出现在 GROUP BY 里聚合函数只作用在非分组列上。这条在严格模式下能过在宽松模式下也能过属于文档里少数几条完全正确的语句。第 2 条SELECT CNO, COUNT(PCODE) FROM SALE GROUP BY CNO HAVING COUNT(PCODE)2也一样正确。但这里有个口径问题COUNT(PCODE)数的是行数不是去重后的产品种类数。在当前“一个客户对一种产品只记一行”的主键设计下两者等价一旦业务允许重复下单这条的结果就会偏大。稳妥写法是COUNT(DISTINCT PCODE)代价是多一次去重排序数据量小的时候无感。3.2 HAVING 过滤的是组WHERE 过滤的是行第 4 条是文档里最值得拿出来讲的一条SELECT SITE, COUNT(CNO) FROM CUSTOMER GROUP BY SITE HAVING SITE 上海;这条能跑出正确结果但写法有问题。SITE上海是个行级条件它不依赖任何聚合值放在 WHERE 里可以先过滤再分组扫描的数据量直接砍掉一大半放在 HAVING 里则是先把全表分完组、算完 COUNT再把不满足条件的组扔掉。我习惯把它改写成这样-- 行级条件前置到 WHERE分组只处理上海客户代价更低 SELECT SITE, COUNT(CNO) AS customer_cnt FROM customer WHERE site 上海 GROUP BY site;改写之后执行计划会从全表扫变成先过滤再聚合。数据量几百行时两者耗时都是 0.00 秒看不出差别但这个习惯一旦养成在千万级表上就是几十倍的差距翻车往往就发生在这种“教学库能跑就行”的写法被直接搬到生产环境的时刻。判断标准其实很简单条件里出现了聚合函数COUNT、SUM、MAX、MIN、AVG就必须放 HAVING条件只涉及原始列就优先放 WHERE。3.3 语义跑偏的三条第 6、7、8 条到底错在哪这三条是整份文档里最容易让人误判的部分——它们都不报错都能返回结果但结果跟题目问的不是一回事。第 6 条要“求库存量超过 500 的产品个数”文档写的是-- 原写法返回的是每个库存档位各自的产品数不是总数 SELECT COUNT(PCODE) FROM PRODUCT GROUP BY STOCKS HAVING STOCKS 500;假设库里有 300、450、600、800 四个库存档位这条会返回两行600 档一行、800 档一行每行是该档位的产品数。题目要的是一个数字。正确写法是把库存条件前置聚合不做分组-- 正确写法先筛行再数总数返回单个标量 SELECT COUNT(*) AS cnt FROM product WHERE stocks 500;第 7 条同理要“求单价在 10~20 元之间产品的个数”文档写成了按PRICE分组再 HAVING 过滤区间返回的是一堆价位和各自的计数。改成WHERE price BETWEEN 10 AND 20再COUNT(*)就对了。这类错误的共同点是把本该在行级做过滤的条件错误地放到了组级。第 8 条更隐蔽-- 原写法OQUANTITY 既不在 GROUP BY 里也不在聚合函数里 SELECT PCODE, OQUANTITY FROM SALE WHERE OQUANTITY 100 GROUP BY PCODE HAVING COUNT(PCODE) 2;两个问题叠在一起。一是OQUANTITY没进 GROUP BY 也没套聚合函数MySQL 5.7 之后默认开启ONLY_FULL_GROUP_BY这条直接报ERROR 1055关掉严格模式能跑但取到的是组内某一行随机的值等于黑匣子。二是COUNT(PCODE) 2的意思是“被 3 个及以上客户订购”题目说的是“至少被两个以上客户订购”口径差一个。正确写法应该把数量条件放进 WHERE计数条件用 2-- 正确写法数量条件筛行客户数条件筛组且用 2 对齐题意 SELECT pcode, SUM(oquantity) AS total_qty FROM sale WHERE oquantity 100 GROUP BY pcode HAVING COUNT(DISTINCT cno) 2;COUNT(DISTINCT cno)数的是“有多少个不同客户”比COUNT(PCODE)更贴合题目里“被几个客户订购”的表述。这两个在桥表上一旦有重复行就会分叉实验里不明显真实订单表里差得远。3.4 用执行计划看分组聚合到底干了什么改完语句之后别只看结果对不对顺手EXPLAIN一下能看清数据库实际走的路径-- 对比改写前后的执行计划重点看 type 和 rows 两列 EXPLAIN SELECT SITE, COUNT(CNO) FROM CUSTOMER GROUP BY SITE HAVING SITE 上海; EXPLAIN SELECT SITE, COUNT(CNO) FROM CUSTOMER WHERE SITE 上海 GROUP BY SITE;type列从ALL全表扫变成ref或rangerows列估算的扫描行数明显下降就说明过滤确实前置了。分组聚合在 MySQL 里通常表现为Using temporary; Using filesort这是建临时表做分组和排序的标记不是错误但数据量大时临时表会落盘性能断崖。教学库上看不出这些代价养成看执行计划的习惯才是这份实验真正能带走的技能。4. 连接查询和嵌套查询同一道题的两条解法4.1 隐式连接改显式 JOIN第 11 条的正确姿势第 11 条要“查询由上海客户订购的且订购数量大于 200 套的所有客户编号、产品编号、联系人和订购数量”文档用的是逗号连接加 WHERE-- 原写法隐式连接过滤条件和连接条件混在 WHERE 里 SELECT SALE.CNO, PCODE, CNAME, OQUANTITY FROM SALE, CUSTOMER WHERE CUSTOMER.CNO SALE.CNO AND CUSTOMER.SITE 上海 AND SALE.OQUANTITY 200;这条能跑结果也对。但隐式连接有两个实际麻烦连接条件和业务过滤条件挤在同一个 WHERE 里语句一长就看不清哪些是“表之间怎么对上”的哪些是“我要筛什么”另外漏写连接条件时会静默退化成笛卡尔积行数暴涨还不报错属于典型的翻车现场。改成显式 JOIN语义层次一下就清楚了-- 显式 JOIN连接条件写在 ON业务过滤写在 WHERE SELECT s.cno, s.pcode, c.cname, s.oquantity FROM sale AS s JOIN customer AS c ON c.cno s.cno WHERE c.site 上海 AND s.oquantity 200 ORDER BY s.cno, s.pcode;ON里只放表与表之间的等值关系WHERE里只放筛选条件一眼能看出这条语句涉及几张表、靠什么字段连起来。表别名s、c也不是为了少打几个字多表查询里字段名冲突CNO在两张表都有时别名是唯一能让语句读得下去的办法。4.2 单值子查询的前提 (SELECT ...)什么时候会炸第 12 条要“查询与美美公司在同一城市的客户公司名称及联系电话”-- 原写法假设 CNAME 唯一且结果里会包含美美自己 SELECT TELE, CNAME FROM CUSTOMER WHERE SITE (SELECT SITE FROM CUSTOMER WHERE CNAME 美美);这里用接子查询隐含了一个强假设子查询最多返回一行。如果客户表里有两家都叫“美美”的公司不同城市的分公司完全可能同名这条直接报ERROR 1242: Subquery returns more than 1 row。教学库里数据是造的不会重名真实数据里同名企业一抓一大把。稳妥写法是把换成IN顺手把“美美自己”排掉——题目问的是“同一城市的客户公司”把自己列进去虽然不算错但业务上没意义-- 用 IN 容忍多行返回并排除自身 SELECT cname, tele FROM customer WHERE site IN (SELECT site FROM customer WHERE cname 美美) AND cname 美美;子查询返回多行这件事用、、这类标量比较符都会炸只有IN、ANY、ALL、EXISTS能接。写子查询之前先问自己一句括号里那玩意儿保证只有一行吗保证不了就用集合运算符。4.3 NOT IN 与 NOT EXISTS第 14 条最该改写的地方第 14 条是整份文档里问题最大的一条-- 原写法! 接多行子查询必然报 1242 SELECT PRODUCT.PCODE, PNAME FROM PRODUCT WHERE PRODUCT.PCODE ! (SELECT SALE.PCODE FROM SALE);SALE.PCODE只要有两行以上这条就报错。就算改成NOT IN也还有第二个坑如果SALE.PCODE里混进了 NULLNOT IN的三值逻辑会让整个条件恒为 UNKNOWN结果集直接变空。这个坑我在真实项目里踩过一次数据里某个外键没做非空约束排查了整整一个下午才定位到。标准解法是用NOT EXISTS-- NOT EXISTS不受 NULL 影响语义是“不存在任何一条订购记录指向该产品” SELECT p.pcode, p.pname FROM product AS p WHERE NOT EXISTS ( SELECT 1 FROM sale AS s WHERE s.pcode p.pcode );也可以用LEFT JOIN ... IS NULL的写法效果等价-- 左连接找“连不上”的行同样不受 NULL 干扰 SELECT p.pcode, p.pname FROM product AS p LEFT JOIN sale AS s ON s.pcode p.pcode WHERE s.pcode IS NULL;两种写法在执行计划上通常都会被优化器改写成反连接anti-join性能接近。选哪个看团队习惯我倾向NOT EXISTS因为语义直白——“不存在”就是“不存在”不用在脑子里过一遍左连接的空值逻辑。4.4 相关子查询让外层表的字段进到内层判断里第 13 条要“查询订购了单价比 A01 号产品高的那些产品的产品编号、客户编号和订购数量”文档写的是-- 原写法子查询与外层无关联属于不相关子查询只会执行一次 SELECT SALE.PCODE, SALE.CNO, OQUANTITY FROM SALE, PRODUCT WHERE SALE.PCODE PRODUCT.PCODE AND PRICE (SELECT PRICE FROM PRODUCT WHERE PRODUCT.PCODE A01);这条逻辑是对的内层子查询只依赖常量A01跟外层当前处理哪一行无关数据库只会算一次拿到 A01 的单价然后外层逐行比较。这类叫不相关子查询效率最高也最不容易出错。相关子查询则相反内层要引用外层的列每处理外层一行就得跑一次内层。第 14 条的NOT EXISTS就是典型的相关子查询。很多人一听“每行跑一次”就觉得性能差实际不是——优化器会把它改写成反连接跟手写 JOIN 的执行计划基本一致。真正的性能陷阱是内层子查询没走索引那才是逐行全表扫。判断一个子查询相不相关看内层有没有引用外层的表别名。有就是相关没有就是不相关。这个区分决定了子查询执行几次也决定了改写方向不相关的可以考虑用变量或 CTE 缓存结果相关的优先考虑能不能改写成 JOIN。5. 避坑与排查14 条语句里跑不出正确结果的几个地方5.1 分组语法类ONLY_FULL_GROUP_BY 报 1055现象第 8 条直接执行报ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column但把同样的语句发到别人机器上能跑。原因MySQL 5.7 之后默认开启ONLY_FULL_GROUP_BY要求 SELECT 列表里的非聚合列必须出现在 GROUP BY 中。旧版本或关掉这个sql_mode的实例会放行但取到的是组内任意一行的值结果不可预期。解决不要为了让它跑通去关sql_mode那等于给自己埋雷。正确做法是把列补进 GROUP BY或者给该列套上聚合函数比如MAX(OQUANTITY)看题目到底要哪个语义。文档第 8 条要的是“订购数量总数”那就SUM(OQUANTITY)别让它裸着出现。5.2 结果集异常类查询返回空或行数翻倍现象第 4 条按SITE上海分组返回 0 行但客户表里明明有上海客户。原因两种可能。一是导入数据时客户端字符集不是utf8mb4中文常量插进去变成了乱码SITE里存的不是“上海”二是比较的两边字符集不一致触发了隐式转换。解决先SELECT DISTINCT site FROM customer;看实际存的是什么再用SHOW VARIABLES LIKE character_set%;核对连接字符集。重建库时把DEFAULT CHARSET utf8mb4和导入命令的--default-character-setutf8mb4都带上两边对齐。另一种是行数翻倍多表连接时漏写连接条件SALE和CUSTOMER直接笛卡尔积行数变成两表行数相乘。解决思路是先单独SELECT COUNT(*)每张表再对比连接后的行数差一个数量级基本就是漏了ON条件。5.3 子查询类1242 报错与 NOT IN 静默返回空现象第 12 条和第 14 条报ERROR 1242: Subquery returns more than 1 row把!改成NOT IN之后不报错了但结果集是空的而SALE表里确实只有部分产品被订购。原因1242 是标量比较符接到了多行子查询NOT IN返回空则是子查询结果里含 NULL三值逻辑让整个条件恒为 UNKNOWN。解决标量比较前先确认子查询的唯一性不确定就换IN需要“不存在”语义时统一用NOT EXISTS或LEFT JOIN ... IS NULL。顺手检查一下外键列有没有加NOT NULL从源头上断掉 NULL 混进来的可能。5.4 环境类外键约束挡住数据导入现象执行data.sql时报ERROR 1452: Cannot add or update a child row: a foreign key constraint fails。原因sale表插入了customer或product里不存在的编号通常是生成脚本里随机取产品时取到了没插进去的行或者导入顺序反了。解决确认导入顺序是先customer、再product、最后sale。生成脚本里用random.sample(products, ...)而不是凭空造编号就能保证引用有效。真需要临时绕过可以SET FOREIGN_KEY_CHECKS0;灌完再SET FOREIGN_KEY_CHECKS1;但灌完记得跑一遍SELECT COUNT(*) FROM sale s LEFT JOIN product p ON p.pcodes.pcode WHERE p.pcode IS NULL;验证没有孤儿行。6. 二次开发把这份实验文档改成能自动判题的练习集这份文档最大的价值不在那 14 条答案而在它提供了一套结构清晰、规模可控的题目集。真正想拿它做二次开发方向是把“人工对答案”换成“机器判题”这样一套题库能反复用也不怕学生互相抄。第一步是给每道题定义标准答案和校验方式。有些题结果是单个标量第 1、5、6、7、9 条直接比值就行有些题返回结果集得比集合而不是比行顺序。我一般用一份 YAML 把题目和答案固定下来- id: q06 title: 库存量超过 500 的产品个数 type: scalar sql: SELECT COUNT(*) FROM product WHERE stocks 500 - id: q14 title: 没有被订购的产品编号和名称 type: set order_sensitive: false sql: | SELECT p.pcode, p.pname FROM product p WHERE NOT EXISTS (SELECT 1 FROM sale s WHERE s.pcode p.pcode)type字段决定比对策略scalar直接比第一个单元格set把结果集转成有序元组集合再比order_sensitive控制要不要在意行序。判题脚本跑学生提交的 SQL把结果和标准答案做集合差差集为空就算通过。这个思路跟在线判题平台是一回事只是规模小得多。第二步是把环境隔离做掉。学生写错一条DELETE或UPDATE就可能把库改烂后面所有人的判题全崩。常见做法是每次判题前用schema.sql和data.sql重建一个独立 schema判完即删或者用事务包住判完ROLLBACK。前一种更彻底代价是重建耗时几十条数据量下可以忽略。第三步才是改造题目本身。原始文档里第 10 条缺失、编号从 9 跳到 11说明这套题原本就不完整。补题的时候按“同一张表、同一个知识点、难度递进”来设计统计查询补一道AVG配合HAVING的嵌套查询补一道EXISTS与NOT EXISTS对称的连接查询补一道三表连接的。每补一道就同步更新 YAML跑一遍判题脚本确认标准答案和样例数据对得上。有一回我图省事改完样例数据没重跑标准答案结果一整套题的判题结果全偏了学生交上来的正确答案被判错返工了一整晚。从那以后我每次动data.sql都强制走一遍“重建库 → 跑全部标准 SQL → 生成答案快照 → 提交”的流程一步不跳。这套流程写进脚本里也就二十行比事后一题一题核对省太多事了。希望这份拆解能帮你把这份实验文档从“交完就忘的作业”变成一套能反复用的练习库。本文还有配套的精品资源点击获取