天天看点

Clickhouse 专栏---数据模型之留存分析

什么是留存,比如在20200701这天操作了“点击banner”的用户有100个,这部分用户在20200702这天操作了“点击app签到”的有20个,那么对于分析时间是20200701,且“点击banner”的用户在次日“点击app签到”的留存率是20%。

背景

关于用户留存模型是各大商业数据分析平台必不可少的功能,企业一般用该模型衡量用户的活跃情况,也是能直接反应产品功能价值的直接指标;如,boss想要了解商城改版后,对用户加购以及后续下单情况的影响等。如下图,这就是一个典型的留存分析功能:

Clickhouse 专栏---数据模型之留存分析

问题

通常实现上述需求的传统做法是多表关联,了解clickhouse的攻城狮都清楚,多表关联简直就是clickhouse的天敌;如一张用户行为日志表中至少包含:用户id、行为事件、操作时间、地点属性等,想分析20200909日河南省注册用户次日的下单情况,那么SQL一般会这么写:

select count(distinct t1.uid) r1, count(distinct t2.uid) r2 from
(
  select uid from action_log where day='20200909' and action='login' and province='河南省'
) as t1 left join
(
  select uid from action_log where day='20200910' and action='order' and  province='河南省'
) as t2
using uid
           

这种方式书写简单、好理解,但是性能会很差,在超大数据集上进行运算是不仅仅影响用户体验,还会因长期占有物理资源而拖垮整个clickhouse上的业务。

解决方法有两种:

  • 使用clickhouse自带的retention函数
  • Roaringbitmap 通过对数据进行压缩和位运算提高查询性能

Roaringbitmap

通过Roaringbitmap进行用户行为分析是腾讯广告业务中常用的一种实现方案,点击查看 ,文章中内容较多这里挑选干货进行讲解:

bitmap可以理解为一个长度很长且只存储0/1数字的集合,如某个用户通过特定的哈希算法映射到位图内时,那么该位置就会被置为1,否则为0;通过这种方式对数据进行压缩,空间利用率可提示数十倍,数据可以很容易被系统cache,大大减少了IO操作。

在查询之前需要先对数据进行预处理,这里额外构建两张表,用来存储用户的位图信息。

  • 用户行为日志表:table_oper_bit
Clickhouse 专栏---数据模型之留存分析

向位图表插入数据,原始数据十几亿,插入后结果只有几万行,而且随着数据范围的再扩大,位图表的数据增量变化也不会很明显。

Clickhouse 专栏---数据模型之留存分析
  • 用户基本信息表:table_attribute_bit 
Clickhouse 专栏---数据模型之留存分析

同理table_attribute_bit插入后数据也得到了极大的压缩,最终数据如下图:

Clickhouse 专栏---数据模型之留存分析
Clickhouse 专栏---数据模型之留存分析
  • 应用案例

a. 操作了某个行为的用户在后续某一天操作了另一个行为的留存:

如“20200701点击了banner的用户在次日点击app签到的留存人数”,就可以用以下的sql快速求解:

Clickhouse 专栏---数据模型之留存分析

b. 操作了某个行为并且带有某个属性的用户在后续的某一天操作了另一个行为的留存:

如“20200701点击了banner且来自广东/江西/河南的用户在次日点击app签到的留存人数”: 

Clickhouse 专栏---数据模型之留存分析

 c. 操作了某个行为并且带有某几个属性的用户在后续的某一天操作了另一个行为的留存:

如“20200701点击了banner、来自广东且新进渠道是小米商店的用户在次日点击app签到的留存人数”:

Clickhouse 专栏---数据模型之留存分析

其中bitmapCardinality用来计算位图中不重复数据个数,在大数据量下会有一定的数据误差,bitmapAnd用来计算两个bitmap的与操作,即返回同时出现在两个bitmap中用户数量

  • 查询速度

    clickhouse集群现状:12核125G内存机器10台。clickhouse版本:20.4.7.67。查询的表都存放在其中一台机器上。测试了查询在20200701操作了行为oper_name_1(用户数量级为3000+w)的用户在后续7天内每天操作了另一个行为oper_name_2(用户数量级为2700+w)的留存数据(用户重合度在1000w以上),耗时0.2秒左右

该方法的确比较灵活,不仅仅能解决留存问题,还有很多关于事件分析的需求等待我们去探索;然而它的缺点是操作复杂,且不支持对实时数据的分析

retention

通过上面的例子不难看出,腾讯的做法虽然提升了查询的性能,但是操作过于复杂,不便于用户理解和后期的维护;关于这些痛点易企秀数仓这边做法是采用retention进行实现

retention function是clickhouse中高级聚合函数,较bitmap的方式实现留存分析会更加简单、高效;语法如下:

retention(cond1, cond2, ..., cond32);
## cond 为判断条件
## 支持最长32个参数的输入,也就是说 至少支持一个完整自然月的留存分析查询
           

其中满足条件1的数据会置为1,之后的每一个表达式成立的前提都要建立在条件1成立的基础之上,这正好符合我们对留存模型的定义

那么我们还以上面的3个场景为例方便对比说明:

  • 20200701点击了banner的用户在次日点击app签到的留存人数
SELECT
    sum(r[1]) AS r1,
    sum(r[2]) AS r2,
    r2/r1
FROM
(
SELECT
    uid,
    retention(date = '20200701' and type='点击banner', date =  '20200702'  and type='点击app签到' ) AS r
FROM action_log
WHERE date IN ('20200701', '20200702')
GROUP BY uid 
)
           
  • 20200701点击了banner且来自广东/江西/河南的用户在次日点击app签到的留存人数
SELECT
    sum(r[1]) AS r1,
    sum(r[2]) AS r2,
    r2/r1
FROM
(
SELECT
    uid,
    retention(date = '20200701' and type='点击banner', date =  '20200702'  and type='点击app签到' ) AS r
FROM action_log
WHERE date IN ('20200701', '20200702')  and province IN ('广东', '江西', '河南')
GROUP BY uid 
)
           
  • 按照上面的方式第三个场景也能很快实现,这里留给大家去尝试...

不过该方式与bitmap比也有缺陷,那就是如果用户日志表中不存储用户属性信息时,就需要与用户属性表进行关联查询,两张大表关联,查询性能会相当慢。

总结

条条大路通罗马,clickhouse不但性能上做到极致,在数据分析实现上也提供了大量遍历的途径。