利用hll数据类型进行数据统计
HyperLogLog
使用场景
在应用中统计去重后的个数,传统的方法通常是这么操作 count(distict(xxx))。 如果数据量变大,或统计频繁,性能会越来越差。
可以考虑一个近似统计计算方法hll
create extension hll;
HyperLogLog 是一种算法, 可以用来估算数据集的基数. 基数是指一个集合中不同值的数目, 等同于 COUNT(DISTINCT field)
返回值. 对于超大数据集来说, 精确的基数统计往往需要消耗大量的内存与时间, 并且消耗的内存与时间会随着数据集基数的增加而成比例增加. 而 HyperLogLog 能够在常数级的内存与时间下, 以极低的误差来获取数据集基数的近似统计.
简单例子
-- 商店的到访统计
CREATE TABLE store_visitors (
store_id integer, -- 商店ID
distinct_visitors hll -- 不重复的客户数
);
-- 新店铺注册
INSERT INTO store_visitors(id, set) VALUES (1, hll_empty());
-- 模拟客户到访过程
UPDATE store_visitors SET set = hll_add(set, hll_hash_integer(12345)) WHERE id = 1;
UPDATE store_visitors SET set = hll_add(set, hll_hash_integer(12346)) WHERE id = 1;
UPDATE store_visitors SET set = hll_add(set, hll_hash_integer(12345)) WHERE id = 1;
-- 统计结果
SELECT hll_cardinality(set) FROM store_visitors WHERE id = 1;
hll 数据结构只使用很少的内存,而且不会随着数据量的增加性能下降。可以用来进行实时统计。
更实用的例子
创建测试数据表及数据
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
user_id bigint,
org jsonb,
created_at timestamp
);
\COPY github_events FROM events.csv CSV
数据源 https://examples.citusdata.com/events.csv
传统统计
-- 每分钟 最小粒度 events数量
CREATE TABLE github_events_rollup_minute
(
created_at timestamp,
event_count bigint
);
-- 载入数据
INSERT INTO github_events_rollup_minute(
created_at,
event_count
)
SELECT
date_trunc('minute', created_at) AS created_at,
COUNT(*) AS event_count
FROM github_events
GROUP BY 1
一些限制
- 不可以进行distinct统计
- 不通维度的统计都需要建表。复杂度高。如按时间统计,按事件类型统计,按时间和事件类型综合统计。
使用hll 统计
得益于hll 可以对两个或多个集合进行合并操作。
-- 一张表搞定多维统计
CREATE TABLE github_events_rollup_minute(
created_at timestamp,
event_type text,
distinct_user_id_count hll
);
-- 载入数据
INSERT INTO github_events_rollup_minute(
created_at,
event_type,
distinct_user_id_count
)
SELECT
date_trunc('minute', created_at) AS created_at,
event_type,
hll_add_agg(hll_hash_bigint(user_id))
FROM github_events
GROUP BY 1, 2;
-- 按时间维度统计
SELECT
EXTRACT(HOUR FROM created_at) AS hour,
hll_cardinality(hll_union_agg(distinct_user_id_count)) AS distinct_count
FROM
github_events_rollup_minute
WHERE
created_at BETWEEN '2016-12-01 00:00:00'::timestamp AND '2016-12-01 23:59:59'::timestamp
GROUP BY 1
ORDER BY 1;
hour | distinct_count
-------+------------------
5 | 10598.637184899
6 | 17343.2846931687
7 | 18182.5699816622
8 | 12663.9497604266
(4 rows)
-- 按事件类型维度统计
SELECT
EXTRACT(HOUR FROM created_at) AS hour,
hll_cardinality(hll_union_agg(distinct_user_id_count)) AS distinct_push_count
FROM
github_events_rollup_minute
WHERE
created_at BETWEEN '2016-12-01 00:00:00'::timestamp AND '2016-12-01 23:59:59'::timestamp
AND event_type = 'PushEvent'::text
GROUP BY 1
ORDER BY 1;
hour | distinct_push_count
------+---------------------
5 | 6206.61586498546
6 | 9517.80542100396
7 | 10370.4087640166
8 | 7067.26073810357
(4 rows)
分布式数据库
在分布式数据库中进行count(distinct(xxx)) 统计,传统方式实现的逻辑
- 在每个分片上进行distinct的结果进行汇总后,再次count(distinct())。最终只能在一台机器上进行聚合。
- map/reduce 性能较差
citus利用hll 轻松解决
CREATE EXTENSION hll;
SET citus.count_distinct_error_rate TO 0.005;
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
user_id bigint,
org jsonb,
created_at timestamp
);
SELECT create_distributed_table('github_events', 'user_id');
\COPY github_events FROM large_events.csv CSV
-- 开始统计
SELECT
COUNT(DISTINCT user_id)
FROM
github_events;
-- 开始统计
SELECT
COUNT(DISTINCT user_id)
FROM
github_events
WHERE
event_type = 'PushEvent'::text;
参数优化
在分析型数据库PostgreSQL版中, HyperLogLog 的误差与内存消耗量受如下参数控制:
- log2m, 该参数控制着 HyperLogLog 对数据集基数估算的误差为:
1.04 / math.sqrt(2 ** log2m)
. 该参数同时也控制着 HyperLogLog 内存消耗量. - regwidth, 该参数与 log2m 一起决定了 HyperLogLog 内存消耗量最多为
(2 ** log2m) * regwidth / 8
字节. 同时该函数也决定了 HyperLogLog 所能估算数据集基数的最大值.
默认值
hll(log2m=11, regwidth=5, expthresh=-1, sparseon=1)
设置参数
select hll_set_defaults(16,5,-1,1);