简介:面向数据库课程学习者与实验教学场景的《数据库实验5嵌套查询》实验报告文档,聚焦SELECT统计查询、聚合函数与嵌套查询的实操训练,适合课后复盘、实验提交和语法查漏补缺,并配有实验目的、内容与思考模块。压缩包内共1个doc文件,约642KB,围绕CPXS数据库组织内容,涵盖COUNT、SUM、MAX、MIN等统计函数、连接查询,以及子查询、派生表和相关子查询等写法。报告结合上海客户订购数量大于200套、与“美美”同城客户、单价高于A01产品、未被订购产品等典型题目,展示分组聚合、HAVING过滤与多层条件嵌套的查询示例。已有931人学习,可作为数据库实验课配套参考,帮助梳理从统计查询到复杂嵌套查询的解题思路。
1. 数据库实验里那串 SELECT,到底在干什么
第一次翻开「数据库实验5嵌套查询」这份文档,大多数人的反应是:满屏大写的 SELECT、FROM、WHERE,像一堆没背下来的咒语。其实数据库查询没那么玄,它就是在跟几张表“提问”——你手上有客户表、产品表、销售表,你想知道“一共几个客户”“哪些产品没人买”,SQL 就是把这句话翻译成机器听得懂的句式。这份实验围绕三件事展开:统计查询、连接查询、嵌套查询。其中嵌套查询是明显的分水岭,学会它,你就能把一个大问题拆成两个小问题分别去问。它适合刚接触 SQL、被 GROUP BY 和多层括号劝退的零基础同学,学完能独立写出实验里那十几条查询。
2. 打地基:SELECT 和五个统计函数怎么写
先别急着看嵌套。嵌套查询是“查询里面套查询”,如果外面那一层你都没写顺,套起来只会更乱。这一章的目标很朴素:让你能独立写出单表查询,并且看懂实验文档里前八条题在干什么。等你把统计函数和分组玩熟了,后面两章才有落脚点。
2.1 一条 SELECT 其实只有三段话
新手最容易犯的错,是把 SELECT 当成一个整体去背。换个角度,它永远在回答三个问题:查什么、从哪查、什么条件下查。写成句式就是SELECT 列 FROM 表 WHERE 条件,三段话拼起来,中间用空格隔开,最后加分号。
SELECT CNAME, SITE -- 查什么:客户名称、所在城市两列 FROM CUSTOMER -- 从哪查:客户表 CUSTOMER WHERE SITE = '南城'; -- 什么条件:只要城市是南城的行这段的意思直白到不用翻译:从客户表里,把南城那些客户的名称和城市挑出来。三个参数你要会改:SELECT后面换成你想要的列名,多个列用逗号隔开、写*表示全部列;FROM后面换成你的表名;WHERE后面换成筛选条件,文本值要用英文单引号包起来,数字不用。
提示:SQL 里的引号必须是英文半角单引号,中文引号会让语句直接报语法错误,这是新手第一个晚上最常见的翻车点。
顺带说一句,SQL 不区分大小写,select和SELECT效果一样,但习惯上把关键字写成大写、表名列名写成小写,读起来一眼就能分清哪是语法、哪是你自己的东西。
2.2 五个统计函数:数数、求和、找最大最小
实验文档里反复出现的 COUNT、SUM、MAX、MIN、AVG,统称聚合函数。它们的共同点是:吃进去一堆行,吐出来一个数。你可以把它们想成 Excel 表格底部的那个状态栏。
| 函数 | 干什么 | 实验里的典型用法 |
|---|---|---|
| COUNT(*) | 数一共有多少行 | 统计客户的数目 |
| COUNT(列名) | 数这一列有多少个非空值 | 数有多少个客户编了号 |
| SUM(列名) | 把这一列的数字加起来 | 求库存量的总和 |
| MAX / MIN | 取这一列的最大 / 最小值 | 求所有产品的最高单价 |
| AVG | 求平均值 | 算平均订单量 |
SELECT COUNT(*) AS 客户总数 FROM CUSTOMER; -- 数行数,结果是 3 SELECT SUM(STOCKS) AS 库存合计 FROM PRODUCT; -- 四行库存相加 SELECT MAX(PRICE) AS 最高单价, MIN(PRICE) AS 最低单价 FROM PRODUCT; -- 一条语句取两个极值AS是起别名,纯粹为了结果好看,不起也不影响正确性。这里真正要留意的是 COUNT 的两种写法:COUNT(*)数的是行数,哪怕整行都是空值也算一行;COUNT(列名)只数这一列非空的值,遇到 NULL 直接跳过。实验里“统计客户数目”用COUNT(*)最稳,别为了显得高级去改。
2.3 GROUP BY 和 HAVING:先分堆,再筛堆
单表查询里最难绕的一段,就是分组。GROUP BY CNO的意思是:把销售表按客户编号分成一堆一堆,同一个客户的记录摞在一起,然后对每一堆分别算一次聚合函数。
SELECT CNO, COUNT(PCODE) AS 产品种类数 -- 每个客户,数他买了几种产品 FROM SALE GROUP BY CNO -- 按客户编号分成若干堆 HAVING COUNT(PCODE) >= 2; -- 只留下种类数达到 2 的堆这段对应的正是实验里“求至少订购两种以上产品的客户编号和产品种类数”。注意HAVING和WHERE的分工:WHERE 在分堆之前过滤单行,HAVING 在分堆之后过滤整堆。写错位置就会报错,因为 WHERE 那一层还没有“堆”的概念,聚合函数根本没法算。
提示:一条完整 SELECT 的执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。你按这个顺序去读语句,比从第一个单词往后读要顺得多。
2.4 第一个能跑起来的例子:把实验库搭出来
光看不练没用。下面这段脚本把实验里的三张表缩成一个最小可跑的版本,字段名沿用原文档,值全部是虚构的占位数据。在 MySQL 或 SQLite 里整段执行,你就有了一个能反复折腾的小库。
-- 客户表:一个客户一行 CREATE TABLE CUSTOMER ( CNO VARCHAR(10), -- 客户编号,如 C01 CNAME VARCHAR(30), -- 客户名称 SITE VARCHAR(20), -- 所在城市 TELE VARCHAR(20) -- 联系电话 ); -- 产品表:一个产品一行 CREATE TABLE PRODUCT ( PCODE VARCHAR(10), -- 产品编号,如 A01 PNAME VARCHAR(30), -- 产品名称 PRICE DECIMAL(10,2), -- 单价 STOCKS INT -- 库存量 ); -- 销售表:一条订购记录一行 CREATE TABLE SALE ( CNO VARCHAR(10), -- 哪个客户买的 PCODE VARCHAR(10), -- 买了哪个产品 OQUANTITY INT -- 订购数量 );建完表再灌几行数据进去,注意插入时列的顺序要和建表时一致:
INSERT INTO CUSTOMER VALUES ('C01','云帆','南城','T-0001'), ('C02','星野','南城','T-0002'), ('C03','北岭','北港','T-0003'); INSERT INTO PRODUCT VALUES ('A01','台灯', 15.00, 800), ('A02','水杯', 12.50, 300), ('B01','背包', 88.00, 120), ('B02','雨伞', 30.00, 60); INSERT INTO SALE VALUES ('C01','A01', 300), ('C01','B01', 150), ('C02','A01', 120), ('C03','A02', 50);现在跑第一条真正的查询:SELECT COUNT(*) FROM CUSTOMER;,结果应该是 3。再跑SELECT SUM(STOCKS) FROM PRODUCT;,800+300+120+60,结果是 1280。如果这两条都对上了,说明你的库搭好了,后面所有例子都可以在这个库里验证。跑不对就回头看两件事:表建成功了吗,数据插进去了吗——用SELECT * FROM CUSTOMER;看一眼,比盯着报错猜快得多。
3. 两张表拼起来才有答案:连接查询入门
3.1 为什么单表问不出“南城客户买了多少”
实验文档里有一道很典型的题:查询由南城客户订购的、且订购数量大于 200 的记录,要输出客户编号、产品编号、联系人和订购数量。你一看就知道,这四个字段根本不在同一张表里——客户编号、联系电话在 CUSTOMER,订购数量在 SALE。单表查询再怎么写也拼不出这四个字段,因为一张表根本不知道另一张表的存在。
这就是连接查询存在的理由:把两张表按某个共同的列“对上”,拼成一张宽表,然后在这张宽表上做筛选。对新手来说,先记住一句话就够了——连接的本质是“配对”,配对的依据是两张表里含义相同的那个列,在实验库里就是 CNO 和 PCODE。
3.2 最朴素的写法:FROM 写两张表,WHERE 负责牵线
SELECT SALE.CNO, PCODE, CNAME, OQUANTITY FROM SALE, CUSTOMER -- 两张表都写进 FROM,逗号隔开 WHERE CUSTOMER.CNO = SALE.CNO -- 牵线:客户编号相等才算同一个人 AND CUSTOMER.SITE = '南城' -- 再筛:只要南城的客户 AND SALE.OQUANTITY > 200; -- 再筛:只要订购量超过 200参数上要注意两点:一是列名前加了表名做前缀,因为 CNO 这张表有、那张表也有,不加前缀数据库不知道该用哪个;二是配对条件CUSTOMER.CNO = SALE.CNO必须写,漏掉它就变成了两张表的笛卡尔积,3 个客户 × 4 条销售记录 = 12 行全部冒出来,看着像答案,其实是错的。
对照我们的示例数据跑一遍,结果只有一行:C01、A01、云帆、300。C02 的 120 不满足大于 200,C03 不在南城,都被过滤掉了。这一步能跑通,你就已经掌握了实验里大半的连接题。
3.3 INNER JOIN 只是换了个写法,别被吓到
同一件事,现代一点写成JOIN ... ON ...:配对条件从 WHERE 挪到 ON 后面,筛选条件还留在 WHERE,读起来层次更清楚。功能上两者完全等价,实验文档用的是前一种,你两种都得看得懂。
SELECT s.CNO, s.PCODE, c.CNAME, s.OQUANTITY FROM SALE AS s -- 给表起短别名 s JOIN CUSTOMER AS c ON c.CNO = s.CNO -- 配对条件写在 ON 后面 WHERE c.SITE = '南城' AND s.OQUANTITY > 200;别名的作用是少打字,s.CNO比SALE.CNO短,表多的时候尤其明显。这里最容易踩的坑是把筛选条件写进 ON:ON只负责怎么配对,WHERE才负责配对完之后留哪些行,两者混在一起写,结果集可能悄悄变样,你还以为是自己数据错了。
还有一个概念要知道但先别深挖:INNER JOIN只保留两边都能配上的行;LEFT JOIN会保留左表全部行,右边配不上的位置填 NULL。实验里基本用不到 LEFT JOIN,等你遇到“想列出所有产品、包括没卖出去的那些”这类需求时再回头看它,那时候你自然就懂了。
3.4 换表名是新手最常犯的错
示例里用的是 CUSTOMER、PRODUCT、SALE 这套名字,但很多同学自己建库时写成了客户表、产品表,或者大小写混着来。写查询之前,先用一条SELECT * FROM 表名;确认表真的存在、列名拼写真的对。这一步花十秒,能省掉半小时的“语法没错为什么报错”。
另外,实验文档里第 11 题写的是TELE CNAME,中间少了个逗号,属于文档笔误,你照抄会直接报错。看到这类地方别慌,把它当文档 bug 修掉就行——真实工作里别人给的示例 SQL 有笔误是常态。
4. 嵌套查询:把大问题拆成两个小问题
4.1 嵌套查询就是“先算括号里的那一句”
到正题了。所谓嵌套查询,字面意思就是一条 SELECT 里面又套了一条 SELECT,里面那条叫子查询,外面那条叫父查询。它的写法很像数学里的括号:先算里面的,把结果当成一个值或者一串值,交给外面那条用。
举个人话例子。你要找“和云帆公司在同一个城市的客户”,你不知道云帆在哪,得先查一次;知道它在南城之后,再去查南城有哪些客户。这两步合成一句话,就是嵌套查询。新手觉得它难,往往不是语法难,而是习惯了一步问完所有事。改成“先问一个小问题,拿答案再问下一个”,思路立刻就通了。
提示:写嵌套查询的时候,永远先把子查询单独拎出来跑一遍,看看它到底返回一个值、一列值、还是一整张表。这个习惯能帮你避开后面一半的坑。
4.2 子查询只吐一个值时,直接拿来比大小
最安全的一种嵌套:子查询只返回单个值,这种叫标量子查询。因为它就是一个数或者一个字符串,所以可以直接放在=、>、<后面。
-- 先查 A01 的单价(15.00),再找单价比它高的产品被谁买过 SELECT s.PCODE, s.CNO, s.OQUANTITY FROM SALE AS s JOIN PRODUCT AS p ON p.PCODE = s.PCODE WHERE p.PRICE > (SELECT PRICE FROM PRODUCT WHERE PCODE = 'A01');拆开看更清楚:括号里那句SELECT PRICE FROM PRODUCT WHERE PCODE='A01'单独跑,结果就是一行一列、值 15.00。外面那句等价于WHERE p.PRICE > 15.00。这里之所以能直接比大小,前提是 A01 在产品表里唯一,子查询保证只返回一行。如果 PCODE 有重复,这句立刻报错“子查询返回多于一行”,那就是第 5 章要讲的坑。
对上我们的示例数据,A02 单价 12.50 不够高,B01 是 88.00,所以最终结果只有一行:B01、C01、150。
4.3 子查询吐一列多行时,得用 IN 接住
第二种情况更常见:子查询返回的是一列、好几行,比如“所有南城客户的编号”。这时候你没法用等号,因为等号只能对上一个值,得换成IN,意思是“在这个集合里”。
-- 查出所有南城客户的编号,再拿这串编号去销售表里捞订单 SELECT CNO, PCODE, OQUANTITY FROM SALE WHERE CNO IN (SELECT CNO FROM CUSTOMER WHERE SITE = '南城');括号里单独跑,返回的是 C01、C02 两个值;父查询就等价于WHERE CNO IN ('C01','C02')。跑出来的结果是 C01 的两条订单加 C02 的一条,共三行。IN的兄弟是NOT IN,意思反过来,但NOT IN有个著名的 NULL 陷阱,第 5 章会专门拆。
还有一对操作符ANY、ALL,实验文档里提到了谓词这个概念。你可以先记个大概:> ANY (子查询)相当于“比最小的那个大”,> ALL (子查询)相当于“比最大的那个还大”。日常写查询用MAX、MIN更直观,ANY、ALL看懂就行,不必强求自己用。
4.4 完整走一遍:找出从来没被订购过的产品
这是实验文档最后一道题,也是最能体现嵌套价值的一道。原文档用的是WHERE PRODUCT.PCODE != (SELECT SALE.PCODE FROM SALE),思路对,但写法埋着雷:只要销售表里有不止一条记录,子查询就返回多行,!=直接报错。这正是新手最容易照抄翻车的地方。
稳妥的写法有两种,先看NOT IN版本:
-- 子查询列出所有出现过的产品编号,父查询要那些不在名单里的 SELECT PCODE, PNAME FROM PRODUCT WHERE PCODE NOT IN (SELECT PCODE FROM SALE WHERE PCODE IS NOT NULL);WHERE PCODE IS NOT NULL这句是保险丝:如果销售表里存在产品编号为 NULL 的记录,NOT IN会因为拿不准“不在名单里”而整体返回空结果。加上它,问题就绕过去了。对照示例数据,SALE 里出现过 A01、A02、B01,PRODUCT 里有 A01、A02、B01、B02,结果自然是 B02 雨伞——进了货但一单没卖出去。
另一种写法用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 -- 内层能不能找到匹配 );SELECT 1在这里只是个占位,EXISTS只关心“有没有行”,不关心取出来的是什么列。这条语句的执行方式是:产品表每一行都拿到内层去试一次,试不到匹配就留下。两种写法结果一样,NOT EXISTS更耐操,实际写实验报告时用哪种都行,但要知道它们的区别在哪。
4.5 三层嵌套也别慌:从最里面往外读
嵌套到三层的时候,人眼容易看花。办法只有一个:把最内层单独复制出来跑,确认结果,再往外包一层,再跑一次。嵌套查询是能一层层验证的,别指望一口气写对。
-- 找订购了"南城客户买过的产品"的那些订单 SELECT CNO, PCODE, OQUANTITY FROM SALE WHERE PCODE IN ( -- 第三层:拿产品编号去捞订单 SELECT PCODE FROM SALE WHERE CNO IN (SELECT CNO FROM CUSTOMER WHERE SITE = '南城') -- 第一、二层 );从最里面那句开始:先查出南城客户编号 C01、C02;往外一层,查出这两个客户买过的产品编号 A01、B01;最外层,把买过这两个产品的订单全捞出来。三层拆成三次单独运行,每次都验证一遍,比一口气写完再调试轻松十倍。
5. 报错与空结果排查:新手最容易踩的五个坑
写完查询点执行,出来的要么是红字报错,要么是一片空白。这两种情况处理方式完全不同,但新手往往分不清是哪一步出的问题。下面五条按“现象—原因—解决”写,几乎覆盖了这份实验里所有会卡住人的地方。
5.1 坑一:GROUP BY 之后 SELECT 里塞了没分组的列
现象:语句报错,提示某列不在 GROUP BY 里;或者不报错,但结果里那一列的值看着莫名其妙。
原因:你按客户编号分了组,一堆记录被压成一行,这时候再去 SELECT 一个没参与分组的列(比如产品名称),数据库不知道该从这一堆里挑哪一个显示。在开启了严格模式的 MySQL 上会直接拒绝执行,在宽松模式下则会随便挑一个,结果不可信。
解决:遵守一条规矩——SELECT 后面出现的列,要么在 GROUP BY 里,要么被聚合函数包着。想让结果里带客户名称,就先把 CUSTOMER 连进来,按客户编号分组时把名称也放进 GROUP BY,或者干脆分组查完编号,再拿编号去关联一次。
5.2 坑二:该用 HAVING 的地方写成了 WHERE
现象:报错说聚合函数不能用在 WHERE 里,整条语句执行不了。
原因:WHERE COUNT(PCODE) >= 2这种写法很符合直觉,但执行顺序上 WHERE 在分组之前就跑完了,那时候还没有“堆”,COUNT 无从算起。
解决:凡是条件里出现聚合函数的,一律搬到 HAVING。条件里只有原始列(比如SITE = '南城')的,留在 WHERE 里性能更好,因为可以先过滤再分组,白算的堆少一些。判断方法很简单:扫一眼条件里有没有 COUNT、SUM、MAX 这些词。
5.3 坑三:子查询返回多行,却用等号去比
现象:报错“Subquery returns more than 1 row”,或者结果只有一行但你预期好几行。
原因:WHERE PCODE != (SELECT PCODE FROM SALE)这种写法,只有当子查询恰好返回一行时才对。销售表一多几条记录,子查询就返回一列多行,等号和不等号都接不住。实验文档最后那道题就是这么写的,照抄必翻车。
解决:先单独跑子查询,看它返回几行。一行就用=、>;多行就用IN、NOT IN,或者改写成EXISTS、NOT EXISTS。养成这个检查习惯,能省下大量调试时间。
5.4 坑四:NOT IN 撞上 NULL,结果一片空白
现象:语句没报错,但一行数据都不返回,而你确信库里明明有该出现的数据。
原因:NOT IN的语义是“不等于名单里的每一个值”,一旦名单里混进一个 NULL,比较结果就变成“未知”,整个条件对任何行都不成立,于是全部被过滤掉。这是 SQL 里最典型的黑匣子行为之一,光看语句根本看不出来。
解决:子查询里加WHERE 列名 IS NOT NULL把 NULL 排除掉,或者直接改用NOT EXISTS。排查的时候先把子查询单独跑,看结果里有没有空白格,一眼就能确认。
5.5 坑五:LIKE 'B%' 写成 LIKE 'B'
现象:你明明知道有 B 开头的产品,WHERE PCODE LIKE 'B'却返回空。
原因:%是通配符,代表任意多个字符。少了它,LIKE 就退化成精确匹配,只认编号刚好等于一个字母 B 的记录。类似的还有大小写问题:部分数据库默认区分大小写,like 'b%'和LIKE 'B%'结果不一样。
解决:前缀匹配统一写LIKE 'B%',包含匹配写LIKE '%B%'。再顺手检查一下值里有没有首尾空格,从别处复制粘贴的数据经常带不可见空格,看着一样实际不一样。查不出来的时候,先SELECT PCODE FROM PRODUCT;把整列打出来肉眼扫一遍,比猜快。
6. 下一步学什么:把这三类查询练成肌肉记忆
这份实验覆盖的其实是 SQL 的三块地基:聚合统计、连接、嵌套。练熟之后,你会发现工作中百分之七八十的取数需求都在这三块里打转。往下的路怎么走,我给一个可以照着排的顺序。
6.1 两周练习路线
| 阶段 | 练什么 | 检验标准 |
|---|---|---|
| 第 1~3 天 | 单表查询 + 五个聚合函数 | 不看文档写出实验前 8 题 |
| 第 4~6 天 | GROUP BY 与 HAVING 混用 | 能说清 WHERE 和 HAVING 的区别 |
| 第 7~10 天 | 两表、三表连接 | 能自己判断该写几个配对条件 |
| 第 11~14 天 | 嵌套查询与 EXISTS | 写完能一眼看出子查询返回几行 |
每天的练法别贪多,一天三五条就够,但每条都要做到两件事:先自己写,再拆开验证。写完之后把子查询单独跑一遍,看看返回的东西是不是你预期的形状,这个动作重复二十次就变成条件反射了。
6.2 一个具体的小技巧
如果你只能带走一个习惯,那就是:从最内层往外写,写一层跑一层。新手最常见的姿势是从外往里憋,脑子里同时装着三层逻辑,憋到一半自己都忘了外面在查什么。改成先把最里面那句跑出结果,再包一层,再跑一次,你会发现嵌套查询突然变得没什么可怕的。
再往下可以碰的东西有索引、视图、窗口函数,但都别急。你现在这份实验文档里的十几条查询,就是最好的练手材料——把每一条都改一改条件重跑一次,比看十篇教程管用。
我第一次带人做这个实验的时候,自己也栽过:一条三层嵌套查了半小时没结果,最后发现是子查询里少写了个IS NOT NULL,数据全被 NULL 吃掉了。从那以后我每次写嵌套查询,都强制先把子查询单独拎出来跑一遍,确认它返回的是一行还是一列多行,再决定外面用等号还是 IN。这个动作看着笨,救过我无数回。希望帮到你。
本文还有配套的精品资源,点击获取