ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

SQL统计查询与嵌套查询实验:聚合分组、连接子查询避坑

SQL统计查询与嵌套查询实验:聚合分组、连接子查询避坑

简介:数据库实验5嵌套查询.doc 面向高校数据库课程学习者,是一份围绕 SELECT 统计查询、常用统计函数、连接查询与嵌套查询四个层级组织的实验报告,可用于课堂实验提交、期末复习与 SQL 语法自查。压缩包内含 1 个 doc 文件,约 642KB,按实验目的、实验内容、实践结论、实验思考等板块成文,打开即可阅读。文档以 CPXS 数据库为背景,逐条列出客户数目统计、库存总量求和、按客户分组计数、单价区间筛选等十余道操作的语句范例,并演示由比较运算符、IN 等谓词构成的子查询与相关子查询写法。已有 931 人学习,读者可据此对照分组与 HAVING 过滤的差异,理解由内到外的查询拆解顺序,在动手练习时快速排查语法与逻辑错误。

1. 从一份跑得通却对不上数的查询清单说起

数据库实验做到第五个,卡点往往不在写不出 SELECT,而是语句跑通了、结果跟标准答案对不上。这份实验五围绕一个客户—产品—销售的 CPXS 库展开,重心是数据统计查询和嵌套查询两条线:一条用 COUNT、SUM、MAX、MIN 配合 GROUP BY、HAVING 做汇总,一条用子查询把“先算出一个值、再拿这个值去筛”的思路落成 SQL。它适合正在交数据库课设或实验报告的学生,也适合需要给新人出一套查询练习题的从业者。十四道题看着零散,其实覆盖的是聚合、连接、子查询三类基本功,把它们按考点重新排一遍,整套实验就从“抄语句”变成“能讲清为什么这么写”。

2. 数据模型与题目分层:三张表撑起十四道查询

2.1 三张核心表的字段语义与约束

做这类查询实验,第一件事不是写 SQL,是把表结构和约束确认清楚。约束决定了你能不能用“一行一个客户”“一行一个产品”这种假设去推结果,也决定了 GROUP BY 出来的计数到底在数什么。项目里出现过的字段有限,客户表有编号、名称、所在城市、联系电话,产品表有编号、名称、单价、库存量,销售表有客户编号、产品编号、订购数量。把这几个字段落成可执行的结构,后面十四道题才有稳定的落脚点。

-- CPXS 库的最小可用结构,够支撑统计、连接、嵌套三类查询 CREATE TABLE CUSTOMER ( CNO CHAR(4) NOT NULL, -- 客户编号,主键 CNAME VARCHAR(40) NOT NULL, -- 客户公司名称 SITE VARCHAR(20) NOT NULL, -- 所在城市 TELE VARCHAR(20), -- 联系电话,允许为空 PRIMARY KEY (CNO) ); CREATE TABLE PRODUCT ( PCODE CHAR(4) NOT NULL, -- 产品编号,主键 PNAME VARCHAR(40) NOT NULL, -- 产品名称 PRICE DECIMAL(8,2) NOT NULL, -- 单价,两位小数 STOCKS INT NOT NULL, -- 库存量 PRIMARY KEY (PCODE) ); CREATE TABLE SALE ( CNO CHAR(4) NOT NULL, -- 客户编号,外键指向 CUSTOMER PCODE CHAR(4) NOT NULL, -- 产品编号,外键指向 PRODUCT OQUANTITY INT NOT NULL, -- 订购数量 PRIMARY KEY (CNO, PCODE), -- 联合主键:同一客户对同一产品只有一条记录 FOREIGN KEY (CNO) REFERENCES CUSTOMER(CNO), FOREIGN KEY (PCODE) REFERENCES PRODUCT(PCODE) );

上面这段 DDL 里,真正影响结果的是 SALE 的联合主键。它意味着一张销售表里,“客户 A 买产品 B”只会出现一行,订购数量记在这一行上。于是COUNT(PCODE)在这个表里数出来的是“涉及多少个不同产品”,COUNT(*)数的是“有多少条订购记录”,两者在 SALE 上是同一个值,但换个场景就不一定了。很多同学写第 8 题时把COUNT(PCODE)当成“订购总量”用,结果条数对了、语义错了,答辩一问就露出破绽。

TELE 允许为空这一点也值得留意。电话号码缺失在真实数据里太常见,而COUNT(TELE)不会把 NULL 算进去,COUNT(*)会。这两个函数在同一张表上给出不同数字,是排查阶段最容易被误判成“数据丢了”的地方。我一般会建议在实验开始前先跑一遍SELECT COUNT(*), COUNT(TELE) FROM CUSTOMER;,把两个数字的差异记在纸上,后面遇到计数对不上时,先回头看这张表里到底有没有空值。

提示:如果手头的库已经把“上海”“美美”这类具体取值写死,照抄语句时记得替换成自己库里实际存储的值,字符串比较是逐字符匹配的,差一个字就是零行。

2.2 把十四道题按考点归成三组

十四道题摊开看像十四种写法,归一下类其实只有三组。分组的依据不是题号顺序,而是“过滤动作发生在哪一步”:是在分组前筛行,还是在分组后筛组,还是先算出一个中间结果再拿它当条件。按这个标准重新排列,实验的脉络会清楚很多,也方便对照检查自己是不是某一类完全没掌握。

组别题号核心考点涉及表关键语法
聚合统计1、2、5、6、7、9单值汇总,不分组或只出一个数PRODUCT、CUSTOMERCOUNT、SUM、MAX、MIN
分组过滤3、4、8按维度分组后再筛组SALE、CUSTOMER、PRODUCTGROUP BY、HAVING
连接与嵌套11、12、13、14跨表取数、子查询做条件全部三张表JOIN、IN、EXISTS

第一组的共性是“答案是一个数”。这类题的坑在于把本可以直接 WHERE 的题写成了 GROUP BY。第 6 题求库存量超过 500 的产品个数,正确答案是一行一个数字;写成GROUP BY STOCKS HAVING STOCKS > 500之后,返回的是每个不同库存值各一行,行数完全对不上。第 7 题求单价在 10 到 20 元之间的产品个数,同样的毛病,GROUP BY PRICE会把单价当成分组维度,结果是“每个价位有多少产品”,不是“这个区间一共有多少产品”。

第二组的共性是“答案是一张按维度排列的小表”。第 3 题按客户汇总订购数量,第 4 题按城市数公司,第 8 题按产品统计客户数和订购量。这三道题的分组列必须出现在 SELECT 里,非分组列要么包在聚合函数里,要么就别放。第 8 题原始写法把 OQUANTITY 直接摆在 SELECT 里、GROUP BY 却只有 PCODE,这在开启了严格分组检查的数据库上会直接报错,在宽松模式下会随机返回组内某一行的值,跑十次可能出十个结果。

第三组的共性是需要“两层思考”。第 12 题要先知道目标公司在哪个城市,再拿这个城市去筛其他公司;第 13 题要先知道 A01 的单价,再拿这个单价去比;第 14 题要先知道哪些产品出现在销售表里,再取反。这三道题正好对应子查询的三种典型用法:标量比较、集合成员判断、存在性判断。分组分对了,练的就不是十四个孤立语法点,而是三条可以迁移到实际业务里的思路。

3. 统计与分组落地:聚合函数、GROUP BY 与 HAVING 的写法边界

3.1 聚合函数的四类题型与标准写法

聚合函数这块,实验里出现的就四个:COUNT、SUM、MAX、MIN,但每道题的落脚点不同,写法上的差别不小。COUNT 关心的是“有多少”,SUM 关心的是“加起来多少”,MAX 和 MIN 关心的是“极值在哪”,它们的共同点是都会把多行压成一行,所以一旦 SELECT 里混进了普通列,就必须有 GROUP BY 兜底。下面这几条是这一类题里最稳的写法。

-- 第 1 题:客户总数。COUNT(*) 数行,不受任何列空值影响 SELECT COUNT(*) AS customer_cnt FROM CUSTOMER; -- 第 2 题:库存总量。SUM 只能作用于数值列 SELECT SUM(STOCKS) AS total_stock FROM PRODUCT; -- 第 5 题:所有产品的最高单价 SELECT MAX(PRICE) AS max_price FROM PRODUCT; -- 第 6 题:库存量超过 500 的产品个数。 -- 这是行级过滤,不是分组过滤,用 WHERE 一步到位 SELECT COUNT(*) AS product_cnt FROM PRODUCT WHERE STOCKS > 500; -- 第 7 题:单价在 10 到 20 元之间的产品个数。 -- BETWEEN 是闭区间,含 10 和 20 两个端点 SELECT COUNT(*) AS product_cnt FROM PRODUCT WHERE PRICE BETWEEN 10 AND 20; -- 第 9 题:以字母 B 开头的产品的最低单价 SELECT MIN(PRICE) AS min_price FROM PRODUCT WHERE PCODE LIKE 'B%';

逐条说一下取舍。第 1 题用COUNT(*)而不是COUNT(CNO),虽然 CNO 是主键不可能为空、两者结果一样,但语义上“数客户”数的是行,写成COUNT(*)更直白,也避免以后表结构变了出现偏差。第 2 题的SUM(STOCKS)如果表里一行都没有,返回的是 NULL 而不是 0,这一点在实验报告里写一句会显得你想过边界情况。

第 6 题和第 7 题是这一组里最容易写歪的。判断标准很简单:题目问的是“产品个数”,答案应该是一个标量;只要 SELECT 里出现了 COUNT 之外的裸列,或者出现了 GROUP BY,那基本就是在回答另一个问题。第 6 题如果一定要用分组写,正确形式是SELECT COUNT(*) FROM (SELECT PCODE FROM PRODUCT WHERE STOCKS > 500) t;,但这是绕远路,没必要。

第 9 题的关键在LIKE 'B%'。百分号匹配任意长度字符,包括零个字符,所以'B'本身也会被匹配上。容易出问题的是大小写:不同数据库的默认排序规则对字符串比较是否区分大小写不一样,某些配置下'b01'也会被'B%'命中。想让结果稳定,可以写成WHERE UPPER(PCODE) LIKE 'B%',代价是无法用到 PCODE 上的普通索引,数据量小的时候完全无所谓。

3.2 WHERE 与 HAVING 的分工:过滤发生在分组前还是分组后

这两者的区别,一句话能说清:WHERE 在分组之前筛行,HAVING 在分组之后筛组。落到具体题目上,判断依据是“过滤条件里有没有聚合函数或分组列”。第 4 题求某城市的客户公司数,城市是 CUSTOMER 表里的普通列,过滤发生在分组前,所以该用 WHERE。原始写法用的是GROUP BY SITE HAVING SITE = '某市',结果是对的,但绕了一圈:先按城市分成若干组,再把不匹配的组扔掉,多做了一倍功。更麻烦的是这种写法会让人形成错误直觉,遇到“按城市分组、只保留客户数大于 3 的城市”时,就分不清哪部分该放 WHERE、哪部分该放 HAVING。

-- 第 4 题推荐写法:先筛行,再分组 SELECT SITE, COUNT(CNO) AS company_cnt FROM CUSTOMER WHERE SITE = '某市' GROUP BY SITE; -- 第 3 题:按客户汇总订购数量,只有分组没有过滤 SELECT CNO, SUM(OQUANTITY) AS total_qty FROM SALE GROUP BY CNO; -- 第 8 题:按产品统计,分组后再用 HAVING 筛掉不满足条件的组 SELECT PCODE, COUNT(DISTINCT CNO) AS customer_cnt, -- 有多少个不同客户买过 SUM(OQUANTITY) AS total_qty -- 这些客户的订购总量 FROM SALE GROUP BY PCODE HAVING COUNT(DISTINCT CNO) >= 2 AND MIN(OQUANTITY) > 100; -- 每个客户的订购量都超过 100

第 8 题的语义值得单独拎出来讲。原题表述是“至少被两个以上客户订购且每个客户订购数量超过 100”,这句话本身有歧义:可以理解成“总量超过 100”,也可以理解成“每一个客户的单笔订购都超过 100”。两种理解写出来的 SQL 不一样,前者用SUM(OQUANTITY) > 100,后者用MIN(OQUANTITY) > 100。我在批改的时候更认第二种,因为“每个客户订购数量超过 100”里的“每个”是限定到客户这一层的。真到答辩被问到,能主动指出这个歧义并给出两种写法,比默写一条语句加分得多。

COUNT(DISTINCT CNO)里的 DISTINCT 也不能省。虽然当前表结构下同一个客户对同一产品只有一行,DISTINCT 加不加结果一样,但一旦以后销售表允许拆单(同一客户同一产品分多次订购),不加 DISTINCT 数出来的就是记录条数而不是客户数。写实验报告时把这一层说明写进去,体现的是对数据模型的理解,不是对语法的记忆。

3.3 分组结果二次筛选:容易写歪的两道题

第 6、7 题已经在前一节改过了,这里补一个更典型的场景:如果题目变成“求订购总量超过 500 的客户编号”,那就必须走 GROUP BY + HAVING,因为“订购总量”这个值在原始行上根本不存在,是先聚合出来的。这时候写成 WHERE 就必然报错,因为 WHERE 执行的时候聚合还没算。

-- 分组后再筛:聚合结果不能出现在 WHERE 里 SELECT CNO, SUM(OQUANTITY) AS total_qty FROM SALE GROUP BY CNO HAVING SUM(OQUANTITY) > 500; -- 对照:WHERE 里只能出现原始列 SELECT CNO, SUM(OQUANTITY) AS total_qty FROM SALE WHERE OQUANTITY > 100 -- 先筛掉单笔小于等于 100 的行,再汇总 GROUP BY CNO;

这两段放在一起看,执行顺序就清楚了:FROM 拿到表,WHERE 逐行筛,GROUP BY 把剩下的行分组,聚合函数在组内计算,HAVING 再把不满足条件的组扔掉,最后 SELECT 决定输出哪些列。记住这个顺序,绝大多数“这条语句该放 WHERE 还是 HAVING”的问题都能自己判断,不用死记。

还有一个常见的误用是把别名写进 WHERE。SELECT SUM(OQUANTITY) AS total_qty ... WHERE total_qty > 500在很多数据库上会直接报“未知列”,因为 WHERE 执行时别名还没产生。别名可以用在 ORDER BY 和 HAVING 里(部分数据库支持 HAVING 用别名),但 WHERE 不行。这类报错信息通常很明确,看到“Unknown column”先想是不是别名用错了位置。

4. 连接与嵌套:从等值连接推进到相关子查询

4.1 三表等值连接的写法与字段限定

第 11 题是整套实验里第一道真正需要跨表的题:要客户编号、产品编号、联系人、订购数量,还要限定“某城市客户”和“订购量大于 200”。客户信息在 CUSTOMER,订购信息在 SALE,两个条件分别落在这两张表上,所以至少要连两张表。写法上有隐式连接和显式 JOIN 两种,结果一样,可读性差别很大。

-- 显式 JOIN 写法:连接条件和过滤条件分开摆放 SELECT s.CNO, s.PCODE, c.CNAME, s.OQUANTITY FROM SALE s JOIN CUSTOMER c ON c.CNO = s.CNO WHERE c.SITE = '某市' AND s.OQUANTITY > 200; -- 隐式连接写法:连接条件混在 WHERE 里,和老教材一致 SELECT SALE.CNO, SALE.PCODE, CUSTOMER.CNAME, SALE.OQUANTITY FROM SALE, CUSTOMER WHERE CUSTOMER.CNO = SALE.CNO AND CUSTOMER.SITE = '某市' AND SALE.OQUANTITY > 200;

两种写法我都不反对,但建议统一用第一种,理由是可维护。隐式连接把“表之间怎么关联”和“业务上筛什么”混在一个 WHERE 里,两张表还能看清,三张表以上就开始数不清哪个条件是连接、哪个条件是过滤,漏掉一个连接条件就变成笛卡尔积,行数翻几倍还不报错。第 13 题要连 SALE 和 PRODUCT 两张表,第 11 题连两张,目前还没到三表,但把习惯养在两张表的时候成本最低。

字段限定符也不能省。SALE 和 CUSTOMER 都有 CNO 列,SELECT 里写裸的CNO在某些数据库上会报“列名歧义”,即使不报错,读代码的人也得猜这个 CNO 来自哪张表。统一写成别名.列名是最省事的做法,表别名用单字母(s、c、p)就够,既短又不丢信息。

4.2 单行子查询:把一个标量值算出来再比较

第 12 题和第 13 题是子查询的入门形态:内层查询返回一个值,外层拿这个值做比较。第 13 题要找“单价比 A01 高的产品的订购记录”,思路分两步,先查出 A01 的单价,再拿这个单价去筛 SALE 和 PRODUCT 的连接结果。

-- 第 13 题:内层返回单个单价,外层做 > 比较 SELECT s.PCODE, s.CNO, s.OQUANTITY FROM SALE s JOIN PRODUCT p ON p.PCODE = s.PCODE WHERE p.PRICE > ( SELECT PRICE FROM PRODUCT WHERE PCODE = 'A01' );

这段能跑通的前提是内层最多返回一行,也就是 PCODE 是 PRODUCT 的主键。主键唯一性保证了这一点,所以用>是安全的。反过来说,如果内层查的是一个可能返回多行的列(比如按名称查、名称没加唯一约束),这条语句就会在某些数据库上直接抛“子查询返回多于一行”的错误,在另一些数据库上悄悄取第一行。这个差异很坑,因为本地跑得好好的,换台机器就报错。

第 12 题的结构相同,但坑更深一层:要找“和目标公司在同一城市的其他公司”。内层按公司名查城市,外层按城市查公司。如果同一城市只有一家叫这个名字的公司,内层返回一行,=能用;一旦数据里出现重名,立刻炸。

-- 第 12 题:稳妥写法,用 IN 兜住内层多行的情况 SELECT CNAME, TELE FROM CUSTOMER WHERE SITE IN ( SELECT SITE FROM CUSTOMER WHERE CNAME = '某公司' ) AND CNAME <> '某公司'; -- 排除目标公司自身

这里有两个细节。一是把=换成IN,语义上等价于“城市属于内层返回的城市集合”,内层返回一行还是多行都不会报错。二是补了CNAME <> '某公司',因为“与某公司同城的客户公司”按常规理解不包含它自己。原题没明说要不要排除自身,但答辩时被问到“为什么结果里有目标公司”,答不上来就很尴尬。主动排除并在报告里写一句,是加分项。

4.3 多行子查询与 EXISTS:第 14 题的稳妥写法

第 14 题求“没有被订购的产品编号和名称”,本质是一个差集运算:PRODUCT 里减去 SALE 里出现过的产品。原写法是WHERE PRODUCT.PCODE != (SELECT SALE.PCODE FROM SALE),这条语句有两个独立的毛病,随便踩中一个结果就是错的。第一个是内层会返回多行,!=后面跟多行在多数数据库上直接报错;第二个是即使数据库容忍了,!=配子查询做的是“不等于其中某一个”的判断,逻辑上根本不是差集。

-- 第 14 题推荐写法一:NOT EXISTS,相关子查询 SELECT p.PCODE, p.PNAME FROM PRODUCT p WHERE NOT EXISTS ( SELECT 1 FROM SALE s WHERE s.PCODE = p.PCODE ); -- 第 14 题推荐写法二:NOT IN,注意 NULL 风险 SELECT PCODE, PNAME FROM PRODUCT WHERE PCODE NOT IN ( SELECT PCODE FROM SALE WHERE PCODE IS NOT NULL );

NOT EXISTS是这类“取反”题里最稳的。它逐行拿 PRODUCT 的 PCODE 去 SALE 里试,找不到就保留,语义直白,而且不受 NULL 影响——因为 EXISTS 只关心“有没有行”,不关心行里的值是什么。NOT IN写法更短,但必须确认子查询结果里没有 NULL,否则整个条件会变成 UNKNOWN,一行都返回不了。当前 SALE.PCODE 是主键的一部分,不可能为空,所以两种写法等价;但把IS NOT NULL写上不亏,是给自己留的保险。

还有第三种写法,用 LEFT JOIN 找右表为空的记录:

-- 第 14 题推荐写法三:左连接后筛右表为空 SELECT p.PCODE, p.PNAME FROM PRODUCT p LEFT JOIN SALE s ON s.PCODE = p.PCODE WHERE s.PCODE IS NULL;

三种写法结果一致,执行计划在不同数据量下各有优劣,但在这个实验的数据规模上感觉不出来。我通常让学生至少写两种并说明为什么等价,这样答辩时不管被问到哪种都能接住。真要选一个作为默认写法,NOT EXISTS的语义最贴近题目表述,“没有被订购的产品”就是“不存在一条订购记录的”。

5. 避坑与排查:五个让结果对不上的高频问题

5.1 现象:GROUP BY 之后结果随机变,重跑一次数不同

原因基本可以锁定在 SELECT 里混了非分组列。第 8 题的原始写法把 OQUANTITY 摆在 SELECT 里、GROUP BY 只有 PCODE,一个产品被多个客户订购时,组内有多行 OQUANTITY,数据库要么直接报错,要么随便挑一行返回。后者更危险,因为它不报错,你会以为语句没问题。

排查方法是把 SELECT 里的裸列逐个检查一遍:这一列要么出现在 GROUP BY 里,要么被聚合函数包住,二者必居其一。解决思路是先想清楚“我要的是每个产品一行,那这一行里的订购数量代表什么”,如果代表总量就用 SUM,代表最大值就用 MAX,代表某一个具体客户的量那就说明分组维度选错了,应该按“产品 + 客户”分组。

-- 反例:OQUANTITY 既不在 GROUP BY 里,也不在聚合函数里 SELECT PCODE, OQUANTITY FROM SALE GROUP BY PCODE; -- 正例:把非分组列包进聚合函数,或加进分组维度 SELECT PCODE, SUM(OQUANTITY) AS total_qty FROM SALE GROUP BY PCODE;

5.2 现象:明明有数据,加了条件之后一行都不返回

先在脑子里过一遍这个条件里有没有 NULL 参与比较。三值逻辑里,任何值和 NULL 做=、!=、>比较,结果都是 UNKNOWN,而 WHERE 只保留结果为 TRUE 的行。最典型的翻车点是NOT IN配上一个可能含 NULL 的子查询:内层结果里只要有一个 NULL,整个NOT IN就永远不成立,返回空集,且不报任何错。

排查办法是先把子查询单独跑一遍,看结果里有没有 NULL。SELECT PCODE FROM SALE WHERE PCODE IS NULL;一条语句就能确认。如果确实有,要么在子查询里加WHERE PCODE IS NOT NULL,要么整体换成NOT EXISTS。第 14 题就属于这一类,虽然当前表结构下 PCODE 不会为空,但换个数据源、换个可空列,同样的写法立刻失效。

5.3 现象:=或!=接子查询时报“返回多于一行”

这属于把单行子查询的假设用在了多行场景上。第 12 题按公司名查城市、第 14 题按产品查订购记录,内层返回几行完全取决于数据。主键或唯一约束能保证单行,其他列不能。开发阶段数据少,恰好只有一行,测试全绿;换成完整数据集就报错,这类问题最耗时间。

解决办法分两种。如果业务上确实只会有一个值(比如按主键查),那就在子查询里加LIMIT 1并接受“取第一条”的语义,但更推荐的做法是保留约束本身、别依赖 LIMIT 掩盖数据问题。如果业务上可能有多个值,直接换成IN或EXISTS,把“等于某一个”改成“属于某个集合”或“存在匹配行”,语义和稳定性都更好。

5.4 现象:连接两张表之后,统计数字翻了好几倍

这是笛卡尔积的典型症状。两表连接时漏写连接条件,或者连接条件写得不够严(比如用了一个不唯一的列做等值),结果集行数会变成两表行数的乘积。第 11 题里 SALE 和 CUSTOMER 靠 CNO 关联,如果 WHERE 里只写了城市条件和数量条件、忘了CUSTOMER.CNO = SALE.CNO,一个客户有多少条订购记录就会被重复匹配客户表的行数倍。

排查时先单独跑SELECT COUNT(*) FROM 表A;和SELECT COUNT(*) FROM 表B;,再跑连接后的行数,如果接近两者乘积,基本可以确定漏了条件。解决就是补上等值连接条件,并且养成用显式 JOIN 的习惯——ON后面写连接条件、WHERE后面写过滤条件,物理上分开,漏写的概率会低很多。

5.5 现象:LIKE 查出来的结果比预期多或少

LIKE 'B%'这类模式匹配,问题通常出在排序规则和通配符理解上。百分号匹配任意长度字符串(含零长度),下划线匹配恰好一个字符,这两个容易混。另外,某些数据库的默认排序规则下字符串比较不区分大小写,'B%'会把'b01'也捞进来;反过来,如果列上建了普通索引,用UPPER(PCODE) LIKE 'B%'虽然结果稳定,但索引可能用不上。

第 9 题只要求最低单价,多匹配几行可能不影响结果(最小值往往不变),但换成“统计以 B 开头的产品个数”就会直接错。解决办法是先跑SELECT PCODE FROM PRODUCT WHERE PCODE LIKE 'B%';看实际命中哪些,确认大小写和边界;需要严格控制时显式写WHERE PCODE LIKE BINARY 'B%'或WHERE PCODE REGEXP '^B',具体语法按手头数据库的文档来,别跨库照搬。

6. 验收与答辩:用一张结果对照表把十四道题跑通

这套实验的验收,核心不是“语句能不能执行”,而是“结果能不能解释”。我一般要求做完之后交一张对照表,每道题写清三件事:这条语句返回几行、每行代表什么业务含义、换一组数据会不会失效。把这张表填满,基本就等于把十四道题的逻辑都过了一遍脑子。

题号预期返回形态关键验证点易错写法
1、2、5单行单列聚合结果是否为空用 COUNT(列) 代替 COUNT(*)
6、7、9单行单列WHERE 是否在分组前生效误加 GROUP BY 变成多行
3、4多行,每行一个维度分组列是否都在 SELECT 里用 HAVING 代替 WHERE
8多行,每行一个产品每个客户订购量都超 100非分组列裸露在 SELECT
11多行,跨两表字段连接条件是否完整漏写等值条件出笛卡尔积
12多行,不含目标公司自身内层是否可能多行用 = 接多行子查询
13多行,单价严格大于内层是否唯一比较方向写反
14多行,SALE 中不存在的产品子查询是否有 NULL用 != 接多行子查询

验收时的具体动作,我习惯按这个顺序走一遍。先把三张表的行数各查一次并记下来,这是后面所有计数的基准。再按题号顺序逐条执行,每执行一条就把结果行数和对照表里的预期形态比一次,行数不对立刻停下来查,不要往下跑。全部跑完后,挑三道题做“改数据验证”:往 SALE 里插一条新记录,看第 3、8、14 题的结果是否按预期变化,变化方向不对说明语句里藏着硬编码或者逻辑漏洞。

答辩环节通常集中在几个点上。第一个高频问题是“为什么这条用 HAVING 不用 WHERE”,答的时候不要背定义,直接说这条语句里过滤条件用到了聚合结果,聚合在分组之后才算出来,所以只能放 HAVING。第二个是“子查询能不能改成连接”,答案是可以,很多场景下连接的可读性和执行效率更好,但要说明改写后如何去重——子查询天然去重,连接不一定,这个差异经常被忽略。第三个是“如果销售表允许同一客户同一产品多条记录,你的语句还对不对”,这道题最能区分背语句和真理解的人,凡是用了 COUNT(PCODE) 而没加 DISTINCT 的,这里都会露馅。

还有一个容易被忽视的准备动作:把自己的语句在两种不同规模的数据上各跑一遍。小数据量下很多错误不显现,比如漏写连接条件,两表各三行时笛卡尔积才九行,看着像正常结果;换成各一百行就是一万行,一眼就能看出不对。我一般会临时复制一份表,插几十行造出来的数据进去跑,专门用来暴露这类问题。

说个我自己踩过的坑。早年做类似的查询练习,第 14 题我用NOT IN写得飞快,本地数据全是干净的,一路绿灯;换了一套带空值的数据之后,结果直接变成零行,我盯着屏幕找了四十分钟,最后把子查询单独跑出来才发现里面有一行 NULL。从那以后我每次写取反类查询,都会强制先跑一遍子查询看有没有空值,有就换NOT EXISTS,没有也顺手加个IS NOT NULL保底。这个习惯后来帮我省下的时间,远超当初多写那几个字符的成本。希望这套按考点重排的思路和这张验收表,能帮你把这份实验五跑得顺一点。

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

RELATED READING

延伸阅读

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