动态权重平均:为什么 A VG() 使不上劲?
普通平均函数 A VG(price) 对所有行一视同仁,但现实业务中,我们往往希望“近期的记录更重要”——比如按时间加权、按销量加权,或者按置信度加权。窗口函数本身不提供这种自带权重的平均聚合,A VG() 无法接收额外的权重参数,硬套上去只会把权重逻辑丢掉。
唯一靠谱的路径是:用窗口函数分别算出分子(加权和)和分母(权重和),再手动相除。公式就是 SUM(value * weight) OVER (...) / SUM(weight) OVER (...)。
手动计算:分子分母必须“门当户对”
这个模式在 MySQL 8.0+、PostgreSQL、SQL Server、Oracle 里都通用,兼容性不错。而且它能配合任意窗口定义,比如 PARTITION BY category ORDER BY date ROWS BETWEEN 3 PRECEDING AND CURRENT ROW。
- 必须用两个独立的
SUM()窗口表达式,别想着在一个A VG()里塞权重——函数本身不支持。 weight列如果为NULL,整行参与计算时分子分母都会变成NULL,所以要提前用COALESCE(weight, 0)或者用WHERE weight IS NOT NULL过滤掉。- 权重如果是小数(比如 0.8、1.2),结果类型可能变成
DOUBLE或DECIMAL,必要时用ROUND(..., 2)来控精度。
举个例子:按品类算滚动加权平均价格,权重用销量。
SELECT category, date, price, sales, ROUND( SUM(price * sales) OVER ( PARTITION BY category ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW )::DECIMAL / NULLIF(SUM(sales) OVER ( PARTITION BY category ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ), 0), 2 ) AS weighted_a vg_priceFROM products;
防除零:NULLIF(..., 0) 不是锦上添花,是雪中送炭
当窗口内所有 sales = 0(或者都 NULL 后被 COALESCE 成 0),分母为 0 就会引发除零错误。不同数据库的反应不一样:PostgreSQL 直接报错,MySQL 返回 NULL,但都不能依赖。
- 所以一律用
NULLIF(SUM(weight) ..., 0)代替裸SUM(weight),这样分母为 0 时能返回NULL,避免报错。 - 别图省事用
WHERE weight > 0预过滤——窗口范围可能跨多行,局部为 0 不代表整个窗口和也为 0。 - 如果业务允许权重为负(虽然极少见),那得额外判断
SUM(weight) = 0而不是光看> 0。
窗口范围:不写 ORDER BY 和 frame clause,动态性就无从谈起
窗口函数如果没有 ORDER BY,那 ROWS BETWEEN ... 就没效果——默认等价于 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,但这个默认行为在不同数据库里不一样:PostgreSQL 默认是 RANGE,MySQL 默认是 ROWS。混乱之中最容易出错。
- 时间序列加权必须显式写
ORDER BY time_col,否则顺序无法保证。 - 明确用
ROWS BETWEEN N PRECEDING AND CURRENT ROW表示“最近 N 条”,用ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW表示“累计到当前”。 - 尽量避免用
RANGE帧,尤其是时间字段——它会合并相同排序值的行,导致权重被重复计入,结果就不对了。
权重平均的核心不是函数多么炫酷,而是分子分母必须严格对齐窗口范围。差一行,结果就变了样。
