news 2026/10/31 7:42:33

13条SQL在MySQL 8报错:分组统计与嵌套查询避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
13条SQL在MySQL 8报错:分组统计与嵌套查询避坑

简介:这份《数据库实验5嵌套查询》文档面向高校数据库课程学生与SQL初学者,围绕统计查询、连接查询和嵌套查询的实验任务展开,可用于实验课对照操作、课后复盘与报告撰写。文档以实验目的、实验内容和思考总结为脉络,覆盖COUNT、SUM、MAX、MIN等统计函数,GROUP BY与HAVING分组筛选,以及子查询、派生表、LIKE、BETWEEN等常见用法,并给出上海客户订单、产品单价比较、未被订购产品等典型题目及SQL示例,便于读者理解查询思路并自行复现。包内共1个doc文件,压缩包约642KB,体积轻便,适合直接查阅和整理。已有931人学习,可作为数据库查询语言入门阶段的配套实验材料,帮助读者从统计汇总逐步过渡到嵌套查询,掌握连接条件与谓词组合的写法,提升实际编写和调试SQL的能力。

1. 实验报告里那 13 条 SQL,有一半在 MySQL 8 上会报错

某同学把数据库实验5嵌套查询的实验报告发过来,说 SQL 在机房那台机器上能跑,回宿舍一贴就红一片。打开一看,问题一点都不玄学:GROUP BY后面漏了聚合、!=接了个会返回多行的子查询、HAVING里塞了本该放WHERE的过滤条件。这份文档围绕 CPXS 库的 CUSTOMER、PRODUCT、SALE 三张表,一共 13 道题,覆盖 COUNT/SUM/MAX/MIN 统计、GROUP BY + HAVING 分组、隐式连接、标量子查询和 IN/EXISTS 判定,是数据库实验课里最典型的一批 SELECT 语法练习,也是面试里被追问"子查询和 JOIN 有什么区别"时最容易露怯的地方。下面按"先把表建起来、再逐题跑通、最后看坑"的顺序走一遍,每一步都能直接抄。

2. 把 CPXS 三张表建起来:字段类型、外键顺序与一份可执行脚本

2.1 三张表怎么连:主键设计与类型选择的理由

CPXS 是"客户—产品—销售"的缩写,三张表的关系非常干净:CUSTOMER 存客户主体信息,主键是客户编号 CNO;PRODUCT 存产品主体信息,主键是产品编号 PCODE;SALE 存的是"哪个客户订了哪个产品多少套",属于典型的联系表,主键用 (CNO, PCODE) 复合主键,同时分别外键指向两张主表。

这里有两个容易被忽略的决策点。第一,CNO 和 PCODE 都定长 4 位('C001'、'A01'),所以用CHAR(4)而不是VARCHAR,定长在索引和比较上更省事;但代价是一旦哪天编号扩到五位,所有相关表都得改结构,实验里图省事可以这么干,真项目里我一般直接留VARCHAR(16)。第二,PRICE 必须用DECIMAL(8,2),绝对不能用FLOAT或DOUBLE,浮点在求和、比较时会出现 15.499999 这种尾巴,第 7 题"单价在 10~20 元之间"的边界判断会莫名其妙少一条记录,这是血泪经验。

TELE 字段要允许 NULL。第 12 题要查"与美美同一城市的客户名称和电话",如果某个客户没登记电话,NOT NULL加上默认空串会让结果里多出一行空字符串,看起来像数据错了。

2.2 建表与灌数据:按依赖顺序执行的完整脚本

外键约束决定了删除顺序:先删子表 SALE,再删 PRODUCT 和 CUSTOMER;建表则反过来。这个顺序弄反了会直接报 1217 或 1451,很多人第一次跑就卡在这。

-- 建议在 MySQL 8.0 下执行;5.7 语法同样兼容 CREATE DATABASE IF NOT EXISTS CPXS DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE CPXS; -- 删除顺序:先子表后主表,否则外键约束会拦住 DROP TABLE IF EXISTS SALE; DROP TABLE IF EXISTS PRODUCT; DROP TABLE IF EXISTS CUSTOMER; CREATE TABLE CUSTOMER ( CNO CHAR(4) NOT NULL COMMENT '客户编号,定长4位', CNAME VARCHAR(30) NOT NULL COMMENT '客户公司名称', SITE VARCHAR(20) NOT NULL COMMENT '所在城市', TELE VARCHAR(20) DEFAULT NULL COMMENT '联系电话,允许为空', PRIMARY KEY (CNO) ) ENGINE=InnoDB; CREATE TABLE PRODUCT ( PCODE CHAR(4) NOT NULL COMMENT '产品编号,定长4位', PNAME VARCHAR(30) NOT NULL COMMENT '产品名称', PRICE DECIMAL(8,2) NOT NULL COMMENT '单价,禁止用 FLOAT', STOCKS INT NOT NULL DEFAULT 0 COMMENT '库存量', PRIMARY KEY (PCODE) ) ENGINE=InnoDB; CREATE TABLE SALE ( CNO CHAR(4) NOT NULL, PCODE CHAR(4) NOT NULL, OQUANTITY INT NOT NULL COMMENT '订购数量', PRIMARY KEY (CNO, PCODE), -- 复合主键:一个客户对同一产品只有一条记录 CONSTRAINT fk_sale_customer FOREIGN KEY (CNO) REFERENCES CUSTOMER(CNO), CONSTRAINT fk_sale_product FOREIGN KEY (PCODE) REFERENCES PRODUCT(PCODE) ) ENGINE=InnoDB;

建完灌数据。这里特意留了一个细节:C005 的 TELE 是 NULL,C01 这个产品一条订单都没有——前者用来验证第 12 题的空值行为,后者正好给第 14 题"没有被订购的产品"提供答案。

INSERT INTO CUSTOMER (CNO, CNAME, SITE, TELE) VALUES ('C001','美美公司','上海','021-1111'), ('C002','华联商厦','上海','021-2222'), ('C003','北方贸易','北京','010-3333'), ('C004','南方实业','广州','020-4444'), ('C005','东方百货','上海', NULL); -- 故意留空,用于验证 NULL 行为 INSERT INTO PRODUCT (PCODE, PNAME, PRICE, STOCKS) VALUES ('A01','空调', 1500.00, 120), ('A02','冰箱', 2200.00, 80), ('B01','台灯', 15.50, 600), -- 库存 > 500,第 6 题的答案 ('B02','电风扇', 12.00, 300), -- 单价落在 10~20,第 7 题 ('B03','电饭煲', 18.00, 150), -- 单价落在 10~20,第 7 题 ('C01','微波炉', 450.00, 60); -- 从未被订购,第 14 题的答案 INSERT INTO SALE (CNO, PCODE, OQUANTITY) VALUES ('C001','A01',220), ('C001','B01',500), ('C002','A01',150), ('C002','B02',260), ('C002','B03',130), ('C003','B01',120), ('C003','A02', 90), ('C004','A02',110), ('C004','B01',300), ('C005','B02', 45);

执行完先做一次行数校验:SELECT COUNT(*) FROM CUSTOMER得 5,PRODUCT 得 6,SALE 得 10。三个数字对不上,说明插入中途报错被跳过了,后面所有统计结果都会连锁失真。

2.3 环境对齐:先确认 sql_mode,再判断是语法问题还是环境问题

同一句 SQL 在一台机器上跑得通、另一台上报 1055,九成是ONLY_FULL_GROUP_BY的差别。MySQL 5.7 起这个模式默认开启,它要求 SELECT 列表里出现的非聚合列必须出现在 GROUP BY 里。实验文档里第 8 题的写法正好踩线。所以拿到环境的第一件事不是抄 SQL,而是跑一句:

-- 查看当前会话的 sql_mode,确认 ONLY_FULL_GROUP_BY 是否开启 SELECT @@sql_mode; -- 仅用于本地验证语法差异,不建议在生产库上关掉这个模式 -- SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';

我的做法是保留默认的严格模式,把 SQL 改对,而不是把模式关掉。关掉之后确实不报错了,但同一句拿到老师机器上照样红,问题只是被藏起来。

SQL Server 环境要注意三处差异:分页用TOP而不是LIMIT;字符串拼接用+而不是CONCAT;CHAR比较时会补尾随空格,'A01 '和'A01'在 SQL Server 里可能被判相等,在 MySQL 里则要考虑排序规则是否忽略尾部空格。这些不影响本实验的主线,但换库跑之前心里要有数。

3. 统计查询的四种写法:COUNT、SUM 与 WHERE / HAVING 的边界

3.1 COUNT(*) 和 COUNT(列) 不是一回事

第 1 题求客户数目,文档给的是SELECT COUNT(*) FROM CUSTOMER,返回 5,没问题。但很多人顺手改成COUNT(TELE),结果变成 4——因为 C005 的电话是 NULL,COUNT(列名)只统计非空值,而COUNT(*)统计行数。这两个在语义上完全不同,不是写法偏好问题。

-- 统计客户总数:不管字段是否为 NULL SELECT COUNT(*) AS 客户总数 FROM CUSTOMER; -- 统计登记了电话的客户数:NULL 不计入 SELECT COUNT(TELE) AS 有电话的客户数 FROM CUSTOMER; -- 两者一起看,差值就是没登记电话的客户数 SELECT COUNT(*) AS 总数, COUNT(TELE) AS 有电话, COUNT(*) - COUNT(TELE) AS 缺电话 FROM CUSTOMER;

COUNT(*)在 InnoDB 上会走最小的二级索引而不是全表扫描,性能上不必担心。真正要警惕的是COUNT(DISTINCT 列):第 8 题问"至少被两个以上客户订购的产品",如果同一客户对同一产品存在多条订单记录(本实验的复合主键规避了这种情况,但真实业务表里很常见),不写 DISTINCT 就会把重复关系算成多个客户。

第 2 题求库存总量,SELECT SUM(STOCKS) FROM PRODUCT返回 1310。SUM 有一个容易被忽略的行为:当结果集为空时它返回 NULL 而不是 0。也就是说如果 PRODUCT 表被清空,这条语句不会报错,而是安静地返回一行 NULL,下游程序拿去做加法就会炸。

-- 空集保护:SUM 空集返回 NULL,用 COALESCE 兜底 SELECT COALESCE(SUM(STOCKS), 0) AS 库存总量 FROM PRODUCT; -- 求最高单价,同样适用于 MAX / MIN SELECT MAX(PRICE) AS 最高单价 FROM PRODUCT; -- 求以字母 B 开头的所有产品的最低单价(第 9 题) SELECT MIN(PRICE) AS B系列最低单价 FROM PRODUCT WHERE PCODE LIKE 'B%';

LIKE 'B%'这个模式有个前提:PCODE 的类型是定长CHAR(4)且存的是 'B01'、'B02' 这类值,前缀匹配不会误伤。如果编号规则改成"B 出现在任意位置",就必须换成LIKE '%B%',那样索引会失效,全表扫描。第 9 题的结果是 12.00,来自 B02 电风扇。

3.2 WHERE 和 HAVING 的执行顺序:第 4、6、7 题的通病

这是整份实验文档里最集中的问题区域。文档里第 4 题写的是:

SELECT SITE, COUNT(CNO) FROM CUSTOMER GROUP BY SITE HAVING SITE = '上海';

句子能跑,结果也对(上海 3 个客户),但语义上是"先按城市分组,再把不是上海的分组丢掉",而题目要的是"先筛出上海的客户,再数个数"。正确写法是WHERE:

-- 正确:先过滤行,再分组统计 SELECT SITE, COUNT(CNO) AS 客户数 FROM CUSTOMER WHERE SITE = '上海' GROUP BY SITE;

执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。WHERE 作用在原始行上,HAVING 作用在分组之后。把行级过滤写进 HAVING,中间结果会先被完整分组再丢弃,数据量大时开销实打实。更麻烦的是可移植性:SQL Server 不支持 HAVING 里引用未参与分组也不参与聚合的列,这条语句换库直接报错。

第 6 题的问题更隐蔽:

-- 文档原写法:聚合被 GROUP BY 摊到了多个组 SELECT COUNT(PCODE) FROM PRODUCT GROUP BY STOCKS HAVING STOCKS > 500;

GROUP BY STOCKS会把库存量相同的产品聚成一组,COUNT(PCODE)算的是每组的数量。本实验里库存超过 500 的只有 B01 一个产品,库存 600 独一份,所以碰巧返回一行 1。但如果库里有库存 520 和 610 两个产品,这条语句会返回两行、每行都是 1,加起来才是答案——结果集行数不确定,程序按单值接收就翻车了。正确写法把条件交给 WHERE:

-- 正确:过滤在前,聚合在后,结果恒定一行 SELECT COUNT(PCODE) AS 库存超500的产品数 FROM PRODUCT WHERE STOCKS > 500;

第 7 题"单价在 10~20 元之间产品的个数"犯的是同一类错:

-- 文档原写法:按价格分组,每个价格一行,还得自己把 count 求和 SELECT PRICE, COUNT(PCODE) FROM PRODUCT GROUP BY PRICE HAVING PRICE BETWEEN 10 AND 20; -- 正确写法:问的是"个数",就不该带 GROUP BY SELECT COUNT(*) AS 中低价产品数 FROM PRODUCT WHERE PRICE BETWEEN 10 AND 20;

BETWEEN 10 AND 20是闭区间,包含 10 和 20 本身。本实验里 15.50、12.00、18.00 三个产品命中,返回 3。如果业务上要求"10 到 20 之间但不含 10",就得写成PRICE > 10 AND PRICE < 20,别再指望 BETWEEN 能加参数。

3.3 GROUP BY 多列与 HAVING 中的聚合条件

第 8 题问"至少被两个以上客户订购且每个客户订购数量超过 100 的产品编号、订购数量总数"。这句话里有两个条件:行级条件(OQUANTITY > 100)和组级条件(订购的客户数超过 2)。分清楚哪个该进 WHERE、哪个该进 HAVING,是这道题的全部考点。

-- 文档原写法存在两个问题:OQUANTITY 是裸列、条件逻辑也不完整 -- SELECT PCODE, OQUANTITY FROM SALE -- WHERE OQUANTITY > 100 -- GROUP BY PCODE -- HAVING COUNT(PCODE) > 2; -- 正确写法:行级条件进 WHERE,组级条件进 HAVING SELECT PCODE, COUNT(DISTINCT CNO) AS 订购客户数, SUM(OQUANTITY) AS 订购总量 FROM SALE WHERE OQUANTITY > 100 GROUP BY PCODE HAVING COUNT(DISTINCT CNO) > 2;

三个细节值得说清。第一,OQUANTITY是裸列,在ONLY_FULL_GROUP_BY下直接报 1055,因为它既不在 GROUP BY 里也没被聚合;文档里那句在旧版本或关闭严格模式下才能跑,而且返回的 OQUANTITY 只是组内随机一行,语义上毫无意义。第二,COUNT(PCODE)统计的是订单行数而不是客户数,本实验用复合主键保证了 (CNO, PCODE) 唯一,所以两者相等;换成真实订单表,一个客户可能下三次单,就必须用COUNT(DISTINCT CNO)。第三,跑完这条语句会返回空结果集——按上面的数据,B01 有 C001、C003、C004 三个客户下单且数量都超过 100,但同时满足OQUANTITY > 100后剩三条,COUNT(DISTINCT CNO) = 3 > 2,应该是能命中的。如果拿到的结果是空的,先回头核对 SALE 表是不是少插了 ('C004','B01',300) 这条。

4. 连接查询与嵌套查询:从隐式连接一路改到 EXISTS

4.1 隐式连接与显式 JOIN:第 11 题的两种等价写法

第 11 题要求"查询由上海客户订购的且订购数量大于 200 套的所有客户编号、产品编号、联系人和订购数量"。文档用的是逗号连接加 WHERE 过滤:

SELECT SALE.CNO, PCODE, CNAME, OQUANTITY FROM SALE, CUSTOMER WHERE CUSTOMER.CNO = SALE.CNO AND CUSTOMER.SITE = '上海' AND SALE.OQUANTITY > 200;

这段能跑,但有两个毛病。一是PCODE、CNAME、OQUANTITY都没加表前缀,靠数据库自己去猜;现在三张表里恰好不重名,一旦 PRODUCT 也进来并且有同名列,立刻报 1052 列名歧义。二是隐式连接把连接条件和过滤条件混在一个 WHERE 里,语句一长就没法读。改成显式 INNER JOIN:

SELECT s.CNO AS 客户编号, s.PCODE AS 产品编号, c.CNAME AS 客户名称, s.OQUANTITY AS 订购数量 FROM SALE s INNER JOIN CUSTOMER c ON c.CNO = s.CNO WHERE c.SITE = '上海' AND s.OQUANTITY > 200 ORDER BY s.CNO, s.PCODE;

ON只放连接条件,WHERE只放过滤条件,这是可读性的底线。结果会返回 C001/A01/220 和 C004/B01/300 两条(C001 的 B01 是 500 也命中,实际是三条)。注意 WHERE 里的c.SITE = '上海'作用在连接后的结果集上,等价于先过滤 CUSTOMER 再连接,优化器一般会自动下推,写的时候不必手动折腾。

如果题目改成"即使客户没有订单也要列出来",就必须用 LEFT JOIN,并且把右表的过滤条件从 WHERE 挪到 ON 里——写在 WHERE 里会把 LEFT JOIN 退化成 INNER JOIN,这是另一个高频翻车点:

-- 想保留没有订单的客户,过滤条件必须写在 ON 上 SELECT c.CNO, c.CNAME, s.PCODE, s.OQUANTITY FROM CUSTOMER c LEFT JOIN SALE s ON s.CNO = c.CNO AND s.OQUANTITY > 200 ORDER BY c.CNO;

4.2 标量子查询的单值假设:第 12、13 题的前提条件

第 12 题要查"与美美公司在同一城市的客户公司名称及联系电话",文档写法是:

SELECT TELE, CNAME FROM CUSTOMER WHERE SITE = (SELECT SITE FROM CUSTOMER WHERE CNAME = '美美');

(原文里TELE CNAME中间漏了逗号,这是打字错误,直接抄会报 1064。)这句 SQL 能成立的前提是子查询只返回一行。而CNAME上既没有主键也没有唯一约束,只要库里有第二家名字里带"美美"的公司,比如"美美商行",子查询就返回两行,整句直接报 1242Subquery returns more than 1 row。

稳妥写法用IN,或者先对子查询去重再取一行:

-- 方案一:用 IN 容纳多行,语义最贴近题干 SELECT CNAME AS 客户名称, TELE AS 联系电话 FROM CUSTOMER WHERE SITE IN (SELECT SITE FROM CUSTOMER WHERE CNAME LIKE '%美美%') AND CNAME NOT LIKE '%美美%'; -- 排除美美自己 -- 方案二:子查询加 LIMIT 1,明确接受"随便取一个同城" SELECT CNAME, TELE FROM CUSTOMER WHERE SITE = (SELECT SITE FROM CUSTOMER WHERE CNAME = '美美' LIMIT 1);

方案一返回 C002 和 C005 两行,其中 C005 的 TELE 是 NULL,界面上会显示成空白,这不是查询写错,是数据本身缺电话。这一点在写前端的时候必须提前和产品确认:NULL 是显示成"-"还是"未登记",别等到联调才发现。

第 13 题"查询订购了单价比 A01 号产品单价高的那些产品的产品编号、客户编号和订购数量",子查询嵌在比较运算符右边:

-- 内层算出 A01 的单价,外层做比较 SELECT s.PCODE AS 产品编号, s.CNO AS 客户编号, s.OQUANTITY AS 订购数量 FROM SALE s INNER JOIN PRODUCT p ON p.PCODE = s.PCODE WHERE p.PRICE > (SELECT PRICE FROM PRODUCT WHERE PCODE = 'A01');

这里子查询是安全的,因为 PCODE 是 PRODUCT 的主键,必定返回 0 行或 1 行。判断一个标量子查询会不会炸,看它的 WHERE 条件是不是落在唯一键或主键上就行,这是能直接用的经验规则。按上面的数据,A01 单价 1500,比它高的只有 A02(2200),而 A02 有 C003 和 C004 两条订单,所以返回两行。

4.3 IN 与 EXISTS:第 14 题为什么不能用 !=

第 14 题"查询没有被订购的产品的编号和名称",文档写的是:

SELECT PRODUCT.PCODE, PNAME FROM PRODUCT WHERE PRODUCT.PCODE != (SELECT SALE.PCODE FROM SALE);

这句在数据量稍大的时候必报 1242,因为 SALE 表里有 10 行,子查询返回 10 个值,!=是标量比较,接不了多行。正确做法有三种,性能依次递增:

-- 写法一:NOT IN,最直观,但要求子查询列非 NULL SELECT PCODE, PNAME FROM PRODUCT WHERE PCODE NOT IN (SELECT PCODE FROM SALE); -- 写法二:NOT EXISTS,相关子查询,子查询列有 NULL 也不受影响 SELECT p.PCODE, p.PNAME FROM PRODUCT p WHERE NOT EXISTS ( SELECT 1 FROM SALE s WHERE s.PCODE = p.PCODE ); -- 写法三:LEFT JOIN + IS NULL,结果集语义最清楚 SELECT p.PCODE, p.PNAME FROM PRODUCT p LEFT JOIN SALE s ON s.PCODE = p.PCODE WHERE s.PCODE IS NULL;

三种都能返回 C01 微波炉。但NOT IN有一个静默陷阱:如果子查询的列里出现 NULL,x NOT IN (1, 2, NULL)的结果不是 TRUE 也不是 FALSE,而是 UNKNOWN,整句直接返回空集,不报错。本实验里 SALE.PCODE 是主键组成部分,不可能为 NULL,所以安全;换成"查没有登记电话的客户"这种题,NOT IN (SELECT TELE FROM CUSTOMER)就一定查不出东西。我一般写NOT EXISTS,一是不用管 NULL,二是 MySQL 8 会把它改写为反半连接,执行计划干净。

5. 避坑与排查:五条"看着能跑、换个环境就红"的记录

5.1 报 1055:SELECT 列表里的裸列不在 GROUP BY 里

现象:执行第 8 题原句,报Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'CPXS.SALE.OQUANTITY' which is not functionally dependent on columns in GROUP BY clause。

原因:MySQL 5.7 起默认开启ONLY_FULL_GROUP_BY。GROUP BY PCODE之后,每组里 OQUANTITY 可能有多个值,数据库不知道该返回哪一个,于是直接拒绝执行。旧版本或关闭严格模式时会随机返回组内第一行,看起来"能跑",但这种结果没有业务意义。

解决:要么把裸列塞进聚合函数(SUM(OQUANTITY)、MAX(OQUANTITY)),要么把它加进 GROUP BY。先想清楚业务要的是"该产品的订购总量"还是"该产品的某一笔订单数量",再决定改哪边,不要为了让语句通过就把列往 GROUP BY 后面一粘。

5.2 聚合结果出现多行:GROUP BY 把 COUNT 拆散了

现象:第 6 题SELECT COUNT(PCODE) FROM PRODUCT GROUP BY STOCKS HAVING STOCKS > 500偶尔返回一行 1,偶尔返回两行都是 1,取数程序一接收就报"结果不唯一"。

原因:GROUP BY 按库存量分组,COUNT 算的是每组内的产品数,不是全局的产品总数。库存值互不相同时,每组只有一条记录,结果自然是一堆 1。

解决:题目问的是"个数",就不该有 GROUP BY。改成SELECT COUNT(*) FROM PRODUCT WHERE STOCKS > 500,结果恒定一行。凡是"求……的个数/总数/平均值"且条件只作用在单行上的题,都先考虑 WHERE + 聚合,而不是 GROUP BY + HAVING。

5.3 报 1242:子查询返回了多行

现象:第 12 题报Subquery returns more than 1 row,第 14 题报同样的错。

原因:=、!=、>这些比较运算符是标量语义,右边只能接一个值。子查询的 WHERE 条件没有落在唯一键上(如CNAME = '美美'),或者根本没写条件只做整列投影(如SELECT SALE.PCODE FROM SALE),都会返回多行。

解决:先单独把子查询拎出来跑一遍,看行数。行数大于 1 就把外层运算符换成IN/NOT IN,或者改成EXISTS/NOT EXISTS的相关子查询。这个"先跑子查询"的动作我每次都做,能省掉一大半猜谜时间。

5.4 结果静默为空:NOT IN 撞上 NULL

现象:SELECT * FROM CUSTOMER WHERE TELE NOT IN (SELECT TELE FROM CUSTOMER WHERE SITE = '上海')返回 0 行,但库里明明有北京的客户。

原因:上海客户里 C005 的 TELE 是 NULL,NOT IN列表里一旦含 NULL,任何值的比较结果都是 UNKNOWN,WHERE 只保留 TRUE 的行,于是整句空转。这是 SQL 三值逻辑最经典的坑,不报错、不警告。

解决:改用NOT EXISTS,或者在子查询里加WHERE TELE IS NOT NULL。我倾向于前者,从根上绕开三值逻辑,不用每次都想"这列会不会是 NULL"。

5.5 字符集不对导致中文条件查不到

现象:WHERE SITE = '上海'在本地跑得好好的,换到另一台服务器上返回空集,也不报错。

原因:建库时没指定字符集,或者客户端连接字符集(character_set_client)和表字符集不一致,'上海' 传过去变成了乱码字节,自然匹配不上。这类问题在新装的 MySQL 上不算少见,尤其是从旧版本迁移过来的库,默认字符集还停在 latin1。

解决:建库时显式写DEFAULT CHARACTER SET utf8mb4,连接串里带上charset=utf8mb4,然后用SHOW VARIABLES LIKE 'character_set%'逐项核对。已经建好的表可以用ALTER TABLE CUSTOMER CONVERT TO CHARACTER SET utf8mb4修,但存量数据要重新校验一遍,别直接在生产库上试。

6. 用 EXPLAIN 和双写法比对,把嵌套查询钉死在结果上

写完能跑只是及格线,能不能确认"跑对了"是另一回事。我判断一条子查询该不该改写成 JOIN,主要看三步。

第一步,单独跑子查询看行数和列的唯一性。SELECT PCODE FROM SALE返回 10 行,那就绝不可能配标量运算符;SELECT PRICE FROM PRODUCT WHERE PCODE='A01'因为 PCODE 是主键,必定一行,配>没问题。这一步不需要任何工具,纯手工就能排掉大半错误。

第二步,用EXPLAIN看优化器到底怎么执行。MySQL 8 对IN子查询有半连接优化,执行计划里可能出现Using where; Start temporary/End temporary,也可能直接改写成 JOIN;而NOT IN在没有 NOT NULL 约束时通常无法做反半连接,会退化成DEPENDENT SUBQUERY,外层每扫一行就执行一次子查询。看到DEPENDENT SUBQUERY就该警惕了,尤其是外层表几万行的时候。

-- 对比两种写法的执行计划,重点看 select_type 和 rows EXPLAIN FORMAT=TRADITIONAL SELECT p.PCODE, p.PNAME FROM PRODUCT p WHERE p.PCODE NOT IN (SELECT PCODE FROM SALE); EXPLAIN FORMAT=TRADITIONAL SELECT p.PCODE, p.PNAME FROM PRODUCT p WHERE NOT EXISTS (SELECT 1 FROM SALE s WHERE s.PCODE = p.PCODE);

第三步,把两种写法都跑一遍,比对行数和排序后的结果集。这一招最土也最有效:

-- 行数比对:两个数字必须相等 SELECT (SELECT COUNT(*) FROM PRODUCT p WHERE p.PCODE NOT IN (SELECT PCODE FROM SALE)) AS not_in_cnt, (SELECT COUNT(*) FROM PRODUCT p WHERE NOT EXISTS (SELECT 1 FROM SALE s WHERE s.PCODE = p.PCODE)) AS not_exists_cnt; -- 逐行比对:EXCEPT / MINUS 在 MySQL 里没有,用 LEFT JOIN 模拟差集 SELECT a.PCODE FROM (SELECT PCODE FROM PRODUCT WHERE PCODE NOT IN (SELECT PCODE FROM SALE)) a LEFT JOIN (SELECT p.PCODE FROM PRODUCT p WHERE NOT EXISTS (SELECT 1 FROM SALE s WHERE s.PCODE = p.PCODE)) b ON a.PCODE = b.PCODE WHERE b.PCODE IS NULL; -- 返回空集说明两份结果完全一致

三个判据可以总结成一张表,选择时对着看就行:

判据选 JOIN选子查询
结果集是否需要保留主表全部行需要,用 LEFT JOIN + IS NULL不需要,用 NOT EXISTS
子查询列是否可能为 NULL不受影响NOT IN 会静默返回空集
外层表数据量大表驱动,优化器可走哈希连接小表驱动,或子查询结果可缓存

最后说个习惯问题。早些年我做课程实验,写完 SQL 看到有结果就直接截图交报告,结果有一次第 14 题用了!=,在只有两条销售记录的小数据集上碰巧跑通了,换成完整数据立刻报 1242,返工到半夜。从那以后我每次写完带子查询的语句,都会强制走一遍"子查询单独跑 → EXPLAIN 看计划 → 换一种等价写法比对行数"的流程,行数对不上就先怀疑 NULL 和重复行,而不是怀疑数据库。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 7:42:10

Product Hunt热榜解析:AI与隐私科技趋势洞察

1. 项目概述Product Hunt 是一个专注于发现和分享最新科技产品的平台&#xff0c;每天都会涌现大量创新产品。"Product Hunt 每日热榜 | 2026-03-15"这个标题指向的是2026年3月15日当天平台上最受欢迎的产品榜单。作为科技从业者和产品爱好者&#xff0c;及时跟踪这些…

作者头像 李华
网站建设 2026/10/31 7:41:27

描述性统计实战:四分位数与均值选择指南

统计学学到描述性统计这一层&#xff0c;很多人会卡在一个微妙的位置&#xff1a;均值、中位数、众数都算得出来&#xff0c;Excel 里敲几个函数也能出结果&#xff0c;但一旦有人问“这批数据到底该用哪个指标说话”&#xff0c;脑子里就开始打架。我当初写这篇学习日记&#…

作者头像 李华
网站建设 2026/10/31 7:41:14

Spark数据倾斜优化实战:原理、场景与解决方案

1. 数据倾斜的本质与典型场景数据倾斜是分布式计算中的经典难题&#xff0c;当我在2016年第一次处理TB级用户行为日志时&#xff0c;就遭遇过某个城市用户量激增导致的shuffle崩溃。这种现象的本质在于数据分布的幂律特性——少数几个key承载了绝大部分数据量。在Spark中具体表…

作者头像 李华
网站建设 2026/10/31 7:40:28

省级DNS地址别照抄:验证运营商递归解析与CDN调度

上周有人在群里甩了张截图&#xff0c;标题写着某运营商32个省的DNS地址大全区&#xff0c;底下跟着一长串以 202.96 开头的服务器IP&#xff0c;说照着填完&#xff0c;家里宽带打开网页要转三圈才出来。这种事我见过太多次了&#xff0c;问题多半不在地址本身&#xff0c;而在…

作者头像 李华
网站建设 2026/10/31 7:40:17

从混淆矩阵到阈值调参:FPR与FNR落地避坑指南

做过几年模型落地的人大概都有过这种经历&#xff1a;离线报告上准确率九十多&#xff0c;AUC 也漂亮&#xff0c;评审会上没人挑得出毛病&#xff0c;结果上线第一周业务方就找过来&#xff0c;说误报太多用户投诉&#xff0c;或者漏放太多风险事件。问题往往不在模型本身&…

作者头像 李华
网站建设 2026/10/31 7:40:15

从GitLab迁移到Gitea:轻量级代码托管的实践指南

1. 从GitLab到轻量级替代品的迁移背景在软件开发团队中&#xff0c;版本控制系统是日常工作的核心基础设施。我们团队曾经使用GitLab作为主要的代码托管平台&#xff0c;但随着项目规模扩大和团队分散化&#xff0c;GitLab的某些特性开始显现出不足。最显著的问题是资源占用——…

作者头像 李华