简介:面向数据库初学者的嵌套查询实验报告,适用于正在学习SQL查询与数据库原理的高校学生。报告覆盖数据库查询语言基础、统计函数、连接查询与嵌套查询四大模块,包含SELECT语句统计、SUM/COUNT/MAX/MIN函数使用,以及子查询、派生表等嵌套查询的具体操作方法。内容提供多个贴近业务场景的SQL示例,如统计客户数目、查询上海客户订购量大于200套的订单、检索与“美美”公司同城市的客户等,有助于读者理解各种连接与嵌套查询的语法及执行逻辑。资源为doc文档,共1个文件,压缩包大小642KB,章节安排从实验目的、实验内容到实践结论与思考逐步递进,结构清晰完整。该资源已有930人学习下载,可作为数据库实验课预习材料或实验报告写作参考,对快速掌握统计查询与嵌套查询技巧很有帮助。
1. 数据库实验5嵌套查询:一道统计查询题暴露的六个SQL习惯
拿到一份数据库实验5嵌套查询文档,别急着复制SQL。我按同一份题目在本地跑了三遍,得到三种“正确”结果:一次COUNT多算,一次子查询直接报错,另一次因为漏掉连接条件把结果集撑到几万行。原因都出在聚合函数、GROUP BY分组、嵌套子查询的边界处理上。这份实验适合两类人:刚学SELECT语句、想搞清楚统计函数怎么用的学生,以及天天写报表SQL但总在分组与子查询上翻车的数据分析新人。把它完整跑通,等于把计数、求和、分组过滤、子查询谓词串成一条可复用的SQL自查路线。
2. 统计查询基础:聚合函数与GROUP BY的配合边界
2.1 COUNT、SUM、MAX、MIN:空值和DISTINCT决定统计结果
实验前几题看着简单,但聚合函数的空值语义最容易埋雷。第一题“统计客户的数目”可以直接写SELECT COUNT(*) FROM CUSTOMER;,可一旦把COUNT(*)换成COUNT(CNO),结果可能就会少几行——只要CNO列里有NULL,COUNT(列名)就会自动跳过。
-- 实验第1题:统计客户数目 SELECT COUNT(*) FROM CUSTOMER; -- 实验第2题:求库存量总和 SELECT SUM(STOCKS) FROM PRODUCT;逻辑说明:COUNT(*)按物理行计数,行存在就计入,不管这一行是不是全为NULL;SUM(STOCKS)则跳过STOCKS为NULL的行,只对非空库存值求和。两者对NULL的容忍度不同,直接决定了统计口径。参数说明上,COUNT(*)不需要指定列名,语义是“总行数”;SUM(列)要求列是数值类型,否则多数数据库会在执行时报类型错误。
为空值行为做个速查表,写实验结论时能直接抄:
| 聚合函数 | 空值处理 | 最容易被误解的地方 |
|---|---|---|
| COUNT(*) | NULL行也计入 | 以为会跳过,实际不会 |
| COUNT(列名) | 跳过NULL值 | 结果比COUNT(*)少,不是查错了 |
| SUM(列名) | 跳过NULL值 | 全列NULL时返回NULL,不是0 |
| MAX / MIN | 跳过NULL值 | 对字符串列也能取首尾值 |
| AVG(列名) | 跳过NULL值 | 分母是“非NULL行数”,不是总行数 |
如果想在库存全为NULL时也返回0,用SELECT COALESCE(SUM(STOCKS), 0) FROM PRODUCT;,这是聚合查询的底线写法。
2.2 GROUP BY分组键:实验第3题的分组逻辑拆解
“求每个客户订购产品数量的总数”这类需求,关键词是“每个客户”,翻译成SQL就是GROUP BY CNO。按客户编号分组后,同一客户的多条订单会被合成一组,SUM只对组内数据求和:
-- 实验第3题:每个客户订购产品数量的总数 SELECT CNO, SUM(OQUANTITY) AS total_quantity FROM SALE GROUP BY CNO;逻辑说明:GROUP BY CNO把SALE表按客户编号切分成多个小组,SUM(OQUANTITY)分别对每个小组的订购数量求和。参数说明里最关键的一条是:SELECT子句中的非聚合列,必须出现在GROUP BY中,否则在数据库的严格模式下直接报错“列不在分组中”。
再看“求至少订购两种以上产品的客户编号和产品种类数”:
-- 实验第3题的变体:至少订购两种以上产品 SELECT CNO, COUNT(DISTINCT PCODE) AS product_kinds FROM SALE GROUP BY CNO HAVING COUNT(DISTINCT PCODE) >= 2;这里我特意把COUNT(PCODE)换成了COUNT(DISTINCT PCODE):如果同一客户分三次订购了同一种产品,普通COUNT会数出3,但产品种类数仍然只有1。这种重复订单在练习数据里不常见,真实业务表里却是常态。
2.3 执行顺序思维:WHERE为什么必须写在GROUP BY之前
实验第4题原给的写法是HAVING SITE='上海',在部分数据库里能跑通,但严谨的分析应该这样写:
-- 实验第4题:所在城市为“上海”的客户公司数 SELECT SITE, COUNT(CNO) AS customer_count FROM CUSTOMER WHERE SITE = '上海' GROUP BY SITE;逻辑说明:WHERE的作用是先筛行、后分组;HAVING的作用是先分组、后筛组。把“上海”这个过滤条件放进WHERE,让数据库在分组前就把非上海客户扔掉,既减少分组开销,又避免HAVING对每一组做无谓的城市判断。
SQL各子句的逻辑执行顺序是:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。记住这个顺序能解释很多现象:WHERE里不能写COUNT(CNO) > 10,因为执行到WHERE时聚合还没发生;而HAVING里可以写聚合条件,是因为它执行在分组之后。
3. 嵌套查询实战:子查询、连接与谓词的组合方式
3.1 标量子查询实战:单价比较与同城公司查询
实验第12题“查询与美美公司在同一城市的客户公司名称及联系电话”,是最典型的标量子查询:
-- 实验第12题:与“美美”公司在同一城市的客户 SELECT TELE, CNAME FROM CUSTOMER WHERE SITE = ( SELECT SITE FROM CUSTOMER WHERE CNAME = '美美' );逻辑说明:内层子查询先查出“美美”所在城市,返回一个单值,外层再用等号比较。标量子查询成立的前提是内层结果必须是“一行一列”。参数说明上,如果CNAME='美美'在客户表里重复出现,内层会返回多行,此时查询直接报错,后面避坑章节会展开。
实验第13题是标量子查询配合连接查询的混合体:
-- 实验第13题:单价比A01产品高的订购记录 SELECT SALE.PCODE, SALE.CNO, OQUANTITY FROM SALE, PRODUCT WHERE SALE.PCODE = PRODUCT.PCODE AND PRICE > ( SELECT PRICE FROM PRODUCT WHERE PCODE = 'A01' );逻辑说明:先执行内层SELECT PRICE FROM PRODUCT WHERE PCODE='A01'拿到A01的单价,再对SALE和PRODUCT做连接后的每一行,用PRICE与该值比较。这里的执行顺序是“先子查询,后外层连接过滤”。参数说明上,PCODE='A01'是子查询的定位参数,如果产品表里不存在A01,子查询返回NULL,外层比较结果全部为“未知”,查询返回空集。
3.2 集合子查询与谓词:用NOT IN改写“没有被订购的产品”
实验第14题原始写法存在明显问题:WHERE PRODUCT.PCODE != (SELECT SALE.PCODE FROM SALE)。当SALE表有多行时,不等号无法与一个结果集直接比较。正确做法是用集合谓词:
-- 实验第14题修正版:没有被订购的产品 SELECT PCODE, PNAME FROM PRODUCT WHERE PCODE NOT IN ( SELECT PCODE FROM SALE );逻辑说明:NOT IN会先把内层查询的PCODE集合取出来,再判断外层PCODE是否不在此集合中,逻辑语义与题目完全吻合。参数说明上,内层集合允许返回多行,这是IN与=最本质的区别:=要求标量,IN接受集合。
不过,NOT IN还有一个更稳健的替代方案,就是相关子查询配合NOT EXISTS:
-- 推荐写法:NOT EXISTS抗NULL SELECT P.PCODE, P.PNAME FROM PRODUCT P WHERE NOT EXISTS ( SELECT 1 FROM SALE S WHERE S.PCODE = P.PCODE );逻辑说明:NOT EXISTS对每个产品逐行判断“是否在SALE中存在匹配记录”,不存在则保留该产品。参数说明上,内层SELECT 1只是为了满足子查询“有返回即真”的语义,具体SELECT什么值不影响结果。NOT EXISTS在子查询集合中出现NULL时不会像NOT IN那样直接“抽空”,这是它更稳的原因。
3.3 连接与子查询的取舍:数据量变大之后谁的效率更稳
实验第11题“查询由上海客户订购且订购数量大于200套的客户编号、产品编号、联系人和订购数量”,用隐式连接可以写,用显式JOIN更清楚:
-- 实验第11题:显式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;逻辑说明:JOIN ON把SALE和CUSTOMER按客户编号关联,WHERE再执行城市和订购数量过滤。参数说明上,S和C是表别名,多表查询里用别名能显著减少字段前缀的书写量;ON C.CNO = S.CNO是连接键,连接键选错会导致数据错位。
连接和子查询不是互斥方案。子查询擅长表达“先算出一个参照值再比较”,连接擅长表达“多表横向拼接后过滤”。现代数据库优化器经常会把能改写的子查询转换成连接执行,所以小数据量上两者差距不明显;但可读性和维护性上,显式JOIN通常优于逗号隐式连接。我的习惯是:能明确写出关联关系时优先JOIN,只有“先算参照物再过滤”这种语义时才保留子查询。
4. 避坑与排查:五个SQL实验结果异常的真实原因
4.1 排查套路:现象、原因、解决三段式
这一章所有坑都按“现象 → 原因 → 解决”展开,这套三段式同样适用于真实报表排查。先确认现象是报错还是结果数量不对,再定位是语法级别还是数据级别的问题,最后再改写法验证。以下五个问题都来自我复跑实验5时实际踩过的。
4.2 坑一:HAVING里直接写列名,部分数据库运行报错
现象:原样执行实验第4题HAVING SITE='上海',在某个数据库环境完美通过,换到另一套数据库直接报错:列SITE在HAVING子句中无效,因为它既不在聚合函数中,也不在GROUP BY子句中。
原因:HAVING的执行语义是“对分组后的结果做过滤”,非聚合列SITE不在分组键的合法范围内;不同数据库对HAVING的列校验宽严程度不同,宽松模式下能跑,严格模式直接拒绝。
解决:把城市过滤条件前移到WHERE,WHERE SITE='上海'在分组前过滤,语义和性能都更合理。
4.3 坑二:COUNT(PCODE)用错位置,“至少两个客户”算成“两笔订单”
现象:复跑实验第8题,手工统计明明只有两个客户,查询结果却把同一客户的三笔重复订单也算成“满足条件”,结果集比预期多。
原因:HAVING COUNT(PCODE)>2统计的是分组内的记录行数,而不是“不同客户数”。同一个客户订购三次,PCODE行数就是3,COUNT(PCODE)大于2被误判为多个客户。
解决:按客户去重计数,改用HAVING COUNT(DISTINCT CNO) > 1,同时把外层SELECT中的OQUANTITY包进SUM聚合:
-- 实验第8题修正版:至少被两个客户订购且数量超过100 SELECT PCODE, SUM(OQUANTITY) AS total_quantity FROM SALE WHERE OQUANTITY > 100 GROUP BY PCODE HAVING COUNT(DISTINCT CNO) > 1;逻辑说明:WHERE先过滤掉订购数量不大于100的行,GROUP BY按产品分组,HAVING判断“有多少不同客户订购过”,SUM对每个产品的有效订购数量求和。参数说明上,COUNT(DISTINCT CNO)是这一题的核心口径,改成COUNT(CNO)或COUNT(*)都会得到偏大的假数据。
4.4 坑三:多行子查询用!=,执行时报错
现象:实验第14题原始写法WHERE PRODUCT.PCODE !=(SELECT SALE.PCODE FROM SALE),运行时报错:子查询返回了多行记录,无法与!=比较。
原因:不等号!=属于标量比较运算符,要求右侧必须是单值;SALE表里只要存在两条以上销售记录,右侧子查询结果就不是标量。
解决:将!=改成NOT IN或NOT EXISTS,使用集合判断语义。建议优先使用NOT EXISTS,原因在下一条坑里。
4.5 坑四:漏掉连接条件,结果集直接爆炸
现象:写实验第11题时把FROM SALE, CUSTOMER后面的WHERE CUSTOMER.CNO=SALE.CNO漏了,查询跑了很久才出结果,返回行数从几条膨胀到几千上万条。
原因:逗号连接没有连接条件时,数据库会对两个表做笛卡尔积,每一行和另一张表的每一行两两组合。SALE有200行、CUSTOMER有300行时,临时结果集就是6万行。
解决:连接条件第一时间写在WHERE或ON里。我的经验是使用显式JOIN语法:JOIN CUSTOMER C ON C.CNO = S.CNO,ON子句强制你填写关联键,漏写的概率比逗号隐式连接低得多。
4.6 坑五:NOT IN子查询混入NULL,结果被抽空
现象:实验第14题改成NOT IN写法后,偶尔出现明明存在“从未被订购的产品”,查询结果却为空集的情况。
原因:SQL三值逻辑在作怪。SALE.PCODE存在NULL值时,PCODE NOT IN (...)的判断结果不是TRUE,而是UNKNOWN,所有行的条件都不成立,整个查询返回空集。
解决:用NOT EXISTS替代NOT IN。NOT EXISTS是逐行相关判断,只要SALE中不存在匹配PCODE的行就返回TRUE,不受NULL值干扰。这也是我在生产环境排查数据报表时,发现“结果莫名缩水”最常出现的元凶。
5. 嵌套查询验证技巧:拆三步再组装,用临时表自证
5.1 拆三步:内层子查询、外层主查询、过滤条件
嵌套查询写完后直接跑,结果对不上很难判断问题出在内层还是外层。我的做法是拆成三步验证,以实验第13题为例:
-- 第一步:单独跑内层子查询,确认参照值是唯一的 SELECT PRICE FROM PRODUCT WHERE PCODE = 'A01'; -- 第二步:把外层连接单独跑,先忽略价格对比 SELECT SALE.PCODE, SALE.CNO, OQUANTITY FROM SALE JOIN PRODUCT ON PRODUCT.PCODE = SALE.PCODE; -- 第三步:把第一步的已知结果代入第二步,加过滤条件 SELECT SALE.PCODE, SALE.CNO, OQUANTITY FROM SALE JOIN PRODUCT ON PRODUCT.PCODE = SALE.PCODE WHERE PRICE > 68;三步跑完,哪一步出错就很清楚:第一步拿不到值时,问题在产品表数据;第二步行数膨胀,问题在连接条件;只有第三步才可能涉及比较逻辑。大多数嵌套查询翻车,翻在“子查询本身没跑通”,而不是外层写错。
5.2 用CTE临时表构造小样本,手工验证聚合口径
在验证“至少被两个以上客户订购”这类口径时,我会用WITH语句构造几行假数据,把业务逻辑先跑通,再去碰真实表:
-- 用CTE模拟重复订单场景 WITH TEST_SALE(cno, pcode, oquantity) AS ( SELECT 'C001','P01',150 UNION ALL SELECT 'C001','P01',120 UNION ALL SELECT 'C002','P01',200 ) SELECT PCODE, COUNT(PCODE) AS row_count, COUNT(DISTINCT CNO) AS customer_count, SUM(OQUANTITY) AS total_quantity FROM TEST_SALE WHERE OQUANTITY > 100 GROUP BY PCODE HAVING COUNT(DISTINCT CNO) > 1;逻辑说明:CTE在内存中模拟了三行销售数据,第一行和第二行是同一客户C001对P01的两次订购,第三行是C002的订购。运行结果里row_count等于3,customer_count等于2,能直观看到两种口径的差异。参数说明上,UNION ALL用于拼接多行临时数据,比INSERT临时表更轻量,适合在查询窗口里快速验证。
从那以后,我每次写完嵌套查询都强制走一遍“拆三步、造样本、对口径”的流程:先让子查询单独出结果,再用CTE临时数据验证聚合逻辑,最后才去碰全量数据。这套方法帮我拦截过不少看着正确、实则在边界条件下出错的SQL,希望帮到你。
本文还有配套的精品资源,点击获取