资讯详情

窗口函数 SUM() OVER() 详解:PARTITION BY 与 ORDER BY 的累计计算逻辑

📅 2026/10/11 23:41:49 | 华诺云谱 👁 阅读
窗口函数 SUM() OVER() 详解:PARTITION BY 与 ORDER BY 的累计计算逻辑
很多人学了窗口函数一看到SUM() OVER(PARTITION BY ... ORDER BY ...)这种写法还是会懵特别是ORDER BY加上之后结果怎么就从“分组总和”变成“累加值”了这篇文章继续走实战路线我会把SUM() OVER()从基础语法到进阶应用一层层剥开重点放在PARTITION BY和ORDER BY组合时背后那套“隐形规则”上。先说明一点市面上很多教程只会告诉你“这是求累计值”却没告诉你为什么是累计值、默认窗口范围是什么时候开始什么时候结束的导致你换个场景又不会了。所以这篇文章适合所有用过 SQL、但窗口函数停留在“会写不会变通”阶段的开发者。全文我会用模拟的订单销售数据做演示所有案例都能直接在主流数据库上跑MySQL 8.0、PostgreSQL、Hive、Spark SQL 语法都兼容你跟着抄就行。1. 窗口函数初印象SUM() OVER() 到底比 GROUP BY 强在哪1.1 一个让你崩溃的业务需求先说一个我在业务中经常遇到的场景运营要给出一张表里面需要同时展示“每个销售当天的订单金额”和“每个销售从月初到当天的累计订单金额”。如果只用GROUP BY你最多能算出来“每个销售的总金额”因为你一旦分组原来的明细行就被折叠了。但需求是要把累计值“贴”到每一行明细后面行的数量不能变额外的累计数只能作为新列加上去。我记着第一次遇到这个需求时我还在用“关联子查询 临时聚合表”的方式做SQL 写得又长又绕跑了半天还容易出性能问题。那时候没有窗口函数这种需求就是折磨人。后来接触了SUM() OVER()才发现这个需求用一列窗口函数就能解决而且语义非常直白PARTITION BY salesperson表示“按销售分组”ORDER BY sale_date表示“组内按日期排序”然后对排好序的数据做累计相加。1.2 窗口函数与传统聚合的本质区别要理解SUM() OVER()核心是理解“窗口”两个字。传统聚合函数比如SUM(sale_amount)配合GROUP BY会把多行合并成一行结果集的行数会变少。窗口函数则不同它也是“按组计算”但计算完的结果会保留原有行数每一行都能看到它所在组的聚合结果。我习惯用一个类比来记忆传统聚合就像把一堆水果榨成一杯果汁你看到的是混合后的整体窗口函数就像给每个水果贴一个标签这个标签上写的是这一筐水果总共多重但水果本身还是一个个摆在那里。SUM() OVER(PARTITION BY ...)做的就是第二种事算完分组总和不折叠行把总和复制给组内每一行。而一旦在OVER()里面加上ORDER BY事情又发生了一次质变——SUM()不再是求整个分组的总和而是变成了求“从分组起点到现在这一行”的累计值。这背后涉及的“窗口框架”概念我放到第 4 章专门讲因为它是理解整个问题的总开关。2. PARTITION BY 和 ORDER BY 的各自分工2.1 PARTITION BY分组但不收敛PARTITION BY的作用和GROUP BY有点像都是指定“按哪些字段分成不同的桶”却有一个显著差异GROUP BY会折叠行PARTITION BY不会。举个例子下面是一个简化的销售表sale_datesalespersonsale_amount2024-01-01张三1002024-01-01李四2002024-01-02张三1502024-01-02李四3002024-01-03张三1802024-01-03李四250看这条 SQLSELECT sale_date, salesperson, sale_amount, SUM(sale_amount) OVER(PARTITION BY salesperson) AS total_by_person FROM sales_data;执行结果里每一个销售的原始行都还在只是每一行旁边多了一列“这个人的总销售额”。张三的三行都显示 430李四的三行都显示 750。这个特性特别适合“原始明细 分组汇总”同时呈现的场景比如你在做报表时既要给明细又要给合计数。用GROUP BY做不到用子查询关联也能做但性能差、代码丑。2.2 ORDER BY排序如何改变 SUM 的计算范围PARTITION BY确定了“在哪个组内计算”ORDER BY则负责确定“在组内按什么顺序计算”。当SUM()遇上ORDER BY后它计算的就不再是“整个分组的总和”而是“按排序顺序从分组第一行到当前行的累计值”。这是窗口函数里最基础也最重要的概念——累计求和。看下面的 SQLSELECT sale_date, salesperson, sale_amount, SUM(sale_amount) OVER(PARTITION BY salesperson ORDER BY sale_date) AS cumulative_amount FROM sales_data;结果变成sale_datesalespersonsale_amountcumulative_amount2024-01-01张三1001002024-01-02张三1502502024-01-03张三1804302024-01-01李四2002002024-01-02李四3005002024-01-03李四250750注意看张三的第三行累计金额是 430这个数字恰好等于张三所有订单之和。而李四的第三行也是 750同样等于全部订单之和。在这个例子里因为聚合到第三行后所有行都“到齐了”所以最后一行看起来和整组总和一样这纯属巧合。关键在中间行第二行累计值是 250它不是张三的全组总和 430而是只累加了两天的结果。这就是ORDER BY的威力——它把“分组”进一步划分为“按顺序逐步扩展的行集合”。2.3 两者一起用时到底按什么逻辑算PARTITION BY和ORDER BY一起出现时计算逻辑可以分解为三步第一步按照PARTITION BY字段值分成若干独立组第二步在每个组内按ORDER BY字段排序第三步对每一行从组内第一行开始累加一直加到当前行得到当前行的窗口结果。这个“当前行之前的所有行 当前行”的范围就是窗口函数里面非常重要的“聚合窗口”或者叫“窗口框架”。它默认是从分组起点到当前行。这个默认行为正是 SQL 标准设计的核心点——没有显式指定窗口框架时只要OVER()里有ORDER BY聚合函数就会默认采用“从分区起点到当前行”的累计框架。如果你不理解这层就无法解释为什么加了ORDER BY结果从“总和”变成了“累计值”。3. 实战案例拆解从销售额累计到同环比计算3.1 案例一各部门销售累计趋势先来个最常见的场景。某个公司有多个部门每天产生销售记录现在需要看每个部门从月初到每天的销售额累计用于绘制趋势图。建表和模拟数据CREATE TABLE dept_daily_sales ( dept_id INT, sale_date DATE, sale_amount DECIMAL(10,2) ); INSERT INTO dept_daily_sales VALUES (1, 2024-01-01, 1200.00), (1, 2024-01-02, 1500.00), (1, 2024-01-03, 1800.00), (2, 2024-01-01, 800.00), (2, 2024-01-02, 1100.00), (2, 2024-01-03, 1600.00), (3, 2024-01-01, 2000.00), (3, 2024-01-02, 1800.00), (3, 2024-01-03, 2400.00);查询语句SELECT dept_id, sale_date, sale_amount, SUM(sale_amount) OVER(PARTITION BY dept_id ORDER BY sale_date) AS dept_cum_amount FROM dept_daily_sales ORDER BY dept_id, sale_date;结果dept_idsale_datesale_amountdept_cum_amount12024-01-011200.001200.0012024-01-021500.002700.0012024-01-031800.004500.0022024-01-01800.00800.0022024-01-021100.001900.0022024-01-031600.003500.0032024-01-012000.002000.0032024-01-021800.003800.0032024-01-032400.006200.00这里如果去掉ORDER BY sale_datedept_cum_amount就会变成每个部门的整体总和你拿到的是“每个部门全月总销售额”而不是“逐日累计”。这一个ORDER BY之差就是“静态分组汇总”和“动态累加”的本质区别。3.2 案例二按时间顺序计算移动累计并计算占比再给一个稍复杂点的需求公司要看每个销售当天的业绩、当天业绩占整个公司当天总业绩的比例以及个人的累计业绩。这个需求如果没有窗口函数你得用两个子查询加两个关联SQL 写起来非常痛苦。有了窗口函数可以一次搞定SELECT sale_date, salesperson, sale_amount, SUM(sale_amount) OVER(PARTITION BY sale_date) AS daily_total, ROUND(sale_amount / SUM(sale_amount) OVER(PARTITION BY sale_date) * 100, 2) AS daily_pct, SUM(sale_amount) OVER(PARTITION BY salesperson ORDER BY sale_date) AS person_cum FROM sales_data ORDER BY sale_date, salesperson;同一个OVER()里面可以放不同的分区和排序组合各自互不干扰。这一行代码里同时出现了“按日期分组算当天总业绩”、“按个人分组算累计业绩”和“自定义占比计算”三种逻辑在同一条 SQL 中完成这对业务报表来说是巨大的效率提升。我在实际项目中经常把多个窗口函数放在同一个 SELECT 里性能上通常是可接受的因为相同分区排序定义可以被数据库优化器复用不要一上来就拆成多个子查询。3.3 案例三用累计值做“首次达到目标”判断累计值不仅仅用于展示它还可以作为进一步逻辑判断的基础。比如运营给定目标金额 5000需要知道每个销售哪一天首次完成了累计业绩超过 5000 的任务。这时候可以把窗口查询作为子查询再在外面筛选WITH cum_data AS ( SELECT salesperson, sale_date, sale_amount, SUM(sale_amount) OVER(PARTITION BY salesperson ORDER BY sale_date) AS cum_amount FROM sales_data ) SELECT salesperson, MIN(sale_date) AS first_reach_date FROM cum_data WHERE cum_amount 5000 GROUP BY salesperson;这个例子展示了一个常见技巧窗口函数可以在子查询、CTE 中先算好再供外部查询进一步过滤。很多人学窗口函数只停留在“能查出来”的层面遇到嵌套使用就不知所措其实只要理解“窗口计算发生在 SELECT 阶段滤发生在 WHERE 之后”就容易想通了——这也是为什么你不能在 WHERE 里直接引用窗口函数的别名。4. 进阶窗口框架Frame是理解累加的关键4.1 默认窗口范围为什么加上 ORDER BY 就变成累计值我见过很多人在这一步栽跟头明明只是给SUM() OVER(PARTITION BY ...)加了个ORDER BY结果整个结果集的意义都变了。原因就是窗口框架的默认值发生了变化。在 SQL 标准中如果OVER()里只有PARTITION BY没有ORDER BY默认的窗口范围是整个分区此时SUM()计算的就是分组总和。如果OVER()里有ORDER BY默认的窗口范围就变成RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW翻译过来就是“从分区开始到当前行”而且这个范围是“范围”方式RANGE不是“行数”方式ROWS。RANGE和ROWS的差别在于RANGE会把排序字段值相同的行都包含进“当前行”的范围而ROWS只严格按物理行数取。举个例子如果有两行是同一天的订单用RANGE做累计时这两行会被当成同一层级处理分别包含对方用ROWS则严格按照行坐标逐行推进。这个差异导致的坑很经典当你用ORDER BY sale_date对多笔同日订单做累计求和时用默认的RANGE两条同一天的记录会拿到相同的累计值都包含对方如果你期待的是逐行累加就得显式指定ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。4.2 手动指定窗口框架ROWS 和 RANGE 的差异显式定义窗口框架的语法是SUM(sale_amount) OVER( PARTITION BY salesperson ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )这里的关键词ROWS表示按物理行取范围CURRENT ROW是当前行UNBOUNDED PRECEDING是分区起点。用ROWS的写法保证了逐行累加不管你有多少笔同日期订单每一行都会把前面的行加上当前行本身的金额。对比一下RANGESUM(sale_amount) OVER( PARTITION BY salesperson ORDER BY sale_date RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )当有两行同日期数据时RANGE模式下两行会互相包含累计结果完全一致。这在业务上其实也有用意比如“按天累计”时同一天的所有订单一起累计体现的是“到这一天为止”而不是“到这一个订单为止”。两种模式没有绝对好坏关键看业务口径。我曾经在线下数仓项目里遇到过一种情况销售明细表里同一个销售同一天有多笔订单运营想要的是“按订单粒度累加”结果因为默认RANGE导致同日订单累计值相同以为代码写错了排查了半天。后来我把默认框架明确改成ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW问题立刻消失。这条经验很重要在写累计类窗口函数时我强烈建议你显式写明窗口框架不依赖数据库的默认行为这样一方面避免不同数据库实现的差异另一方面也让读 SQL 的人一眼知道你预期的口径是什么。5. 常见坑与排查实录5.1 坑一ORDER BY 字段有重复值导致累计结果不符合预期这是窗口函数使用中出现频率最高的问题网上讨论也最多但很多帖子都讲得含含糊糊。我直接给结论如果你的业务口径是“按物理行逐行累加”使用ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW如果想让相同排序键值的行共享同一个累计值使用RANGE或省略不写。在实践中绝大多数业务需求是前者即严格按行累加。尤其当你ORDER BY的字段不是唯一键时这个坑几乎百发百中。我之前处理过一个库存流水表ORDER BY operation_time同一秒内有多笔入库默认RANGE导致累计库存数跳变后期对不上账。最后在两个地方做了修正一是给ORDER BY追加一个唯一字段比如自增 ID保证排序稳定二是显式使用ROWS窗口框架。5.2 坑二窗口函数结果集中混入 GROUP BY 分组的错误写法有时候会被要求“既要明细又要分类汇总”新手常犯的错误是先把分组结果算出来再去关联明细导致最后的 SUM 值被重复计算多次。比如SELECT a.sale_date, a.salesperson, a.sale_amount, SUM(b.person_total) AS wrong_total FROM sales_data a JOIN ( SELECT salesperson, SUM(sale_amount) AS person_total FROM sales_data GROUP BY salesperson ) b ON a.salesperson b.salesperson GROUP BY a.sale_date, a.salesperson, a.sale_amount;这种情况在旧版本数据库里逻辑混乱用窗口函数却能非常优雅地解决SELECT sale_date, salesperson, sale_amount, SUM(sale_amount) OVER(PARTITION BY salesperson) AS correct_total FROM sales_data;窗口函数不需要 JOIN、不需要 GROUP BY 折叠行只添加一列聚合值。这也是为什么我遇到类似的“明细汇总”组合需求时第一选择就是窗口函数而不是子查询 JOIN。5.3 坑三OVER() 里不写 PARTITION BY 导致全局累计如果你写SUM(sale_amount) OVER(ORDER BY sale_date)没有PARTITION BY那整个表会被当成一个大组按日期全局累计。这在某些场景下是有意为之比如公司总的累积销售额趋势但很多时候是新手的无心之失尤其当表里有多个部门、多个销售时全局累计会得到毫无业务意义的数据。数据量一大这个错很难通过抽查发现。我建议凡是看到SUM() OVER(ORDER BY ...)而思绪里没有任何分组的 SQL都要停下来确认到底要不要全局累计。如果是多实体数据表通常都需要PARTITION BY一个业务主体字段。5.4 坑四窗口函数和 WHERE 的先后顺序窗口函数在 SQL 执行逻辑中位于WHERE条件之后、ORDER BY之前。这意味着你不能直接在WHERE中引用窗口函数的计算结果如果你想筛选窗口计算的产物比如累计值大于某个数需要把窗口查询包一层子查询或 CTE。经常有初学者写出这种 SQLSELECT salesperson, sale_date, SUM(sale_amount) OVER(PARTITION BY salesperson ORDER BY sale_date) AS cum_amount FROM sales_data WHERE cum_amount 1000;在多数数据库里这会直接报错提示“未知列 cum_amount”。正确写法是SELECT * FROM ( SELECT salesperson, sale_date, SUM(sale_amount) OVER(PARTITION BY salesperson ORDER BY sale_date) AS cum_amount FROM sales_data ) t WHERE cum_amount 1000;这种“包一层”的思路在窗口函数使用中非常普遍你需要习惯它。6. 性能优化与索引设计心得窗口函数用起来爽性能却是另一个问题。如果一个表有几百万行PARTITION BY加上ORDER BY的累计计算会要求数据库对每个分区都维持一个排序状态代价不小。我总结几条实战调优经验第一尽量在源数据层面压缩扫描范围。比如只取近 30 天数据、只取特定部门的记录用 WHERE 先将数据规模缩小再做窗口计算。窗口函数虽说是 SQL 标准功能但对数据量的敏感度比普通聚合高得多。第二窗口函数依赖排序时合理的索引能让数据库省去反复排序的开销。如果业务上频繁执行类似SUM(...) OVER(PARTITION BY dept_id ORDER BY sale_date)这样的查询在(dept_id, sale_date)上建立一个联合索引会很有帮助。不要过度索引但针对高频窗口查询建索引是合理的优化手段。第三能用单个窗口函数算完的不要拆成多个窗口函数嵌套。同一分区排序定义可以放在一个OVER()里做多次不同聚合而不是写多个OVER()因为数据库可以重用排序结果。比如SELECT sale_date, sale_amount, SUM(sale_amount) OVER(PARTITION BY dept_id ORDER BY sale_date) AS cum_amount, COUNT(sale_id) OVER(PARTITION BY dept_id ORDER BY sale_date) AS cum_count比写成两个独立子查询后关联要高效得多。第四如果只是求“整组总和”而不需要累计那就不要加ORDER BY。没有ORDER BY的窗口函数会比有ORDER BY的少做一次排序在超大分组上性能差异可能非常明显。第五注意内存和临时表空间。排序量极大时数据库可能落盘到临时文件性能断崖式下降。遇到这种情况优先检查数据过滤条件是不是太弱或者把一些不需要窗口函数参与的字段先过滤掉。7. 最后补充几个实用技巧如果只记住这篇文章里的几句话我建议记这几句一、SUM() OVER(PARTITION BY ... ORDER BY ...)等于“分组 排序 逐步累计”三件事同时发生去掉ORDER BY就只是“分组总和”。二、遇到累计值出现“同排序值同结果”的情况不要第一时间觉得数据库的问题先想想RANGE和ROWS的默认差异。三、窗口函数的结果不能直接在WHERE中引用必须套一层子查询或 CTE。四、写出窗口函数 SQL 后一定要拿几条数据手算验证一下结果尤其是累计值的边界。我见过太多人写完不验证就上线结果数仓报表里数字对不上排查到凌晨。再给一个跟日期相关的实际技巧如果PARTITION BY想按“月份”分组但表里只有sale_date字段有两种常见处理方式SUM(sale_amount) OVER(PARTITION BY DATE_FORMAT(sale_date, %Y-%m) ORDER BY sale_date)或者提前在数据预处理阶段生成一个month_id字段用month_id分区。后者对跨数据库兼容性更好也方便后续GROUP BY和窗口函数共用同一个月份字段。关于PARTITION BY多个字段的情况比如“部门 销售”两个维度直接写SUM(sale_amount) OVER(PARTITION BY dept_id, salesperson ORDER BY sale_date)这是一个非常常见的多维分区写法每一组都是独立的累计序列互不影响。我自己在实际项目中最常用的窗口函数其实不是 RANK 或 LAG而是SUM() OVER(PARTITION BY ... ORDER BY ...)。它在做留存分析、漏斗分析、累计达成率时都是主力工具。现在如果再遇到业务方说“给我一张表每一行都要带累计值”我基本不用思考条件反射写出来的就是这个函数。希望这篇文章能帮你在面对SUM() OVER(PARTITION BY ... ORDER BY ...)时不再靠背方案而是真正明白它背后分步执行的逻辑。表结构变化、排序字段变化、框架模式变化你都能自然知道结果会怎么变这才是掌握了这个函数。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑