SQL无原生MOVING_VARIANCE函数,需用AVG(val*val)-POWER(AVG(val),2)手动推导总体方差,样本方差需修正分母并用NULLIF防除零,注意浮点误差、性能优化及异常值影响。

SQL 里没有内置的 MOVING_VARIANCE 函数
绝大多数 SQL 引擎(包括 PostgreSQL、MySQL 8.0+、Snowflake、BigQuery)不提供原生的「移动方差」窗口函数。你查文档或试 VAR_SAMP OVER (ORDER BY ... ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) 会发现:它算的是**当前窗口内所有行的总体/样本方差**,不是「每滑动一步就重算一次方差」的移动过程——但这个行为本身是符合窗口定义的,问题出在方差计算对窗口边界变化特别敏感,稍不注意就得到错误序列。
- 方差依赖均值,而移动窗口的均值本身就在变,不能缓存复用
-
VAR_POP和VAR_SAMP在小窗口(比如只有 1–2 行)下会返回NULL或无意义值(样本方差分母为 0) - PostgreSQL 的
stats_agg扩展能做在线统计更新,但需要额外安装,不属于标准 SQL
用 AVG() 和 AVG(x*x) 手动推导移动方差
方差可拆解为:E[X²] - E[X]²。只要窗口内能同时算出均值和平方均值,就能推出样本/总体方差。这是最通用、不依赖扩展的写法,兼容所有支持标准窗口函数的引擎。
- 总体方差(
VAR_POP等价):AVG(val * val) OVER w - POWER(AVG(val) OVER w, 2) - 样本方差(
VAR_SAMP等价)需修正分母:(COUNT(*) OVER w * AVG(val * val) OVER w - POWER(SUM(val) OVER w, 2)) / NULLIF(COUNT(*) OVER w - 1, 0) - 务必用
NULLIF防止除零;小窗口(如ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)时,COUNT(*) = 1会导致样本方差分母为 0 - 示例(PostgreSQL/BigQuery):
SELECT
ts,
val,
-- 移动窗口:最近 3 条(含当前)
AVG(val) OVER (ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg,
AVG(val * val) OVER (ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) -
POWER(AVG(val) OVER (ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2)
AS moving_var_pop
FROM metrics;MySQL 8.0+ 要小心 POWER() 和浮点精度
MySQL 对 POWER(x, 2) 的处理在某些版本中会引入隐式类型转换,导致 AVG(val * val) - POWER(AVG(val), 2) 出现微小负值(比如 -1e-15),实际应为 0。这不是逻辑错,而是浮点误差放大。
- 解决方法:用
GREATEST(0, ...)包裹结果,强制非负 - 或者改用显式平方:
AVG(val) OVER w * AVG(val) OVER w,比POWER()更稳定 - 如果字段是
DECIMAL,先CAST(val AS DOUBLE)再运算,避免整数溢出干扰 - 别依赖
ROUND(..., 6)治标——得先保证中间步骤没崩
性能瓶颈往往卡在窗口排序和重复计算
上面的手动公式要跑 3 次窗口计算(AVG(val)、AVG(val*val)、COUNT(*)),每多一个都会让执行计划多扫一遍数据。尤其当分区大、排序字段无索引时,延迟明显。
- 优先确保
ORDER BY字段有索引(如时间戳ts) - 如果只关心固定宽度(如 7 天滚动),考虑用自连接或 LATERAL JOIN 预聚合,而不是纯窗口函数
- BigQuery 中,用
ARRAY_AGG(val ORDER BY ts LIMIT 7)+ UDF 计算方差,有时比多次窗口扫描更快 - 真正高频场景(如实时风控),建议把移动方差逻辑下推到应用层或用物化视图预计算
方差对异常值极度敏感,移动窗口又会让单个离群点在多个结果中反复出现。上线前一定拿含极端值的测试数据跑一遍,看是否触发了意料之外的 NULL 或溢出 —— 这比语法正确难防得多。

















