ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

数据库实验5嵌套查询:统计函数、连接查询与子查询避坑指南

数据库实验5嵌套查询:统计函数、连接查询与子查询避坑指南

简介:这份文档面向数据库课程学习者,聚焦数据统计查询与嵌套查询的实验训练,可用于高校数据库原理课程实验复盘、SQL语句对照练习和查询思路梳理。内容围绕CPXS数据库展开,先交代掌握SELECT统计查询、COUNT/SUM/MAX/MIN等主要函数、连接查询和嵌套查询谓词等实验目的,再以统计客户数目、库存总量、至少订购两种产品的客户、上海客户公司数、库存超500的产品数、单价区间产品数、B开头产品最低单价等题目串联知识点,并给出子查询比较、同城客户查询、未被订购产品查询等嵌套写法,便于理解分组统计、HAVING过滤与多表关联条件筛选。资源包共1个doc文件,约642KB,轻量便于直接查阅。已有931人学习下载,适合需要补齐数据库查询语法、巩固实验操作或准备课程作业的读者参考。

1. 先弄明白一件事:数据库实验里的“嵌套查询”到底在查什么

很多新手打开数据库实验5的题目,看到“查询与某公司在同一城市的客户”就卡住:明明只认识一条 SELECT,怎么题目里又套一条 SELECT?你可以把嵌套查询想成“先算括号里的小答案,再拿小答案去问外层”。比如先查出示例公司甲在哪个城市,再拿这个城市去筛客户。它解决的是“条件本身也要查一次”的问题。数据库实验5里的统计、连接、嵌套是一条线:先会数数,再会对表,最后才会套娃。适合刚学 SQL、想跑通实验又怕写错的人。别急着背语法,先跟着把第一个计数查询跑出来,手上有结果,心里就不发怵。

2. 为什么新手先学统计函数:COUNT、SUM、MAX、MIN先把数算明白

统计函数是嵌套查询的地基。你后面写“比平均单价高”“和某公司同城”“至少被两个客户订过”,子查询经常返回一个数字或一组编号。这个数字怎么来?就是 COUNT、SUM、MAX、MIN 这些函数算出来的。新手一上来就写三层 SELECT,十有八九会翻车,不是因为嵌套难,而是因为连“这一组数据到底怎么数”都没想清楚。

这一章的目标很朴素:你能看着一张表说出“我要数行、求和、找最大最小、按客户分组”,并且知道 WHERE 和 HAVING 分别卡在哪一步。下面用示例库里的 CUSTOMER、PRODUCT、SALE 三张表练手,字段都来自数据库实验5:客户表有 CNO、CNAME、SITE、TELE,产品表有 PCODE、PNAME、PRICE、STOCKS,销售表有 CNO、PCODE、OQUANTITY。你不需要先理解索引、事务这些词,先把查询结果跑出来。

2.1 从一张客户表开始:SELECT 和 COUNT 的第一口

先建一个最小练习环境。下面的字段只保留实验里会用到的部分,城市和公司名都用虚拟值,避免你对着真实数据分心。建表语句跑完,再插几条示例数据,后面每一节都能直接接上。

-- 客户表:CNO 是客户编号,SITE 是所在城市 CREATE TABLE CUSTOMER ( CNO VARCHAR(10) PRIMARY KEY, CNAME VARCHAR(50), SITE VARCHAR(30), TELE VARCHAR(30) ); -- 产品表:PCODE 是产品编号,PRICE 是单价,STOCKS 是库存量 CREATE TABLE PRODUCT ( PCODE VARCHAR(10) PRIMARY KEY, PNAME VARCHAR(50), PRICE DECIMAL(10,2), STOCKS INT ); -- 销售表:记录哪个客户订了哪个产品、订了多少 CREATE TABLE SALE ( CNO VARCHAR(10), PCODE VARCHAR(10), OQUANTITY INT ); INSERT INTO CUSTOMER VALUES ('C01', '示例公司甲', '城市A', '1000001'), ('C02', '示例公司乙', '城市A', '1000002'), ('C03', '示例公司丙', '城市B', '1000003'); INSERT INTO PRODUCT VALUES ('A01', '示例产品A', 12.50, 300), ('B01', '示例产品B', 18.00, 800), ('B02', '示例产品C', 25.00, 120); INSERT INTO SALE VALUES ('C01', 'A01', 120), ('C01', 'B01', 220), ('C02', 'B01', 150), ('C03', 'A01', 80);

这段代码里,PRIMARY KEY 表示主键,CNO 和 PCODE 不能重复;VARCHAR 用来放编号和名称,DECIMAL 用来放金额,INT 用来放数量和库存。INSERT 只是给练习准备几行数据,方便你看到“数出来是多少”。真实实验库如果已经建好表,就不用重复建,直接从下面的查询开始。

第一个统计查询只做一件事:数客户有多少家。

-- 数 CUSTOMER 表里一共有多少行,也就是多少家客户 SELECT COUNT(*) AS 客户数 FROM CUSTOMER;

COUNT() 数的是行数,不管某一列是不是空。结果应该是 3。这里给 COUNT() 起了别名“客户数”,只是让结果表头更好读,不起别名也不影响查询。新手常问 COUNT() 和 COUNT(CNO) 有什么区别:如果 CNO 是主键、每行都有值,两者结果一样;如果某一列允许为空,COUNT(列名) 会跳过空值。统计客户数量用 COUNT() 更直接。

接着看库存总和与最高单价。SUM 只对数字列有意义,MAX 和 MIN 也是。你要是把 SUM 套在 CNAME 上,数据库会直接报错,这不是玄学,是类型不对。

-- 求所有产品库存量的总和 SELECT SUM(STOCKS) AS 库存总量 FROM PRODUCT; -- 求所有产品里的最高单价 SELECT MAX(PRICE) AS 最高单价 FROM PRODUCT;

SUM(STOCKS) 把 PRODUCT 表里每一行的 STOCKS 加起来,当前示例数据是 300+800+120=1220。MAX(PRICE) 找的是 PRICE 列里最大的那个值,当前是 25.00。参数说明就一句:括号里写你要统计的列名,列里必须是数字或可计算的类型。结果只有一行,因为这里没有分组,整张表被当成一组。

2.2 GROUP BY 和 HAVING:为什么“先分组再筛”是新手分水岭

不分组时,整张表是一大组;一分组,数据库会按某一列的值把行拆成小堆,再对每一堆做统计。GROUP BY 就是“按谁分堆”。比如求每个客户订购产品数量的总数,就要按 CNO 分堆,再对每堆的 OQUANTITY 求和。

-- 按客户编号分组,算每个客户订购数量的总数 SELECT CNO, SUM(OQUANTITY) AS 订购总量 FROM SALE GROUP BY CNO;

CNO 是分组列,SUM(OQUANTITY) 是每组的汇总值。结果里 C01 是 120+220=340,C02 是 150,C03 是 80。SELECT 里出现 CNO 和聚合函数,是因为 CNO 在 GROUP BY 里,数据库知道每一行结果代表哪个客户。你要是再塞一个没有分组的 OQUANTITY 进去,很多数据库会报错,或者给出让你看不懂的结果。

HAVING 是分组之后的筛选。WHERE 在分组前筛行,HAVING 在分组后筛组,这个顺序是新手最容易混的地方。看一个实验里常见的需求:至少订购两种以上产品的客户编号和产品种类数。

-- 按客户分组,只留下产品种类数大于等于 2 的客户 SELECT CNO, COUNT(DISTINCT PCODE) AS 产品种类数 FROM SALE GROUP BY CNO HAVING COUNT(DISTINCT PCODE) >= 2;

COUNT(DISTINCT PCODE) 里的 DISTINCT 表示同一个产品只算一次。实验原文里用的是 COUNT(PCODE),它数的是订单行数;如果同一个客户对同一个产品有两行记录,COUNT(PCODE) 会把重复产品也算进去。你要的是“产品种类数”,就用 COUNT(DISTINCT PCODE);你要的是“订单行数”,才用 COUNT(PCODE)。这不是抠字眼,结果口径不同,后面嵌套查询的答案就会跟着变。

HAVING 后面可以直接写聚合函数,也可以写别名,具体看数据库支持;初学阶段建议写完整表达式,别依赖别名,换一个环境就容易翻车。WHERE 里不能写 COUNT 这类聚合函数,因为那时还没分组。记住一句话:先 WHERE 筛行,再 GROUP BY 分堆,再 HAVING 筛堆,最后 SELECT 出列。

2.3 函数套条件:BETWEEN、LIKE 与统计结果怎么读

统计函数经常和条件一起用。求库存量超过 500 的产品个数,可以写成 WHERE STOCKS > 500 再 COUNT(*),也可以按库存分组后 HAVING,但两种写法结果形态不一样。新手实验里容易把“个数”查成“每个库存值各有多少个”,读结果时才发现不对。

-- 直接筛出库存超过 500 的产品,再数行数 SELECT COUNT(*) AS 库存超500的产品数 FROM PRODUCT WHERE STOCKS > 500;

WHERE 先留下 STOCKS 大于 500 的行,COUNT(*) 再数这些行。当前示例里只有 B01 的 800 符合,结果是 1。你要是写成 GROUP BY STOCKS HAVING STOCKS > 500,得到的是按库存值分组后的多行结果,不是单一的“产品个数”。实验里两种写法都可能出现,但读题时先问自己:我要一行总数,还是每个分组一行?

再看 BETWEEN 和 LIKE。求单价在 10 到 20 元之间的产品个数,BETWEEN 的边界是包含的,10 和 20 都算在内。求以字母 B 开头的产品最低单价,LIKE 'B%' 里的 % 表示后面任意字符。

-- BETWEEN 包含两端边界 SELECT COUNT(*) AS 价格区间产品数 FROM PRODUCT WHERE PRICE BETWEEN 10 AND 20; -- LIKE 'B%' 表示以 B 开头,后面任意字符 SELECT MIN(PRICE) AS B开头产品最低价 FROM PRODUCT WHERE PCODE LIKE 'B%';

第一条查询当前会数到 A01 的 12.50 和 B01 的 18.00,共 2 个;B02 的 25.00 不在区间内。第二条查询只看 PCODE 以 B 开头的产品,也就是 B01 和 B02,最低价是 18.00。LIKE 的大小写行为跟数据库的排序规则有关,入门时先按你实验环境里的结果为准,不要凭感觉认定 B 和 b 一定一样。

这一章练到这里,你应该能独立写出“数客户、求总和、找最大最小、按客户分组、分组后筛、带条件统计”这几类查询。它们看起来不花哨,却是后面连接查询和嵌套查询的零件。子查询返回的那个数字,往往就是这里算出来的;你现在能读懂结果,后面才不会把“子查询返回多行”误当成语法错误。

3. 连接查询入门:两张表怎么“对上号”再一起筛

统计查询是在一张表里数数,连接查询开始处理“客户在客户表,订单在销售表,产品价格在产品表”这种分开存的信息。新手第一次写连接,最常见的困惑不是 JOIN 这个词,而是“这两张表凭什么能连起来”。答案不复杂:它们有共同的编号列。客户表的 CNO 和销售表的 CNO 对上,产品表的 PCODE 和销售表的 PCODE 对上,连接条件就是把这两列相等写出来。

这一章先不讲花哨的 LEFT JOIN、RIGHT JOIN,先把最常用的内连接写顺。你要能看懂“FROM SALE, CUSTOMER WHERE CUSTOMER.CNO = SALE.CNO”这种旧式写法和“FROM SALE JOIN CUSTOMER ON ...”这种新式写法,它们表达的是同一类事情。数据库实验5里的第 11 题就是典型:查询由某城市客户订购的、且订购数量大于 200 套的客户编号、产品编号、联系人和订购数量。城市值在练习里换成你库里的虚拟值,下面统一用“城市A”。

3.1 先把表关系画在纸上:主键和外键不是玄学

先看三张表的最小关系。CUSTOMER 的 CNO 是客户编号,PRODUCT 的 PCODE 是产品编号,SALE 里同时有 CNO 和 PCODE,表示“哪个客户订了哪个产品”。SALE 里的 CNO 指向 CUSTOMER 的 CNO,SALE 里的 PCODE 指向 PRODUCT 的 PCODE。你不需要背“外键约束”的定义,先把这个对应关系画出来:一个客户可以有多条销售记录,一个产品也可以出现在多条销售记录里。

表关键列作用
CUSTOMERCNO、CNAME、SITE、TELE存客户编号、名称、城市、电话
PRODUCTPCODE、PNAME、PRICE、STOCKS存产品编号、名称、单价、库存
SALECNO、PCODE、OQUANTITY存谁订了什么、订了多少

连接查询的本质是:从 SALE 里拿一条订单,用它的 CNO 去 CUSTOMER 找对应客户,用它的 PCODE 去 PRODUCT 找对应产品,然后把需要的列拼成一行结果。要是连接条件写漏了,SALE 的每一行会和 CUSTOMER 的每一行强行配对,结果行数暴涨,这就是新手常见的“重复行”来源。

连接条件写在 ON 后面,筛选条件写在 WHERE 后面。旧式写法把连接条件和筛选条件都塞进 WHERE,读起来容易混;新式 JOIN 把“怎么连”和“筛什么”分开了,更推荐新手先用这种写法。下面两个查询结果一样,你可以对照看。

-- 旧式写法:连接条件放在 WHERE 里 SELECT SALE.CNO, SALE.PCODE, CUSTOMER.CNAME, SALE.OQUANTITY FROM SALE, CUSTOMER WHERE SALE.CNO = CUSTOMER.CNO AND CUSTOMER.SITE = '城市A' AND SALE.OQUANTITY > 200; -- 新式写法:JOIN 后面写连接条件,WHERE 只筛结果 SELECT S.CNO, S.PCODE, C.CNAME, S.OQUANTITY FROM SALE AS S JOIN CUSTOMER AS C ON C.CNO = S.CNO WHERE C.SITE = '城市A' AND S.OQUANTITY > 200;

旧式写法里,SALE.CNO = CUSTOMER.CNO 是连接条件,CUSTOMER.SITE = '城市A' 和 SALE.OQUANTITY > 200 是筛选条件。新式写法用 AS 给表起了短别名 S 和 C,JOIN 后面紧跟 ON C.CNO = S.CNO,WHERE 只留下城市和数量条件。别名不是必须的,但表名一长、列名重复时,别名能让 SQL 短很多。ON 后面写两张表怎么对应,WHERE 后面写你要的业务条件,这个分工先养成习惯。

3.2 用 WHERE 连接两张表:城市客户大额订单的例子

把上面的例子拆成步骤。第一步,先看 SALE 表里有哪些订单;第二步,把 CUSTOMER 接进来,看每条订单对应哪家客户;第三步,加上城市和数量条件。你不要一上来就写完整 SQL,先在纸上列出“我要的列来自哪张表”:客户编号来自 SALE 或 CUSTOMER 都行,产品编号来自 SALE,联系人来自 CUSTOMER,订购数量来自 SALE。列找对了,连接条件就不容易漏。

-- 第一步:先看订单原始数据 SELECT CNO, PCODE, OQUANTITY FROM SALE; -- 第二步:接上客户表,先不筛条件,观察结果行数 SELECT S.CNO, S.PCODE, C.CNAME, C.TELE, S.OQUANTITY FROM SALE AS S JOIN CUSTOMER AS C ON C.CNO = S.CNO; -- 第三步:加城市和数量条件 SELECT S.CNO, S.PCODE, C.CNAME, C.TELE, S.OQUANTITY FROM SALE AS S JOIN CUSTOMER AS C ON C.CNO = S.CNO WHERE C.SITE = '城市A' AND S.OQUANTITY > 200;

第一步只看 SALE,结果有 4 行。第二步接上 CUSTOMER,结果还是 4 行,因为每个 CNO 都能在客户表里找到对应行;如果某个 CNO 在客户表里不存在,内连接会把它丢掉。第三步筛城市A且数量大于 200,当前示例里 C01 的 B01 订单 220 符合,C02 的 B01 订单 150 不符合。结果里出现 CNAME 和 TELE,是因为它们来自 CUSTOMER;S.CNO、S.PCODE、S.OQUANTITY 来自 SALE。每个列前面带表别名,数据库就知道去哪个表取,不会因为两张表都有 CNO 而报“列名不明确”。

参数怎么改:城市值换成你实验库里的实际值,200 换成题目要求的阈值,CNAME、TELE 换成题目要的联系人列。要是实验表里联系人列名不叫 CNAME,就按你库里的列名改,不要硬抄示例。连接条件 C.CNO = S.CNO 不能省,省了就会变成笛卡尔积,行数等于客户数乘订单数,结果看起来“多了一堆重复记录”。

3.3 连接查询和嵌套查询的分工:什么时候别硬写子查询

同一个需求,连接查询和嵌套查询都能写。比如“查询城市A客户的大额订单”,可以用 JOIN 把客户表接进来筛城市,也可以用子查询先查出城市A的客户编号,再在外层筛 SALE。两种写法没有绝对高低,入门阶段先看可读性:需要同时显示客户名称、电话和订单数量时,JOIN 更直接;只需要拿一个条件去筛另一张表时,子查询更贴近人的思路。

-- 用子查询实现同一件事:先找城市A的客户编号,再筛订单 SELECT CNO, PCODE, OQUANTITY FROM SALE WHERE CNO IN ( SELECT CNO FROM CUSTOMER WHERE SITE = '城市A' ) AND OQUANTITY > 200;

这段 SQL 先跑括号里的子查询,得到城市A的客户编号集合,比如 C01、C02;外层再用 CNO IN 这个集合去筛 SALE,并加上数量大于 200。IN 适合子查询返回多个值的情况,如果子查询只返回一个值,可以用 =,但新手先养成“可能返回多个就用 IN”的习惯,能少踩很多坑。连接查询把两张表并排拼起来,子查询把内层结果当成外层的一个条件,这就是它们最直观的分工。

数据库实验5里第 11 题要求同时出现客户编号、产品编号、联系人和订购数量,用 JOIN 写最顺;第 13 题“查询单价比 A01 号产品高的产品订单”,更适合先用子查询查出 A01 的单价,再拿这个单价去比较。你不需要强迫自己只用一种写法,先把题目拆成“内层要算出什么、外层要拿这个结果筛什么”,再决定用连接还是嵌套。连接查询这一关过了,嵌套查询才不会变成背模板。

4. 嵌套查询第一步:把子查询当成“括号里的小答案”

嵌套查询的写法是外层一个 SELECT,内层一个 SELECT。内层先执行,算出一个值或一组值,外层再拿这个结果去筛选。新手最容易犯的错,是把内层当成“装饰”随便塞进去,结果外层用 = 接一个返回多行的子查询,数据库直接报错。你先别追求三层套三层,先把单值子查询、比较运算符子查询、NOT IN 和 NOT EXISTS 这几种最常见的形态跑通。

这一章对应数据库实验5里的第 12、13、14 题:同城市客户、比 A01 单价高的订单、没有被订购的产品。题目里的公司名和城市值都换成虚拟值,逻辑不变。每写一个嵌套查询,先单独跑内层,确认它返回一行还是一列多行,再写外层。这个习惯比你背十条语法都管用。

4.1 单值子查询:找“和某公司在同一城市”的客户

第 12 题的需求是:查询与“示例公司甲”在同一城市的客户公司名称及联系电话。人的思路分两步:先查示例公司甲在哪个城市,再查这个城市里还有哪些客户。子查询正好对应第一步,外层对应第二步。

-- 内层:先查示例公司甲所在的城市 SELECT SITE FROM CUSTOMER WHERE CNAME = '示例公司甲'; -- 外层:用内层返回的城市去筛其他客户 SELECT CNAME, TELE FROM CUSTOMER WHERE SITE = ( SELECT SITE FROM CUSTOMER WHERE CNAME = '示例公司甲' );

内层返回一个城市值,比如“城市A”。外层 WHERE SITE = 这个值,就能找到同城的客户。这里用 = 的前提是内层只返回一行;如果客户表里有两家都叫“示例公司甲”,内层返回两行,外层就会报“子查询返回多于一行”。真实实验里公司名一般不会重复,但你要知道这个限制。想避开这个坑,可以把 = 换成 IN,或者在内层加一个能唯一确定客户的条件,比如客户编号。

结果里会包含示例公司甲自己。如果题目要求“其他客户”,外层再加一个条件 CNAME <> '示例公司甲'。这个条件写在外层,不要写进内层,内层只负责回答“城市是什么”。嵌套查询的每一层只做一件事,读起来才不绕。参数说明:CNAME 是客户名称列,SITE 是城市列,字符串要用单引号包住;换成你库里的列名时,内外层要对应同一张表的同一列。

4.2 比较运算符配子查询:比 A01 单价高的产品订单

第 13 题是:查询订购了单价比 A01 号产品单价高的那些产品的产品编号、客户编号和订购数量。这里的内层不是找城市,而是找 A01 的单价;外层拿 SALE 和 PRODUCT 连接后,用 PRICE > 内层单价来筛。

-- 内层:查 A01 号产品的单价 SELECT PRICE FROM PRODUCT WHERE PCODE = 'A01'; -- 外层:连接销售表和产品表,筛单价高于 A01 的订单 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' );

内层返回 A01 的单价 12.50。外层先把 SALE 和 PRODUCT 按 PCODE 连起来,得到每条订单对应的产品单价,再筛单价大于 12.50 的记录。当前示例里 B01 单价 18.00、B02 单价 25.00,符合条件;A01 自己的订单不符合。外层用 JOIN 是因为结果要产品编号、客户编号和订购数量,SALE 里有客户和数量,PRODUCT 里有单价,两张表都要用。

这里的比较运算符 > 只能接单值子查询。内层 WHERE PCODE = 'A01' 如果匹配到两行,外层就会报错。产品编号如果是主键,通常不会重复;你要是拿产品名称去查,就可能返回多行,这时要么改用 IN,要么先确认名称是否唯一。参数怎么改:A01 换成题目指定的产品编号,> 换成题目要求的比较方向,SELECT 里要的列按题目增减。不要在外层 WHERE 里再写一遍 PCODE = 'A01',那会把 A01 自己的订单也筛进来,和题意相反。

4.3 NOT IN / NOT EXISTS 的入门用法:没被订购的产品怎么查

第 14 题是:查询没有被订购的产品的编号和名称。人的思路是“产品表里有哪些产品,销售表里没有出现过的就是没被订购”。用 NOT IN 写,先查 SALE 里出现过的产品编号,再让 PRODUCT 排除这些编号。

-- NOT IN:先查销售表里出现过的产品编号,再排除它们 SELECT PCODE, PNAME FROM PRODUCT WHERE PCODE NOT IN ( SELECT PCODE FROM SALE WHERE PCODE IS NOT NULL );

内层 SELECT PCODE FROM SALE 返回一列产品编号,外层用 NOT IN 排除这些编号。当前示例里 SALE 出现过 A01 和 B01,PRODUCT 里的 B02 没出现过,所以结果是 B02。内层加 WHERE PCODE IS NOT NULL 是防一手:如果 SALE 的 PCODE 里有空值,NOT IN 可能让整个结果变成空,这是 SQL 的三值逻辑,不是你的眼睛花了。入门阶段先记住“NOT IN 遇到 NULL 容易出问题”,要么过滤空值,要么改用 NOT EXISTS。

-- NOT EXISTS:对每个产品,检查销售表里有没有对应记录 SELECT P.PCODE, P.PNAME FROM PRODUCT AS P WHERE NOT EXISTS ( SELECT 1 FROM SALE AS S WHERE S.PCODE = P.PCODE );

NOT EXISTS 的写法是相关子查询:内层用到了外层的 P.PCODE,数据库会对 PRODUCT 的每一行跑一次内层,检查 SALE 里有没有相同产品编号。SELECT 1 只是占位,表示“有记录就行”,不需要真的取某一列。这个写法对 NULL 更稳,读起来也更贴近“不存在对应销售记录”的意思。新手先会 NOT IN,再会 NOT EXISTS,两者都能解决“没被订购”的问题,但后者在数据有空值时更让人放心。

嵌套查询练到这里,你已经覆盖了单值子查询、比较运算符子查询、IN、NOT IN、NOT EXISTS。写的时候先单独跑内层,看它返回一行还是一列;外层用 = 还是 IN,取决于内层结果的行数。数据库实验5里的嵌套题不会故意为难你,难的是你在一张纸上把“内层回答什么、外层拿它筛什么”说清楚。说清楚了,SQL 只是把这个思路翻译出来。

5. 新手避坑排查:数据库实验5嵌套查询最容易翻车的 5 个地方

这一章不堆新语法,专门处理你写完 SQL 后“结果不对、报错、查不到”的场景。数据库实验5的题目看起来短,但连接条件、分组口径、子查询返回值、NULL、中文条件这几个地方,任何一个没对齐,结果就会让你怀疑人生。下面每条都按“现象、原因、解决”走,你遇到问题时可以直接对号入座。

5.1 现象:结果多出一堆重复行;原因:连接条件漏了;解决:先数表再写 WHERE

现象很典型:你只想查城市A客户的大额订单,结果每一条订单重复出现好几次,客户名称还来回换。原因通常是 FROM 后面写了两张表,却忘了写两张表的连接条件,数据库把两张表做了笛卡尔积。SALE 有 4 行、CUSTOMER 有 3 行,没连接条件就会出 12 行,看起来像“数据重复”,其实是强行配对。

-- 错误:漏了 C.CNO = S.CNO,行数会膨胀 SELECT S.CNO, S.PCODE, C.CNAME, S.OQUANTITY FROM SALE AS S JOIN CUSTOMER AS C WHERE C.SITE = '城市A'; -- 正确:先写连接条件,再写筛选条件 SELECT S.CNO, S.PCODE, C.CNAME, S.OQUANTITY FROM SALE AS S JOIN CUSTOMER AS C ON C.CNO = S.CNO WHERE C.SITE = '城市A' AND S.OQUANTITY > 200;

解决步骤不复杂:先单独 SELECT COUNT(*) 看 SALE 有多少行,再接上 CUSTOMER 看行数有没有变化;如果接上后行数暴涨,先检查 ON 后面的连接条件。旧式写法就把连接条件放在 WHERE 的第一行,别和业务筛选混在一起。你每写一个 JOIN,都在纸上写一句“这两张表用哪个编号对应”,连接条件就不会漏。

5.2 现象:子查询报“返回多行”;原因:用了 = 却返回多个值;解决:IN 或加条件

现象是数据库直接报错,提示子查询返回多于一行。原因是你用 = 接了一个可能返回多行的子查询。比如按客户名称查城市,如果名称不唯一,内层会返回多个城市,外层 WHERE SITE = (...) 就接不住。这不是嵌套查询本身的问题,是你给 = 塞了它处理不了的结果。

-- 有风险:如果“示例公司甲”不唯一,内层返回多行,= 会报错 SELECT CNAME, TELE FROM CUSTOMER WHERE SITE = ( SELECT SITE FROM CUSTOMER WHERE CNAME = '示例公司甲' ); -- 更稳:用 IN 接一列多行 SELECT CNAME, TELE FROM CUSTOMER WHERE SITE IN ( SELECT SITE FROM CUSTOMER WHERE CNAME = '示例公司甲' );

解决时先单独跑内层,看结果是一行还是一列多行。只返回一行,= 可以用;可能返回多行,就换 IN。另一种办法是让内层条件更唯一,比如用 CNO 而不是 CNAME。IN 的语义是“等于这个集合里的任意一个值”,读起来也比 = 更宽容。数据库实验5里很多嵌套题内层只返回一行,但你最好养成先验证的习惯。

5.3 现象:GROUP BY 报错或结果怪;原因:SELECT 列不在分组里;解决:先分组再选列

现象是 GROUP BY 查询报错,或者结果里某个数量列的值对不上。原因通常是 SELECT 里出现了没有放进 GROUP BY、也没有被聚合函数包住的列。比如按产品编号分组,却直接选 OQUANTITY,数据库不知道你要取这一组里的哪一行数量。

-- 有风险:OQUANTITY 没有分组,也没有被聚合 SELECT PCODE, OQUANTITY FROM SALE GROUP BY PCODE; -- 正确:要每组总量,就用 SUM 包住数量列 SELECT PCODE, SUM(OQUANTITY) AS 订购总量 FROM SALE GROUP BY PCODE;

解决思路:先问自己这一行结果代表什么。按 PCODE 分组,一行代表一个产品,那么数量列要么求和,要么求平均,要么求最大最小;不能直接裸选。实验里“至少被两个以上客户订购且每个客户订购数量超过 100”的题目,还要注意 COUNT(PCODE) 数的是订单行,COUNT(DISTINCT CNO) 数的才是客户数。口径选错,结果看起来像数据库在骗你,其实是你把“行数”当成了“客户数”。

5.4 现象:NULL 让 NOT IN 失效;原因:三值逻辑;解决:NOT EXISTS 或过滤 NULL

现象是明明有没被订购的产品,NOT IN 却返回空结果。原因多半是子查询的那一列里出现了 NULL。NOT IN 的语义是“不等于集合里的每一个值”,当集合里有 NULL 时,比较结果既不是真也不是假,整个条件可能不成立。新手看到空结果,常以为数据不存在,其实是 NULL 在捣乱。

-- 防 NULL:先过滤掉空的产品编号 SELECT PCODE, PNAME FROM PRODUCT WHERE PCODE NOT IN ( SELECT PCODE FROM SALE WHERE PCODE IS NOT NULL ); -- 更稳:用 NOT EXISTS,不依赖 NOT IN 的 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 PCODE FROM SALE,看有没有空值;有空值就加 WHERE PCODE IS NOT NULL,或者直接换 NOT EXISTS。NOT EXISTS 只关心“有没有对应记录”,空值不会把整个结果拖垮。数据库实验5第 14 题很适合用这个写法练手,你写一次就能记住 NULL 这个坑。

5.5 现象:中文条件查不到;原因:空格、引号或值不一致;解决:先查原值再比对

现象是 SQL 没报错,但 WHERE CNAME = '示例公司甲' 查不到任何行。原因可能是你输入的值和表里的值不完全一致:前后有空格、全角半角不同、名称里多了看不见的字符,或者你用了双引号但环境把双引号当成了列名。中文条件最容易让人以为“数据库不支持中文”,其实多数时候是值没对上。

-- 先看表里到底存了什么,包括城市和公司名的原值 SELECT CNO, CNAME, SITE, LENGTH(CNAME) AS 名称长度 FROM CUSTOMER; -- 确认值一致后,再用单引号写条件 SELECT CNO, CNAME, SITE FROM CUSTOMER WHERE SITE = '城市A';

解决步骤:先 SELECT 原值,把表里的名称复制出来比对;检查字符串是不是用单引号包住;检查有没有多余空格。LENGTH 只是帮你看名称长度,不同数据库函数名可能不同,用你环境里能跑的长度函数就行。中文值本身没有问题,问题是“你看到的值”和“表里存的值”是否完全一样。每次查不到,先查原值,再怀疑语法。

6. 下一步学什么:把实验5的查询练成自己的手感

下一步不要急着学窗口函数、存储过程,先把数据库实验5里的查询按“统计、连接、嵌套”三组重写一遍。第一组把 COUNT、SUM、MAX、MIN、GROUP BY、HAVING 各写两条,每条都单独跑内层或分组结果;第二组把第 11 题用 JOIN 写一遍,再用子查询写一遍,比较结果行数;第三组把第 12、13、14 题各写两版,一版用 IN 或 =,一版用 NOT EXISTS 或 JOIN。每组写完,用 COUNT(*) 对一下行数,别只看第一行数据。

阶段练什么达标标准
第 1 周统计函数加 GROUP BY能说清 WHERE 和 HAVING 的顺序
第 2 周两表 JOIN 加筛选能独立写出连接条件,不漏 ON
第 3 周单值子查询和 IN会先单独跑内层,判断返回行数
第 4 周NOT IN 与 NOT EXISTS遇到 NULL 知道换写法

具体技巧:写嵌套查询时,先把内层单独跑出来,确认它返回一行还是一列;外层要用 = 还是 IN,看内层结果决定。你可以给内层加注释,写清楚“这里查城市”“这里查单价”,别让三层 SELECT 挤在一起。再进一步,可以学 WITH 子句,把子查询提到前面命名,读起来像流水账。

-- 用 WITH 给子查询起名,适合下一步练习 WITH same_city AS ( SELECT SITE FROM CUSTOMER WHERE CNAME = '示例公司甲' ) SELECT CNAME, TELE FROM CUSTOMER WHERE SITE IN (SELECT SITE FROM same_city);

我第一次练嵌套查询时,总想一口气写三层,结果外层报错,内层返回三行,查了半小时才发现是 = 接不住多行。从那以后我每次写子查询都先单独跑内层,再把结果接回外层,这个习惯比背语法管用。希望帮到你。

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

RELATED READING

延伸阅读

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