在SQL里处理窗口函数时,一个常见的场景是:按股票分组,然后按时间顺序,看每一笔成交时该股票的历史最高价是多少。很多人第一反应是直接用 MAX(price) OVER (PARTITION BY stock_code),但这样拿到的是整个分组的静态最大值,而不是随时间滚动的累计最高。真正能实现“逐行累积历史最高”的写法,下面这个例子可以说得很清楚。
正确写法是MAX(price) OVER (PARTITION BY stock_code ORDER BY trade_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),它按股票代码分组、按时间排序并累积计算历史最高价;不加ORDER BY则返回全分组静态最大值,加RANGE在时间重复时可能产生歧义。

MAX() OVER() 的基本写法和分区逻辑
直接用 MAX(price) OVER (PARTITION BY stock_code) 就能拿到每只股票的历史最高成交价,但要注意:它返回的是「当前行所在分组内的最大值」,不是「截至当前行的时间点的最大值」。如果你要的是「每个时间点上该股票的历史最高价(即从最早到当前行的累计最高)」,必须加上 ORDER BY trade_time 和 ROWS UNBOUNDED PRECEDING。
PARTITION BY stock_code按股票代码分组,确保只在同一只股票内比较- 不加
ORDER BY→ 计算的是整个分组的静态最大值(所有行都一样) - 加
ORDER BY trade_time→ 默认是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,但对MAX()来说,RANGE和ROWS在时间列唯一时等效;若trade_time有重复,建议显式写成ROWS UNBOUNDED PRECEDING避免歧义
处理时间重复或未排序导致的结果异常
常见错误是查出来的 max_price_so_far 在同一股票里突然变小,或者多行结果一致——这往往是因为 ORDER BY 缺失或 trade_time 有重复且没指定窗口帧。
- 如果表本身没按
trade_time排序,OVER(ORDER BY trade_time)仍有效,但执行计划可能更重;建议查询前确保索引覆盖(stock_code, trade_time) - 当多笔成交发生在同一秒(比如高频交易),
RANGE模式会把它们全归入同一窗口,导致MAX()提前“看到”后面的数据;改用ROWS UNBOUNDED PRECEDING可严格按物理顺序累积 - 示例写法:
MAX(price) OVER (PARTITION BY stock_code ORDER BY trade_time, id ROWS UNBOUNDED PRECEDING),用id做第二排序键消除并列
和 GROUP BY + MAX() 的关键区别
MAX() OVER() 是窗口函数,不压缩行数;而 GROUP BY stock_code 会把每组压成一行。你要的是「每笔成交旁边附上它截至此刻的历史最高价」,就必须用窗口函数,不能用聚合。
- 错例:
SELECT stock_code, MAX(price) FROM trades GROUP BY stock_code→ 只返回每只股票一个值,丢失原始明细 - 对例:
SELECT *, MAX(price) OVER (PARTITION BY stock_code ORDER BY trade_time ROWS UNBOUNDED PRECEDING) AS max_price_so_far FROM trades→ 每行都有对应的历史最高价 - 性能提示:窗口函数比 GROUP BY + JOIN 回原表更快,尤其数据量大时;但需注意
PARTITION BY列的选择性——低基数(如几十个股票代码)效率高,高基数(如百万用户ID)可能触发大量分组排序
MySQL 8.0+ 和 PostgreSQL 的兼容性细节
语法基本一致,但 MySQL 在早期 8.0 版本中对 ROWS UNBOUNDED PRECEDING 支持不稳定,PostgreSQL 则从 8.4 起就支持完整窗口定义。
- MySQL 8.0.2+ 安全可用;低于此版本可能报错
This type of clause is not allowed in this context,此时只能依赖子查询模拟(性能差) - PostgreSQL 允许省略
ROWS,默认就是ROWS模式;MySQL 必须显式声明,否则默认是RANGE - 如果用的是 SQL Server,语法相同,但注意其
ORDER BY子句中不允许使用表达式(如DATE(trade_time)),需提前计算好列
窗口函数真正难的不是写法,而是想清楚「你到底要哪个‘历史’——是全局最高、还是随时间滚动的最高」。一旦 ORDER BY 和 ROWS/RANGE 混用错了,结果就静默出错,很难肉眼发现。
