PostgreSQL插件之hll
背景
在现实生活中,我们通常需要统计一个集合中不重复的元素个数。在数据库中通过使用如下的SQL语句进行精确统计:
CREATE TABLE tbl(id INT, a INT);
INSERT INTO tbl VALUES(1, 1), (2, 1), (3, 2), (4, 2), (5, 2), (6, 3);
SELECT COUNT(DISTINCT a) FROM tbl;
count
-------
3
(1 row)
为了计算 count-distinct 通常使用哈希或者排序的方式计算不重复元素个数。但是,如果数据量比较大,哈希或者排序消耗的内存较大。HyperLogLog 是处理这个问题的一个概率算法,但是其消耗资源较少。
postgresql-hll插件引入了一个新的数据类型hll,它是一个HyperLogLog数据结构。下面对其使用进行详细介绍。
支持版本
- PostgreSQL 12
- PostgreSQL 11
- PostgreSQL 10
- PostgreSQL 9.6
- PostgreSQL 9.5
安装
在华为云使用postgresql-hll,请参考:https://support.huaweicloud.cn/usermanual-rds/rds_09_0043.html 进行安装和卸载。
使用
CREATE TABLE hll_tbl (
id integer,
set hll
);
--- 插入空的HLL
INSERT INTO hll_tbl(id, set) VALUES (1, hll_empty());
--- 增加一个被散列的整数值到HLL中
UPDATE hll_tbl SET set = hll_add(set, hll_hash_integer(12345)) WHERE id = 1;
--- 增加一个被散列的字符串到HLL中
UPDATE hll_tbl SET set = hll_add(set, hll_hash_text('hello world')) WHERE id = 1;
--- 计算HLL的基数
SELECT hll_cardinality(set) FROM hll_tbl WHERE id = 1;
数据仓库示例
我们假设有一个事实表,记录了用户对我的站点的访问、他们做了什么以及他们来自哪里。表中有上亿行数据,表扫描需要几分钟(或者至少需要很多秒)。
CREATE TABLE facts (
date date,
user_id integer,
activity_type smallint,
referrer varchar(255)
);
insert into facts select timestamp '2014-01-10 10:00:00' +
random() * (timestamp '2021-01-20 20:00:00' - timestamp '2014-01-10 10:00:00'),
generate_series(1, 10000000), 1, 'no use';
insert into facts select timestamp '2014-01-10 10:00:00' +
random() * (timestamp '2021-01-20 20:00:00' - timestamp '2014-01-10 10:00:00'),
generate_series(1, 10000000), 1, 'no use';
如果想快速(毫秒级别)知道每天有多少独立用户访问了站点,可以建立一个聚合表:
CREATE TABLE daily_uniques (
date date UNIQUE,
users hll
);
INSERT INTO daily_uniques(date, users)
SELECT date, hll_add_agg(hll_hash_integer(user_id))
FROM facts
GROUP BY 1;
我们首先对user_id进行散列,然后按天将这些散列后的值聚合为一个hll。现在我们可以计算每天的hll基数:
SELECT date, hll_cardinality(users) FROM daily_uniques;
如果想要得到这周独立的用户访问数呢?
SELECT hll_cardinality(hll_union_agg(users))
FROM daily_uniques
WHERE date >= '2012-01-02'::date AND date <= '2012-01-08'::date;
操作符
插件中已经添加了一些操作符,使用hll变得不那么冗长。它们是最常用函数的简单别名。
散列函数
- hll_hash_boolean(boolean)
- hll_hash_smallint(smallint)
- hll_hash_integer(integer)
- hll_hash_bigint(bigint)
- hll_hash_bytea(bytea)
- hll_hash_text(text)
- hll_hash_any(any)
示例如下:
SELECT hll_hash_boolean(TRUE);
SELECT hll_hash_boolean(TRUE, 123/*hash seed*/);
SELECT hll_hash_smallint(4::smallint);
SELECT hll_hash_smallint(4::smallint, 123/*hash seed*/);
SELECT hll_hash_integer(21474836);
SELECT hll_hash_integer(21474836, 123/*hash seed*/);
SELECT hll_hash_bigint(223372036854775808);
SELECT hll_hash_bigint(223372036854775808, 123/*hash seed*/);
SELECT hll_hash_bytea(E'\\xDEADBEEF');
SELECT hll_hash_bytea(E'\\xDEADBEEF', 123/*hash seed*/);
SELECT hll_hash_text('foobar');
SELECT hll_hash_text('foobar', 123/*hash seed*/);
SELECT hll_hash_any(123);
SELECT hll_hash_any(123, 123/*hash seed*/);
注意:hll_hash_any会动态地分派给适当的特定类型的函数,这使得它比它包装的特定类型的函数要慢。只有在不知道输入类型时才使用它。
聚集函数
如果要从表或结果集中创建一个hll,请使用hll_add_agg。这里的命名并不是特别有创意:它是一个聚合函数,将值添加到空的hll中。
SELECT date, hll_add_agg(hll_hash_integer(user_id))
FROM facts
GROUP BY 1;
上面的示例将为每个包含每天用户的日期提供一个hll。如果您想汇总已经存储到单个hll中的一个hll列表,请使用hll_union_agg。再次说明:它是一个聚合函数,将值合并到一个空的hll中。
SELECT EXTRACT(MONTH FROM date), hll_cardinality(hll_union_agg(users))
FROM daily_uniques
GROUP BY 1;
窗口是hll功能的另一个主要例子。执行滑动窗口惟一计数通常涉及一些generate_series技巧,对于已经计算过的滑动窗口则非常简单。
SELECT date, #hll_union_agg(users) OVER seven_days
FROM daily_uniques
WINDOW seven_days AS (ORDER BY date ASC ROWS 6 PRECEDING);
- 点赞
- 收藏
- 关注作者
评论(0)