订阅更新

GROUP BY的3个隐藏陷阱,你踩过几个?

SQL手册 第 6 题 约 8 分钟读完

本文目录
  1. 总结
  2. 陷阱一:SELECT里出现了没有GROUP BY的非聚合列
  3. 陷阱二:GROUP BY之后想过滤,WHERE还是HAVING?
  4. 陷阱三:COUNT的三种写法结果不同
  5. HAVING的实战用法
  6. 面试加分:GROUP BY的性能优化

总结

陷阱正确做法
----------------
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,习惯统一也方便维护。

刷题进度保存在当前浏览器