简介:这份数据库实验报告面向高校计算机及相关专业学生,聚焦数据统计查询与嵌套查询的实操训练,帮助读者掌握SELECT语句、统计函数、连接查询及子查询的综合运用。资源包内含1个doc文档,约642KB,内容围绕CPXS数据库展开,涵盖COUNT、SUM、MAX、MIN等统计函数的使用,INNER JOIN、LEFT JOIN、RIGHT JOIN等连接查询的语法,以及子查询、派生表等嵌套查询操作符与谓词的实践。文档以实验目的、实验内容、实践结论、相关知识点和实验思考为脉络,收录了统计客户数目、求库存总和、查询上海客户订购记录、比较产品单价等十余道典型习题及SQL参考写法,便于对照练习与复盘。目前已有930人学习下载,适合正在学习数据库课程、需要完成实验报告或巩固查询语法的读者参考使用。
1. 从一份“数据库实验5嵌套查询.doc”说起:为什么统计查询和子查询总在实验课上翻车
很多人第一次接触 SQL 嵌套查询,都是在类似“数据库实验5嵌套查询.doc”这样的实验文档里。文档不长,十几条 SELECT 语句,覆盖 COUNT、SUM、MAX、MIN、GROUP BY、HAVING、多表连接和子查询。看起来只是课堂作业,但真正把这些语句逐条跑通,你会发现它其实是一份浓缩的 SQL 实战清单:统计函数怎么配合分组、HAVING 和 WHERE 到底谁先执行、子查询返回多行时为什么直接报错、NOT IN 遇到 NULL 为什么会静默返回空结果。
这份资源适合三类人:正在做数据库实验、需要一份可复现脚本的学生;工作中写报表 SQL、被分组统计绕晕的初级开发;以及想系统梳理 SELECT 执行顺序的转行者。它不教你装数据库,也不讲索引优化,它解决的是一个更基础的问题——把“统计查询 + 嵌套查询”这条线彻底走通。下面我按实验文档里的真实语句,拆开讲每一步怎么落地、参数怎么改、哪里最容易翻车。
2. 统计查询落地:COUNT、SUM、GROUP BY 与 HAVING 的执行顺序
2.1 先建表再谈查询:CPXS 数据库的最小可用结构
实验文档里反复出现 CUSTOMER、PRODUCT、SALE 三张表,但没给建表语句。要复现,得先补上。常见做法是按实验语义反推字段:CUSTOMER 存客户编号、公司名、城市、电话;PRODUCT 存产品编号、名称、单价、库存量;SALE 存客户编号、产品编号、订购数量。下面这段 SQL 在 MySQL 和 SQL Server 上都能跑,字段类型按实验里出现的比较和运算来定。
-- 客户表:CNO 客户编号,CNAME 公司名,SITE 城市,TELE 电话 CREATE TABLE CUSTOMER ( CNO VARCHAR(10) PRIMARY KEY, CNAME VARCHAR(50), SITE VARCHAR(30), TELE VARCHAR(20) ); -- 产品表:PCODE 产品编号,PNAME 名称,PRICE 单价,STOCKS 库存量 CREATE TABLE PRODUCT ( PCODE VARCHAR(10) PRIMARY KEY, PNAME VARCHAR(50), PRICE DECIMAL(10,2), STOCKS INT ); -- 销售表:CNO + PCODE 联合主键,OQUANTITY 订购数量 CREATE TABLE SALE ( CNO VARCHAR(10), PCODE VARCHAR(10), OQUANTITY INT, PRIMARY KEY (CNO, PCODE), FOREIGN KEY (CNO) REFERENCES CUSTOMER(CNO), FOREIGN KEY (PCODE) REFERENCES PRODUCT(PCODE) );逻辑说明:SALE 表用 (CNO, PCODE) 做联合主键,是因为实验第 2 题要统计“至少订购两种以上产品的客户”,同一个客户对同一个产品只应有一条订购记录。参数上,OQUANTITY 用 INT 足够,PRICE 用 DECIMAL(10,2) 避免浮点误差。注意外键约束会让插入顺序变成先 CUSTOMER、再 PRODUCT、最后 SALE,否则直接报外键冲突。
2.2 COUNT 和 SUM:统计函数不是孤立的,它们和分组绑在一起
实验第 1 题SELECT COUNT(*) FROM CUSTOMER是最简单的行数统计。但第 2 题开始变味:SELECT CNO, COUNT(PCODE) FROM SALE GROUP BY CNO HAVING COUNT(PCODE)>=2。这里有两个关键点:COUNT(PCODE) 只统计 PCODE 非 NULL 的行,如果某条销售记录的 PCODE 为空,它不会被计入;HAVING 是对分组后的结果过滤,不能写成 WHERE COUNT(PCODE)>=2,因为 WHERE 在分组前执行,聚合函数还不存在。
-- 统计客户总数 SELECT COUNT(*) AS customer_count FROM CUSTOMER; -- 求库存量总和 SELECT SUM(STOCKS) AS total_stocks FROM PRODUCT; -- 至少订购两种以上产品的客户编号和产品种类数 SELECT CNO, COUNT(PCODE) AS product_kinds FROM SALE GROUP BY CNO HAVING COUNT(PCODE) >= 2; -- 每个客户订购产品数量的总数 SELECT CNO, SUM(OQUANTITY) AS total_quantity FROM SALE GROUP BY CNO;参数说明:COUNT(*)统计所有行,包括 NULL;COUNT(PCODE)跳过 PCODE 为 NULL 的行。SUM(OQUANTITY)如果遇到 NULL 会忽略,但整组全 NULL 时返回 NULL,不是 0。常见做法是用COALESCE(SUM(OQUANTITY),0)兜底。执行顺序上,FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY,记住这条链,HAVING 能写什么、不能写什么就清楚了。
2.3 WHERE 和 HAVING 的分工:一个过滤行,一个过滤组
实验第 4 题SELECT SITE, COUNT(CNO) FROM CUSTOMER GROUP BY SITE HAVING SITE='上海'其实暴露了一个写法问题:SITE='上海' 是行级过滤,放在 WHERE 里更高效,HAVING 应该留给聚合条件。第 6 题SELECT COUNT(PCODE) FROM PRODUCT GROUP BY STOCKS HAVING STOCKS>500同理,STOCKS>500 是行条件,放 WHERE 能让分组前就减少数据量。
-- 更优写法:行条件放 WHERE,聚合条件放 HAVING SELECT SITE, COUNT(CNO) AS company_count FROM CUSTOMER WHERE SITE = '上海' GROUP BY SITE; -- 库存量超过 500 的产品个数 SELECT COUNT(PCODE) AS product_count FROM PRODUCT WHERE STOCKS > 500; -- 单价在 10~20 元之间产品的个数 SELECT COUNT(PCODE) AS price_range_count FROM PRODUCT WHERE PRICE BETWEEN 10 AND 20;逻辑说明:WHERE 在分组前过滤行,能走索引;HAVING 在分组后过滤组,通常要等聚合算完。把行条件误放 HAVING,结果一样但性能差,数据量大时差距明显。BETWEEN 是闭区间,包含 10 和 20,如果实验要求开区间得改成PRICE > 10 AND PRICE < 20。第 9 题WHERE PCODE LIKE 'B%'里,B% 的百分号是通配符,如果产品编号本身含下划线,还要注意_在 LIKE 里也代表任意单字符,需要转义。
3. 嵌套查询落地:子查询返回单行、多行和 NULL 的三种命运
3.1 标量子查询:返回单值的子查询能直接比较
实验第 12 题和第 13 题是典型的标量子查询。第 12 题先查出“美美”所在城市,再用这个城市查同城客户;第 13 题先查出 A01 的单价,再查单价高于它的产品。这类子查询必须保证返回单行单列,否则数据库直接报错。
-- 查询与“美美”公司在同一城市的客户公司名称及联系电话 SELECT CNAME, TELE FROM CUSTOMER WHERE SITE = ( SELECT SITE FROM CUSTOMER WHERE CNAME = '美美' ); -- 查询订购了单价比 A01 高的产品的产品编号、客户编号和订购数量 SELECT S.PCODE, S.CNO, S.OQUANTITY FROM SALE S JOIN PRODUCT P ON S.PCODE = P.PCODE WHERE P.PRICE > ( SELECT PRICE FROM PRODUCT WHERE PCODE = 'A01' );参数说明:第一个子查询如果“美美”有多条记录,=会报“子查询返回多于一行”。稳妥做法是加LIMIT 1或用IN。第二个查询用了表别名 S 和 P,避免 SALE 和 PRODUCT 都有 PCODE 时的歧义。子查询里的PCODE='A01'如果拼错,子查询返回空集,外层>比较结果全是 NULL,最终返回空结果——这是最隐蔽的翻车点之一。
3.2 IN 和 NOT IN:多行子查询的甜区和 NULL 陷阱
实验第 14 题SELECT PRODUCT.PCODE, PNAME FROM PRODUCT WHERE PRODUCT.PCODE != (SELECT SALE.PCODE FROM SALE)写法有问题:子查询返回多行时!=直接报错。正确做法是用NOT IN或NOT EXISTS。但NOT IN遇到子查询结果含 NULL 时,整个条件会变成 UNKNOWN,返回空结果。
-- 正确写法一:NOT IN,但要求子查询结果无 NULL SELECT PCODE, PNAME FROM PRODUCT WHERE PCODE NOT IN ( SELECT PCODE FROM SALE WHERE PCODE IS NOT NULL ); -- 正确写法二: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 IN的语义是“不等于列表中的每一个值”,只要列表里有 NULL,任何值跟 NULL 比较都返回 UNKNOWN,WHERE 只保留 TRUE,所以结果为空。NOT EXISTS是相关子查询,逐行判断是否存在匹配,NULL 不影响布尔判断。常见做法是优先用NOT EXISTS,尤其是子查询列可能为空时。如果坚持用NOT IN,务必在子查询里加WHERE PCODE IS NOT NULL。
3.3 相关子查询与派生表:什么时候该换写法
实验第 11 题用多表连接完成了“上海客户订购数量大于 200”的查询,其实也可以用相关子查询。相关子查询的特点是内层引用外层的列,逐行执行,逻辑清晰但性能通常不如 JOIN。派生表则是把子查询放在 FROM 里,当成临时表用。
-- 相关子查询写法:查询上海客户订购数量大于 200 的记录 SELECT S.CNO, S.PCODE, C.CNAME, S.OQUANTITY FROM SALE S JOIN CUSTOMER C ON S.CNO = C.CNO WHERE C.SITE = '上海' AND S.OQUANTITY > 200; -- 派生表写法:先统计每个客户的总订购量,再筛大于 500 的 SELECT t.CNO, t.total_qty FROM ( SELECT CNO, SUM(OQUANTITY) AS total_qty FROM SALE GROUP BY CNO ) t WHERE t.total_qty > 500;参数说明:派生表必须起别名,MySQL 里叫t,否则报“Every derived table must have its own alias”。相关子查询在数据量大时可能被优化器改写成 JOIN,但不要依赖这一点。如果实验环境是 SQL Server,派生表里用TOP要小心;如果是 MySQL 8.0,可以用 CTE(WITH)替代派生表,可读性更好。选型上,多表连接适合表之间有关联键、结果集需要合并列的场景;子查询适合“先算一个值/一组值,再用它过滤”的场景。
4. 避坑与排查:嵌套查询实验里最容易翻车的五个点
4.1 子查询返回多行,=直接报错
现象:执行WHERE SITE = (SELECT SITE FROM CUSTOMER WHERE CNAME='美美')时,数据库报“Subquery returns more than 1 row”。原因:CNAME 没有唯一约束,“美美”可能对应多条客户记录。解决:确认业务上是否允许重名,如果允许,改用IN;如果只取一条,子查询加LIMIT 1或TOP 1,并明确排序规则。
4.2 NOT IN 遇到 NULL,结果静默为空
现象:WHERE PCODE NOT IN (SELECT PCODE FROM SALE)返回 0 行,但明明有产品没被订购。原因:SALE.PCODE 允许 NULL,子查询结果含 NULL,NOT IN整体变成 UNKNOWN。解决:子查询加WHERE PCODE IS NOT NULL,或改用NOT EXISTS。这个坑没有报错,只能靠结果数量反查,血泪经验是统计类查询跑完先看一眼行数是否符合预期。
4.3 GROUP BY 后 SELECT 非聚合列,MySQL 不报错但结果随机
现象:SELECT CNO, PCODE, COUNT(*) FROM SALE GROUP BY CNO在 MySQL 5.7 之前能跑,但 PCODE 返回的是组内任意一行的值。原因:SQL 标准要求 SELECT 列表里的非聚合列必须出现在 GROUP BY 里,MySQL 旧版本放宽了。解决:把 PCODE 放进 GROUP BY,或用ANY_VALUE(PCODE)明确表示“任意值可接受”。实验环境如果是 SQL Server 或 PostgreSQL,这条直接报错,反而更安全。
4.4 连接查询漏写连接条件,变成笛卡尔积
现象:FROM SALE, CUSTOMER WHERE CUSTOMER.SITE='上海'忘了写CUSTOMER.CNO=SALE.CNO,结果行数爆炸。原因:多表查询没有连接条件时,数据库做笛卡尔积。解决:用显式JOIN ... ON语法替代逗号连接,连接条件写在 ON 里,过滤条件写在 WHERE 里,结构更清晰。跑之前先用SELECT COUNT(*)估算行数,和单表行数乘积对比。
4.5 LIKE 通配符和转义字符混淆
现象:WHERE PCODE LIKE 'B%'想查 B 开头,结果把B_01也查出来了,因为_在 LIKE 里是任意单字符。原因:LIKE 的通配符%和_没有转义。解决:用ESCAPE子句指定转义符,比如LIKE 'B\_%' ESCAPE '\',或者改用LEFT(PCODE,1)='B'。不同数据库默认转义符不同,MySQL 默认是反斜杠,SQL Server 需要用ESCAPE显式声明。
5. 进阶技巧:把实验文档变成可复用的 SQL 验证脚本
实验文档里的语句是零散的,真正要验证自己写对了,得有一套可重复执行的脚本。我一般会建一个lab5_check.sql,按“建表 → 插数据 → 逐题查询 → 断言结果”的顺序组织。插数据时故意埋几个边界值:一个客户订购两种产品、一个产品库存为 0、一个客户城市为 NULL、一个产品编号以 B 开头且含下划线。这样跑完所有查询,能一次性暴露 NULL、多行子查询、LIKE 转义这几类问题。
-- 边界测试数据:覆盖 NULL、多产品、B 开头含下划线 INSERT INTO CUSTOMER VALUES ('C01','美美','上海','021-1111'), ('C02','华联','上海','021-2222'), ('C03','北方','北京','010-3333'), ('C04','空城',NULL,'000-0000'); INSERT INTO PRODUCT VALUES ('A01','螺丝',15.00,600), ('B01','扳手',25.00,300), ('B_02','钳子',18.00,800), ('C01','胶带',5.00,0); INSERT INTO SALE VALUES ('C01','A01',150), ('C01','B01',80), ('C02','A01',250), ('C02','B_02',120), ('C03','B01',90);逻辑说明:C04 的 SITE 为 NULL,用来验证第 12 题子查询遇到 NULL 时的行为;B_02 含下划线,用来验证 LIKE 转义;C01 订购两种产品,满足 HAVING COUNT(PCODE)>=2;C01 的 A01 订购量 150,C02 的 A01 订购量 250,用来验证“订购数量大于 200”的过滤。跑完这些数据,再逐条执行实验查询,对比预期行数。
验证方法上,我习惯用SELECT包一层计数:把实验查询作为子查询,外面套SELECT COUNT(*) FROM (...),看返回行数是否和手算一致。比如第 14 题“没有被订购的产品”,手算应该是 C01(胶带),因为 A01、B01、B_02 都被订购过。如果NOT IN写法返回 0 行,说明踩了 NULL 坑;如果返回多行,说明子查询逻辑写错了。
还有一个技巧是把实验里的!=全部替换成NOT EXISTS,然后对比两种写法的结果集差异。差异行就是 NULL 或重复值导致的边界情况。这个对比过程比单纯跑通实验更有价值,因为它逼你理解每种写法的语义边界。从那以后我每次写嵌套查询,都会先问自己三个问题:子查询会不会返回多行?子查询列有没有 NULL?外层比较符是=、IN还是NOT IN?这三个问题过一遍,基本不会再翻车。希望帮到你。
本文还有配套的精品资源,点击获取