导读:本期聚焦于宋琮安创作的《为什么SQL窗口函数定义中的ROWS与RANGE性能差异大?分析底层实现机制》,敬请观看详情。窗口函数里ROWS和RANGE只差一个单词,查询耗时却可能相差几十倍,这不是玄学而是底层实现的巨大鸿沟。ROWS按物理行号计算边界,数据库只需维护一个滑动指针就能增量聚合;RANGE按逻辑值计算边界,遇到重复值时边界会整段扩张,SQL Server等引擎必须借助序列化key匹配,容易退化为嵌套循环或调度器模式,还要额外物化排序列。本文从窗口边界的判定逻辑入手,对比两种模式在执行计划、内存占用、排序开销上的差异,结合EXPLAIN输出与真实耗时测试,给出在业务允许时优先用ROWS换性能的改写技巧。

写窗口函数的时候,OVER (ORDER BY ... ROWS BETWEEN ...)OVER (ORDER BY ... RANGE BETWEEN ...) 在语法上只差一个单词,很多人默认不写框架子句时也从未留意过——实际上省略框架时的默认值就是RANGE UNBOUNDED PRECEDING AND CURRENT ROW。而这两者在真实数据量下的性能差距可能达到几十倍,尤其是排序列存在大量重复值的时候。要理解这个差距,必须回到窗口边界判定的底层实现上。

为什么SQL窗口函数定义中的ROWS与RANGE性能差异大?分析底层实现机制

ROWS与RANGE的语义差异是性能差距的根源

ROWS是物理行的概念:它以当前行在分区内的位置为锚点,ROWS BETWEEN 1 PRECEDING AND CURRENT ROW 表示严格取当前行的前一行和当前行,共两行,不多不少。数据库引擎处理时只需要知道“当前是第几行”,边界的判定是一个简单的整数比较。

RANGE则是逻辑值的概念:它以当前行排序列的取值为锚点,RANGE BETWEEN 1 PRECEDING AND CURRENT ROW 表示取所有排序值落在 [当前值-1, 当前值] 区间内的行。关键点来了——如果排序列有重复值,这些重复行会被一次性纳入窗口。也就是说RANGE的窗口大小是数据分布决定的,行数不固定,引擎在扫描到某一行时无法立刻知道窗口边界在哪里,必须回头扫描值相同的兄弟行。

举个具体例子,假设按amount排序的值为 10, 20, 20, 20, 30。对第四行(amount=20)执行 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW 窗口是第三、四行共两行;而执行 RANGE BETWEEN 1 PRECEDING AND CURRENT ROW(amount差为1以内)窗口包含所有amount在19到20之间的行,也就是第二、三、四行共三行。排序列重复越多,两种模式的窗口差异越大,RANGE需要处理的行也就越多。

执行计划层面的实现差异

以SQL Server为例,看它的实际执行计划可以非常直观地观察到差别。使用ROWS模式时,执行计划里的Window Aggregate算子会显示为普通的流式聚合,引擎维护一个固定大小的缓冲区(比如窗口是1 PRECEDING时缓冲区就是1行),每读入一行就做增量计算:新行加入聚合,滑出窗口的旧行从聚合中扣除。整个过程是O(n)的,一遍扫描完成。

而RANGE模式在SQL Server的实现中,你会发现执行计划多了一个序列化key匹配的环节。因为引擎需要判断“哪些行的排序值与当前值相等(或落在区间内)”,这个判断不再是整数行号的比较,而要对排序列的值做序列化后逐key匹配。在排序列重复值很多的情况下,一个窗口可能覆盖成千上万行,引擎要么退化为对每个不同值做一次嵌套循环式的回扫,要么采用segment top这类调度模式。Plan里的实际行数(Actual Rows)往往会比ROWS模式高出一个数量级,这就是耗时膨胀的直接原因。

PostgreSQL的实现同样体现了这个差异。ROWS模式的窗口函数在internal执行层面用行号直接定位帧的起止,帧推进是单调的、可增量的;RANGE模式则必须在W-window帧计算时反复比较排序值,帧起点只有在遇到下一个不同值时才能推进。当数据倾斜严重(比如某个值占了表的一半行数)时,PostgreSQL的RANGE窗口内存中需要物化整个帧,work_mem的压力也随之上升。可以在EXPLAIN ANALYZE的输出里观察WindowAgg节点的耗时:

EXPLAIN ANALYZE
SELECT
    user_id,
    amount,
    SUM(amount) OVER (ORDER BY amount
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sum_rows,
    SUM(amount) OVER (ORDER BY amount
        RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sum_range
FROM orders;
-- 排序列amount重复度高时,sum_range对应的WindowAgg节点
-- 自身耗时通常是sum_rows节点的数倍到数十倍

排序与去重的额外开销

窗口函数执行的前置条件是数据按PARTITION BY和ORDER BY排好序。对ROWS来说,排序键的重复值无所谓,任何稳定的排序结果都能直接使用。但对RANGE来说,引擎还需要识别“值相同的连续段”,一些实现会为此在排序key上追加额外的标记列,或者在WindowAgg内部维护一个eqfunction做逐值比较。这些都是ROWS模式不需要付出的成本。

更隐蔽的一点是RANGE配合区间偏移(如 RANGE BETWEEN 5 PRECEDING AND 5 FOLLOWING)时,某些引擎无法简单地用双向指针滑动,必须借助索引查找或对已物化的分区做二分定位。窗口越大、偏移越宽,随机访问的代价越明显。而ROWS无论偏移多大,本质上都只是两个指针在有序流上的单调移动。

内存方面也有差异。ROWS的增量聚合只需要保存窗口宽度的行数,ROWS BETWEEN 10 PRECEDING AND 10 FOLLOWING最多缓存20行左右的状态;RANGE的窗口行数不确定,引擎必须把整个当前帧的输入行保留在内存或workfile中,重复值高时可能触发溢出到磁盘(SQL Server中表现为tempdb spills,PostgreSQL中表现为窗口物化带来的IO),这是除CPU之外的第二个性能杀手。

业务改写:什么时候能用ROWS替代RANGE

理解了实现差异,优化思路就很清晰了:在业务语义允许的前提下,尽量用ROWS改写。一个典型场景是“取时间戳完全相同的行视为同一时刻”,如果业务上每个时刻只有一行数据(比如主键粒度),RANGE和ROWS的结果完全一致,直接写ROWS就能白拿性能。

另一种常见的改写技巧是给排序列追加唯一列,消除重复值:ORDER BY amount, id 配合ROWS使用。因为加了唯一列之后排序组合不再重复,RANGE语义中“同行”的概念退化为“同一行”,两种模式结果一致,但执行走的是ROWS的快路径。来看一个累计求和的改写对比:

-- 原写法:RANGE模式,排序列amount重复多时性能差
SELECT user_id, amount,
       SUM(amount) OVER (PARTITION BY user_id ORDER BY create_time
         RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_sum
FROM orders;

-- 改写:追加唯一id消除重复,改用ROWS,语义等价
SELECT user_id, amount,
       SUM(amount) OVER (PARTITION BY user_id ORDER BY create_time, id
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_sum
FROM orders;

当然改写前必须确认语义等价性:如果业务明确要求“同一时刻的行共享同一个累计值”(比如所有同分的选手并列排名),那就必须保留RANGE,此时能做的是减少排序列的重复度、控制分区大小,或者预先用GROUP BY压缩重复值再开窗。另外要注意,不写框架子句时默认就是RANGE模式,这也是很多人无意中踩坑的地方——显式写出 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 常常是一个零风险的性能优化。

小结

ROWS与RANGE的性能差异不是实现质量的偶然,而是语义复杂度决定的必然:物理行号可以增量推进,逻辑值边界则需要逐值比较、整段扩张、额外物化。排查这类问题时,养成先看执行计划里Window Aggregate节点实际行数的习惯,再检查排序列的重复度(NDV,number of distinct values),基本就能定位是RANGE在拖后腿。能改写成ROWS的场景不要犹豫,语义必须保留RANGE的场景则要控制数据倾斜,避免单帧过大导致的CPU和内存双重压力。

SQL窗口函数ROWS与RANGE数据库性能优化修改时间:2026-09-06 19:42:42

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260906/51754.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。