简介:这是一份面向数据库课程学习者的实验报告,围绕“数据统计查询与嵌套查询”展开,对应教材中的数据库查询语言基础、统计函数、连接查询和嵌套查询等核心章节。内容通过具体案例演示了SELECT语句的统计用法,SUM、COUNT、MAX、MIN等常用聚合函数,INNER JOIN、LEFT JOIN、RIGHT JOIN等连接查询,以及子查询、派生表等嵌套查询方式。报告中的SQL实例覆盖了客户数目统计、库存量总和、单价区间计数、上海客户订购数量大于200套的查询、与“美美”公司同城市的客户信息查找等典型场景,并附有完整语句和实验目的,便于理解查询语法与HAVING、LIKE、子查询等谓词及操作符的实际用法。资源为1个doc文档,压缩包大小约642KB,内容紧凑。目前已有930人学习下载,适合作为数据库实验报告撰写参考,也可用于备考复习或日常练习,帮助系统掌握统计查询与嵌套查询的关键技能。
1. 数据库实验五里的嵌套查询:为什么统计 SQL 总在分组这一步翻车
很多同学做数据库实验时,SELECT 单表查询都能写,一到“统计查询 + 嵌套查询”就开始对着结果怀疑人生:明明语句没报错,数字就是不对;明明是同一个查询,换个写法结果就变了。这份数据库实验文档恰好把这类问题集中在一个实验里——14 道题覆盖 COUNT、SUM、MAX、MIN 聚合函数,GROUP BY 与 HAVING 分组过滤,隐式连接与子查询嵌套,基本把数据库查询语言里最容易出错的那一层全过了一遍。适合正在做数据库课程实验的在校生,也适合刚入职需要补 SQL 基本功的初级开发。我把这份实验从头到尾拆了一遍,把每一道题的意图、写法边界和容易踩的坑整理成下面的笔记,照着跑一遍,比你单独刷几十道题管用。
2. 统计查询:把 COUNT、SUM、MAX、MIN 用对,先别急着写子查询
2.1 聚合函数的执行边界:SELECT 语句不是你想的顺序
先看这份实验里最简单的几个统计查询:题目 1 统计客户数目,题目 2 求库存量总和,题目 5 求所有产品的最高单价。它们分别是 COUNT、SUM、MAX 三个聚合函数的单表应用,语法上没有难度,但很多人在这一步就开始埋坑。
我刚带实习生做这个实验时,总会让他们先回答一个问题:SELECT 语句里,FROM、WHERE、GROUP BY、HAVING、SELECT、ORDER BY 这几个子句,数据库实际按什么顺序执行?答案不是书写顺序。真实执行顺序是:FROM 先确定数据源,WHERE 过滤行,GROUP BY 分组,HAVING 过滤组,SELECT 做投影和计算,最后 ORDER BY 排序。这个顺序决定了你在 WHERE 里能不能用聚合函数、在 HAVING 里能不能引用列别名。
回到题目 1:
-- 统计客户数目 SELECT COUNT(*) FROM CUSTOMER;COUNT() 统计的是行数,不是某个列的值。它不会忽略任何行,哪怕整行都是 NULL 也会被算进去。很多初学者分不清 COUNT() 和 COUNT(列名),实际上 COUNT(列名) 会忽略该列为 NULL 的行,两者在表中有空值时结果可能不一致。题目 2 的 SUM(STOCKS) 也必须注意,SUM 只对数值列有意义,如果 STOCKS 列里有 NULL,SUM 默认忽略它,而不是把它当 0 处理——这在后续计算平均值时会产生更隐蔽的偏差。
2.2 题目 2、5 的完整复现:先把表建出来再谈查询
要复现这份实验,我习惯先把三张表的结构建出来。CUSTOMER 表存放客户信息,PRODUCT 表存放产品信息,SALE 表存放订购记录,这是实验文档里能明确推断出的三张表结构:
-- 建库建表(简化结构,按实验涉及的字段即可) CREATE DATABASE CPXS; USE CPXS; CREATE TABLE CUSTOMER ( CNO CHAR(10) PRIMARY KEY, -- 客户编号 CNAME VARCHAR(50), -- 客户名称 SITE VARCHAR(50), -- 所在城市 TELE VARCHAR(20) -- 联系电话 ); CREATE TABLE PRODUCT ( PCODE CHAR(10) PRIMARY KEY, -- 产品编号 PNAME VARCHAR(50), -- 产品名称 PRICE DECIMAL(10,2), -- 单价 STOCKS INT -- 库存量 ); CREATE TABLE SALE ( CNO CHAR(10), -- 客户编号 PCODE CHAR(10), -- 产品编号 OQUANTITY INT -- 订购数量 );注意 SALE 表的主键在设计时应是 (CNO, PCODE) 联合主键,因为一个客户可以订购多个产品,一个产品也可以被多个客户订购,单独任何一列都不唯一。实验文档里没提主键设计,但如果你要在这张表上做题目 8 那种“至少被两个以上客户订购”的统计,联合主键是保证数据不重复的前提。
表建好后,题目 2 和题目 5 就是直接套函数:
-- 题目2:求库存量的总和 SELECT SUM(STOCKS) AS TOTAL_STOCKS FROM PRODUCT; -- 题目5:求所有产品的最高单价 SELECT MAX(PRICE) AS MAX_PRICE FROM PRODUCT;SUM 和 MAX 都返回单行单列的结果,不需要 GROUP BY。这里有个容易被忽略的点:如果 PRODUCT 表是空表,SUM 会返回 NULL 而不是 0,MAX 也会返回 NULL。很多人在报表里看到 NULL 就以为数据出错了,实际上这是聚合函数对空集的正常行为。如果业务上需要空表显示 0,要用 COALESCE(SUM(STOCKS), 0) 包一层。我在实际项目中处理库存报表时,基本都会加这个保护,否则前端图表直接显示空白,排查半天才发现是空表问题。
3. GROUP BY 与 HAVING:先筛行还是先筛组,差一步结果就差一截
3.1 WHERE 与 HAVING 的分工:分组前的条件放前面,分组后的条件放后面
题目 3、4、6、7、8 这一组是这份实验的核心难点。它们表面上都是“分组 + 条件筛选”,但条件的筛选时机完全不同。题目 3 求每个客户订购产品数量的总数,题目 4 求所在城市为“上海”的客户公司数,题目 8 求至少被两个以上客户订购且每个客户订购数量超过 100 的产品编号和订购数量总数。
先看题目 3 和题目 4 的对比:
-- 题目3:求出每个客户订购产品数量的总数 SELECT CNO, SUM(OQUANTITY) AS TOTAL_QUANTITY FROM SALE GROUP BY CNO; -- 题目4:求所在城市为“上海”的客户公司数 SELECT SITE, COUNT(CNO) AS CUSTOMER_COUNT FROM CUSTOMER GROUP BY SITE HAVING SITE = '上海';题目 3 里 GROUP BY CNO 把订购记录按客户分组,SUM(OQUANTITY) 对每组求和。这里 SELECT 中出现的列只有两种:要么是 GROUP BY 后面的分组列,要么是聚合函数。如果 SELECT 里写了非分组列且没用聚合函数包裹,MySQL 5.7 之前的版本不报错但结果随机,MySQL 8.0 和标准 SQL 会直接报错。这个“分组列之外不能随便 SELECT”的规则,是实验里最容易忽视的。
题目 4 的写法值得单独分析。GROUP BY SITE 之后 HAVING SITE = '上海',这个条件过滤的是分组结果,等效于 WHERE SITE = '上海' 放在分组前过滤行。两种写法结果一样,但执行路径不同:WHERE 在分组前过滤,参与分组的行数少,性能更好。HAVING 主要用于过滤聚合结果,比如 COUNT(CNO) >= 2 这种条件。所以题目 4 更合适的写法是先 WHERE 再 GROUP BY:
-- 题目4的推荐写法:先过滤行,再分组统计 SELECT SITE, COUNT(CNO) AS CUSTOMER_COUNT FROM CUSTOMER WHERE SITE = '上海' GROUP BY SITE;实际项目中我很少在 HAVING 里写非聚合条件,因为这类条件完全可以下推到 WHERE,减少分组计算量。HAVING 只留给“分组后才知道答案”的条件,比如题目 8 的“至少被两个以上客户订购”。
3.2 题目 6 的写法疑点:GROUP BY STOCKS 为什么不对劲
实验文档里题目 6 的原始写法是:
-- 题目6:求库存量超过 500 的产品个数(原始写法) SELECT COUNT(PCODE) FROM PRODUCT GROUP BY STOCKS HAVING STOCKS > 500;这个写法初看没毛病,但跑出来的结果很可能不是你要的“产品个数”。问题在于 GROUP BY STOCKS 会先把产品按库存量分组,然后 HAVING STOCKS > 500 过滤出库存量大于 500 的组,最后 COUNT(PCODE) 统计的是“组数”,不是“产品个数”。如果两个产品的库存量都是 600,它们会被分到同一组,COUNT(PCODE) 算出来是 1 而不是 2。
这个题目是这份实验里最大的陷阱。文档本身的意图是求“满足条件的行数”,正确做法有两种:
-- 写法一:WHERE 过滤后再统计 SELECT COUNT(PCODE) AS PRODUCT_COUNT FROM PRODUCT WHERE STOCKS > 500; -- 写法二:如果不加 WHERE,用条件计数 SELECT COUNT(CASE WHEN STOCKS > 500 THEN 1 END) AS PRODUCT_COUNT FROM PRODUCT;写法一是标准做法,WHERE 在分组前过滤行,COUNT(PCODE) 统计剩余的行数。写法二利用 CASE WHEN 返回 NULL 时被 COUNT 忽略的特性,实现单次扫描的条件计数。我一般推荐写法一,可读性强,而且能走索引。写 SQL 时有个习惯可以帮你避开这类问题:看到 GROUP BY 先问自己一句“我要的是每一组的信息,还是所有行的汇总?”题目 6 要的是所有产品里库存大于 500 的个数,是行级过滤,不是分组统计,所以根本不需要 GROUP BY。题目 8 才是真正需要分组统计的:
-- 题目8:至少被两个以上客户订购且每个客户订购数量超过100的产品 SELECT PCODE, SUM(OQUANTITY) AS TOTAL_QUANTITY FROM SALE WHERE OQUANTITY > 100 -- 先过滤行:每个客户的订购数量超过100 GROUP BY PCODE HAVING COUNT(PCODE) >= 2; -- 再过滤组:产品被至少两个客户订购这里 WHERE 和 HAVING 的分工一目了然。WHERE OQUANTITY > 100 先剔除订购数量不大于 100 的记录,GROUP BY PCODE 后 HAVING COUNT(PCODE) >= 2 判断剩余的记录来自多少个客户。注意 COUNT(PCODE) 统计的是每个分组内的记录条数,如果同一个客户对同一产品有多条订购记录,这里会把它们算成多个客户。要精确统计“客户数”应该用 COUNT(DISTINCT CNO),但这份实验的 SALE 表如果按 (CNO, PCODE) 联合主键设计,同一客户同一产品只有一条记录,COUNT(PCODE) 与 COUNT(DISTINCT CNO) 结果一致。这里就是典型的“表结构决定 SQL 写法”,做实验前先确认主键,能省很多排查时间。
4. 连接查询:从隐式连接到 JOIN 的等价写法与笛卡尔积风险
4.1 隐式连接与 INNER JOIN:结果一样,但可读性和风险不一样
题目 11 是这份实验里第一道多表查询题:查询由上海客户订购的且订购数量大于 200 套的所有客户编号、产品编号、联系人和订购数量。文档里给的是隐式连接写法:
-- 题目11:隐式连接写法 SELECT SALE.CNO, SALE.PCODE, CUSTOMER.CNAME, SALE.OQUANTITY FROM SALE, CUSTOMER WHERE CUSTOMER.CNO = SALE.CNO AND CUSTOMER.SITE = '上海' AND SALE.OQUANTITY > 200;隐式连接就是把多张表放在 FROM 后面,用 WHERE 写连接条件。这种写法在早期 SQL 教材里很常见,但存在一个隐患:如果 WHERE 里漏写了连接条件,就会产生笛卡尔积,结果集变成两张表行数的乘积。比如 SALE 表有 100 行、CUSTOMER 表有 80 行,漏掉连接条件会得到 8000 行,而且不报错。
同样的查询用 INNER JOIN 写会更安全:
-- 题目11:INNER JOIN 等价写法 SELECT S.CNO, S.PCODE, C.CNAME, S.OQUANTITY FROM SALE AS S INNER JOIN CUSTOMER AS C ON C.CNO = S.CNO WHERE C.SITE = '上海' AND S.OQUANTITY > 200;这段代码里,S 和 C 是表别名。别名的作用不只是省打字——当多张表有同名字段时(比如 CNO 在 SALE 和 CUSTOMER 里都存在),你必须用“别名.字段名”的方式消除歧义。INNER JOIN 把连接条件写在 ON 里,过滤条件写在 WHERE 里,逻辑上分层清晰,数据库优化器也能更准确地判断执行计划。我在实际工作中写多表查询基本只用 JOIN 写法,隐式连接只在快速临时排查数据时用一下。
题目 11 里有个细节:SELECT 子句中 SALE.CNO 和 SALE.PCODE 用了表名前缀,而 CNAME 和 OQUANTITY 没有。OQUANTITY 只在 SALE 表里存在,不加前缀没问题;但 CNO 在两张表里都有,必须加前缀。很多数据库报“列名不明确”错误,就是因为查了两张表的同名字段却没加前缀。建议养成习惯:多表查询时所有列都带上表别名前缀,宁可多写几个字母,别让排查名字的时间超过写 SQL 的时间。
4.2 同表查询时先写子查询还是自连接:从题目 12 说起
题目 12 的语义是:查询与“美美”公司在同一城市的客户公司名称及联系电话。抽象一下就是:在 CUSTOMER 表里,找到 SITE 等于“美美”所在城市的其他客户。这涉及的是同一张表,文档给的是子查询写法:
-- 题目12:子查询写法 SELECT CNAME, TELE FROM CUSTOMER WHERE SITE = ( SELECT SITE FROM CUSTOMER WHERE CNAME = '美美' );子查询先查出“美美”的 SITE,外层查询用它做等值匹配。这里请注意:内层查询返回的必须是单行单列,否则这个 = 会报错。如果表里有多个“美美”公司,这个 SQL 就直接炸了。
同样的需求也可以用自连接实现:
-- 题目12:自连接等价写法 SELECT A.CNAME, A.TELE FROM CUSTOMER AS A INNER JOIN CUSTOMER AS B ON A.SITE = B.SITE WHERE B.CNAME = '美美' AND A.CNO != B.CNO; -- 排除“美美”自己自连接把一张表当成两张表用,A 和 B 都是 CUSTOMER 的别名,连接条件 A.SITE = B.SITE 表示“城市相同”,最后用 A.CNO != B.CNO 排除自己。两种写法结果等价,但适用场景有差异:子查询适合“先查出条件值再外层检索”的简单场景,易读;自连接适合需要在同一张表内做多行关联的场景,比如查找同一城市的所有客户对。
我个人的选择标准是:内层查询只是用来取一个值,用子查询;内层查询要返回一组行或者需要和外部做更复杂的关联,用自连接或派生表。题目 12 的原始写法对“美美”公司必须唯一这个前提很敏感,如果实验数据里出现两条“美美”记录,你会在这一步第一次遇到“子查询返回多行”的报错——这个坑在下一章单独展开,因为它太典型了。
5. 嵌套查询避坑:三处容易翻车的写法与修复记录
5.1 单值子查询返回多行:等值比较的隐性前提
现象:题目 13 的查询跑到一半,数据库报错,错误信息类似“子查询返回了超过 1 行”。题目 13 原意是查询订购了单价比“A01”号产品高的那些产品的产品编号、客户编号和订购数量:
-- 题目13:出错版本 SELECT SALE.PCODE, SALE.CNO, SALE.OQUANTITY FROM SALE, PRODUCT WHERE SALE.PCODE = PRODUCT.PCODE AND PRICE > ( SELECT PRICE FROM PRODUCT WHERE PRODUCT.PCODE = 'A01' );原因:内层查询SELECT PRICE FROM PRODUCT WHERE PCODE = 'A01'期望返回单行单列,但 PRODUCT 表里如果存在两条 PCODE 为 'A01' 的记录,内层就返回两行,外层使用>比较时数据库无法确定和哪一行比较,直接报错。这个问题在真实业务里更容易出现:产品表缺少主键约束,或者 PCODE 有重复数据,实验数据不一定能触发,但换一批数据立刻翻车。
解决:商品编号在业务上应当唯一,但 SQL 不依赖业务假设。两种修复思路:一是用聚合函数保证单值,二是改用 IN 或 EXISTS 处理多值结果。我一般这样修:
-- 修复方案一:确保单行 SELECT SALE.PCODE, SALE.CNO, SALE.OQUANTITY FROM SALE, PRODUCT WHERE SALE.PCODE = PRODUCT.PCODE AND PRICE > ( SELECT MAX(PRICE) FROM PRODUCT WHERE PRODUCT.PCODE = 'A01' ); -- 修复方案二:改写为连接 + 分组,避免单值依赖 SELECT SALE.PCODE, SALE.CNO, SALE.OQUANTITY FROM SALE, PRODUCT WHERE SALE.PCODE = PRODUCT.PCODE AND PRICE > ( SELECT AVG(PRICE) FROM PRODUCT WHERE PRODUCT.PCODE = 'A01' );方案一加了 MAX,强制内层返回单值。方案二把“比 A01 单价高”的语义改成“比 A01 的平均单价高”,虽然语义略变,但业务上更稳健。核心教训是:所有使用=、>、<关联子查询的地方,都要先确认内层查询真的只返回一行。无法确认时,要么加聚合函数,要么换 IN。
5.2 NOT IN 遇到 NULL:结果突然变成空集
现象:题目 14 查询“没有被订购的产品编号和名称”,我用文档里的写法跑出来一行都没有,但 PRODUCT 表里明显有没被订购的产品:
-- 题目14:有隐患的写法 SELECT PRODUCT.PCODE, PRODUCT.PNAME FROM PRODUCT WHERE PRODUCT.PCODE != ( SELECT SALE.PCODE FROM SALE );这里有两个问题叠加。首先,内层SELECT SALE.PCODE FROM SALE通常返回多行,!=遇到多行直接报错。其次,就算把!=改成 NOT IN,只要 SALE.PCODE 列里存在 NULL,NOT IN 的结果就永远是空集,因为 NULL 参与比较时既不是真也不是假,整个条件变为 UNKNOWN,行被全部过滤掉。
解决:用 NOT EXISTS 改写。NOT EXISTS 判断的是“存在性”,不涉及 NULL 比较,安全度最高:
-- 题目14:推荐写法 SELECT P.PCODE, P.PNAME FROM PRODUCT AS P WHERE NOT EXISTS ( SELECT 1 FROM SALE AS S WHERE S.PCODE = P.PCODE );这段代码的语义是:对 PRODUCT 表里的每一行,检查 SALE 表里是否存在 PCODE 相等的记录,如果不存在,说明该产品没有被订购。内层 SELECT 1 只是存在性判断,不关心具体列值。NOT EXISTS 比 NOT IN 的性能和正确性都更稳,我处理“不在某个集合里”这类需求时默认用它。
5.3 COUNT(PCODE) 与 COUNT(*) 不是一回事:统计口径的偏差
现象:做题目 3 或题目 8 时,统计结果比预期少了几行,但 SQL 语法完全正确。原因出在 COUNT 的参数上。COUNT(*) 统计行数,COUNT(PCODE) 统计 PCODE 列非 NULL 值的个数。如果 SALE 表里存在 PCODE 为 NULL 的记录,COUNT(PCODE) 会把这些行忽略。题目 3 按 CNO 分组后,某个客户只订购过 PCODE 为 NULL 的产品,他的分组显示数量为 0,甚至可能因为所有行都被忽略而根本不显示。
解决:统计行数一律用 COUNT(),统计某个字段非空值数量才用 COUNT(字段名)。比如题目 8 里 HAVING COUNT(PCODE) >= 2,我通常改成 HAVING COUNT() >= 2,因为业务上关心的是“有几条订购记录”,而不是“有几个非空产品编码”。同理,题目 6 里 COUNT(PCODE) 改成 COUNT(*) 更稳妥。需要统计不重复客户数时,不要用 COUNT(CNO),要用 COUNT(DISTINCT CNO)。这三者的区别在实验数据质量不高时很难发现,一旦上线跑真实数据,报表数字对不上,排查成本比写 SQL 的成本高得多。
6. 嵌套查询的验证习惯:把子查询拆开跑,再合起来跑
嵌套查询最怕的不是写不出来,而是写出来不知道对不对。我验证嵌套查询有一套固定流程:先单独跑内层查询,看返回的数据形态;再带一个简单的外层条件跑,最后才跑完整版本。以内层子查询为例,先问三个问题:返回几行?返回几列?有没有 NULL?这三个问题的答案直接决定外层该用=、IN还是EXISTS。
以题目 14 为例,先单独跑内层:
-- 第一步:单独确认内层数据形态 SELECT PCODE FROM SALE;拿到结果后数一下行数,看一下有没有 NULL。如果只有一行,=或!=可以继续用;如果多行,立刻换IN或EXISTS。第二步用一个最外层的限定条件验证整体查询是否能跑通,比如临时加一个 PCODE 已知的产品做对照。第三步才把条件补全,执行完整查询。另外每写一个嵌套查询,我会用 EXPLAIN 看执行计划:
EXPLAIN SELECT P.PCODE, P.PNAME FROM PRODUCT AS P WHERE NOT EXISTS ( SELECT 1 FROM SALE AS S WHERE S.PCODE = P.PCODE );重点看 type 列是否出现 ALL(全表扫描),如果两张表的数据量都不小,NOT EXISTS 通常能把内层查询优化成索引查找,type 指向 ref 或 eq_ref。如果全表扫描,考虑在 SALE.PCODE 上加索引。这个验证习惯帮我避开了多次线上事故——SQL 写出来不报错只是第一步,执行计划和数据形态才是真正的黑匣子。从那以后我每次写完嵌套查询,都强制自己把这三步走一遍:拆开内层、确认数据、看执行计划。希望帮到你。
本文还有配套的精品资源,点击获取