近日,一项关于数据库函数行为差异的技术发现引发开发者社区关注:在部分主流数据库系统中,使用 APPROX_COUNT_DISTINCT 函数进行近似去重计数时,会将数值 -0.0 与 0.0 视为同一个值;而使用 GROUP BY 进行分组统计时,两者却会被区分对待,导致两种查询方式返回截然不同的计数结果。这一看似边缘的细节,暴露出浮点数表示与聚合语义之间长期存在的深层矛盾。
问题重现:同一列数据,两种统计口径
技术专家通过一个简单示例演示了该问题。假设一张表中含有以下数值列:
0.0, 0.0, -0.0, 1.0
若使用 GROUP BY 分组查询:
SELECT val, COUNT(*) FROM t GROUP BY val;
结果为:
0.0 → 2
-0.0 → 1
1.0 → 1
即 GROUP BY 将 -0.0 和 0.0 视为两个不同的分组,分别计数。
然而,若改用 APPROX_COUNT_DISTINCT 统计该列不同值的数量:
SELECT APPROX_COUNT_DISTINCT(val) FROM t;
结果返回 3(而不是 4),因为该近似算法将 -0.0 和 0.0 合并为同一个唯一值。若按 GROUP BY 的语义,不同值应为 4(0.0、0.0 重复、-0.0、1.0)。
这种不一致意味着,用户在同一个数据库里,使用不同查询方式会得出互相矛盾的“不同值数量”结论,在数据分析、报表统计甚至计费审计中可能引发严重偏差。
根源:IEEE 754 浮点数与哈希算法的“零值困境”
为什么会出现这种差异?核心在于浮点数的二进制表示和聚合操作所采用的不同等价判定逻辑。
在 IEEE 754 标准中,-0.0 与 0.0 具有不同的符号位,因此它们在数学上被认为是不同的二进制表示。GROUP BY 在多数数据库实现中基于完整的二进制值(包括符号位)进行分组,因此将二者视为不同组。
而 APPROX_COUNT_DISTINCT 这类近似算法(常见于 Presto、DuckDB、ClickHouse 等系统的 HyperLogLog 或 Count-Min Sketch 实现)通常先将数据通过哈希函数映射为哈希值,再利用哈希值的分布估算基数。许多哈希函数在处理浮点数时,会将 -0.0 与 0.0 归一化为相同的字节序列,或直接依据数值相等逻辑(即 -0.0 == 0.0 为真)进行去重,导致二者共享同一个哈希桶,从而被合并计算。
更值得警惕的是,这种差异并非某一数据库独有的 bug,而是横跨多个系统的普遍行为。测试显示,类似问题同样存在于使用近似去重函数的数仓产品中。许多开发者假设“相同数学值的不同表示应该被视为同一个值”,而另一些开发者则坚持“原始字节不同就应为不同分组”,这反映了系统设计上长期未统一的哲学分歧。
实际影响:从报表异常到金钱损失
在真实业务中,这类问题不容小觑。
金融场景:账户余额为 -0.0 通常代表“负零”,即极小负数(如 -0.0001)经过四舍五入后的结果。若系统需要统计“非零余额账户数量”,使用 GROUP BY 时 -0.0 会被计入非零分组(因为不等于 0.0 的分组),而使用 APPROX_COUNT_DISTINCT 时则可能被计入零值分组,导致风控指标不同。
数据治理:多源数据合并时,来自不同系统的数值可能分别带有 -0.0 和 0.0 表示。如果依赖 APPROX_COUNT_DISTINCT 做“去重后唯一值总量”校验,而下游又用 GROUP BY 做明细分组,那么上下游之间的基数核对永远对不上。
机器学习特征工程:在生成分类特征时,若把 -0.0 与 0.0 映射为不同类别,模型训练和推理阶段使用不同的聚合函数,将导致特征分布偏移,影响模型稳定性。
专家建议:统一数据清洗与显式归一化
面对这一隐患,资深数据工程师建议采取以下措施:
-
提前归一化:在数据写入或查询前,显式将
-0.0统一转换为0.0,避免系统间解释歧义。例如使用CASE WHEN val = 0.0 THEN 0.0 ELSE val END或在 ETL 阶段执行val + 0.0(多数语言中-0.0 + 0.0会得到0.0)。 -
慎用近似函数做精确核对:
APPROX_COUNT_DISTINCT本质上是为超大规模数据提供低误差估算,不应与基于精确分组的GROUP BY结果进行严格一致性校验。若业务要求精确计数,应改用COUNT(DISTINCT col)。 -
在文档层面明确语义:数据库厂商应在函数说明中明确浮点数的相等判定规则,避免用户踩坑。同时,用户可向开源社区提交 issue,推动实现统一处理逻辑。
展望:标准亟待明确
该问题本质上是数据语义(数值相等 vs. 二进制相等)与实现算法(哈希 vs. 精确比较)之间长期割裂的缩影。随着数据湖、实时分析等架构普及,APPROX_COUNT_DISTINCT 的使用愈发广泛。业界需要对浮点零值的处理制定统一规范——究竟是将 -0.0 视为 0.0 的别名,还是保留其独立表示的语义?在 SQL 标准中,-0.0 与 0.0 之间的比较本就返回“相等”,但分组操作又默认“按值分组”。解决这一矛盾,需要数据库设计者在“数学价值”与“存储表示”之间寻找更清晰的平衡点。
对于广大数据从业者而言,这次看似微小的发现再次提醒我们:在大数据应用的每一个角落,浮点数的幽灵始终如影随形。唯有保持对基础细节的敬畏,才能避免在复杂的数仓管道中栽跟头。