订阅更新

PARTITION BY 和 GROUP BY 到底有什么区别?

SQL手册 第 18 题 约 9 分钟读完

本文目录
  1. 总结
  2. 一句话区别
  3. 看个例子
  4. 什么时候用哪个?
  5. 面试高频题:计算占比
  6. 进阶:窗口函数的三大组成部分
  7. 实战:一条SQL同时输出明细、小计、总计
  8. GROUP BY和PARTITION BY能混用吗?

总结

对比项GROUP BYPARTITION 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之后的结果了。

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