📖 本篇简介:聚合与分组是 SQL 最基础也最容易踩坑的部分,看似简单,实则在面试中能衍生出 GROUP BY 严格模式、WHERE 与 HAVING 区别、COUNT(*) 与 COUNT(列) 差异、NULL 在聚合中的行为、ROLLUP/CUBE、GROUPING SETS、并行聚合等大量考点。本篇基于 MySQL 8.0,系统梳理常用聚合函数、分组规则、ONLY_FULL_GROUP_BY 模式、WITH ROLLUP、GROUP_CONCAT、BIT 聚合等高频知识点,并通过真实案例讲清楚"为什么我 GROUP BY 后查的列报错"。学完后你能避开绝大多数聚合类面试坑。
目录
- 题目 1:WHERE 与 HAVING 的区别,能否互换
- 题目 2:COUNT(*)、COUNT(1)、COUNT(列) 三者区别与性能
- 题目 3:ONLY_FULL_GROUP_BY 是什么,为什么 5.7 默认开启
- 题目 4:GROUP BY 后 SELECT 非聚合列为什么报错
- 题目 5:SUM、AVG、MAX、MIN 对 NULL 的处理
- 题目 6:COUNT(*) 遇到 NULL 行会怎么算
- 题目 7:GROUP BY 能否使用索引,什么情况下会 filesort
- 题目 8:WITH ROLLUP 的作用,MySQL 8.0 有什么增强
- 题目 9:GROUP_CONCAT 的作用、长度限制与坑
- 题目 10:DISTINCT 与 GROUP BY 去重性能谁好
- 题目 11:聚合函数能否嵌套,比如 SUM(AVG(x))
- 题目 12:GROUP BY 多个列的语义是什么
- 题目 13:HAVING 中能否使用 SELECT 别名
- 题目 14:BIT_AND、BIT_OR、BIT_XOR 聚合函数的用途
- 题目 15:如何在分组后筛选"组内满足条件的行数"
- 题目 16:分组查询慢,有哪些优化手段
- 题目 17:MySQL 8.0 支持 GROUPING SETS 吗,怎么替代
- 题目 18:STDDEV、VARIANCE 等统计聚合函数什么时候用
- 题目 19:聚合查询返回空结果时 COUNT 的行为
- 题目 20:常见坑与注意事项
- 速查表:聚合函数速记
- 复习清单
- 练习题
题目 1:WHERE 与 HAVING 的区别,能否互换
考点
- 执行时机不同:WHERE 在分组前,HAVING 在分组后
- WHERE 不能用聚合函数,HAVING 可以
- 性能差异:提前过滤减少分组数据量
参考答案
1. WHERE:在分组/聚合之前过滤行,不能使用聚合函数,作用于原始行。
2. HAVING:在 GROUP BY 分组之后过滤组,可以使用聚合函数,作用于分组结果。
3. 执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。
4. 性能原则:能用 WHERE 过滤的就不要放到 HAVING,提前过滤能减少分组的数据量。
-- 正确:WHERE 先过滤 2026 年的数据,再按部门分组
SELECT dept, COUNT(*) FROM emp
WHERE hire_date >= '2026-01-01'
GROUP BY dept
HAVING COUNT(*) > 5; -- HAVING 过滤人数大于 5 的部门
追问
- 追问:HAVING 后不写 GROUP BY 行不行?应对:可以,整个结果集当成一个大组,但很少这么用。
- 追问:WHERE 里能用聚合函数吗?应对:不能,因为执行顺序在分组之前。
题目 2:COUNT(*)、COUNT(1)、COUNT(列) 三者区别与性能
考点
- COUNT(*) 统计所有行含 NULL
- COUNT(列) 排除该列为 NULL 的行
- InnoDB 下三者性能几乎无差别
参考答案
1. COUNT(*):统计结果集的所有行,不忽略 NULL,是 SQL 标准统计行数的写法。
2. COUNT(1):与 COUNT(*) 等价,InnoDB 优化器会把它当成 COUNT(*) 处理,结果完全一样。
3. COUNT(列名):统计该列不为 NULL 的行数;列中有 NULL 时结果小于 COUNT(*)。
4. 性能:InnoDB 下 COUNT(*)、COUNT(1)、COUNT(主键) 性能几乎无差别;MyISAM 无 WHERE 时 COUNT(*) 是 O(1),因为有元数据缓存。
-- 假设 emp 有 1000 行,其中 manager 列有 50 行 NULL
SELECT COUNT(*), -- 1000
COUNT(1), -- 1000
COUNT(manager) -- 950
FROM emp;
追问
- 追问:COUNT(*) 在 InnoDB 下为什么慢?应对:InnoDB 要遍历索引或数据页数行,没有元数据缓存;大表要做近似计数用 show table status 或外部缓存。
- 追问:COUNT(主键) 比 COUNT(*) 快吗?应对:在 InnoDB 下优化器选最小的索引扫描,差异极小,别纠结。
题目 3:ONLY_FULL_GROUP_BY 是什么,为什么 5.7 默认开启
考点
- SQL 标准要求 SELECT 列要么在 GROUP BY 中,要么被聚合
- 5.7 前默认不开启,导致结果不确定
- 8.0 延续默认开启
参考答案
1. ONLY_FULL_GROUP_BY:MySQL 的 SQL 模式之一,开启后要求 SELECT 列表中的非聚合列必须全部出现在 GROUP BY 子句中,否则报错。
2. 为什么开启:在未开启时,SELECT dept, name, COUNT(*) FROM emp GROUP BY dept 这种语句能执行,但 name 取哪个部门的"任意一行"是不确定的,结果随数据顺序变化。
3. MySQL 5.7 起默认开启该模式,8.0 延续;这是对 SQL 标准的对齐。
4. 临时关闭(不推荐):SET SESSION sql_mode = (REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));。
追问
- 追问:业务代码在 5.6 升到 8.0 后报 GROUP BY 错误怎么办?应对:检查 SQL,把非聚合列加进 GROUP BY,或用 ANY_VALUE() 包起来(明确告诉 MySQL 取任意值)。
- 追问:ANY_VALUE() 是什么?应对:8.0 提供的函数,包住某列表示"我知道取值不确定,取一行就行",用于绕过 ONLY_FULL_GROUP_BY。
题目 4:GROUP BY 后 SELECT 非聚合列为什么报错
考点
- ONLY_FULL_GROUP_BY 规则
- 函数依赖(Functional Dependency)例外
- 正确写法
参考答案
-- 错误:name 既不在 GROUP BY 中,也没有聚合
SELECT dept, name, COUNT(*) FROM emp GROUP BY dept;
-- ERROR 1055: Expression #2 of SELECT list is not in GROUP BY clause...
-- 正确 1:把 name 加进 GROUP BY
SELECT dept, name, COUNT(*) FROM emp GROUP BY dept, name;
-- 正确 2:用聚合函数包起来
SELECT dept, MAX(name), COUNT(*) FROM emp GROUP BY dept;
-- 正确 3:用 ANY_VALUE 明确接受任意值
SELECT dept, ANY_VALUE(name), COUNT(*) FROM emp GROUP BY dept;
1. 报错是因为 MySQL 不知道在该部门的多行 name 中要返回哪一个。
2. 函数依赖例外:如果 GROUP BY 的是主键(如 GROUP BY emp_id),那么 SELECT 其他列(name、dept)是允许的,因为主键唯一确定一行。
3. 业务上真的要"每个部门随便挑一个员工名",用 ANY_VALUE。
追问
- 追问:GROUP BY 主键后 SELECT 其他列为啥不报错?应对:主键函数依赖,MySQL 能证明其他列唯一确定。
- 追问:GROUP BY 唯一索引列也一样吗?应对:是的,只要列上有 NOT NULL 的唯一约束即可。
题目 5:SUM、AVG、MAX、MIN 对 NULL 的处理
考点
- 聚合函数自动忽略 NULL
- AVG 是 SUM/非NULL行数,不是除以总行数
- 全 NULL 时返回 NULL
参考答案
1. SUM/AVG/MAX/MIN:自动忽略 NULL 行,只对非 NULL 值计算。
2. AVG 的坑:AVG(col) = SUM(col) / COUNT(col),分母是"非 NULL 行数",不是总行数。
3. 全 NULL 输入:SUM/AVG 返回 NULL(不是 0),MAX/MIN 也返回 NULL。
4. COUNT(*) 例外:不忽略任何行。
-- 假设 score 列值为 (10, 20, NULL)
SELECT SUM(score), -- 30
AVG(score), -- 15 (不是 (10+20+0)/3=10)
COUNT(score), -- 2
COUNT(*) -- 3
FROM t;
追问
- 追问:AVG 想把 NULL 当 0 算怎么办?应对:
AVG(IFNULL(score,0))。 - 追问:SUM 全 NULL 返回什么?应对:NULL,不是 0;要 0 用
COALESCE(SUM(x),0)。
题目 6:COUNT(*) 遇到 NULL 行会怎么算
考点
- COUNT(*) 不忽略 NULL
- COUNT(列) 忽略该列 NULL
- 业务中常用来统计"有多少订单"
参考答案
1. COUNT(*):统计所有行,不论列是否为 NULL,行数就是行数。
2. COUNT(列名):只统计该列 IS NOT NULL 的行。
3. COUNT(DISTINCT 列):统计该列去重后的非 NULL 值个数。
-- order 表 5 行,其中 pay_time 列 2 行是 NULL
SELECT COUNT(*), -- 5
COUNT(pay_time), -- 3
COUNT(DISTINCT user_id) -- 用户去重数
FROM orders;
追问
- 追问:COUNT(1) 和 COUNT(*) 有区别吗?应对:InnoDB 下没有,等价。
- 追问:要统计"有支付的订单数"用哪个?应对:COUNT(pay_time) 或 COUNT(*) WHERE pay_time IS NOT NULL,后者可读性更好。
题目 7:GROUP BY 能否使用索引,什么情况下会 filesort
考点
- 最左前缀原则
- 索引顺序避免临时表与排序
- EXPLAIN 的 Extra 字段
参考答案
1. 能用到索引:GROUP BY 的列顺序与联合索引最左前缀一致,且索引有序,MySQL 可直接利用索引顺序分组,避免临时表和 filesort。
2. filesort 出现:GROUP BY 列与索引顺序不匹配,或出现 ORDER BY 不同方向,MySQL 建临时表做分组。
3. EXPLAIN 看 Extra:
Using index:覆盖索引,最佳。Using temporary; Using filesort:分组产生临时表+排序,需优化。
-- 联合索引 idx_dept_hire(dept, hire_date)
-- 下面这条 GROUP BY 能利用索引顺序
SELECT dept, YEAR(hire_date), COUNT(*)
FROM emp GROUP BY dept, YEAR(hire_date); -- YEAR() 函数让索引失效,会 filesort
-- 想利用索引,GROUP BY 列要与索引列完全一致
SELECT dept, hire_date, COUNT(*) FROM emp GROUP BY dept, hire_date;
追问
- 追问:GROUP BY 默认会排序吗?应对:8.0 之前 GROUP BY 隐式排序,8.0 取消了隐式排序,要排序必须显式 ORDER BY。
- 追问:怎样去掉 GROUP BY 的排序开销?应对:8.0 默认不排序;旧版本加
ORDER BY NULL。
题目 8:WITH ROLLUP 的作用,MySQL 8.0 有什么增强
考点
- 生成多级小计与合计行
- GROUPING() 函数区分超级聚合行
- 8.0 支持 ORDER BY 与 ROLLUP 共存
参考答案
-- 按部门 + 年份分组,并自动生成小计与总计
SELECT dept, YEAR(hire_date) AS yr, COUNT(*)
FROM emp
GROUP BY dept, YEAR(hire_date) WITH ROLLUP;
-- 结果会多出:每个部门一行 dept=部门, yr=NULL 的小计;最后一行 dept=NULL,yr=NULL 的总计
1. 作用:在 GROUP BY 结果基础上,自动生成各层级的小计与最终合计行,无需 UNION ALL。
2. NULL 表示超级聚合行:ROLLUP 产生的小计行中,被"汇总掉"的列会显示 NULL,容易与业务数据本身的 NULL 混淆。
3. GROUPING(列):8.0 提供,返回 1 表示该行是 ROLLUP 产生的超级聚合行,0 表示普通分组行。
4. 8.0 增强:支持 ROLLUP 与 ORDER BY 共存(之前版本互斥),并支持 GROUP BY ... WITH ROLLUP 之外的显式 ROLLUP() 语法。
追问
- 追问:ROLLUP 能按任意层级聚合吗?应对:MySQL 只支持从右到左逐级回退,不支持 GROUPING SETS 那种任意组合。
- 追问:要多级小计但不想从右到左怎么办?应对:用 UNION ALL 拼接多个 GROUP BY,或迁到支持 GROUPING SETS 的引擎。
题目 9:GROUP_CONCAT 的作用、长度限制与坑
考点
- 把分组多行拼成一个字符串
- group_concat_max_len 限制
- 默认分隔符与排序
参考答案
-- 每个部门的员工名字拼成逗号分隔字符串
SELECT dept, GROUP_CONCAT(name ORDER BY salary DESC SEPARATOR '、') AS members
FROM emp
GROUP BY dept;
1. 作用:把分组内多行的某列拼成一个字符串,常用于"一个部门有哪些人"。
2. 长度限制:受 group_concat_max_len 系统变量控制,默认 1024 字节,超长会被静默截断!线上要注意。
3. 默认分隔符:逗号 ,;可通过 SEPARATOR 修改。
4. 支持 ORDER BY:在聚合内排序拼接。
-- 查看/调整长度限制
SHOW VARIABLES LIKE 'group_concat_max_len';
SET SESSION group_concat_max_len = 102400; -- 会话级调大
追问
- 追问:拼出来有 NULL 怎么办?应对:GROUP_CONCAT 自动忽略 NULL,与 SUM 一致。
- 追问:线上拼出来字符串被截断查不到原因?应对:先查 group_concat_max_len,再看结果长度是否超限。
题目 10:DISTINCT 与 GROUP BY 去重性能谁好
考点
- 去重本质都是排序/临时表
- 有索引时两者差不多
- GROUP BY 能顺带聚合更灵活
参考答案
1. DISTINCT col 与 GROUP BY col 去重语义等价:都是把重复行折叠。
2. 性能:在有合适索引时,两者优化器生成的执行计划几乎一样,都走索引扫描或临时表。
3. 选择建议:
- 只去重不聚合:用 DISTINCT,语义清晰。
- 要顺便做 COUNT/SUM 等聚合:用 GROUP BY。
4. DISTINCT 多列:SELECT DISTINCT a,b FROM t 是按 (a,b) 组合去重。
-- 两种写法等价
SELECT DISTINCT dept FROM emp;
SELECT dept FROM emp GROUP BY dept;
追问
- 追问:DISTINCT 能不能作用在部分列?应对:不能,DISTINCT 作用于整行 SELECT 列表;要对某列去重用 GROUP BY。
- 追问:COUNT(DISTINCT col) 性能?应对:需要对该列去重计数,数据量大时会产生临时表,比 COUNT(*) 慢。
题目 11:聚合函数能否嵌套,比如 SUM(AVG(x))
考点
- SQL 标准不允许聚合嵌套
- 解决:包子查询/CTE
- 窗口函数也不能嵌套
参考答案
1. 不能直接嵌套:SELECT SUM(AVG(salary)) FROM emp GROUP BY dept 报语法错误。
2. 原因:聚合函数是对行集计算,SQL 标准不允许一层 SELECT 里同时嵌套两层聚合。
3. 正确做法:先内层 GROUP BY 算出每个 dept 的 AVG,外层再 SUM:
-- 错误:SELECT SUM(AVG(salary)) FROM emp GROUP BY dept;
-- 正确:
SELECT AVG(dept_avg) AS avg_of_dept_avg
FROM (
SELECT dept, AVG(salary) AS dept_avg FROM emp GROUP BY dept
) t;
追问
- 追问:那 SUM(MAX(x)) 呢?应对:同样不行,必须包两层。
- 追问:HAVING 里能写 COUNT(*) > COUNT(DISTINCT user_id) 吗?应对:这种同层两个聚合是允许的,因为它们并列而不是嵌套。
题目 12:GROUP BY 多个列的语义是什么
考点
- 按列组合分组
- 等价于 (col1,col2) 相同的行归为一组
- 顺序影响可读性不影响结果
参考答案
1. 语义:GROUP BY a, b 把 (a,b) 值相同的行分到同一组;NULL 与 NULL 在分组中视为相等。
2. 行数:结果行数 = SELECT COUNT(DISTINCT a, b) FROM t。
3. 顺序:GROUP BY a,b 与 GROUP BY b,a 结果集相同(不考虑排序),但索引利用可能不同。
4. 搭配 ROLLUP:GROUP BY a,b WITH ROLLUP 会先按 a,b 分组,再生成 a 的小计,最后总计。
-- 按部门 + 岗位分组统计人数
SELECT dept, job, COUNT(*) FROM emp GROUP BY dept, job;
追问
- 追问:GROUP BY a,b 时 a 相同但 b 不同会分一组吗?应对:不会,必须 (a,b) 都相同才一组。
- 追问:多列 GROUP BY 怎么建索引?应对:联合索引列顺序与 GROUP BY 一致,最左前缀。
题目 13:HAVING 中能否使用 SELECT 别名
考点
- MySQL 扩展支持 HAVING 引用别名
- WHERE 不支持
- ORDER BY 也支持
参考答案
1. MySQL 特有扩展:MySQL 允许 HAVING、ORDER BY 引用 SELECT 列表中定义的别名,这在 Oracle/PostgreSQL 中不允许。
2. WHERE 不允许:因为 WHERE 执行早于 SELECT。
3. 示例:
SELECT dept, AVG(salary) AS avg_sal
FROM emp
GROUP BY dept
HAVING avg_sal > 10000; -- MySQL 允许引用别名
4. 可移植性建议:写 HAVING AVG(salary) > 10000 更标准,跨数据库兼容。
追问
- 追问:WHERE 中能用别名吗?应对:不能,MySQL 也不支持。
- 追问:ORDER BY 能用别名吗?应对:可以,MySQL 与 SQL 标准都允许。
题目 14:BIT_AND、BIT_OR、BIT_XOR 聚合函数的用途
考点
- 位运算聚合
- 权限位、开关位合并
- 与常规聚合区别
参考答案
1. BIT_AND / BIT_OR / BIT_XOR:对分组内每行的某列做按位与/或/异或聚合。
2. 典型场景:用户权限位合并——每个用户在某个资源上有一组权限位,要算整个部门的权限并集,用 BIT_OR。
3. 示例:
-- 每个部门的权限位并集(位或)
SELECT dept, BIT_OR(permission) AS dept_perm
FROM user_role
GROUP BY dept;
-- 判断部门是否有人同时具备 bit0 和 bit1:BIT_AND(permission) & 3 = 3
SELECT dept, BIT_AND(permission) AS all_have
FROM user_role GROUP BY dept;
追问
- 追问:BIT_XOR 有什么用?应对:找"出现奇数次的位",类似校验和。
- 追问:空组返回什么?应对:BIT_AND 全 1、BIT_OR 全 0、BIT_XOR 0。
题目 15:如何在分组后筛选"组内满足条件的行数"
考点
- HAVING COUNT(CASE WHEN ...)
- 条件聚合
- 行转列
参考答案
-- 求每个部门中工资 > 10000 的员工人数,并筛出至少 3 人的部门
SELECT dept,
COUNT(*) AS total,
SUM(CASE WHEN salary > 10000 THEN 1 ELSE 0 END) AS high_cnt
FROM emp
GROUP BY dept
HAVING high_cnt >= 3;
1. 思路:用 SUM(CASE WHEN 条件 THEN 1 ELSE 0 END) 做条件计数。
2. 这就是经典的"行转列":把每个分组内满足不同条件的行数作为不同列。
3. 比 COUNT(*) WHERE salary>10000 GROUP BY dept 更灵活,后者只能算一个指标。
追问
- 追问:SUM(CASE...) 和 COUNT(IF...) 哪个好?应对:等价,COUNT(IF(cond,1,NULL)) 也行,前者更标准。
- 追问:要同时统计多个条件怎么办?应对:写多个 SUM(CASE WHEN...) 列即可。
题目 16:分组查询慢,有哪些优化手段
考点
- 索引覆盖分组列
- 提前 WHERE 过滤
- 减少返回行数
- 临时表调优
参考答案
1. 建合适索引:(GROUP BY 列, ...) 联合索引,让分组利用索引顺序,避免 filesort。
2. WHERE 提前过滤:把能在分组前过滤的条件都放到 WHERE,减少分组数据量。
3. 只 SELECT 需要的列:避免 SELECT *,减少临时表内存占用。
4. 调大临时表参数:tmp_table_size、max_heap_table_size 适当调大,避免落盘。
5. 预聚合:把高频分组结果物化成汇总表/统计表,用空间换时间。
6. EXPLAIN 验证:看 Extra 是否还有 Using temporary; Using filesort。
追问
- 追问:临时表落盘怎么发现?应对:看
Created_tmp_disk_tables状态变量与Created_tmp_tables比值,大就说明落盘多。 - 追问:大表分组聚合怎么扛?应对:汇总表 + 定时任务刷新,或者引入 OLAP 引擎(ClickHouse)。
题目 17:MySQL 8.0 支持 GROUPING SETS 吗,怎么替代
考点
- GROUPING SETS 是 OLAP 标准
- MySQL 不支持
- 用 UNION ALL 替代
参考答案
1. MySQL 8.0 不支持 GROUPING SETS / CUBE,只支持 WITH ROLLUP。
2. GROUPING SETS:可以一次查询同时返回按 (a)、按 (b)、按 (a,b)、总计 多种粒度的聚合。
3. 替代写法:用 UNION ALL 拼多个 GROUP BY:
-- 等价于 GROUPING SETS ((dept, yr), (dept), (yr), ())
SELECT dept, YEAR(hire_date) AS yr, COUNT(*) FROM emp GROUP BY dept, YEAR(hire_date)
UNION ALL
SELECT dept, NULL, COUNT(*) FROM emp GROUP BY dept
UNION ALL
SELECT NULL, YEAR(hire_date), COUNT(*) FROM emp GROUP BY YEAR(hire_date)
UNION ALL
SELECT NULL, NULL, COUNT(*) FROM emp;
4. 这种写法冗长但能跑;更复杂的多维分析建议迁到 ClickHouse/Doris。
追问
- 追问:WITH ROLLUP 与 GROUPING SETS 区别?应对:ROLLUP 是固定从右到左层级回退,GROUPING SETS 可任意组合维度。
- 追问:什么时候该考虑 OLAP 库?应对:报表/BI 查询多维度聚合、单表千万级以上、MySQL 分组扛不住时。
题目 18:STDDEV、VARIANCE 等统计聚合函数什么时候用
考点
- 标准差、方差
- 变异系数
- 业务场景
参考答案
1. STDDEV_POP / STDDEV_SAMP:总体/样本标准差。
2. VAR_POP / VAR_SAMP:总体/样本方差。
3. 业务场景:
- 监控接口响应时间的波动:STDDEV 大说明不稳定。
- 评估薪资离散程度:变异系数 = STDDEV/AVG。
4. 这些函数日常业务用得少,但面试中提到"统计聚合"会加分。
-- 各部门工资的均值与标准差
SELECT dept,
AVG(salary) AS avg_sal,
STDDEV_POP(salary) AS std_sal
FROM emp GROUP BY dept;
追问
- 追问:STDDEV 与 STDDEV_POP 区别?应对:STDDEV 是 STDDEV_POP 的别名,8.0 推荐显式写 _POP/_SAMP。
- 追问:空结果返回什么?应对:NULL。
题目 19:聚合查询返回空结果时 COUNT 的行为
考点
- 无 GROUP BY 的聚合总是返回一行
- 有 GROUP BY 时可能返回 0 行
- COUNT 空表返回 0,SUM 返回 NULL
参考答案
-- 无 GROUP BY:即使表空,也返回一行
SELECT COUNT(*), SUM(salary) FROM emp WHERE 1=2;
-- 结果:COUNT(*)=0, SUM(salary)=NULL
-- 有 GROUP BY:没有分组匹配时返回 0 行
SELECT dept, COUNT(*) FROM emp WHERE 1=2 GROUP BY dept;
-- 结果:空结果集(0 行)
1. 无 GROUP BY 的聚合:永远返回一行,COUNT 是 0,SUM/AVG 是 NULL。
2. 有 GROUP BY 的聚合:没有行匹配 WHERE 时返回空结果集(0 行),应用层要注意判空。
3. 业务坑:代码里 rs.next() 后直接取第一行,有 GROUP BY 但无分组数据时会抛 NoSuchElementException。
追问
- 追问:SUM 空表返回 NULL 还是 0?应对:NULL;要 0 用 COALESCE。
- 追问:怎么让分组查询无数据时也返回一行?应对:外层包
SELECT ... FROM (SELECT 1) dummy LEFT JOIN (...) t。
题目 20:常见坑与注意事项
考点
- 聚合高频坑汇总
- NULL 与分组
- 隐式排序变化
参考答案
1. 坑 1:AVG 自动忽略 NULL,分母不是总行数。
2. 坑 2:GROUP_CONCAT 被 group_concat_max_len 静默截断,线上拼字符串超长要先查参数。
3. 坑 3:5.7/8.0 默认 ONLY_FULL_GROUP_BY,老 SQL 升级后报错,要改 SQL 或用 ANY_VALUE。
4. 坑 4:GROUP BY 在 8.0 不再隐式排序,旧代码依赖隐式排序会出问题。
5. 坑 5:COUNT(列) 与 COUNT(*) 不等价,列有 NULL 时结果不同。
6. 坑 6:聚合函数不能嵌套,要多层聚合必须包子查询。
7. 坑 7:HAVING 引用别名是 MySQL 扩展,跨库移植时要改回完整聚合表达式。
8. 坑 8:WITH ROLLUP 产生的 NULL 与业务 NULL 混淆,用 GROUPING() 区分。
追问
- 追问:升级到 8.0 后 GROUP BY 结果顺序变了?应对:8.0 去掉了隐式排序,业务必须显式 ORDER BY。
- 追问:HAVING 里写 WHERE 条件性能差?应对:能用 WHERE 就放 WHERE,提前过滤减少分组开销。
速查表:聚合函数速记
| 函数 | 作用 | 忽略 NULL | 空结果返回 |
|------|------|-----------|-----------|
| COUNT(*) | 统计行数 | 否 | 0 |
| COUNT(列) | 统计非 NULL 行数 | 是 | 0 |
| SUM(列) | 求和 | 是 | NULL |
| AVG(列) | 平均(SUM/非NULL数) | 是 | NULL |
| MAX/MIN | 最大最小 | 是 | NULL |
| GROUP_CONCAT | 拼接字符串 | 是 | NULL |
| BIT_AND/OR/XOR | 位聚合 | 是 | 全1/全0/0 |
| STDDEV_POP | 总体标准差 | 是 | NULL |
| 写法 | 语义 |
|------|------|
| WHERE | 分组前过滤,不能用聚合 |
| HAVING | 分组后过滤,能用聚合 |
| GROUP BY a,b | 按 (a,b) 组合分组 |
| WITH ROLLUP | 自动生成小计/总计 |
| DISTINCT | 对整行 SELECT 列表去重 |
复习清单
| 题目 | 核心结论 | 易错点 |
|------|----------|--------|
| 1 | WHERE 在分组前,HAVING 在分组后 | 把过滤条件都堆到 HAVING |
| 2 | COUNT(列) 忽略 NULL,COUNT(*) 不忽略 | 误以为 COUNT(1) 更快 |
| 3 | 5.7+ 默认 ONLY_FULL_GROUP_BY | 升级后 SQL 报错不修 |
| 4 | 非聚合列必须在 GROUP BY 中 | 主键函数依赖例外不知道 |
| 5 | AVG 分母是非 NULL 行数 | 想把 NULL 当 0 忘写 IFNULL |
| 6 | COUNT(*) 不忽略 NULL | COUNT(列) 统计有值行数 |
| 7 | 联合索引可避免 filesort | GROUP BY 列加函数导致索引失效 |
| 8 | ROLLUP 自动小计,NULL 是聚合行 | 业务 NULL 与聚合 NULL 混淆 |
| 9 | GROUP_CONCAT 默认 1024 字节截断 | 线上拼字符串被截断不排查 |
| 10 | DISTINCT 与 GROUP BY 去重等价 | DISTINCT 想作用部分列 |
| 11 | 聚合函数不能嵌套 | SUM(AVG(x)) 直接写报错 |
| 12 | 多列 GROUP BY 按组合分组 | 顺序影响索引利用 |
| 13 | MySQL 允许 HAVING 用别名 | 跨库移植不兼容 |
| 14 | BIT_OR 合并权限位 | 空组返回值记错 |
| 15 | SUM(CASE WHEN) 条件聚合 | 行转列写不对 |
| 16 | 提前 WHERE + 联合索引优化分组 | 临时表落盘不监控 |
| 17 | MySQL 不支持 GROUPING SETS | 多维分析硬写 UNION ALL |
| 18 | STDDEV/VAR 统计波动 | 业务误用总体/样本口径 |
| 19 | 无 GROUP BY 聚合总返回一行 | 有 GROUP BY 无数据返回 0 行 |
| 20 | NULL、ROLLUP、ONLY_FULL_GROUP_BY 三大坑 | 8.0 隐式排序变化 |
练习题
1. 题目:orders(order_id, user_id, amount, status) 中统计每个用户的订单数和已支付订单数。
- 参考答案:
SELECT user_id,
COUNT(*) AS total_cnt,
SUM(CASE WHEN status='PAID' THEN 1 ELSE 0 END) AS paid_cnt
FROM orders
GROUP BY user_id;
2. 题目:为什么 SELECT dept, name, COUNT(*) FROM emp GROUP BY dept 在 MySQL 8.0 报错?怎么改?
- 参考答案:因为 ONLY_FULL_GROUP_BY 要求非聚合列 name 必须在 GROUP BY 中;可改为
GROUP BY dept, name或ANY_VALUE(name)。
3. 题目:score 列值为 (10, 20, NULL),AVG(score) 结果是多少?为什么?
- 参考答案:15,因为 AVG 忽略 NULL,分母是 2 不是 3。
4. 题目:GROUP_CONCAT 拼出来的字符串比预期短,可能原因?
- 参考答案:group_concat_max_len 默认 1024 字节,超长静默截断;
SET SESSION group_concat_max_len=102400调大。
5. 题目:要在一次查询中同时返回按 dept 分组和按 dept+job 分组的人数,MySQL 怎么写?
- 参考答案:用 UNION ALL 拼两个 GROUP BY,或用 WITH ROLLUP(若层级固定)。
下一篇预告:第 13 章 EXPLAIN执行计划面试精讲
文章回复
0 条公开回复