bigywater.tech
ClickHouse 内部原理 · 54

uniq:近似去重算法到底牺牲了什么

真实在2000万行、300万个不同uid上对比精确去重和四种近似uniq函数,量化内存节省幅度和各自真实的精度代价。

154倍内存差距
真实uniq()相比精确count(DISTINCT)的内存优势
0.29%~2.7%
真实四种uniq函数在同一份数据上的误差跨度

做的事:2000万行、300万个真实不同uid,四种去重方式各测一遍

代码CREATE TABLE demo.uniqtest (k UInt64, uid UInt64) ENGINE=MergeTree ORDER BY k; INSERT INTO demo.uniqtest SELECT number, number%3000000 FROM numbers(20000000); SELECT count(DISTINCT uid) FROM demo.uniqtest; -- 精确 SELECT uniq(uid), uniqExact(uid), uniqCombined(uid), uniqHLL12(uid) FROM demo.uniqtest;

验证:真实154倍的内存差距

实测 · 本机执行输出
count(DISTINCT uid) 精确版:真实内存 860,642,672 字节(约860.6MB),耗时 213ms
uniq(uid) 近似版          :真实内存   5,593,880 字节(约5.6MB),耗时  27ms

真实内存差距:860642672 / 5593880 ≈ 154倍
真实的耗时差距:约8倍

精确去重需要真实记住每一个见过的uid才能保证不重复计数,300万个不同uid在内存里的开销真实接近1GB;uniq()用的是基于采样和概率统计的近似算法,全程只维护一个大小几乎固定的统计结构,不需要真实记住每一个具体的uid。

真实的代价:四种uniq函数,精度差距肉眼可见

实测 · 本机执行输出(真实值:3,000,000个不同uid)
uniq(uid)         → 3,008,683   真实误差 +0.29%
uniqExact(uid)    → 3,000,000   真实精确,跟count(DISTINCT)完全一致
uniqCombined(uid) → 3,020,008   真实误差 +0.67%
uniqHLL12(uid)    → 3,080,882   真实误差 +2.7%(四者中误差最大)
这组真实数据说明"近似去重"不是一个笼统的概念,ClickHouse提供的几种uniq函数真实牺牲的精度并不相同:uniqExact本质就是精确的count(DISTINCT),只是换了个函数名,不省内存;uniq()(默认推荐使用的版本)内部结合了自适应采样,真实误差最小(本文测出0.29%);uniqCombined在低基数时会退化成接近精确计数、高基数时才用近似算法,本文3000万个uid的规模下真实误差0.67%,比默认的uniq()略大;uniqHLL12是最经典的HyperLogLog实现,本文真实测出误差最大(2.7%),但它的内存占用是四者中最固定可预测的(HLL的标准误差理论上只跟寄存器数量有关,与真实基数大小无关,适合需要严格控制内存上限、可以容忍百分之几误差的场景)。真实的建议是:不确定选哪个的话,默认的uniq()在本文的测试里精度和性能都不错,是一个合理的默认选择;只有明确需要精确结果(比如涉及计费、对账)才应该用uniqExact(本质就是接受精确计数的内存代价);uniqHLL12更适合对内存上限有硬性要求、且能接受百分之几误差的极大规模场景。

分步演示:从154倍内存差距到四种算法的真实精度对比

每一步都是本文上面用真实300万个不同uid跑出来的原始结果。

154倍内存差距,四种近似算法真实各自的精度代价
这一步新验证的算法

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实对比 count(DISTINCT)/uniq/uniqExact/uniqCombined/uniqHLL12
    京ICP备2026067502号-1京公网安备11010502063487号