背景
最近使用impala做查询的时候,遇到一个sql 中使用多个列 count(distinct) 查询导致报错的情况,报错内容如下:
org.apache.hive.service.cli.HiveSQLException: AnalysisException: all DISTINCT aggregate functions need to have the same set of parameters as count(DISTINCT ); deviating function: count(DISTINCT ) Consider using NDV() instead of COUNT(DISTINCT) if estimated counts are acceptable. Enable the APPX_COUNT_DISTINCT query option to perform this rewrite automatically.
上面的报错,简单来说就是count(distinct column)需要换成NDV(column)。
使用方法
如果设置如下参数:
set APPX_COUNT_DISTINCT=true;
则所有的count(distinct col)会在底层计算的时候转成 ndv() 函数,也就是说,在sql中可以直接使用count(distinct col)
如果不配置上述参数,则在sql 中直接写 ndv(col) 也可以
误差精度
由于ndv采用的是 HyperLogLog (HLL) 算法来对数据进行估算,所以会存在一定的误差。之所以使用 HLL算法,是因为其带来的好处是 极大的速度提升,以及很小的计算内存。
通过官方文档可以知道:
impala4.0.0之后,可以使用scala 参数控制计算精度,使用方法为 ndv(col, scala), 其中scala 为 1-10的整数
其它版本或者不写的情况下改参数默认值为 10.
scala值越大,精度也高,但是所带来的内存消耗越大,占用的内存计算公式是 2^(8+scala) bytes, 当scala = 10 的时候,大约占用内存 0.25 M。
当scala=10 时, 最坏误差为 0.42%,平均误差为 0.17%,占用内存 256KB(测试用一组10亿条整数 的数据集)
官方还给了一些测试案例:
select count(distinct col1) from sample_data;
±--------------------+
| count(distinct col1)|
±--------------------+
| 100000 |
±--------------------+
Fetched 1 row(s) in 20.13s
select cast(ndv(col1) as bigint) as col1 from sample_data;
±---------+
| col1 |
±---------+
| 139017 |
±---------+
Fetched 1 row(s) in 8.91s
误差将近 39% !!! 可见这个函数并不适用于数据量比较小 或者 数据要求完全准确 的情况。
参考官方文档:
https://impala.apache.org/docs/build/html/topics/impala_ndv.html
https://issues.apache.org/jira/browse/IMPALA-10538
替换方案
上一点误差精度中提到,对于 要求数据准确的场景,ndv 并不适合,那怎么办呢? 这里提供一种解决思路:
假设有一张student 表,有 name、age、…等多个字段,没有主键,求 不同年龄段的学生数。
sql如下:
select sum(age_0), sum(age_1), sum(age_2), sum(age_3)
from (
SELECT 1 age_0
, max(case when age <= 10 then 1 end) age_1
, max(case when age > 10 and age <= 20 then 1 end)age_2
, max(case when age > 20 and age <= 30 then 1 end)age_3
from student
)
上面的思路就是先通过 group by 求max,再取sum
原理
通过官网文档,ndv使用的是 HyperLogLog (HLL) 来进行计算的,这是一个通过概率来估算唯一值的算法,所以天然会产生误差。那HLL是何方算法呢?
为了更好地了解这个算法,先通过一个故事了解下其中的精髓:
收集电话号码 (Collecting phone numbers)
我和 汤米 一起参加一个会议,当了场地之后,我俩决定打个赌:看谁遇到的新人最多! 然后我们开始 四处闲聊,并统计我们遇到了多少人。
会议结束后,汤米走到我的身边,比了个手势,说 17. 然后我跟他说,我与 47 人聊过天, 显然我赢了 !
但是 汤米 有点沮丧,因为他始终认为我 多次 跟相同的人聊过天,他坚信我总共大约跟 15-17个人聊过天。所以,赌约失效了。
我们决定在,下一次的活动中,记下我们交流过的人的姓名,以确保我们在计算的是唯一身份的人,而不只是对话的总数。在最后碰头的时候,我们都拿着一个很长的名字清单, 汤米比我多遇到了两次! 我们笑了起来,在讨论计算唯一性的方法时,汤米提出了一个好主意:
“我们不能简单地用笔和纸来统计不同的名字统计,这是不切实际的! 今天,我与65位不同的人进行了交谈,而在纸上统计他们的名字实在是很痛苦。 我漏掉了3次,所以不得不从头开始!”
我说: “是的,我知道,但是我们还有其他选择吗?”
汤米: “如果在下一次会议上,我们不问姓名,而是问人们电话号码的后5位如何? 获胜的条件是 手上号码数中 在前面 0 最多的人 ”
我 : “等等汤米,你太快了! 慢一点,给我一个例子……”
汤米: “当然,我们只要问每个人的后5位数字,好吗? 假设他们回答“ 54701”。 没有前导零,因此前导零的最长序列是0。您与之交谈的下一个人说 ‘02561’ 这是前导零! 因此,您最长的序列现在是1。”
我: “现在,假设您告诉我您最长的零序是5-您必须已经与成千上万的人交谈,以找到电话号码为00000的人!”
end.
这就是 HLL 简单的工作原理。
大概计算步骤如下:
1、计算 输入值得 hash,得到比特串(由 1 和 0 组成)
2、通过第一个1出现位置的最大值,来预估总共有多少个不同的数字(整体基数)。
为了更好控制误差,采用了分桶平均的思想 以及 偏差修正 来消减误差。
详细的介绍见:https://en.wikipedia.org/wiki/HyperLogLog
参考:https://blog.csdn.net/cumian8165/article/details/108159272