暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL HLL插件介绍

晟数学院 2021-04-16
813

点击“蓝字”关注我们


01

前言


HLL是 HyperLogLog 数据结构的简称。PostgresSQL 通过插件的方式引入了这种新的数据类型hll。HyperLogLog 是一个具有固定大小,类似于集合结构,用于可调精度的不同值计数。例如,在1280字节的hll数据结构中,它可以在很小的误差范围内估算出数百亿的不同值计数。


02

算法


hll可以被视为层次结构的不同集合/不同值计数算法的组合,并向上移动该层次结构的规则。为了区分上述描述算法,将其命名为以下:


♠ EMPTY

表示空集的常量值


♠ EXPLICIT

集合中确定的,唯一的,排序完整的整数列表,该列表保持一个固定的基数


♠ SPARSE

HyperLogLog是基于映射的“惰性”实现,是一种基于概率集合的数据结构。仅将非零寄存器的索引和值存储在 map 中,直到非零寄存器的数量超过固定的基数。


♠ FULL

HyperLogLog 是一个完全物化,基于列表的实现。它将每个寄存器的值显式存储在按寄存器索引排序的列表中。


03

基本概念


  • 基数计数

    用来统计一个集合中不重复的元素的个数。

  • 基数计数实现

    假设一个集合为Su,用列举法表示{2,3,1,4,5,9},如果此时有一个新的元素Xi=3要加入到集合Su中。如果Su中包含该元素,那么该元素将不会被加入到集合Su中,否则,加入该元素到集合中,计数值为Su,即基数值为元素的中非相同值的个数。如集合中{1,2,3,5,2},基数为4,因为2是 DV(Distinct Value),不被计算到基数中。


    该实现有两个问题:

  1. 当集合无限增加,元素增多时,相应的存储内存也会无限增长

  2. 当集合无线增加,元素增多,判断是否包含待加入元素的成本也将增加。


04

实现动机


最初扩充原始HLL算法如下:


  • 一般情况下,一个HLL占用 regwidth * 2^log2m 位存储。

  • 典型使用中,log2m = 11 和 regwidth = 5时,将请求需要10240位或者1280个字节。即 5 * 2^11 = 5 * 2048 = 10240

  • 还有一种就是有很多的字节

最初的 HLL 算法的第一个补充来自于实现1280个字节需要160个64位整数的大小。因此,如果希望在低基数下获得更高的准确性,则可以将一组明确的输入保留为64位整数的排序列表,直到达到第161个不同的值为止。这将为提供流中不同值的真实表示,同时需要相同数量的内存。(这是EXPLICIT算法)。


第二个是不需要存储值为零的寄存器。可以简单地将一组具有非零值的寄存器表示为从索引到值的映射。该映射存储为索引值对的列表,这些索引值对是长度为log2m + regwidth的bit-packed的“short words”。(这是SPARSE算法。)


结合这两种增强,得到了一个“promotion hierarchy”,可以对算法进行调整以提高准确性,内存或性能。


初始化和存储新的hll对象将仅分配一个小的小标记值,该值表示空集(EMPTY)。当添加前几个值时,唯一整数的排序列表存储在EXPLICIT集中。当希望停止权衡内存的准确性时,已排序列表中的值将“promoted”为基于SPARSE映射的HyperLogLog结构。最后,当有足够的寄存器时,基于映射的HLL将转换为位打包的FULL HLL结构。


自然地,EMPTY和EXPLICIT表示的基数估计是准确的,而SPARSE和FULL表示的准确性受原始HLL算法提供的保证的约束。


05

安装和使用HLL


解压安装包

    [postgres@pgserver plugin]$ ls
    postgresql-hll-2.15.1 postgresql-hll-2.15.1.tar.gz
    [postgres@pgserver plugin]$ cd postgresql-hll-2.15.1
    [postgres@pgserver postgresql-hll-2.15.1]$ ls
    CHANGELOG.md expected include Makefile REFERENCE.md src
    DEVELOPER.md  hll.control  LICENSE  README.md  sql           update


    编译安装

      [postgres@pgserver postgresql-hll-2.15.1]$ make -j24 && make install


      测试

        [postgres@pgserver postgresql-hll-2.15.1]$ psql -d postgres
        psql (13.2)
        Type "help" for help.


        postgres=# CREATE EXTENSION hll;
        CREATE EXTENSION
        postgres=


        构建数据

          CREATE TABLE helloworld
          (
          id integer,
          set hll
          )
          ;
          --- Insert an empty HLL
          INSERT INTO helloworld
          (id,
          set
          )
          VALUES
          (1,
          hll_empty()
          )
          ;


          --- Add a hashed integer to the HLL
          UPDATE
          helloworld
          SET
          set = hll_add(set, hll_hash_integer(12345))
          WHERE
          id = 1
          ;


          --- Or add a hashed string to the HLL
          UPDATE
          helloworld
          SET
          set = hll_add(set, hll_hash_text('hello world'))
          WHERE
          id = 1
          ;


          --- Get the cardinality of the HLL
          SELECT
          hll_cardinality(set)
          FROM
          helloworld
          WHERE
          id = 1
          ;
          postgres=# SELECT
          postgres-# hll_cardinality(set)
          postgres-# FROM
          postgres-# helloworld
          postgres-# WHERE
          postgres-# id = 1
          postgres-# ;
          hll_cardinality
          -----------------
          2
          (1 row)


          postgres=# SELECT * FROM helloworld ;
          id | set
          ----+------------------------------------------
            1 | \x128b7faaebcf97601e5541533f6046eb7f610e


          从上面的示例中得到,首先构建了一个空的hll集合,然后向该集合中添加了两个值,那么得到的该hll的基数计数就是2。


          接下来看一个更加实用的用例:

          创建网站访问事实表和用户日访问表

            CREATE TABLE facts (
            date date,
            user_id integer,
            activity_type smallint,
            referrer varchar(255)
            );




            CREATE TABLE daily_uniques (
            date date UNIQUE,
            users hll
            );


            然后给网站访问表中插入过去1000天的访问数据(此处由于没有实际的数据,只能模拟过去1000天的数据)

              [postgres@pgserver ~]$ psql -d postgres -f ~/test.sql


              查看表

                postgres=# SELECT count(*) FROM facts ;
                count
                ----------
                 50000000


                5000万数据


                然后根据日期,对user_id进行hash处理,聚合每天用户访问网站的数据到 hll结构中。

                  INSERT INTO daily_uniques(date, users)
                  SELECT date, hll_add_agg(hll_hash_integer(user_id))
                  FROM facts
                      GROUP BY 1;


                  查看表数据

                    postgres=# SELECT count(*) FROM daily_uniques ;
                    count
                    -------
                    1000
                    (1 row)


                    只有1000行数据


                    现在查找一下每天hll的基数计数值

                      postgres=# SELECT date, hll_cardinality(users) FROM daily_uniques LIMIT 10;
                      date | hll_cardinality
                      ------------+-------------------
                      2021-02-06 | 9725.852733707077
                      2021-02-21 | 9725.852733707077
                      2021-02-02 | 9725.852733707077
                      2021-02-08 | 9725.852733707077
                      2021-02-10 | 9725.852733707077
                      2021-02-03 | 9725.852733707077
                      2021-02-14 | 9725.852733707077
                      2021-02-22 | 9725.852733707077
                      2021-02-11 | 9725.852733707077
                       2021-02-20 | 9725.852733707077


                      此刻,可能会想,可以用 COUNT DISTINCT做到基数统计。但是这里只能看到每天多少个唯一身份的用户访问了网站。


                      倘若想要查看每一周的唯一值呢?


                      HLL可以这样处理

                        postgres=# SELECT hll_cardinality(hll_union_agg(users)) FROM daily_uniques WHERE date >= '2018-10-02'::date AND date <= '2018-10-08'::date;
                        hll_cardinality
                        -------------------
                        9725.852733707077
                        (1 row)


                        或者查看一年中的每个月访问情况

                          postgres=# SELECT EXTRACT(MONTH FROM date) AS month, hll_cardinality(hll_union_agg(users))
                          postgres-# FROM daily_uniques
                          postgres-# WHERE date >= '2019-01-01' AND
                          postgres-# date < '2020-01-01'
                          postgres-# GROUP BY 1;
                          month | hll_cardinality
                          -------+-------------------
                          3 | 9725.852733707077
                          7 | 9725.852733707077
                          8 | 9725.852733707077
                          12 | 9725.852733707077
                          5 | 9725.852733707077
                          10 | 9725.852733707077
                          11 | 9725.852733707077
                          9 | 9725.852733707077
                          4 | 9725.852733707077
                          1 | 9725.852733707077
                          2 | 9725.852733707077
                               6 | 9725.852733707077


                          等等。因此,可以得到hll可以很好的来计算DV值,并且很快。


                          关于更多内容,大家可以去访问github网站来获取。



                          推荐阅读

                          PostgreSQL 中的 shared buffer

                          2020-08-28

                          PostgreSQL Libpq 客户端接口

                          2020-10-21



                          文章转载自晟数学院,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                          评论