MySQL 8.0+ 无原生 PRODUCT(),需用 EXP(SUM(LN(ABS(val)))) 处理正数,但必须用 CASE 分类处理零值(直接得0)、负数个数(奇偶定符号)及浮点误差,否则结果静默失真。

MySQL 8.0+ 用 EXP(SUM(LN())) 实现分组累乘积
直接用 SUM() 或 AVG() 那套聚合思路行不通,SQL 标准里没有原生的 PRODUCT() 函数。MySQL 8.0+ 最稳妥的做法是利用对数恒等式:∏xᵢ = EXP(∑LN(xᵢ)),前提是所有值都 > 0。
常见错误是忽略 LN() 对非正数的报错——只要组内任意一个值 ≤ 0,整组就返回 NULL 或报错(取决于 SQL mode)。
- 必须加
WHERE value > 0过滤,或用CASE WHEN处理边界 -
LN(0)和LN(负数)均非法,会中断计算 - 浮点精度误差不可避免,比如
1.2 * 3.4 * 5.6算出来可能是22.847999999999997 - 示例:
SELECT group_id, EXP(SUM(LN(value))) AS product_val FROM t WHERE value > 0 GROUP BY group_id;
PostgreSQL 用 NUMERIC 类型配合 EXP(SUM(LN()))
PostgreSQL 支持 NUMERIC,比 MySQL 的 DOUBLE 更适合做精确乘法,但同样依赖对数换底。关键差异在于:PostgreSQL 的 LN() 对 NUMERIC 入参支持更好,且可配合 ROUND(..., 10) 控制小数位。
注意它不支持 LOG() 直接以任意底数运算,必须用自然对数换底;另外,NULL 值会被 SUM() 自动忽略,但 LN(NULL) 还是 NULL,所以无需额外处理 NULL,但需防 0 和负数。
- 若字段含
NULL,LN()返回NULL,SUM()会跳过该行 —— 行为符合预期 - 想保留含 0 的组?得先转成特殊标记,例如:
CASE WHEN value = 0 THEN -999999 ELSE LN(value) END,后续再识别 -999999 并设 product 为 0 - 示例(带四舍五入):
SELECT group_id, ROUND(EXP(SUM(LN(value))), 10) AS product_val FROM t WHERE value > 0 GROUP BY group_id;
SQL Server 没有 LN(),改用 LOG()(自然对数)并注意版本兼容性
SQL Server 2012+ 的 LOG() 默认就是自然对数(不是常用对数),这点和文档早期描述有出入,实测 LOG(10) ≈ 2.302585,确认是 LN()。但旧版本(如 2008 R2)的 LOG() 是以 10 为底,必须写成 LOG(value) / LOG(2.718281828459) 才等效。
另一个坑是:SQL Server 对 float 类型的累加误差比 MySQL 更明显,尤其当数值量级差异大时(如 1e-5 × 1e8),建议统一转成 decimal 再算对数。
- 务必检查
@@VERSION,避免在老环境用错对数底数 -
LOG(0)报错,LOG(负数)返回NULL(取决于 SET ARITHABORT) - 示例(兼容写法,显式用自然对数):
SELECT group_id, EXP(SUM(LOG(CAST(value AS FLOAT)))) AS product_val FROM t WHERE value > 0 GROUP BY group_id;
遇到负数、零、NULL 怎么办?别硬扛,分类处理更可靠
真实业务数据几乎不可能全为正数。与其在 EXP(SUM(LN())) 里塞一堆 CASE 判断,不如拆解逻辑:先统计符号与零的数量,再决定结果类型。
核心判断链:是否有 0 → 有则积为 0;否则统计负数个数 → 奇数个则积为负,偶数个为正;最后对绝对值做累乘。
- 零值优先级最高:只要组内任一
value = 0,整个 product 就是 0 - 负数个数用
SUM(CASE WHEN value 计算 - 绝对值累乘仍走
EXP(SUM(LN(ABS(value)))),但要确保 ABS 后 > 0(即排除了 0) - 最终表达式会变长,但可读性和健壮性大幅提升,尤其适合报表类场景
实际写的时候,最容易被忽略的是浮点误差累积和数据库版本对 LOG() 底数的定义差异——这两处一旦出错,结果偏差不会报错,只会静默漂移。










