postgresql-hll 是基于 HyperLogLog 算法的 PostgreSQL 扩展。它引入了自定义数据类型 hll,用于以极低的存储成本和极快的计算速度解决海量数据(如亿级甚至更大规模)的基数估算(Count Distinct)问题。
- 高效的存储与性能:hll 数据对象占用的存储空间非常小且固定(通常小于 16KB),不再随原始数据量的增长而线性增加。计算速度极快,通常能在毫秒级完成。
- 原生的聚合能力:支持对 hll 对象进行任意维度的无损合并(Union),例如可以轻松地将每日的用户访问记录(DAU)合并为每周(WAU)或每月(MAU)的去重用户数,而无需重新扫描原始数据。
- 可靠的估算精度:默认配置下的标准误差约为 0.8%,能够在准确性和资源消耗之间取得良好平衡。插件还内置了稀疏(sparse)和密集(dense)两种存储模式,并能根据数据量自动切换,以优化存储效率。
- 网站/App 活跃度统计:计算日活跃用户(DAU)、月活跃用户(MAU)等核心指标。
- 大规模去重计数:统计海量日志中的独立 IP 数、不同商品/内容的访问用户数等。
- 实时数据分析:利用其高效的合并特性,进行滑动窗口分析,如计算“过去 7 天的滚动 UV”。
在使用 hll 扩展前,请务必了解并评估以下关键限制,以避免在生产环境中出现意外情况。
- hll 提供的是基数的估算值,而非精确值。虽然误差率很低,但它本质上是一种概率统计模型。因此,严禁将其用于任何要求 100% 精确的场景,例如财务记账、库存管理、订单计数等。
- hll 对象存储的是原始数据经过哈希计算后的摘要信息,无法从中还原出任何原始值。例如,您无法从一个聚合了用户 ID 的 hll 对象中恢复出具体的 User ID 列表。它仅用于“计数”,而非“追溯”。
- 如果您的数据表中包含了 hll 类型的列,那么该表的读取和写入将强依赖于 hll 插件。如果卸载该插件(DROP EXTENSION hll;),所有 hll 列的数据将变得不可识别(显示为乱码或错误),这等同于数据丢失。
- hll 对象的合并(Union)操作要求它们的哈希配置完全一致。如果您在不同系统或环境(例如,在 Java 服务端计算哈希值,在 PostgreSQL 中聚合)中使用 hll,必须确保两端使用的哈希算法与种子(Seed)完全相同。否则,生成的 hll 对象将无法合并,计算出的并集结果也将是错误的。
- 在使用 DTS(数据传输服务)或其他工具进行数据库迁移、同步或备份恢复时,必须确保目标数据库实例已提前安装了 hll 插件。否则,在数据导入时,hll 类型的数据列会因类型无法识别而导致迁移失败或数据损坏。
在需要使用 hll 功能的数据库中,执行以下 SQL 命令来安装扩展。
安装成功后,您可以通过查询 pg_extension 系统表来确认。
如果查询有返回结果,则说明插件已成功安装并可以使用。
警告
卸载插件会导致所有 hll 类型的数据列失效,数据将无法读取。在执行此操作前,请务必确认已没有业务依赖 hll 数据,或已完成数据迁移。
如果您确认不再需要 hll 插件,可以执行以下命令进行卸载。
postgresql-hll 插件的核心工作流通常包含三个步骤:
- 哈希(Hash):将需要去重计数的原始值(如用户 ID、IP 地址等)通过哈希函数转换为哈希值。
- 聚合(Aggregate):将这些哈希值添加入一个 hll 数据对象中进行聚合。
- 计算(Calculate):通过 hll 对象估算基数(去重数),或将多个 hll 对象合并以计算并集。
hll 插件不直接处理原始数据,而是处理其哈希值。最常用的方式是结合使用 hll_hash_* 系列函数和 hll_add_agg 聚合函数。
示例:按天统计独立访客(UV)
假设我们有一张记录用户访问行为的原始日志表 page_views:
现在,我们希望预先计算每天的 UV 数据,并存入一张结果表 daily_uniques 中。
- 首先,创建一个用于存储每日 UV 数据的表,其中 users 列的类型为 hll。
- 从 page_views 表中读取数据,使用 hll_hash_text 将 user_id 转换为哈希值,然后使用 hll_add_agg 按天聚合生成 hll 对象,并插入到 daily_uniques 表中。
说明
hll 提供了多种哈希函数以支持不同数据类型,例如 hll_hash_bigint、hll_hash_int、hll_hash_bytea 等。请根据您的数据类型选择合适的哈希函数。
当数据以 hll 类型存储后,可以使用 hll_cardinality 函数来获取其估算的基数(即去重后的数量)。
示例:查询某一天的 UV
无损合并是 postgresql-hll 最强大的功能之一。您可以将存储在不同行(例如不同日期的记录)的 hll 对象进行合并,从而计算出总时间范围内的去重数,而无需重新扫描海量的原始数据。
示例:计算月活跃用户(MAU)
使用 hll_union_agg 函数,可以将一个月内所有天数的 hll 对象合并成一个,然后计算其总基数。
由于 hll 的合并操作性能极高,它非常适合用于计算“滚动时间窗口”内的指标,例如“过去 7 天的滚动活跃用户”。
示例:计算 7 日滚动 UV
通过结合窗口函数(Window Function)和 hll_union_agg,可以高效地实现滑动窗口计算。
在创建 hll 对象时,插件会使用一套默认参数(如 log2m=11, regwidth=5 等),这套参数决定了 hll 对象的精度和存储大小。在绝大多数场景下,默认配置(误差率约为 ±0.8%)已经提供了最佳的性能和精度平衡,用户无需手动调整。
- hll_empty():此函数创建一个基于默认参数的空 hll 对象。
- hll_add_agg(...):此聚合函数同样基于默认参数来构建 hll 对象。
警告
具有不同参数配置的 hll 对象之间无法进行合并(Union)操作。为了确保数据的可合并性,强烈建议在整个数据库中始终使用统一的默认配置,避免在创建或聚合时手动指定不同的参数。
- 问:为什么 hll 计算的结果和精确 COUNT(DISTINCT ...) 的结果有偏差?答: 这是正常现象。hll 是一种概率性数据结构,其设计目标是以极小的存储空间换取一个足够精确的估算值。它本身存在固有的统计误差(默认配置下约为 ±0.8%)。如果您需要 100% 的精确结果,请使用传统的 COUNT(DISTINCT ...),但需要承受其带来的巨大计算和内存开销。
- 问:为什么两个 hll 对象无法合并(Union)?答: 合并失败通常是因为两个 hll 对象的内部配置参数不一致。例如,一个 hll 对象在创建时指定了非默认的 log2m 值,而另一个使用了默认值。请确保所有参与合并的 hll 对象都使用完全相同的配置。最简单的做法是,始终使用 hll_empty() 和 hll_add_agg 的默认行为,不要手动指定参数。
- 问:为什么 hll 数据列显示为乱码或查询报错?答: 这种情况最常见的原因是当前的数据库连接中 hll 插件未安装或未启用。请执行 CREATE EXTENSION IF NOT EXISTS hll; 来确保插件可用。另一种可能是插件被意外卸载,这将导致数据无法恢复,请参见“注意事项”中的风险说明。
- 问:数据迁移(DTS)后,hll 列的数据为什么无法使用?答: 这通常是因为在数据导入到目标库时,目标库尚未安装 hll 插件。数据迁移工具无法识别 hll 类型,可能导致数据被错误地当作普通二进制数据导入。请务必在迁移任务开始前,在目标数据库中提前安装好 hll 插件。