max() over() 默认窗口帧为 range between unbounded preceding and current row,若未指定 order by,则无法定位“当前行”,导致全量扫描;即使有 order by,也需显式指定 rows between unbounded preceding and unbounded following 才能计算全局最大值。

为什么 MAX OVER() 会全量扫描?
因为窗口函数 MAX() OVER() 默认的窗口帧(frame)是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,但如果你没显式指定 ORDER BY,数据库就无法确定“当前行”在哪——于是退化为对整个分区做全量聚合。更关键的是:**即使你加了 ORDER BY,只要没配 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,它就不是“历史最高”,而是“到当前行为止的最大值”**。
- 想算每个产品的「所有历史记录中的最高销量」,必须显式声明全范围窗口帧
- MySQL 8.0+、PostgreSQL、SQL Server 都支持该语法;旧版 MySQL(
- 全量扫描本身不可避——毕竟要遍历所有行才能知道最大值,但加索引能加速扫描过程(比如在
product_id和sales_qty上建联合索引)
MAX() OVER(PARTITION BY product_id) 不够用
这个写法看似合理,实则危险:它隐式使用默认帧,而默认帧依赖排序。如果漏写 ORDER BY,不同数据库行为不一致——PostgreSQL 报错,MySQL 8.0 允许但结果等价于全分区最大值(因无序时“当前行”无意义,引擎可能退化处理),SQL Server 则直接报错。
- 正确写法必须带完整帧定义:
MAX(sales_qty) OVER(PARTITION BY product_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) -
RANGE帧在存在重复sales_qty时可能合并行,导致逻辑偏差;ROWS更可靠 - 别依赖“不写帧就自动全量”——这是副作用,不是规范行为,未来版本可能收紧
替代方案:用聚合 + JOIN 比窗口函数更可控?
当数据量极大或执行计划显示窗口函数触发大量临时表/排序时,传统两步法反而更稳:
SELECT t.*, m.max_sales FROM sales_table t JOIN ( SELECT product_id, MAX(sales_qty) AS max_sales FROM sales_table GROUP BY product_id ) m ON t.product_id = m.product_id;
- 优点:语义清晰,索引友好(
GROUP BY走索引快),各数据库兼容性好 - 缺点:不能在单次扫描中复用中间结果(比如同时算最高、最低、平均),需多次关联
- 注意:如果原表有
NULL的sales_qty,MAX()自动忽略,但需确认业务是否允许忽略
容易被忽略的边界:空值和分区键缺失
当某产品没有任何销售记录(即 product_id 在 sales_table 中完全不存在),窗口函数不会返回该产品;而 GROUP BY 方案同样遗漏——两者都只基于事实表行存在才计算。若需补全所有产品(包括零销量),必须 LEFT JOIN 产品主表。
-
MAX()遇到全分区都是NULL时返回NULL,不是 0 —— 业务上常需COALESCE(MAX(...), 0) - 分区键(如
product_id)含NULL值时,这些行会被归入同一“NULL分区”,通常不是预期行为,建议提前过滤或WHERE product_id IS NOT NULL - 时间范围未限定(比如没加
WHERE sale_date ),“历史最高”就会随新数据不断变化,实际使用中几乎总要加时间约束
ROWS BETWEEN ... 就指望“历史最高”,等于把逻辑交给数据库猜。











