导读:本期聚焦于阿里山老登创作的《PostgreSQL慢查询优化:如何用HyperLogLog实现毫秒级近似去重计数?》,敬请观看详情。大数据量下COUNT(DISTINCT)为什么会拖垮PostgreSQL?排序和哈希聚合需要处理全部去重值,内存和CPU开销随基数线性增长。HyperLogLog用固定内存的概率数据结构给出基数估计,误差通常控制在2%以内。本文从PostgreSQL慢查询场景切入,介绍postgresql-hll扩展的安装、hll列维护、近似聚合与合并方法,并对比精确去重与近似计数在千万级数据下的性能差异。通过实际SQL演示如何在不改动业务查询结构的前提下,把秒级甚至分钟级的去重统计压缩到毫秒级,同时明确误差范围与适用边界。

业务统计中的日活用户数、独立访客数往往依赖一条看似简单的SQL:SELECT COUNT(DISTINCT user_id) FROM user_visits WHERE ...。当明细表只有几十万行时,这条查询毫秒返回;但当表增长到千万甚至亿级后,同样的查询可能耗时数十秒,数据库CPU和I/O被严重消耗。PostgreSQL为了精确去重,必须对user_id列做排序或哈希聚合,所有去重值都要放进工作内存,超出work_mem就写临时文件。对于允许小幅误差的统计场景,HyperLogLog提供了一种固定内存的近似计数方案,能把这个过程从秒级压缩到毫秒级。

PostgreSQL慢查询优化:如何用HyperLogLog实现毫秒级近似去重计数?

为什么COUNT(DISTINCT)会成为慢查询

在PostgreSQL中,COUNT(DISTINCT col) 的执行计划通常是Aggregate节点,内部使用哈希表记录已经出现过的值。如果去重基数很大,哈希表无法完全放入work_mem,PostgreSQL会切换到基于磁盘的聚合,产生大量临时文件读写。更糟的是,这种查询无法利用索引只扫描部分数据,必须读取所有满足条件的行。对于用户访问日志这类大表,扫描本身就很耗时,再加上去重排序或哈希计算,最终查询时间可能从几十秒到几分钟。

实际优化时,增加work_mem、调整并行度只能缓解部分压力,无法从根本上消除去重计算的复杂度。只要业务要求精确统计不同user_id的数量,数据库就必须处理全部去重值,复杂度至少是O(n)并伴随额外内存或磁盘开销。如果业务方表示可以接受1%到2%的误差,那么近似计数就成为一个性价比极高的选择。

HyperLogLog正是专门解决基数估计问题的概率数据结构。它用极小的固定内存记录集合的近似势,不需要存储每个元素本身,因此处理速度和存储开销都远低于精确去重。接下来从原理说明为何它能做到这一点。

HyperLogLog如何用固定内存估计基数

HyperLogLog的核心思想是把每个元素先哈希成一个64位整数,然后观察哈希值二进制表示中从低位或高位开始的连续零的个数。连续零的个数越多,说明出现这种模式所需要的元素数量越大,这与抛硬币出现连续正面的概率类似。HLL把哈希空间划分成m个桶,每个桶只保留该桶中哈希值前导零的最大值。根据所有桶的平均值,再乘上桶数量和偏差修正系数,就能得到集合基数的估计值。

桶的数量m由精度参数log2m决定,m等于2的log2m次方。PostgreSQL的postgresql-hll扩展默认log2m为11,即2048个桶,整个结构大约只占用1.5KB左右。标准误差约为1.04除以根号m,当m为2048时误差约2.3%。如果提高log2m到16,误差可以降到0.5%以内,但内存会增加到几十KB。这里的内存是固定值,与原始数据量完全无关,这是它相比精确去重最大的优势。

另一个重要特性是可合并性。两个HLL结构可以通过并集操作合并,合并后的HLL能估计两个集合并集的基数。这非常适合按天存储UV,再合并成周UV、月UV,而不需要保留每天的去重用户ID列表。合并操作在内存中只需一次桶最大值比较,开销极低。

在PostgreSQL中启用HyperLogLog

PostgreSQL本身不含HLL类型,需要安装postgresql-hll扩展。在Debian或Ubuntu上,可以通过apt install postgresql-14-hll或者对应版本包安装,也可以从源码编译。安装完成后,在数据库中执行CREATE EXTENSION hll;即可使用hll类型和相关函数。部分云数据库如果未提供该扩展,可能需要联系云厂商或使用第三方镜像。

设计统计表时,通常不把hll列直接放在明细表上,因为每次插入明细都更新hll会带来写放大。更常见的方式是保留原始访问日志表,另建一张按天汇总的统计表,用hll列保存当天用户ID的近似集合。可以定期通过调度任务批量重建,或在应用层双写维护。下面是一套完整示例。

-- 创建扩展
CREATE EXTENSION IF NOT EXISTS hll;

-- 用户访问明细表
CREATE TABLE user_visits (
    id bigserial PRIMARY KEY,
    user_id bigint NOT NULL,
    visit_time timestamp NOT NULL
);

-- 按天统计UV,user_hll列保存用户ID近似集合
CREATE TABLE daily_uv (
    stat_date date PRIMARY KEY,
    user_hll hll NOT NULL
);

-- 批量构建当天HLL,WHERE条件中的比较符号已转义
INSERT INTO daily_uv (stat_date, user_hll)
SELECT current_date, hll_add_agg(hll_hash_bigint(user_id))
FROM user_visits
WHERE visit_time >= current_date::timestamp
  AND visit_time < (current_date + 1)::timestamp;

-- 查询当天近似UV
SELECT stat_date, hll_cardinality(user_hll) AS uv
FROM daily_uv
WHERE stat_date = current_date;

-- 合并最近7天
SELECT hll_cardinality(hll_union_agg(user_hll)) AS weekly_uv
FROM daily_uv
WHERE stat_date >= current_date - 6;

上面的hll_hash_bigint负责把bigint类型的user_id哈希成HLL可处理的64位值,hll_add_agg是聚合函数,将一批哈希值添加进同一个HLL结构,hll_cardinality返回近似基数。如果用户ID是字符串,可以使用hll_hash_text。日常查询只需读取daily_uv中几KB的hll列,速度极快。

如果不想每天重建,也可以在明细表上建触发器,每当插入一行访问记录,就用hll_add更新对应日期的hll列。这种方式会稍微增加写入开销,但查询端可以随时拿到实时近似UV。对于并发写密集的场景,可以把更新放进队列异步执行。

性能对比与适用场景

在千万级用户访问日志上,精确的COUNT(DISTINCT user_id)通常需要几秒甚至几十秒,具体取决于硬件和work_mem设置。而预先维护的HLL统计表查询只需读取一个或几个hll列,执行hll_cardinality计算,整个响应时间在毫秒级。即便需要合并过去30天的数据,也只是合并30个几KB的结构,开销可以忽略。

当然,近似计数不是银弹。默认精度下误差约2%,如果某个用户的访问数据刚好落在哈希边界附近,估算值可能略高或略低。对于要求绝对准确的场景,比如财务对账、订单数量、支付笔数,仍然必须使用精确COUNT。HLL更适合用于运营分析、风控监控、趋势看板等允许小幅误差的场景。如果对误差要求更高,可以通过提高log2m参数降低误差,但会增加存储空间。

使用HLL前还需要注意扩展兼容性以及数据保留策略。原始明细表仍应保留,以便需要精确回算或审计时使用。HLL列不适合用来判断某个用户是否访问过,它只能回答大约有多少个不同的用户。另外,HLL结构一旦生成就不可逆,无法提取原始用户ID,因此不能替代去重明细表。只要把这些边界梳理清楚,HyperLogLog就能成为PostgreSQL慢查询优化中非常实用的工具。

PostgreSQL慢查询优化HyperLogLog近似计数修改时间:2026-09-25 20:18:36

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0925/61845.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。