GROUP BY的3个隐藏陷阱,你踩过几个?
本文目录
总结
| 陷阱 | 正确做法 |
|---|---|
| ------ | ---------- |
| SELECT出现非聚合、非GROUP BY列 | 要么加到GROUP BY里,要么用聚合函数包裹 |
| WHERE里用聚合函数 | 用HAVING代替WHERE |
| COUNT(column)漏掉NULL | 统计行数用COUNT(*),统计非空值用COUNT(column) |
| HAVING用法 | 场景 |
|---|---|
| ----------- | ------ |
| HAVING COUNT > 1 | 找重复数据 |
| HAVING COUNT = 1 | 找唯一记录 |
| HAVING组合条件 | 多维度过滤异常数据 |
GROUP BY看起来简单,但面试官从这个点出发,可以考SQL执行顺序、NULL处理、性能优化、数据倾斜——一个知识点串起一条线。
明天 DAY 7 深入聊同比环比的各种算法,不只是LAG/LEAD,还有跨年同比、累计同比这些复杂场景。
你在写GROUP BY的时候踩过什么坑?评论区交流~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
GROUP BY是SQL里最基础的操作,但越基础的东西越容易出问题。面试中经常有人在GROUP BY上翻车,不是不会写,而是对它的执行逻辑理解不够深。今天聊三个GROUP BY的高频陷阱,再讲讲HAVING的实战用法。
陷阱一:SELECT里出现了没有GROUP BY的非聚合列
先看一个经典错误:
-- ❌ 这条SQL在严格模式下会报错
SELECT
channel,
creative_id, -- 既没在GROUP BY里,也没被聚合
SUM(cost) AS total_cost
FROM ad_daily_cost
GROUP BY channel
creative_id既没有在GROUP BY里,也没有被聚合函数包裹,那每个channel分组里有多个creative_id,数据库该返回哪一个?
MySQL的坑: MySQL在sql_mode没开ONLY_FULL_GROUP_BY的情况下,这条SQL居然能跑通,它会随机返回一个creative_id。很多人在MySQL上养成了坏习惯,到了Hive、Spark、PostgreSQL上就报错了。
面试正确回答: SELECT中的每一列,要么出现在GROUP BY里,要么被聚合函数包裹。这是SQL标准。
陷阱二:GROUP BY之后想过滤,WHERE还是HAVING?
这个问题的本质是SQL的执行顺序。面试经常直接问"说一下SQL的执行顺序",其实考的就是这个。
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
翻译成大白话:
1. FROM: 先确定从哪张表取数据
2. WHERE: 在分组前过滤行(此时还没有聚合值)
3. GROUP BY: 分组
4. HAVING: 在分组后过滤组(此时可以用聚合值)
5. SELECT: 计算要返回的列
6. ORDER BY: 排序
7. LIMIT: 截取
所以:
-- ❌ 错误:WHERE里不能用聚合函数
SELECT channel, SUM(cost) AS total_cost
FROM ad_daily_cost
GROUP BY channel
WHERE SUM(cost) > 10000 -- 报错!
-- ✅ 正确:用HAVING过滤聚合后的结果
SELECT channel, SUM(cost) AS total_cost
FROM ad_daily_cost
GROUP BY channel
HAVING SUM(cost) > 10000
一句话记忆:WHERE过滤行,HAVING过滤组。 WHERE在GROUP BY之前执行,HAVING在GROUP BY之后执行。
陷阱三:COUNT的三种写法结果不同
SELECT
channel,
COUNT(*) AS cnt_star,
COUNT(1) AS cnt_one,
COUNT(creative_id) AS cnt_col
FROM ad_daily_cost
GROUP BY channel
COUNT(*)和COUNT(1)的结果完全一样,都是统计行数,包含NULL行。但COUNT(creative_id)不一样——它只统计creative_id不为NULL的行数。
这个区别在面试中经常被问到。我举个实际例子:
-- 广告归因表,部分数据没有归因到具体渠道
| user_id | channel |
|---------|----------|
| u001 | Facebook |
| u002 | NULL | -- 自然流量,没归因
| u003 | TikTok |
| u004 | NULL |
SELECT
COUNT(*) AS total_users, -- 4
COUNT(channel) AS attributed_users -- 2
FROM attribution_log
我在做广告归因分析的时候,一开始用COUNT(channel)统计总转化数,结果漏掉了所有自然流量用户,数据和投放团队对不上。后来改成COUNT(*)才对。这种细微差别在数据准确性上影响很大。
HAVING的实战用法
HAVING不只是"过滤聚合值"这么简单,实际工作中有很多巧妙的用法。
用法1:找重复数据
数据质量排查最常用的SQL之一:
-- 找出同一天有重复记录的用户
SELECT user_id, login_date, COUNT(1) AS cnt
FROM user_login
GROUP BY user_id, login_date
HAVING COUNT(1) > 1
我做埋点治理的时候,这条SQL几乎每天都在跑。1000多个埋点事件,总有几个存在重复上报的问题,用这条SQL一查一个准。
用法2:找只出现一次的数据
-- 找出只在一个渠道投放过的素材(独占素材)
SELECT creative_id, COUNT(DISTINCT channel) AS channel_cnt
FROM ad_daily_cost
GROUP BY creative_id
HAVING COUNT(DISTINCT channel) = 1
用法3:组合条件过滤
-- 找出月消耗超过10万,但平均日消耗低于5000的渠道(可能存在集中投放问题)
SELECT
channel,
SUM(cost) AS monthly_cost,
AVG(cost) AS avg_daily_cost,
MAX(cost) AS max_daily_cost
FROM ad_daily_cost
WHERE dt BETWEEN '2025-01-01' AND '2025-01-31'
GROUP BY channel
HAVING SUM(cost) > 100000
AND AVG(cost) < 5000
这种查询在实际业务中很有价值——月总量大但日均低,说明某几天有集中爆量,可能是预算没控好或者投放策略有问题。
面试加分:GROUP BY的性能优化
面试官可能追问GROUP BY在大数据场景下的性能问题:
问题1:GROUP BY导致数据倾斜怎么办?
如果某个分组的数据量远大于其他分组(比如channel='Facebook'占了80%的数据),Shuffle之后某个Reducer会特别慢。
解法:两阶段聚合。
-- 第一阶段:加随机后缀打散
SELECT
CONCAT(channel, '_', CAST(FLOOR(RAND() * 10) AS STRING)) AS channel_salt,
SUM(cost) AS partial_cost
FROM ad_daily_cost
GROUP BY CONCAT(channel, '_', CAST(FLOOR(RAND() * 10) AS STRING))
-- 第二阶段:去掉后缀,合并
SELECT
SPLIT(channel_salt, '_')[0] AS channel,
SUM(partial_cost) AS total_cost
FROM stage1
GROUP BY SPLIT(channel_salt, '_')[0]
先在channel后面加一个随机数,把一个大分组打散成10个小分组,分别聚合后再合并。这个思路和MapReduce中的Combiner异曲同工。
问题2:GROUP BY和DISTINCT哪个快?
-- 这两个在大多数场景下性能一样
SELECT DISTINCT channel FROM ad_daily_cost
SELECT channel FROM ad_daily_cost GROUP BY channel
在Hive/Spark里,DISTINCT和GROUP BY都会触发Shuffle,执行计划基本一致。但如果同时有聚合需求,GROUP BY更灵活。实际工作中我几乎都用GROUP BY,习惯统一也方便维护。