日常算波动率用STDDEV_SAMP(样本标准差,分母n-1);数据为全体时才用STDDEV_POP(总体标准差,分母n);MySQL 5.7+/PostgreSQL中STDDEV默认等价于STDDEV_SAMP,但Oracle及旧版MySQL可能指向STDDEV_POP。

STDDEV 和 STDDEV_SAMP 有什么区别?
直接说结论:日常算波动率,用 STDDEV_SAMP;如果数据就是全体、不抽样(极少见),才用 STDDEV_POP。MySQL 5.7+ 和 PostgreSQL 默认的 STDDEV 是 STDDEV_SAMP 的别名,但 Oracle 和旧版 MySQL 里 STDDEV 可能指向 STDDEV_POP —— 这是第一个容易翻车的地方。
关键差异在分母:STDDEV_SAMP 除以 n-1(样本标准差),STDDEV_POP 除以 n(总体标准差)。分组内只有 1 条数据时,STDDEV_SAMP 返回 NULL(数学上无定义),而 STDDEV_POP 返回 0 —— 如果你没意识到这点,后续除零或空值传播会悄悄污染结果。
- 查清你用的数据库版本和文档,别默认
STDDEV就是样本标准差 - 显式写
STDDEV_SAMP比依赖别名更安全 - 分组前先
COUNT(*)看看每组是否至少有 2 行,避免大量NULL
按分组算波动率,为什么结果全是 NULL?
常见现象:写了 GROUP BY category,再套 STDDEV_SAMP(amount),但输出里波动率列全是 NULL。大概率是某几组只有一条记录,或者 amount 字段本身含大量 NULL —— STDDEV_SAMP 会自动忽略 NULL 值,但如果过滤后只剩 0 或 1 个有效值,结果就是 NULL。
别急着改逻辑,先加个检查:
SELECT category, COUNT(*) cnt, COUNT(amount) non_null_cnt, STDDEV_SAMP(amount) stddev FROM sales GROUP BY category;
-
COUNT(*)和COUNT(amount)不等,说明该组有NULL值混入 - 若
non_null_cnt < 2,STDDEV_SAMP必为NULL - 想把单值组的波动率设为 0,得用
CASE WHEN COUNT(amount) > 1 THEN STDDEV_SAMP(amount) ELSE 0 END
波动率要归一化(变异系数),怎么防除零?
单纯的标准差受量纲影响,比如销售额单位是“万元”还是“元”,数值差一千倍。真正反映相对波动的,是变异系数:STDDEV_SAMP(x) / AVG(x)。但 AVG(x) 可能为 0(比如某组所有值都是 0),直接除会报错或得 NULL。
不能只靠 COALESCE(AVG(x), 1) 硬兜,那会扭曲业务含义。稳妥做法是分层判断:
- 先用
HAVING COUNT(amount) > 1排除无效分组 - 再用
CASE WHEN ABS(AVG(amount)) < 1e-6 THEN NULL ELSE STDDEV_SAMP(amount)/AVG(amount) END防浮点零 - 如果业务允许,把分母换成
NULLIF(AVG(amount), 0),让除零直接产出NULL,比报错更可控
窗口函数里嵌套 STDDEV_SAMP 为啥报错?
想看每个订单在其所属类目下的“偏离程度”,写 STDDEV_SAMP(amount) OVER (PARTITION BY category),结果提示“aggregate function calls cannot be nested”。这是因为 STDDEV_SAMP 本身是聚合函数,不能直接放进窗口函数——你得换思路。
正确解法是两步走:先用子查询或 CTE 算出各分组的 STDDEV_SAMP 和 AVG,再和原表 JOIN;或者用现代数据库支持的「窗口聚合」语法(PostgreSQL 14+、MySQL 8.0.23+):
SELECT *,
STDDEV_SAMP(amount) OVER (PARTITION BY category) AS cat_stddev,
AVG(amount) OVER (PARTITION BY category) AS cat_avg
FROM sales;
- 老版本 MySQL 不支持在窗口中直接用
STDDEV_SAMP,必须拆成两层查询 - 注意窗口函数里的
STDDEV_SAMP默认仍是样本标准差,无需额外指定 - 如果数据量大,两层 JOIN 可能比窗口慢,优先确认执行计划是否走了索引

















