忘了从哪一天开始,身边越来越多的同事在写SQL时碰上了同一个困惑:明明只是想把"每个订单的金额"和"这位销售员的总金额"放在同一行展示,用GROUP BY怎么写都别扭——要么报语法错误,要么就得绕一大圈子做自连接。直到有人开始用窗口函数,一行OVER()就把问题解决了。于是问题变成了:"窗口函数和GROUP BY到底有什么区别?我什么时候该用哪个?"
这篇文章就用一张销售订单表,把这两个东西从输出效果、执行原理、适用场景到实战代码彻底讲透。不管你是刚接触SQL的数据分析新手,还是写了好几年SQL但一直绕着窗口函数走的开发,看完应该都能直接上手。
1. 场景还原:同样的数据,GROUP BY和窗口函数分别看到了什么
1.1 先造一张能说明问题的销售订单表
空谈概念没有意义,我们先建立一张具体的表,后面所有对比都基于它。
CREATE TABLE sales_order ( order_id INT, sales_name VARCHAR(20), region VARCHAR(20), order_date DATE, amount DECIMAL(10,2) );其中order_id是订单编号,sales_name是销售员代号,region是销售区域,order_date是下单日期,amount是订单金额。为了方便计算,销售员我用小A、小B、小C、小D这种代号,区域用东区、北区、西区,避免被真实数据带偏。
插入几条测试数据:
INSERT INTO sales_order VALUES (1, '小A', '东区', '2024-01-05', 1200.00), (2, '小A', '东区', '2024-01-18', 800.00), (3, '小A', '东区', '2024-02-10', 1500.00), (4, '小B', '东区', '2024-01-08', 2000.00), (5, '小B', '东区', '2024-02-12', 2300.00), (6, '小C', '北区', '2024-01-15', 950.00), (7, '小C', '北区', '2024-02-03', 1400.00), (8, '小C', '北区', '2024-02-20', 1700.00), (9, '小D', '西区', '2024-01-22', 3000.00), (10, '小D', '西区', '2024-02-08', 1100.00);这张表一共10行订单,4个销售员。接下来我要把同一个需求分别用GROUP BY和窗口函数实现,输出结果摆在一起,区别一眼就能看出来。
1.2 GROUP BY的输出:多行被折叠成一行的汇总
先跑一段最常规的写法:按销售员统计总金额。
SELECT sales_name, SUM(amount) AS total_amount FROM sales_order GROUP BY sales_name;输出长这样:
| sales_name | total_amount |
|---|---|
| 小A | 3500.00 |
| 小B | 4300.00 |
| 小C | 4050.00 |
| 小D | 4100.00 |
注意这个输出:原始表10行,这里变成了4行。每个销售员的多笔订单被"折叠"成了一行汇总。这不是偶然,这是GROUP BY的根本逻辑——它把满足条件的行按分组键归成若干组,每组输出一行。组内其他行的信息(比如每笔订单具体发生在哪一天、订单号是多少)在这个结果里已经看不到了,因为它们不在分组键里,也不是聚合函数的输入。
很多人第一次接触GROUP BY时都背过一句话:"SELECT后面出现的列,要么在GROUP BY里,要么在聚合函数里。"这句话背后的原因就在这里:其他列在分组之后已经失去了"某一行"的语义,数据库也不知道该展示哪一条,所以干脆禁止你查询。
1.3 窗口函数的输出:行数不变,每行旁边多挂一个计算结果
现在用窗口函数实现同样的"按销售员统计总金额":
SELECT order_id, sales_name, region, order_date, amount, SUM(amount) OVER (PARTITION BY sales_name) AS total_amount FROM sales_order;输出长这样:
| order_id | sales_name | region | order_date | amount | total_amount |
|---|---|---|---|---|---|
| 1 | 小A | 东区 | 2024-01-05 | 1200.00 | 3500.00 |
| 2 | 小A | 东区 | 2024-01-18 | 800.00 | 3500.00 |
| 3 | 小A | 东区 | 2024-02-10 | 1500.00 | 3500.00 |
| 4 | 小B | 东区 | 2024-01-08 | 2000.00 | 4300.00 |
| 5 | 小B | 东区 | 2024-02-12 | 2300.00 | 4300.00 |
| 6 | 小C | 北区 | 2024-01-15 | 950.00 | 4050.00 |
| 7 | 小C | 北区 | 2024-02-03 | 1400.00 | 4050.00 |
| 8 | 小C | 北区 | 2024-02-20 | 1700.00 | 4050.00 |
| 9 | 小D | 西区 | 2024-01-22 | 3000.00 | 4100.00 |
| 10 | 小D | 西区 | 2024-02-08 | 1100.00 | 4100.00 |
这里的关键变化是:输出依然是10行,一行都没少。原始明细列order_id、order_date、amount全部保留,只是在每一行旁边追加了一个新列total_amount——这一列的值是"该行所属销售员的总金额"。
这个"追加列"的动作,就是窗口函数的核心工作方式。它不像GROUP BY那样把行合并掉,而是以每一行为单位,在一个"窗口"范围内做计算,然后把结果原样挂回这一行。
1.4 两者的第一层本质区别:血缘粒度完全不同
好,看到这里,第一层区别已经浮出水面了:
- GROUP BY 输出的粒度是"分组",组的数量取决于分组键的组合,结果行数一定少于或等于原始行数。
- 窗口函数输出的粒度还是"行",原始行数不变,只是每行带上了一个由窗口计算出来的附加信息。
这个差异往深了说,是"血缘粒度"的差异。GROUP BY之后的查询结果,每一行的语义不再是"某一条订单",而是"某一组订单的概括";而窗口函数的结果,每一行仍然是"某一条订单",窗口计算只是给这条订单补充了上下文信息。
正是这个差异,决定了绝大多数场景下你该选谁:
- 如果业务上只需要"分组汇总后的数字",GROUP BY干净利落。
- 如果业务上需要"明细行 + 分组汇总数字"同时出现,窗口函数是首选。
2. 深层拆解:行数只是表象,执行顺序和聚合作用域才是根本
2.1 SQL各子句的执行顺序,决定了窗口函数能做什么
很多教程会直接告诉你"GROUP BY对分组聚合,窗口函数对结果集逐行计算",听懂了又好像没听懂。真正能让你理解两者差异的,是搞清SQL各子句的执行顺序。
标准SQL的逻辑执行顺序大致是:
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT而聚合函数和窗口函数都在SELECT阶段进行求值,但有先后:
- 普通聚合函数(配合GROUP BY)在SELECT阶段先按组计算,每组得到一个值。
- 窗口函数在SELECT阶段基于"当前已经形成的查询结果集"逐行计算。
这句话有两个重要推论。
推论一:窗口函数可以引用前面已经算好的聚合结果。比如SUM(SUM(amount)) OVER ()这种嵌套是合法的,因为内层SUM(amount)在GROUP BY阶段已经得到分组汇总值,外层窗口函数再对着这些汇总值做一次全局累计。这一点在做"分组占比""分组累计"时非常实用,后面案例会用到。
推论二:窗口函数是在WHERE之后才执行的。所以你在WHERE里没法直接写AVG(amount) OVER (...) > 100,因为WHERE执行时窗口结果还没生成。想过滤窗口函数的结果,必须包一层子查询。这是新手最容易踩的坑,我放在第4章细讲。
2.2 聚合作用域不同:组是一个桶,窗口是一条滑动的框
GROUP BY的"组"像一个桶,把符合条件的行一股脑装进去,然后对桶里的行做聚合,桶与桶之间互不影响。分组键决定了桶的划分方式,划分完之后,行与行之间的先后顺序对聚合结果没有影响。
窗口函数的"窗口"则是一块在结果集上"滑动"的视野区域。OVER()子句负责定义这块区域怎么圈定:
PARTITION BY决定按什么字段分区,作用类似"分组",但不折叠行;ORDER BY决定窗口内按什么顺序排列;ROWS或RANGE子句进一步决定窗口的边界,比如"从当前行往前数2行到当前行"。
用一个生活化类比:GROUP BY像把全班同学按小组站好,然后每组报一个人数;窗口函数像每个同学都从自己的位置出发,回头看自己小组里有多少人,自己还在队伍里不动。前者队伍变短了,后者队伍还是那么长,但每个人手里多了一张写着"小组人数"的纸条。
2.3 SELECT层的列权限差异:GROUP BY失去的列,窗口函数都保留
前面提到GROUP BY对查询列有严格限制。下面两段SQL放在一起,差异就非常直观。
这是错误的:
-- 错误的写法:order_id既不在GROUP BY里,也不在聚合函数里 SELECT order_id, sales_name, SUM(amount) FROM sales_order GROUP BY sales_name;在MySQL 8.0及以上版本,默认开启ONLY_FULL_GROUP_BY模式,这条SQL直接报错。原因前面说过:order_id在分组后不具备确定语义,数据库不知道应该展示这一组里的哪一条。这在业务上其实是一件好事,它逼着你明确自己的查询意图,避免拿到一份不确定的结果。
而用窗口函数,则可以放心地把所有明细列写在SELECT里,因为每一行数据都还在:
SELECT order_id, sales_name, order_date, amount, SUM(amount) OVER (PARTITION BY sales_name) AS total_amount FROM sales_order;窗口函数只负责"附加计算",不干预行本身的生存状态。所以它能访问到当前行的所有列,同时又能拿到窗口范围内的聚合结果。这个能力是自连接、子查询替代方案所不具备的便利。
2.4 一张表理清所有关键差异
把上面的分析收敛成一张对比表,方便随时翻阅:
| 对比维度 | GROUP BY | 窗口函数 |
|---|---|---|
| 输出行数 | 多行折叠为一行,行数减少 | 行数不变,每行附加计算结果 |
| 输出粒度 | 分组级别 | 行级别 |
| SELECT可访问列 | 仅分组键和聚合函数结果 | 原表所有列 + 窗口计算结果 |
| 聚合作用域 | 分组"桶"内 | 分区"窗口"内,可随行滑动 |
| 是否保留明细 | 不保留 | 完全保留 |
| 典型场景 | 汇总报表、去重归并 | 明细旁挂汇总、占比、排名、累计、移动计算 |
| 结果过滤方式 | HAVING直接过滤组 | 需包子查询后在WHERE过滤 |
| 是否依赖行顺序 | 不依赖 | 高度依赖PARTITION BY和ORDER BY定义 |
这张表不是我随便列的,每一行背后都对应着一类具体的SQL写法和潜在的报错场景。你在实际写SQL时如果拿不准,先对着这张表判断"我要的是行粒度还是组粒度",答案基本就出来了。
3. 实战案例一:明细带汇总、占比、排名,窗口函数一套带走
3.1 案例A:订单明细后面挂上销售员总金额
这是最经典的需求,也是自连接方案的痛点。先用GROUP BY + 子查询 + JOIN硬凑出来,你感受一下复杂度:
SELECT s.order_id, s.sales_name, s.amount, t.total_amount FROM sales_order s JOIN ( SELECT sales_name, SUM(amount) AS total_amount FROM sales_order GROUP BY sales_name ) t ON s.sales_name = t.sales_name;这段代码的逻辑是:先用子查询算出每个销售员的总金额,再回到原表做连接,把总额拼到每一行后面。功能上没问题,但代码长了将近一倍,而且要额外扫描一次原表。
窗口函数的写法是:
SELECT order_id, sales_name, amount, SUM(amount) OVER (PARTITION BY sales_name) AS total_amount FROM sales_order;一个OVER(PARTITION BY sales_name)就把子查询和连接都省了。你可能会想:"这不是偷懒吗?"其实不只是省代码,更重要的是执行效率。自连接方案至少要把sales_order表读两遍(一遍聚合,一遍取明细),而窗口函数在理想情况下可以一遍扫描完成。数据量小无所谓,到了千万级业务表,这个差异是很现实的。
3.2 案例B:每笔订单占销售员的金额占比
占比类需求在生活中极其常见:看每个销售员不同订单的金额贡献,或者看某个产品在品类中的销售占比。用窗口函数实现非常顺滑:
SELECT order_id, sales_name, amount, amount / SUM(amount) OVER (PARTITION BY sales_name) AS amount_pct FROM sales_order;输出会多一列amount_pct,比如小A的三笔订单占比分别是34.29%、22.86%、42.86%,加起来正好100%。在这个例子里,同一行的amount和SUM(amount) OVER(...)都参与了计算,这正是窗口函数"不丢行还能聚合"的能力体现。
如果把同样的需求抛给GROUP BY,就会遇到一个尴尬:amount / SUM(amount)这个表达式在分组之后根本没有合法的amount可用,因为amount不是分组键。你必须再写一次子查询或者自连接才能拿到"每行的金额和该行销售员的总额",这就是自连接写法在占比类需求里严重啰嗦的原因。
3.3 案例C:区域内金额排名,并取出每组Top 2
排名是窗口函数独占的优势领域。GROUP BY能做到的最多是"按区域统计总额",但"区域内的订单排名"这种需求,GROUP BY完全无能为力——因为它根本没有行粒度。
先看排名:
SELECT order_id, sales_name, region, amount, RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rk FROM sales_order;这里PARTITION BY region把数据按区域分区,ORDER BY amount DESC让每个分区内按照金额从高到低排序,RANK()对这个顺序逐个编号。注意:窗口函数里的ORDER BY并不改变结果集本身的输出顺序,它只影响窗口内如何编号和排序。最终输出的10行依然保持查询的原始顺序,只是每行多了一列rk。
如果要拿每个区域金额最大的两笔订单,包一层子查询即可:
SELECT order_id, sales_name, region, amount, rk FROM ( SELECT order_id, sales_name, region, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rk FROM sales_order ) t WHERE rk <= 2;用ROW_NUMBER()而不是RANK(),是因为这里我只关心"前两行"。如果用了RANK(),遇到金额相同的情况会产生并列名次,可能一下冒出3行。这是个很经典的细节:排名并列就选RANK(),取固定行数用ROW_NUMBER()。
3.4 案例D:累计销售额与移动平均
继续加码。需求变成"按销售员看订单金额的累计进度"。窗口函数的写法:
SELECT order_id, sales_name, order_date, amount, SUM(amount) OVER ( PARTITION BY sales_name ORDER BY order_date ) AS cumulative_amount FROM sales_order;这里除了PARTITION BY sales_name,还加了ORDER BY order_date。加了这个ORDER BY之后,窗口不再是整个销售员分区了,而变成了"从分区起点到当前行"的累计区间。所以小A在2024-01-05那行显示的是1200,到2024-01-18那行就变成了2000(1200+800),到2024-02-10那行变成3500。
移动平均是另一个高频场景,比如"最近3笔订单的平均金额"。写起来也只需要加一个窗口边界:
SELECT order_id, sales_name, order_date, amount, AVG(amount) OVER ( PARTITION BY sales_name ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS moving_avg_3 FROM sales_order;ROWS BETWEEN 2 PRECEDING AND CURRENT ROW的意思是:窗口覆盖当前行以及前面两行,共最多3行。如果当前行之前不足两行,就只算已有的行。移动平均在统计订单波动、观察趋势平滑度的时候非常有用,GROUP BY根本做不了这种"逐行滑动的窗口",这也是窗口函数名字的由来。
4. 实战案例二:GROUP BY与窗口函数组合,报表里最常见的粒度转换
4.1 先按月聚合,再算累计和同环比
很多报表需求不是单纯的"明细+汇总",而是"先汇总,再在汇总结果上做进一步计算"。比如:先统计每个销售员每个月的销售额,然后看他的月度累计、环比增长率。这两种需求单独用GROUP BY或窗口函数都别扭,组合起来反而干净。
第一步,用GROUP BY把明细数据聚合到"销售员-月份"粒度:
SELECT sales_name, DATE_FORMAT(order_date, '%Y-%m') AS ym, SUM(amount) AS month_amount FROM sales_order GROUP BY sales_name, DATE_FORMAT(order_date, '%Y-%m');得到小A 2024-01等于2000.00,2024-02等于1500.00;小B 2024-01等于2000.00,2024-02等于2300.00。注意这里GROUP BY的输出粒度变成了"销售员+月份",每个销售员每月一行。
第二步,把上面的结果包进子查询,再用窗口函数算月度累计:
SELECT sales_name, ym, month_amount, SUM(month_amount) OVER ( PARTITION BY sales_name ORDER BY ym ) AS cumulative_amount FROM ( SELECT sales_name, DATE_FORMAT(order_date, '%Y-%m') AS ym, SUM(amount) AS month_amount FROM sales_order GROUP BY sales_name, DATE_FORMAT(order_date, '%Y-%m') ) t ORDER BY sales_name, ym;内层的GROUP BY把订单折叠成月度汇总,外层的窗口函数再逐月累计。这里就体现了前面说的推论一:窗口函数是在GROUP BY之后、SELECT阶段执行的,所以它可以直接消费SUM(amount)这个聚合结果,甚至SUM(SUM(amount))这种写法都是合法的。
环比(和上个月比)就更好写了,用窗口函数里的LAG:
SELECT sales_name, ym, month_amount, LAG(month_amount) OVER ( PARTITION BY sales_name ORDER BY ym ) AS prev_month_amount, month_amount - LAG(month_amount) OVER ( PARTITION BY sales_name ORDER BY ym ) AS month_diff FROM ( SELECT sales_name, DATE_FORMAT(order_date, '%Y-%m') AS ym, SUM(amount) AS month_amount FROM sales_order GROUP BY sales_name, DATE_FORMAT(order_date, '%Y-%m') ) t ORDER BY sales_name, ym;LAG(month_amount)取的是同一个销售员、按月份排序后上一行(也就是上个月)的金额。每个销售员的第一行没有前一行,所以prev_month_amount是NULL,对应的month_diff也是NULL,正好在报表里显示成"—"。如果需要同比(和去年同月比),把排序改成按月份字符串或日期,配合LAG取第12行即可,思路完全一样。
这种"GROUP BY + 窗口函数"的组合,几乎覆盖了日常报表的百分之八九十的粒度转换场景。先聚合到目标粒度,再在该粒度上做窗口计算,是处理这类需求最稳定的套路。
4.2 WINDOW子句:避免重复一大段OVER定义
在上面的代码里,你会发现同一个OVER(PARTITION BY sales_name ORDER BY ym)被写了三遍。代码短的时候还能忍,一旦窗口定义复杂起来,比如还要带上ROWS BETWEEN ...的边界条件,重复写就容易出错,而且一改就要改多处。
MySQL 8.0、PostgreSQL等主流数据库都支持WINDOW子句,可以给一段窗口定义起个名字,之后直接用名字引用。改造如下:
SELECT sales_name, ym, month_amount, SUM(month_amount) OVER w AS cumulative_amount, LAG(month_amount) OVER w AS prev_month_amount FROM ( SELECT sales_name, DATE_FORMAT(order_date, '%Y-%m') AS ym, SUM(amount) AS month_amount FROM sales_order GROUP BY sales_name, DATE_FORMAT(order_date, '%Y-%m') ) t WINDOW w AS (PARTITION BY sales_name ORDER BY ym) ORDER BY sales_name, ym;WINDOW w AS (...)把窗口定义提取出来,后面两个OVER w都引用同一个窗口。需要调整分区或排序时,只要改WINDOW这一行即可。这条语法适合"同一个窗口被多个函数复用"的场景,代码会清爽很多。
4.3 一个典型报表:区域销售额占比与累计占比
组合用法的另一个典型是帕累托分析,也就是"看每个区域的销售额占比,以及按贡献排序后的累计占比"。这个需求如果只用GROUP BY,占比能算出来,但累计占比还得再写排名逻辑,很痛。用组合写法是这样的:
SELECT region, SUM(amount) AS region_amount, SUM(amount) / SUM(SUM(amount)) OVER () AS region_pct, SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC) / SUM(SUM(amount)) OVER () AS cum_pct FROM sales_order GROUP BY region;一眼看去似乎复杂,拆开看其实不难。内层SUM(amount)是GROUP BY按区域聚合的结果,SUM(SUM(amount)) OVER ()是全部区域的总额,SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC)则是把各区域按金额降序排列后的累计值。两列相除,就得到了占比和累计占比。
这里有一个细节值得注意:SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC)的窗口会随着每行移动,加上了ORDER BY,默认窗口就是"从第一行到当前行",刚好是累计效果。如果不加ORDER BY,就是全区域总额,等于没区别。这个细节很容易搞混,建议拿实际数据跑一遍,亲眼看看两种写法输出差在哪里。
5. 最容易踩的坑:窗口函数和GROUP BY的边界地带
5.1 PARTITION BY不是GROUP BY,忘了它就等于全局开窗
写窗口函数时最常犯的一个错误,是忘记写PARTITION BY,或者在不需要分区的地方误以为分区会自动生效。
比如这段代码:
SELECT order_id, sales_name, amount, SUM(amount) OVER () AS total_amount FROM sales_order;OVER ()里没有写任何分区,意思是"整个结果集作为一个窗口",所以每一行的total_amount都是全部订单的总和21350.00。这有可能是你想要的全局总和,也有可能不是。如果你本意是"每个销售员的总金额",那这个结果就完全错了。
还有更隐蔽的坑:SUM(amount) OVER (ORDER BY order_date),只写了排序没写分区。因为有ORDER BY,窗口默认从分区起点累计到当前行,但由于没有分区,它会把所有销售员的订单混在一起按日期累计。小A 2024-01-05那行是1200,下一行小B 2024-01-08那行就变成3200(1200+2000)——看起来像是"总累计",却完全没有按人区分。
我自己早年就犯过这个错,跑出来的累计曲线特别陡,折腾了大半天才发现是漏了PARTITION BY sales_name。所以现在写窗口函数我有个习惯:先确认"这个窗口要在哪个维度内计算",然后再动手写PARTITION BY,缺了它就往全局窗口的方向想。
5.2 窗口函数的结果不能直接在WHERE里过滤
分组聚合可以用HAVING直接过滤组,这是GROUP BY的便利之处:
SELECT sales_name, SUM(amount) FROM sales_order GROUP BY sales_name HAVING SUM(amount) > 4000;但窗口函数的结果没有这么"直接"。前面说过,窗口函数在SELECT阶段才计算,而WHERE在SELECT之前执行。所以下面的写法会报错:
-- 错误:WHERE里不能使用窗口函数 SELECT order_id, sales_name, amount FROM sales_order WHERE amount < AVG(amount) OVER (PARTITION BY sales_name);正确做法是包一层子查询:
SELECT order_id, sales_name, amount, avg_amount FROM ( SELECT order_id, sales_name, amount, AVG(amount) OVER (PARTITION BY sales_name) AS avg_amount FROM sales_order ) t WHERE amount < avg_amount;内层先通过窗口函数算出每个销售员的平均金额,外层再按条件过滤。理解这个执行顺序之后,你就不需要去死记"窗口函数不能出现在WHERE"这个规则了,而是能推导出为什么。
5.3 版本与实现差异:不是所有数据库都支持窗口函数
窗口函数虽然已经普及很多年,但不同数据库的支持时间差别很大。MySQL直到8.0版本才正式支持窗口函数;MariaDB从10.2版本开始支持;SQLite从3.25版本开始支持。如果还在用MySQL 5.7或者更老版本,上面写的所有OVER语法都会直接报语法错误。
那老版本怎么办?只能退回子查询和自连接的老路子。比如"明细带分组总额"这个需求,用第3.1节里的自连接写法也能实现,只是代码更啰嗦、性能也可能更差。所以如果你在生产环境跑的是老版本数据库,写代码前先确认版本,免得辛辛苦苦写完被DBA打回。
另外还有一个细节上的差异:MySQL的窗口函数不支持COUNT(DISTINCT 列) OVER (...)这种写法,如果确实需要"分区内的去重计数",得先用子查询去重,或者换用DENSE_RANK()配合边界技巧绕一下。不同数据库在这里的行为不完全一致,碰到COUNT(DISTINCT)和窗口函数组合时,建议先做个最小化验证。
5.4 性能与选型:什么情况下窗口函数反而帮倒忙
很多文章只讲窗口函数"好用",却很少提它的代价。窗口函数在计算时通常需要对分区和排序字段做排序操作,大量数据使用窗口函数时,排序开销和内存压力是比较可观的。如果某个窗口函数在OVER里要排序的字段没有合适的索引,大表上的执行计划很可能出现明显的排序步骤,跑起来比普通的GROUP BY慢。
我的经验是分场景判断:
- 只做分组汇总,不要明细:用GROUP BY。它的执行路径成熟,走哈希聚合或索引聚合,通常比窗口函数简单。
- 需要明细行 + 汇总信息:优先考虑窗口函数。虽然它可能排序,但通常比"子查询 + 自连接"少一次全表扫描。
- 数据量巨大且窗口排序字段无索引:先看能不能用GROUP BY + JOIN绕过去,再用
EXPLAIN对比两种写法的实际开销,别凭感觉选。
数据库的优化器有时很聪明,有时又很死板。同样是窗口函数,PARTITION BY字段是否有索引,执行计划可能天差地别。遇到性能问题,不要急着抱怨数据库,先EXPLAIN看一眼排序和扫描行数,通常能快速定位瓶颈。
回到最开始的问题。窗口函数和GROUP BY不是非此即彼的对立关系,而是两种不同粒度的工具。GROUP BY把多行折叠成组,适合纯粹的分组汇总;窗口函数在一行不丢的前提下给每行补上上下文信息,适合明细和统计信息并存的场景。而真正复杂的报表需求,往往是先用GROUP BY把粒度归拢好,再用窗口函数做累计、排名和同环比。把这两个工具放在各自擅长的位置上,SQL写起来会顺手很多。
我个人最初学习窗口函数时,也被OVER的语法难住过,但后来我把每一个需求都先问自己一句:"我要的输出粒度到底是什么?"是组,就查分组;是行,就查行再开窗。这么想清楚之后,语法问题反而不是问题了。希望你也能把这种方法带进自己的SQL里,少踩几个我踩过的坑。