PARTITION BY 和 GROUP BY 到底有什么区别?
总结
| 对比项 | GROUP BY | PARTITION BY |
|---|---|---|
| -------- | ---------- | ------------- |
| 结果行数 | 减少(多行变一行) | 不变(每行保留) |
| 能看明细吗 | 不能 | 能 |
| 能做行间比较吗 | 不能 | 能(LAG/LEAD) |
| 能做累计/滑动吗 | 不能 | 能(ROWS BETWEEN) |
| 能做占比吗 | 需要子查询+JOIN | 一个OVER就搞定 |
| 性能 | 一次聚合 | 一次扫描+分区计算 |
一句话记忆:GROUP BY是"压缩数据",PARTITION BY是"给每行加标签"。
明天 DAY 19 聊WITH子句(CTE)的用法和性能,CTE不只是让SQL好看,在某些场景下还有性能上的讲究。
你更喜欢用GROUP BY + JOIN还是窗口函数?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
这两个都是"分组",但行为完全不同。很多人用了好几年SQL,还是分不清什么时候用GROUP BY、什么时候用窗口函数的PARTITION BY。今天用一个案例把它们的区别彻底讲清楚。
一句话区别
GROUP BY: 分组后聚合,多行变一行,结果行数减少
PARTITION BY: 分组后计算,每行都保留,结果行数不变
看个例子
数据:广告每日消耗表
| dt | channel | cost |
|------------|----------|------|
| 2025-01-01 | Facebook | 5000 |
| 2025-01-01 | TikTok | 3000 |
| 2025-01-02 | Facebook | 5500 |
| 2025-01-02 | TikTok | 3200 |
| 2025-01-03 | Facebook | 4800 |
| 2025-01-03 | TikTok | 3500 |
GROUP BY: 按渠道汇总
SELECT channel, SUM(cost) AS total_cost
FROM ad_daily_cost
GROUP BY channel
结果只有2行:
| channel | total_cost |
|----------|------------|
| Facebook | 15300 |
| TikTok | 9700 |
原来6行数据被压缩成了2行。你无法同时看到每天的明细和渠道总计。
PARTITION BY: 每行都带上渠道汇总
SELECT
dt,
channel,
cost,
SUM(cost) OVER (PARTITION BY channel) AS channel_total
FROM ad_daily_cost
结果还是6行:
| dt | channel | cost | channel_total |
|------------|----------|------|---------------|
| 2025-01-01 | Facebook | 5000 | 15300 |
| 2025-01-02 | Facebook | 5500 | 15300 |
| 2025-01-03 | Facebook | 4800 | 15300 |
| 2025-01-01 | TikTok | 3000 | 9700 |
| 2025-01-02 | TikTok | 3200 | 9700 |
| 2025-01-03 | TikTok | 3500 | 9700 |
每行都保留了原始明细,同时多了一列渠道总计。这就是窗口函数的魅力——既要明细,又要汇总。
什么时候用哪个?
用GROUP BY的场景: 你只关心聚合结果,不需要明细。
按月统计总消耗
按渠道统计用户数
按地区汇总销售额
用PARTITION BY(窗口函数)的场景: 你既要明细又要聚合,或者需要行间比较。
每行带上组内排名(ROW_NUMBER)
每行带上组内占比(cost / SUM(cost) OVER)
每行和前一行对比(LAG)
累计求和、滑动平均
面试高频题:计算占比
这是PARTITION BY最经典的应用——每个渠道每天的消耗占该渠道总消耗的百分比:
SELECT
dt,
channel,
cost,
SUM(cost) OVER (PARTITION BY channel) AS channel_total,
ROUND(cost * 100.0 / SUM(cost) OVER (PARTITION BY channel), 2) AS pct
FROM ad_daily_cost
如果用GROUP BY实现,需要先汇总再JOIN回去:
SELECT
a.dt,
a.channel,
a.cost,
b.channel_total,
ROUND(a.cost * 100.0 / b.channel_total, 2) AS pct
FROM ad_daily_cost a
JOIN (
SELECT channel, SUM(cost) AS channel_total
FROM ad_daily_cost
GROUP BY channel
) b ON a.channel = b.channel
两种写法结果一样,但窗口函数版明显更简洁,少了一次子查询和JOIN。在大数据场景下,少一次JOIN就少一次Shuffle,性能也更好。
进阶:窗口函数的三大组成部分
SUM(cost) OVER (
PARTITION BY channel -- 1. 分区子句:按什么分组
ORDER BY dt -- 2. 排序子句:组内按什么排序
ROWS BETWEEN ... AND ... -- 3. 窗口帧:计算范围
)
这三个子句不是都必须写的:
| 写法 | 含义 |
|---|---|
| ------ | ------ |
| SUM(cost) OVER () | 全局汇总(没有分组、没有排序) |
| SUM(cost) OVER (PARTITION BY channel) | 按渠道汇总(有分组、没有排序) |
| SUM(cost) OVER (PARTITION BY channel ORDER BY dt) | 按渠道累计(有排序,默认从头到当前行) |
| SUM(cost) OVER (ORDER BY dt ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) | 全局7天滑动和 |
面试常见陷阱:加不加ORDER BY结果不一样。
-- 没有ORDER BY:每行都是组内总和(15300)
SUM(cost) OVER (PARTITION BY channel)
-- 有ORDER BY:每行是从分组第一行到当前行的累计和
SUM(cost) OVER (PARTITION BY channel ORDER BY dt)
DAY 5讲累计求和的时候提过,加了ORDER BY之后默认的窗口帧变成了RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,也就是从头到当前行的累计。很多人不理解为什么加了ORDER BY结果就变了,就是因为默认窗口帧变了。
实战:一条SQL同时输出明细、小计、总计
这个需求在做报表的时候非常常见。投放团队要看一张表:每天每渠道的消耗明细,每个渠道的小计,所有渠道的总计。
SELECT
dt,
channel,
cost,
-- 渠道小计
SUM(cost) OVER (PARTITION BY channel) AS channel_subtotal,
-- 日期小计
SUM(cost) OVER (PARTITION BY dt) AS daily_subtotal,
-- 全局总计
SUM(cost) OVER () AS grand_total,
-- 渠道占比
ROUND(cost * 100.0 / SUM(cost) OVER (), 2) AS pct_of_total,
-- 渠道内日占比
ROUND(cost * 100.0 / SUM(cost) OVER (PARTITION BY channel), 2) AS pct_of_channel
FROM ad_daily_cost
WHERE dt BETWEEN '2025-01-01' AND '2025-01-03'
ORDER BY channel, dt
一条SQL,6个维度的分析指标全出来了。如果用GROUP BY来实现这个需求,至少需要3次子查询+2次JOIN,代码量翻好几倍。
我每天出广告日报就是用类似的写法。原来是分3条SQL分别统计明细、渠道汇总、总计,再用Python拼到一起。后来改成一条SQL用窗口函数搞定,代码量少了70%,维护起来也方便多了。
GROUP BY和PARTITION BY能混用吗?
可以。GROUP BY先聚合,然后在聚合结果上用窗口函数做进一步计算:
-- 先按月+渠道聚合,再用窗口函数算环比
SELECT
month,
channel,
monthly_cost,
LAG(monthly_cost, 1) OVER (PARTITION BY channel ORDER BY month) AS prev_month_cost,
ROUND(
(monthly_cost - LAG(monthly_cost, 1) OVER (PARTITION BY channel ORDER BY month)) * 100.0
/ NULLIF(LAG(monthly_cost, 1) OVER (PARTITION BY channel ORDER BY month), 0),
2
) AS mom_rate
FROM (
SELECT
DATE_FORMAT(dt, 'yyyy-MM') AS month,
channel,
SUM(cost) AS monthly_cost
FROM ad_daily_cost
GROUP BY DATE_FORMAT(dt, 'yyyy-MM'), channel
) monthly_data
这个写法DAY 7讲月环比的时候用过。记住执行顺序:先GROUP BY,再SELECT中的窗口函数。 窗口函数看到的数据已经是GROUP BY之后的结果了。