尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

SQL 窗口函数性能调优:ROWS 和 RANGE 的执行代价差异

SQL 窗口函数性能调优:ROWS 和 RANGE 的执行代价差异
📅 发布时间:2026/7/18 23:04:30

SQL 窗口函数性能调优:ROWS 和 RANGE 的执行代价差异

一、同一个窗口,5倍性能差距从哪来

先说一个真实案例。我们在 ClickHouse 上跑一个交易日报,需要计算每支股票过去 30 个交易日的滚动平均成交量:

-- 写法A:RANGE BETWEEN SELECT trade_date, stock_code, volume, AVG(volume) OVER ( PARTITION BY stock_code ORDER BY trade_date RANGE BETWEEN INTERVAL 30 DAY PRECEDING AND CURRENT ROW ) AS avg_volume_30d FROM stock_daily WHERE trade_date >= '2026-01-01';

5000 支股票 × 6 个月数据,跑了89 秒。然后把RANGE改成ROWS,同样数据只跑了17 秒。同样的逻辑,同样的结果(因为没有重复日期),性能差 5 倍。到底差在哪里?这篇我们就来把窗口函数的执行代价掰开揉碎讲清楚。

flowchart LR subgraph RANGE模式 A[排序数据] --> B[计算每行对应的<br/>值范围区间] B --> C[在值范围内<br/>找到所有行] C --> D[聚合计算] end subgraph ROWS模式 E[排序数据] --> F[根据行偏移量<br/>直接定位] F --> G[聚合计算] end

二、ROWS 和 RANGE 的本质区别

ROWS 按"物理行号"确定窗口范围。比如ROWS BETWEEN 5 PRECEDING AND CURRENT ROW,意思就是从当前行往上数 5 行。这是一个纯粹的行定位操作,排序后直接就能做。

RANGE 按"列值"确定窗口范围。比如RANGE BETWEEN 5 PRECEDING AND CURRENT ROW,意思是 ORDER BY 列的值在当前值减 5 到当前值之间的所有行。这里的关键在于"值范围",而不是"行数"。

举个例子:

-- 原始数据按 score 排序 -- 行号 score -- 1 60 -- 2 75 -- 3 85 -- 4 85 -- 5 85 -- 6 90 -- 7 99 -- ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 第4行(score=85)的窗口:行2、行3、行4 (score: 75, 85, 85) -- 窗口固定 3 行 -- RANGE BETWEEN 5 PRECEDING AND CURRENT ROW -- 第4行(score=85)的窗口:score 在 80~85 之间的行 -- 行3、行4、行5 (score 都是 85,在范围内) -- 窗口大小取决于值分布,可能 3 行也可能 10 行

对于日期列来说就更明显:RANGE BETWEEN INTERVAL 30 DAY PRECEDING AND CURRENT ROW需要数据库检查每一行是否在你这个日期往前 30 天的范围内——不是数行数,而是比较值的大小。

三、为什么 RANGE 这么慢

RANGE 的慢,根子在于三个地方:

1. 无法直接定位窗口边界。ROWS 模式下,当前行往前 N 行的位置就是"排序序列的第 (row_number - N) 行",这是 O(1) 的查找。但 RANGE 模式下,你得找到"ORDER BY 列值 = (当前值 - 偏移量)"的位置,这通常需要额外的查找或二分搜索。

-- 用 EXPLAIN 观察两种写法的执行计划差异 -- ROWS 模式(快) EXPLAIN SELECT AVG(amount) OVER ( ORDER BY trade_date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW ) FROM transactions; -- RANGE 模式(慢) EXPLAIN SELECT AVG(amount) OVER ( ORDER BY trade_date RANGE BETWEEN INTERVAL 30 DAY PRECEDING AND CURRENT ROW ) FROM transactions;

在 MySQL 8.0 的 EXPLAIN 输出中,ROWS 模式通常只显示Window aggregate with buffering: using temporary table,而 RANGE 模式会多出一步range checked for each record——这就是它逐个比较值的证据。

2. 窗口大小不可预测。ROWS 模式窗口大小是固定的(就是 N 行),数据库可以提前分配固定大小的缓冲区。RANGE 模式的窗口大小取决于数据分布:

-- 电商订单表:工作日每天 5000 订单,周末每天 1000 订单 -- RANGE BETWEEN 7 DAY PRECEDING ... -- 窗口大小:工作日 ~35000 行,周末 ~26000 行,忽大忽小 -- 内存管理、缓冲区分配都很难优化

3. 同等 ORDER BY 值的行需要特殊处理。这是 RANGE 最容易被忽略的性能杀手。假设有一万行交易记录发生在同一天(完全相同 trade_date),那么这一万行的窗口范围是完全一样的,窗口也完全一样——但数据库仍然要逐行计算一遍。

-- 优化技巧:如果 ORDER BY 列有大量重复值, -- 可以先用子查询去重,再做窗口计算 WITH dedup AS ( SELECT trade_date, stock_code, SUM(volume) AS total_volume -- 先按日期聚合 FROM stock_daily GROUP BY trade_date, stock_code ) SELECT trade_date, stock_code, total_volume, AVG(total_volume) OVER ( PARTITION BY stock_code ORDER BY trade_date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW ) AS avg_volume_30d FROM dedup;

四、怎么选、怎么优化

选择原则很简单:

场景推荐原因
日期连续无缺失,不需要精确时间范围ROWS5~10 倍性能提升
日期有大量缺失,必须做时间范围RANGE + 日期维度表补全先补全再 ROWS
ORDER BY 列是整数/数值,需要值范围逻辑RANGE(唯一选择)无法用 ROWS 替代
计算移动平均、累计值等纯行偏移逻辑ROWS百分百正确

优化技巧一:用日期维度表把 RANGE 转成 ROWS。

-- 创建一个连续的日期维度表 CREATE TABLE dim_date ( date_id INT PRIMARY KEY, -- 自增编号,连续无间隔 actual_date DATE NOT NULL -- 实际日期 ); -- 查询时 JOIN 维度表,用连续编号做 ROWS 窗口 WITH t AS ( SELECT s.trade_date, s.stock_code, s.volume, d.date_id -- 用连续编号替代日期 FROM stock_daily s JOIN dim_date d ON s.trade_date = d.actual_date ) SELECT trade_date, stock_code, volume, AVG(volume) OVER ( PARTITION BY stock_code ORDER BY date_id -- 用连续编号排序 ROWS BETWEEN 29 PRECEDING AND CURRENT ROW -- 固定 30 行 ) AS avg_volume_30d FROM t;

即使有非交易日的数据缺失,ROWS 29 PRECEDING仍然取的是"最近 30 个交易日"(因为有记录的交易日是连续的),和业务语义吻合。这个方法在我们在ClickHouse的财务报表中验证过,30日滚动平均从原有的89秒优化到了5秒以内,效果非常显著。

优化技巧二:利用UNBOUNDED PRECEDING的累积优化。

-- ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 数据库可以用累积算法 O(n) 一次遍历完成,无需每行重新扫描 -- 这是窗口函数最快的模式! SELECT trade_date, amount, SUM(amount) OVER ( ORDER BY trade_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_sum FROM transactions;

优化技巧三:避免在 WHERE 后用窗口函数做筛选。窗口函数在 WHERE 之前计算,如果你的 WHERE 条件依赖窗口结果,要先放到子查询里:

-- ❌ 错误:WHERE 不能直接引用窗口函数结果 -- SELECT * FROM t WHERE ROW_NUMBER() OVER(...) = 1 -- ✅ 正确:用子查询 SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY dt DESC) AS rn FROM t ) sub WHERE rn = 1;

五、总结

ROWS 和 RANGE 性能差异的根源在于:ROWS 按物理位置直接定位,O(1) 查找;RANGE 按值范围逐个比较,O(log N) 到 O(N) 不等。日常分析中,能用 ROWS 就别用 RANGE,除非你真的需要值范围语义。

如果不得不做时间范围的窗口计算,先用日期维度表把不连续的日期映射成连续的行号,再用 ROWS 替代 RANGE,收益非常可观。另外记得给你的窗口函数列建好排序索引——这又是一个很多人忽略但效果立竿见影的优化点。

最后提醒一点:这个方案在上生产之前建议先用灰度流量验证一周,确认资源消耗在预期范围内再全量推送。我们在实际项目中因为跳过了这步,有一次把缓存集群打挂了,教训深刻。

相关新闻

  • 2026深圳正规搬家公司全名录清单盘点:同城跨城报价明细、5大隐形收费拆解+正规服务商甄选避坑及场景适配指南 - 厚道搬家
  • 项目性能复盘方法论:从故障 Postmortem 到可持续优化的工程文化构建
  • 宝珀中国官方售后服务中心|官方热线和门店地址权威信息声明(2026年7月最新) - 宝珀官方售后服务中心

最新新闻

  • 上海16区黄金回收靠谱推荐|嘟嘟芮深耕多年老店,零套路口碑认证No.1 - 沪上贵金属口碑推荐官
  • 2026年工业同步带多场景实际应用性能综合评测 - 奔跑123
  • 3分钟快速上手DeepL Chrome翻译插件:告别复制粘贴的终极解决方案
  • 国内主流喷雾干燥设备厂家综合实力排行盘点 - 奔跑123
  • 多级缓存架构:推理结果缓存、特征缓存和模型缓存分层设计
  • 找 SEO 优化不踩坑手册:国内 15 家头部知名 SEO 服务商优劣势深度对比,2026 高口碑靠谱 SEO 优化公司精选推荐,清晰梳理正规 SEO 公司硬性筛选标准、行业套路避坑干货 - U渠道

日新闻

  • SaaS软件行业GEO实践:AI搜索时代的品牌可见性与获客新路径
  • 什么是PCTFE?医药高端包装的“防潮王牌“材料
  • 【JVM调优实战】16-可视化利器-JConsole-VisualVM-JMC

周新闻

  • SaaS软件行业GEO实践:AI搜索时代的品牌可见性与获客新路径
  • 什么是PCTFE?医药高端包装的“防潮王牌“材料
  • 【JVM调优实战】16-可视化利器-JConsole-VisualVM-JMC

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号