ByteHouse 根据用户行为分析使用场景,定制了部分函数,主要包括:
相比拼装 SQL或者使用 ClickHouse 原生函数,使用 ByteHouse 自研的专用函数更为高效。本文档详述了这些函数的使用方式。
下面是一张示例用表,是由用户行为事实表 Log 和用户维度表 User 组成常用的星型模型。
后文将基于此表为大家演示函数的具体用法。
CREATE TABLE -- 事件日志表 default.LOG ( `Visitor` UInt32, `EventName` String, `EventTime` DateTime, `Province` String, `City` String ) ENGINE = Distributed('your_cluster', 'default', 'LOG_Example_local') CREATE TABLE -- 用户表 default.User ( `Visitor` UInt32, `Fvisit_Time` DateTime, `Fvisit_Province` String, `Fvisit_City` String ) ENGINE = Distributed('your_cluster', 'default', 'User_Example_local') -- 仅展示 Distributed 表 Schema,local 表略
选定一段时间范围,观察此时间范围内每一个时间单位的符合某维度的用户在一段时间范围之后的留存。
留存计算逻辑简介:
genArrayIf
函数将初访(first_events
)及回访(return_events
)行为数据预聚合到指定的时间槽位中,得到 Array(UInt8)
格式的访问情况位集 bitset
。这一步将时间范围切分成离散的时间槽位,并且用位集表示每个槽位上事件发生与否。retention2
**聚合用户初访(first_events
)、回访(return_events
)位集得到每个时间槽对应的留存数据。first_events数组:[13,2] 对应的bit 表示:0000 0010 0000 1101 对应的10进制数 2 13 含义:在第1,3,4,10 天发生了目标事件 return_events 数组:[5,3] 对应的bit 表示:0000 0011 0000 0101 对应的10进制数 3 5 含义:在第1,3,9,10 天发生了目标事件
--示例: SELECT retention2(10)([13, 2], [5, 3]) ┌─retention2(10)([13, 2], [5, 3])───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ [1,0,1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1] │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
上述例子中,即是时间槽位数量为 10 天,槽位时长为 1 天,first_event 与 return_event 为 [13,2],[5,3] 前提下的留存数组。返回的留存数组 number_stepsnumber_steps 的数组,在上例中,是长度为1010 的数组,因此这个数组应该切分成 10 行去看结果,留存数组的每一行代表以 first_events 中第 i 天为起始事件观察时间,与 return_events 中的事件按位做“与”操作的结果。
1,0,1,0,0,0,0,0,1,1, --以第1天为起始观察点,用户在第1,3,9,10 天进行了回访,所以1,3,9,10列为1 0,0,0,0,0,0,0,0,0,0, --以第2天为起始观察点,第二天没有发生任何事件,所以第二行都是0 0,0,1,0,0,0,0,0,1,1, --以第3天为起始观察点,用户在第3,9,10 天进行了回访,所以3,9,10列为1 0,0,0,1,0,0,0,0,1,1, --以第4天为起始观察点,用户在第9,10 天进行了回访,所以9,10列为1。对第4天本身来说,虽然没有回访事件,但默认也在对应列输出1 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,1
将这些留存数组按城市、渠道等聚合,就可以得到留存数据,如:
select city, retention2(3)(..., ...) from ... group by city --得到某一城市的留存数组为: 22,11,1 0,5,3 0,0,1
genArrayIf(number_steps, begin_timestamp, step_time)(timestamp, if_expr)
输入参数:
number_steps
:时间槽位的数量。begin_timestamp
:分析开始时间戳,如数据筛选的范围从 2022-06-01 开始,所以开始时间戳为 toUInt64(toUnixTimestamp('2022-06-01')) = 1654012800。step_time
:1个时间槽位持续时长。timestamp
: 事件时间戳所在列。需要先转换成时间戳,再转换成Uint64 格式。if_expr
:对留存条件的定义,满足条件的将会被认为行为发生。输出参数:
Array(UInt8)
格式的访问情况位集 bitset
。示例:
在下面的例子中,返回结果表达了7天中,每个单位时间(1天)中是否发生了 EventName = 'E0002'
的事件。
SELECT Visitor, genArrayIf(7, 1654012800, 86400)(toUInt64(toUnixTimestamp(LOG.EventTime)), EventName = 'E0002') AS return_events FROM LOG WHERE (EventTime >= '2022-06-01') AND (EventTime < '2022-06-08') AND (EventName = 'E0002') GROUP BY Visitor
retention2(number_steps)(first_events, return_events)
输入参数:
number_steps
: 时间槽位的数量。number_steps
取值和genArrayIf
中的number_steps
保持一致。说明
注意:
number_steps
取值范围受 初访(first_events)、回访(return_events)位集 大小的限制的,规则是(events_array.size()-1)*8 < number_steps <= events_array.size()*8
。比如下面示例中 first_events 的 UInt8 类型元素个数为 2,则传入的窗口大小应该为 (8, 16]
间某个值。first_events
:初访事件的位集,需要genArrayIf
返回。return_events
:回访事件的位集,需要genArrayIf
返回。输出参数:
Array(UInt64)
格式的留存数组。观察一周 7 天的窗口内,按城市分组,首访('E0001')到回访('E0002')留存情况。
SELECT City, retention2(7)(first_events, return_events) FROM ( SELECT City, Visitor, genArrayIf(7, 1654012800, 86400)(toUInt64(toUnixTimestamp(LOG.EventTime)), EventName = 'E0001') AS first_events FROM LOG WHERE (EventTime >= '2022-06-01') AND (EventTime < '2022-06-08') AND (EventName = 'E0001') GROUP BY Visitor, City ) AS t1 ANY LEFT JOIN ( SELECT Visitor, genArrayIf(7, 1654012800, 86400)(toUInt64(toUnixTimestamp(LOG.EventTime)), EventName = 'E0002') AS return_events FROM LOG WHERE (EventTime >= '2022-06-01') AND (EventTime < '2022-06-08') AND (EventName = 'E0002') GROUP BY Visitor ) AS t2 ON t1.Visitor = t2.Visitor GROUP BY City ORDER BY City ASC SETTINGS distributed_product_mode = 'local', distributed_group_by_no_merge = 0
返回:
得到 7*7 的数组,如上图中 city1 解析为:
109777,61903,62185,62116,62064,62066,61952, 0,109978,61922,62500,61817,62671,62417, 0,0,110033,62193,62177,62230,62287, 0,0,0,109715,61833,62142,62119, 0,0,0,0,110598,62493,62515, 0,0,0,0,0,109508,61909, 0,0,0,0,0,0,110746
可根据需要,计算出从 Day1->Day6 的 1 日转化率。
根据实际需求,可以将时间槽修改为周、月等。
业务场景:选定一段时间范围,观察此时间范围内每一个时间单位(天)内用户按一定时间范围划分的漏斗分层汇总情况。
漏斗计算逻辑简介:
finderFunnel(window, start_timestamp, check_granularity, watch_numbers)(server_timestamp, client_timestamp, check_event1, check_event2...)
window
:分析观察的窗口总时长时长,单位和client_timestamp
一致,下例中为 86400 *7 秒,也即 7 天。start_timestamp
: 分析开始时间戳,如分析从 2022-06-01 开始,所以开始时间戳为 toUInt64(toUnixTimestamp('2022-06-01')) = 1654012800
check_granularity
: 一个观察步长持续时间,多大粒度内来计算转化分析,单位和client_timestamp
一致,如 1 天则为 86400(单位:秒)。watch_numbers
: 观察几个步长,如check_granularity
是86400(1 天),watch_numbners = 7
,指代从start_timestamp
开始观察 7 天每天的漏斗情况server_timestamp
:事件发生服务器时间戳列,需转化为UInt64 类型,用于函数运行时,计算事件所属时间槽位/步进。client_timestamp
:事件发生客户端时间戳列,需转化为UInt64 类型,用于函数运行时对数据进行排序。check_event
: 计算转化的事件列表,在window
定义的分析时长范围内满足条件的将会被认为事件触发有效。如有3个事件,则输入:EventName = 'E0001', EventName = 'E0002', EventName = 'E0003'funnelRep(number_steps, evnet_count)(funnel_res)
number_steps
:UInt 型数字,包含的时间槽位数量,通常和 finderFunnel
函数中watch_numners
保持一致。evnet_count
: UInt 型数字,表示事件转化链上事件总数,通常和finderFunnel
函数中check_event
的时间数量保持一致,funnel_res
:转化步骤列表,由 finderFunnel
子查询产生,包含每一个用户产生的所有转化步骤列表。返回结果:
number_steps
个子数组,第一个子数组是汇总的结果,第二个子数组是第一个时间槽在window
定义的分析时长范围内的漏斗计算结果,以此类推。以下示例指代返回 从 2022/6/1-2022/6/8 的 7 天中, 'E0001', 'E0002', 'E0003' 3 个事件的转化漏斗:
SELECT funnelRep(7, 3)(funnel_res) FROM ( SELECT finderFunnel(7 * 86400, 1654012800, 86400, 7)(toUInt64(toUnixTimestamp(LOG.EventTime)), toUInt64(toUnixTimestamp(LOG.EventTime)), LOG.EventName = 'E0001', LOG.EventName = 'E0002', LOG.EventName = 'E0003') AS funnel_res FROM LOG LEFT JOIN ( SELECT * FROM User AS t ) AS U ON LOG.Visitor = U.Visitor WHERE (EventTime >= '2022-06-01 00:00:00') AND (EventTime <'2022-06-08 00:00:00') AND (EventName IN ('E0001', 'E0002', 'E0003')) GROUP BY Visitor ) SETTINGS distributed_product_mode = 'local', distributed_group_by_no_merge = 1
上面的示例SQL 返回为
┌─funnelRep(7, 3)(funnel_res)─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ [[5994501,5944849,56423],[3791527,3780488,39431],[3792876,3767458,33024],[3792031,3733089,26709],[3791519,3656585,20668],[3791859,3480158,14385],[3793389,3077233,8691],[3792344,2143108,3641]] │
选定一段时间范围,分析此时间范围内用户行为路径,在前端展示效果大致如下
路径分析计算逻辑简介:
pathSplit(session_time, level)(timestamp, event, prop)
输入参数:
session_time
:路径分析的时间窗口,单位和timestamp
保持一致,下例中为 600 秒,也即只计算首个event 发生后 10 分钟内形成的路径;level
:路径深度,超过 x 个节点截断路径;timestamp
:时间戳 所在列,需要先转换成时间戳,再转换成 Uint64 格式;event
:事件列,仅支持数值类型,需要通过 multiIf 显式指定;如下例中的:multiIf(EventName = 'E0001', 1, EventName = 'E0002', 2, EventName = 'E0003', 3, 0) AS e
prop
:属性列,没有时传 ''
占位;输出参数(中间结果):
返回二维数组:[(e1, p1), (e2, p2), (e1, p2)]
event
列中 multiif
定义的事件编号prop
pathCount(y, x)(path, cnt1, cnt2)
pathCount 返回界面展示需要的数据,x 表示横向的路径深度(如上图为 4),
前端展示需要的数据其实就是每个浅蓝色方块代表的node,和浅蓝色方块之间的变edge,都是由pathCount输出
y
:纵向单层展示深度,如上图中黄色圈的部分,即 y = 5;x
:横向路径深度,超过 x 个节点截断路径,如上图中 x = 4;path
:pathSplit
的返回 path 列(需通过 arrayJoin 展开);cnt1
:路径权重1,通常直接传 1 就行,用于计算路径 去重后的次数;cnt2
:路径权重2,通常传pathSplit
子查询里的 count()
求出的 cnt ,用来求路径 不去重的总次数。返回格式:
node_index
:格式为:(event, prop)
,事件节点展示。
event
为事件的下标编号,若 之前 pathSplit 时 event 填写 multiIf(EventName = 'E0001', 1, EventName = 'E0002', 2, EventName = 'E0004', 3, 0),则此时返回 1 对应 'E0001' ,2对应 'E0002',3对应 'E0004';prop
为 pathSplit 传入的 prop。nodes
:二维数组。表示每个节点在各层级(level,下标从 0 开始)出现的去重次数、次数,格式为:(level, node_index, cnt1, cnt2)
。
(level, from_node_index, to_node_index, cnt1, cnt2)
指定一个用户 Visitor = 964049 来说明。
该示例为 2022/6/1-2022/6/2 中,发生 'E0001', 'E0002', 'E0004' 事件的路径数据,展示发生事件后 10 分钟内的页面信息,展示的页面路径深度为 3 层。
首先先通过 pathSplit 计算中间结果:
SELECT Visitor,pathSplit(600, 3) (toUInt64(toUnixTimestamp(LOG.EventTime)), multiIf(EventName = 'E0001', 1, EventName = 'E0002', 2, EventName = 'E0004', 3, 0) AS e, '') AS path FROM LOG WHERE (EventTime >= '2022-06-01 00:00:00') AND (EventTime < '2022-06-02 00:00:00') AND (EventName IN ('E0001', 'E0002', 'E0004')) and Visitor = 964049 GROUP BY Visitor --返回: ┌─Visitor─┬─path────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ 964049 │ [[(1,''),(2,''),(3,'')],[(1,'')],[(1,''),(2,''),(3,'')],[(1,''),(2,''),(2,'')],[(1,'')],[(1,'')],[(1,'')],[(1,''),(3,''),(3,'')],[(1,''),(2,''),(3,'')],[(1,''),(1,''),(3,'')],[(1,'')],[(1,''),(2,''),(3,'')],[(1,'')],[(1,''),(2,''),(2,'')]] │ └─────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
再通过arrayJoin 把单行的path 返回的二维数组表示的多条path展开成多行单条path,计算每一条path出现的次数
SELECT path AS path, count() AS cnt FROM (SELECT arrayJoin(pathSplit(600, 3)(toUInt64(toUnixTimestamp(LOG.EventTime)), multiIf(EventName = 'E0001', 1, EventName = 'E0002', 2, EventName = 'E0004', 3, 0) AS e, '')) AS path FROM LOG WHERE (EventTime >= '2022-06-01 00:00:00') AND (EventTime < '2022-06-02 00:00:00') AND (EventName IN ('E0001', 'E0002', 'E0004')) and Visitor = 964049 GROUP BY Visitor) GROUP BY path --返回 ┌─path───────────────────┬─cnt─┐ │ [(1,'')] │ 6 │ │ [(1,''),(1,''),(3,'')] │ 1 │ │ [(1,''),(2,''),(3,'')] │ 4 │ │ [(1,''),(2,''),(2,'')] │ 2 │ │ [(1,''),(3,''),(3,'')] │ 1 │ └────────────────────────┴─────┘
再通过 pathCount 计算出最终的 3*3 结果(路径深度为 3,同时每层至多展示的节点为 3 个):
SELECT pathCount(3, 3) (path, 1, cnt) FROM (SELECT path AS path, count() AS cnt FROM (SELECT arrayJoin(pathSplit(600, 3)(toUInt64(toUnixTimestamp(LOG.EventTime)), multiIf(EventName = 'E0001', 1, EventName = 'E0002', 2, EventName = 'E0004', 3, 0) AS e, '')) AS path FROM LOG WHERE (EventTime >= '2022-06-01 00:00:00') AND (EventTime < '2022-06-02 00:00:00') AND (EventName IN ('E0001', 'E0002', 'E0004')) and Visitor = 964049 GROUP BY Visitor) GROUP BY path ) --返回 ┌─pathCount(5, 3)(path, 1, cnt)─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ ([(1,''),(3,''),(2,'')], [[0,0,5,14],[1,0,1,1],[1,1,1,1],[1,2,2,6],[2,1,3,6],[2,2,1,2]], [[0,0,0,1,1],[0,0,1,1,1],[0,0,65535,1,6],[0,0,2,2,6],[1,2,2,1,2],[1,1,1,1,1],[1,0,1,1,1],[1,2,1,1,4]]) │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
结果为:
node_index
:[(1,''),(3,''),(2,'')],表示事件编号映射为 0->1(E0001),1->3(E0004),2->2(E0002)
nodes
:[[0,0,5,14],[1,0,1,1],[1,1,1,1],[1,2,2,6],[2,1,3,6],[2,2,1,2]]
edge:[0,0,0,1,1],[0,0,1,1,1],[0,0,65535,1,6],[0,0,2,2,6],[1,2,2,1,2],[1,1,1,1,1],[1,0,1,1,1],[1,2,1,1,4]
可以将结果信息补充为页面路径分析,如