【用户画像】将数据迁移到ClickHouse(源码实现)、位图的介绍(bitmap)、位图在用户分群中的应用、位图的使用

文章目录

  • 一 数据迁移至Clickhouse
    • 1 为何要迁移
    • 2 方案选型
    • 3 任务目标
    • 4 设计分析
    • 5 代码实现
      • (1 )pom.xml
      • (2)配置文件
      • (3)创建库
    • 6 打包发布
  • 二 在clickhouse中宽表转换为Bitmap表
    • 1 为什么用位图(Bitmap)?
      • (1)存储成本
      • (2)天然去重
      • (3)快速定位
      • (4)集合间计算
      • (5)优势场景
      • (6)局限性
    • 2 Bitmap在用户分群中的应用
      • (1)现状
      • (2)传统解决方案
      • (3)更好的方案
    • 3 在clickhouse中使用Bitmap表
      • (1) SQL实现
      • (2) 在clickhouse中使用Bitmap表
        • 建表和数据
        • 数据转换
        • 创建Bitmap表
        • 插入数据
        • 对Bitmap进行查询
          • 对Bitmap进行查询
          • 范围值查询
        • 函数总结

一 数据迁移至Clickhouse

1 为何要迁移

标签计算完成后保存在hive虽然可以查询但是性能非常糟糕。而标签的使用往往是即时的。最常见的场景就是“用户分群”,也称“人群圈选”、“圈人”等等。

分群操作就是根据多个标签组合,产生一个用户集合,供营销、广告等部门使用。而这些操作计算量大,产生结果需要时效性高。

2 方案选型

选择方案最重要的依据就是数据量和时效性要求。

时效性 数据量 分群方案
能接受隔天 无所谓 HIVE宽表
即时产生 千万以下,标签百级 OLAP宽表(Elasticsearch,Clickhouse,Tidb…)
即时产生 亿级,标签千级 Bitmap方式(Clickhouse,doris)

适合的才是最好的,此任务选择用Clickhouse实现Bitmap方式存储。

3 任务目标

把hive中标签宽表数据,写入至Clickhouse的宽表。

4 设计分析

  • 读取hive的宽表,在clickhouse中建立对应的宽表。

    因为并不是hive表到hive表,所以并不能够直接用insert select 解决。

  • 先通过把数据查询成为Dataframe ,再通过行动算子写入至Clickhouse的宽表。

5 代码实现

搭建模块 – task-export-ck

(1 )pom.xml

在poml文件中添加配置

    <dependencies>
        <dependency>
            <groupId>com.hzy.userprofilegroupId>
            <artifactId>task-commonartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>
    dependencies>

    <build>
        <plugins>
            
            <plugin>
                <groupId>net.alchim31.mavengroupId>
                <artifactId>scala-maven-pluginartifactId>
                <version>3.4.6version>
                <executions>
                    <execution>
                        
                        <goals>
                            <goal>compilegoal>
                            <goal>testCompilegoal>
                        goals>
                    execution>
                executions>
            plugin>

            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-assembly-pluginartifactId>
                <version>3.0.0version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependenciesdescriptorRef>
                    descriptorRefs>
                configuration>
                <executions>
                    <execution>
                        <id>make-assemblyid>
                        <phase>packagephase>
                        <goals>
                            <goal>singlegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>

(2)配置文件

core-site.xml、hdfs-site.xml、hive-site.xml、hive-site.xml、log4j.properties与之前模块无异,主要用于本地调试。

config.properties

#mysql配置
mysql.url=jdbc:mysql://hadoop101:3306/user_profile_manage2022?characterEncoding=utf-8&useSSL=false
mysql.username=root
mysql.password=123456

clickhouse.url=jdbc:clickhouse://hadoop101:8123/user_profile2022

ClickHouse有两个对外开放的端口号:8123和9000

8123,适用于JDBC,短连接,HTTP协议

9000,适用于Client,长连接,TCP协议

添加执行clickhouse的Sql语句的工具类。

因为该工具也会被其他模块使用,所以放在task-common下的util层

package com.hzy.userprofile.util

import java.sql.{Connection, DriverManager, Statement}
import java.util.Properties

object ClickHouseUtil {

  private val properties: Properties = MyPropertiesUtil.load("config.properties")
  val CLICKHOUSE_URL = properties.getProperty("clickhouse.url")

  def executeSql(sql: String ): Unit ={
    Class.forName("ru.yandex.clickhouse.ClickHouseDriver");
    val connection: Connection = DriverManager.getConnection(CLICKHOUSE_URL, null, null)
    val  statement: Statement = connection.createStatement()
    statement.execute(sql)
    connection.close()
  }
}

(3)创建库

clickhouse-client -m

create database user_profile1009;

完成测试后,可以在ClickHouse中user_profile1009中看到对应的数据。

6 打包发布

添加流程任务,因为要在合并宽表任务之后执行,所以级别设为300,调度任务之后,再次测试执行,等待结果。

注意注释掉//.setMaster("local[*]"),打包,上传。

启动远程任务提交器,内网穿透。

二 在clickhouse中宽表转换为Bitmap表

1 为什么用位图(Bitmap)?

(1)存储成本

假设有个1,2,5的数字集合,如果常规的存储方法,要用3个Int32空间。其中一个Int32就是32位的空间。三个就是3*32Bit,相当于12个字节。

如果用Bitmap怎么存储呢,只用8Bit(1个字节)就够了。每一位代表一个数,位号就是数值,1标识有,0标识无。如下图:

7 6 5 4 3 2 1 0
0 0 1 0 0 1 1 0

这样的一个字节可以存8个整数,每一个数的存储成本实质上是1Bit。

也就是说Bitmap的存储成本是Array[Int32]的1/32,是Array[Int64]的1/64。

好处一: 如果有一个超大的无序且不重复的整数集合,用Bitmap的存储成本是非常低的。

(2)天然去重

好处二: 因为每个值都只对应唯一的一个位置,不能存储两个值,所以Bitmap结构可以天然去重。

(3)快速定位

如果有一个需求,比如想判断数字“3”是否存在于该集合中。若是传统的数字集合存储,那就要逐个遍历每个元素进行判断,时间复杂度为O(N)。

但是若是Bitmap存储只要查看对应的下标数的值是0还是1即可,时间复杂度为O(1)。

查询3

7 6 5 4 →3 2 1 0
0 0 1 0 0 1 1 0

好处三:非常方便快速的查询某个元素是否在集合中。

(4)集合间计算

如果有另一个集合2、3、7,想查询这两个集合的交集。

传统方式[1,2,5]与[2,3,7] 取交集就要两层循环遍历。

而Bitmap只要把00100110和10001100进行与操作就行了。而计算机做与、或、非、异或 等等操作是非常快的。

如下:

7 6 5 4 3 2 1 0
1 0 0 0 1 1 0 0

&

7 6 5 4 3 2 1 0
0 0 1 0 0 1 1 0

=

6 5 4 3 2 1 0
0 0 0 0 0 1 0 0

好处四:集合与集合之间的运行非常快。

(5)优势场景

综上,Bitmap非常适合的场景:

  • 海量数据的压缩存储
  • 去重存储
  • 判断值存在于集合
  • 集合之间的交并差

(6)局限性

当然这种方式也有局限性:

  • 只能存储正整数字而不是字符串

  • 存储的值必须是无序不重复

  • 不适合存储稀疏的集合,比如一个集合存了三个数[5,1230000,88880000] 这三个数,用Bitmap存储的话其实就不太划算。(但是clickhouse使用的RoaringBitmap,优化了这个稀疏问题。)

    RoaringBitmap是一种混合的结构,将整个的数据空间分成一段一段的,如0-1000,1000-2000等,这样就可以将每一段去独立的管理。RoaringBitmap中有两种存储方式,使用Bitmap或者使用数组存储,如果数据很稀疏则使用数组存储十分划算,反之使用Bitmap存储划算。于是,在RoaringBitmap中会存在一个阈值,超过阈值使用Bitmap存储,最终将这两种数据结构组合起来,以解决数据稀疏的问题。

2 Bitmap在用户分群中的应用

(1)现状

首先,如下是用户的标签宽表

用户 性别 年龄 偏好
1 90后 数码
2 70后 书籍
3 90后 美食
4 80后 书籍
5 90后 美食

如果想根据标签划分人群,比如:年龄:90后 + 偏好:美食。

(2)传统解决方案

那么无非对列值进行遍历筛选,如果优化也就是列上建立索引,但是当这张表有1000个标签列时,如果要索引生效并不是每列有索引就行,要每种查询组合建一个索引才能生效,索引数量相当于1000个列排列组合的个数,这显然是不可能的。

(3)更好的方案

那么更好的办法是按字段重组成Bitmap。

将年龄和偏好分别提取出来。

年龄 Array Bitmap
90后 1,3,5 00101010
80后 4 00010000
70后 2 00000100
性别 Array Bitmap
1,2,3 00001110
4,5 00110000
偏好 Array Bitmap
数码 1 00000010
美食 3,5 00101000
书籍 2,4 00010100

如果能把数据调整成这样的结构,想进行条件组合,就简单了。

比如: [美食] + [90后] = Bitmap[3,5] & Bitmap[1,3,5] = 3,5 这个计算速度相比宽表条件筛选是非常非常快的。

3 在clickhouse中使用Bitmap表

最终想得到的结果如下图:
【用户画像】将数据迁移到ClickHouse(源码实现)、位图的介绍(bitmap)、位图在用户分群中的应用、位图的使用_第1张图片

现原始表结构为:
【用户画像】将数据迁移到ClickHouse(源码实现)、位图的介绍(bitmap)、位图在用户分群中的应用、位图的使用_第2张图片

转换过程如下图:
【用户画像】将数据迁移到ClickHouse(源码实现)、位图的介绍(bitmap)、位图在用户分群中的应用、位图的使用_第3张图片

(1) SQL实现

-- 将两列值拼成数组
select [1 as a, 2 as b];
-- 将两列值拼成元组
select (1 as a, 2 as b);
-- 元组外面再嵌套数组
select [(1 as a, 2 as b),(3,4)];
-- 再炸开
select arrayJoin( [(1 as a, 2 as b),(3,4)] );
-- 切开
select rs_col.1 , rs_col.2 from (select arrayJoin( [(1 as a, 2 as b),(3,4)] ) rs_col ) rs_t;

(2) 在clickhouse中使用Bitmap表

以上面的表举例:

建表和数据
create table user_tag_merge 
(   uid UInt64,
	gender String,
	agegroup String,
	favor String
)engine=MergeTree()
order by (uid);

模拟数据

insert into user_tag_merge values(1,'M','90后','sm');
insert into user_tag_merge values(2,'M','70后','sj');
insert into user_tag_merge values(3,'M','90后','ms');
insert into user_tag_merge values(4,'F','80后','sj');
insert into user_tag_merge values(5,'F','90后','ms');

原始数据如下:

【用户画像】将数据迁移到ClickHouse(源码实现)、位图的介绍(bitmap)、位图在用户分群中的应用、位图的使用_第4张图片

数据转换

现依据上图流程,依次对数据进行处理

-- 拼
select uid, [ ('gender',gender),('agegroup',agegroup),('favor',favor)] from user_tag_merge;
-- 炸:用arrayJoin炸开,类似于hive中的explode
select uid, arrayJoin( [ ('gender',gender),('agegroup',agegroup),('favor',favor)] ) tv
from user_tag_merge;
-- 切
select tv.1,tv.2,uid
from
( select uid, arrayJoin( [ ('gender',gender),('agegroup',agegroup),('favor',favor)] ) tv
from user_tag_merge) user_tag;
-- 聚
select tv.1,tv.2,groupArray(uid)
from
( select uid, arrayJoin( [ ('gender',gender),('agegroup',agegroup),('favor',favor)] ) tv
from user_tag_merge) user_tag
group by tv.1,tv.2;
-- 聚(bitmap)
select tv.1,tv.2,groupBitmapState(uid)
from
( select uid, arrayJoin( [ ('gender',gender),('agegroup',agegroup),('favor',favor)] ) tv
from user_tag_merge) user_tag
group by tv.1,tv.2;
-- bitmap的结构本身无法用正常文本显示,为看出效果,再嵌套一层数组
select tv.1,tv.2,bitmapToArray(groupBitmapState(uid))
from
( select uid, arrayJoin( [ ('gender',gender),('agegroup',agegroup),('favor',favor)] ) tv
from user_tag_merge) user_tag
group by tv.1,tv.2;
创建Bitmap表
create table user_tag_value_string
 ( 
    tag_code String,
	tag_value String ,
	us AggregateFunction(groupBitmap,UInt64)
)engine=AggregatingMergeTree()
 partition by  (tag_code) 
 order by (tag_value);

Bitmap表必须选择AggregatingMergeTree引擎。

对应的Bitmap字段,必须是AggregateFunction(groupBitmap,UInt64),groupBitmap标识数据的聚合方式,UInt64标识最大可存储的数字长度。

业务结构上,稍作了调整。把不同的标签放在了同一张表中,但是因为根据tag_code进行了分区,所以不同的标签实质上还是物理分开的。

插入数据

groupBitmapState():将多行的值聚合成一个bitmap值。

insert into user_tag_value_string
select tv.1,tv.2,groupBitmapState(uid)
from
( select uid, arrayJoin( [ ('gender',gender),('agegroup',agegroup),('favor',favor)] ) tv
from user_tag_merge) user_tag
group by tv.1,tv.2;

-- 查看数据是否正确,再转成数组
select tag_code, tag_value, bitmapToArray(us) from user_tag_value_string;

结果如下:

【用户画像】将数据迁移到ClickHouse(源码实现)、位图的介绍(bitmap)、位图在用户分群中的应用、位图的使用_第5张图片
【用户画像】将数据迁移到ClickHouse(源码实现)、位图的介绍(bitmap)、位图在用户分群中的应用、位图的使用_第6张图片

以上操作就是通过一句sql,将在ClickHouse处理的宽表变成一个位图表。

对Bitmap进行查询
对Bitmap进行查询

使用这个Bitmap表进行查询。

比如想查询[90后]+[美食]的用户条件组合查询

bitmapAnd(bitmapa,bitmapb):求交集

select bitmapToArray(
bitmapAnd ( (select us from user_tag_value_string where tag_value='90后' and tag_code='agegroup') ,
(select us from user_tag_value_string where tag_value='ms' and tag_code='favor') )
);

首先用条件筛选出us, 每个代表一个Bitmap结构的uid集合,找到两个Bitmap后用bitmapAnd函数求交集。 然后为了观察结果用bitmapToArray函数转换成可见的数组。

范围值查询
  • 比如要取 [90后]或者[80后] + [美食]
  • 或者消费金额大于1000 + [女性]

groupBitmapState 和groupBitmapMergeState区别

前者 把普通值聚合成bitmap ,后者 是bitmap之间进行并集的聚合

select bitmapToArray(
bitmapAnd ( (select groupBitmapMergeState(us) from user_tag_value_string where tag_value in ('90后','80后') and tag_code='agegroup') ,
(select us from user_tag_value_string where tag_value='ms' and tag_code='favor') )
);

先对多个年龄组取并集,然后去交集。

询时,有可能需要针对某一个标签,取多个值,甚至是一个区间范围,那就会涉及多个值的userId集合,因此需要在子查询内部用groupBitmapMergeState进行一次合并,其实就多个集合取并集。

比如要取 [90后]或者[80后] + [美食]或者[书籍]

select bitmapToArray(
bitmapAnd ( (select groupBitmapMergeState(us) from user_tag_value_string where tag_value in ('90后','80后') and tag_code='agegroup') ,
(select groupBitmapMergeState(us) from user_tag_value_string where tag_value in ('ms','sj') and tag_code='favor') )
);
函数总结
函数
arrayJoin 宽表转Bitmap表需要行转列,要用arrayJoin把多列数组炸成行。
groupBitmapState 把聚合列的数字值聚合成Bitmap的聚合函数
bitmapAnd 求两个Bitmap值的交集
bitmapOr 求两个Bitmap值的并集
bitmapXor 求两个Bitmap值的差集(异或)
bitmapToArray 把Bitmap转换成数值数组
groupBitmapMergeState 把一列中多个bitmap值进行并集聚合。 (连续值)
bitmapCardinality 求Bitmap包含的值个数
select tag_value,bitmapCardinality(us) from user_tag_value_string;

更多其他函数可以参考官网。

你可能感兴趣的:(用户画像,big,data,clickhouse,database,sql)