高效大數據開發之 bitmap 思想的應用

做者:xmxiong,PCG 運營開發工程師正則表達式

數據倉庫的數據統計,能夠概括爲三類:增量類、累計類、留存類。而累計類又分爲歷史至今的累計與最近一段時間內的累計(好比滾動月活躍天,滾動周活躍天,最近 N 天消費狀況等),藉助 bitmap 思想統計的模型表能夠快速統計最近一段時間內的累計類與留存類。sql

1、背景數組

數據倉庫的數據統計,能夠概括爲三類:增量類、累計類、留存類。而累計類又分爲歷史至今的累計與最近一段時間內的累計(好比滾動月活躍天,滾動周活躍天,最近 N 天消費狀況等),藉助 bitmap 思想統計的模型表能夠快速統計最近一段時間內的累計類與留存類。微信

2、業務場景網絡

咱們先來看幾個最近一段時間內的累計類與留存類的具體業務問題,做爲作大數據的你建議先不要急着往下閱讀,認真思考一下你的實現方案:app

1.統計最近 30 天用戶的累計活躍天(每一個用戶在 30 天裏有 N 天使用微視 app,N 爲 1-30,而後將月活躍用戶的 N 天加總)?ide

2.統計最近 7 天的用戶累計使用時長?函數

3.統計最近 30 天有播放的累計用戶數?性能

4.統計最近 30 天活躍用戶有多少在最近 30 天裏有連續 3 天及以上活躍?大數據

5.統計 28 天前活躍用戶的 一、三、七、1四、28 天留存率?

3、傳統解決方案

在進入本文真正主題以前,咱們先來看看常規的解決思路:1.統計最近 30 天用戶的累計活躍天?

--用dau表(用戶ID惟一),取最近30天分區,sum(活躍日期)。 
select 
    sum(imp_date) active_date 
  from 
    weishi_dau_active_table 
  where 
    imp_date>=20200701 
    and imp_date<=20200730

2.統計最近 7 天的用戶累計使用時長?

--用dau表(用戶ID惟一),取最近7天分區,sum(使用時長)。 
select 
    sum(log_time) log_time 
  from 
    weishi_dau_active_table 
  where 
    imp_date>=20200701 
    and imp_date<=20200707

3.統計最近 30 天有播放的累計用戶數?

--用用戶播放表(用戶ID惟一),取最近30天分區,count(distinct if(播放次數>0,用戶ID,null))。 
select 
    count(distinct if(play_vv_begin>0,qimei,null)) play_user 
  from 
    weishi_play_active_table 
  where 
    imp_date>=20200701 
    and imp_date<=20200730

4.統計最近 30 天活躍用戶有多少在最近 30 天裏有連續 3 天及以上活躍?

--用dau表(用戶ID惟一),取最近30天分區,關聯兩次最近30天分區,關聯條件右表分別爲imp_date-1,imp_date-2。 
select 
    count(distinct a.qimei) active_num 
  from 
  ( select 
        imp_date 
        ,qimei 
      from 
        weishi_dau_active_table 
      where 
        imp_date>=20200701 
        and imp_date<=20200730 
   )a 
  join --第一次join,先取出連續2天的用戶,由於7月1日用戶與7月2號-1天關聯得上,表示一個用戶在1號和2號都活躍 
  ( select 
        date_sub(imp_date,1) imp_date 
        ,qimei 
      from 
        weishi_dau_active_table 
      where 
        imp_date>=20200701 
        and imp_date<=20200730 
   )b 
   on 
    a.imp_date=b.imp_date 
    and a.qimei=b.qimei 
  join --第二次join,取出連續3天的用戶,由於第一次join已經取出連續兩天活躍的用戶了,再拿這些7月1日用戶關聯7月3日-2天關聯得上,表示一個用戶在1號和3號都活躍,結合第一步join得出用戶至少3天連續活躍了 
  ( select 
        date_sub(imp_date,2) imp_date 
        ,qimei 
      from 
        weishi_dau_active_table 
      where 
        imp_date>=20200701 
        and imp_date<=20200730 
   )c 
   on 
    a.imp_date=c.imp_date 
    and a.qimei=c.qimei

固然這裏也能夠用窗口函數 lead 來實現,經過求每一個用戶後 1 條日期與後 2 條日期,再拿這兩個日期分佈 datediff 當前日期是否爲日期相差 1 且相差 2 來判斷是否 3 天以上活躍,可是這個方法也仍是避免不了拿 30 天分區統計,統計更多天連續活躍時的擴展性很差的狀況 5.統計 28 天前活躍用戶的 一、三、七、1四、28 天留存率?

--用dau表(用戶ID惟一),取統計天的活躍用戶 left join 一、三、七、1四、28天后的活躍用戶,關聯得上則說明對應天有留存。 
select 
    '20200701' imp_date 
    ,count(distinct if(date_sub=1,b.qimei,null))/count(distinct a.qimei) 1d_retain_rate 
    ,count(distinct if(date_sub=3,b.qimei,null))/count(distinct a.qimei) 3d_retain_rate 
    ,count(distinct if(date_sub=7,b.qimei,null))/count(distinct a.qimei) 7d_retain_rate 
    ,count(distinct if(date_sub=14,b.qimei,null))/count(distinct a.qimei) 14d_retain_rate 
    ,count(distinct if(date_sub=28,b.qimei,null))/count(distinct a.qimei) 28d_retain_rate 
  from 
    weishi_dau_active_table partition (p_20200701)a 
  left join 
  ( select 
        datediff(imp_date,'20200701') date_sub 
        ,qimei 
      from 
        weishi_dau_active_table 
      where 
        datediff(imp_date,'20200701') in (1,3,7,14,28) 
   )b 
   on 
    a.qimeib=b.qimei

4、傳統解決方案存在的問題

1.天天大量中間數據重複計算,好比昨天最近 30 天是 8 月 1 日~ 8 月 30 日,今天最近 30 天爲 8 月 2 日~ 8 月 31 日,中間 8 月 2 日~ 8 月 30 日就重複計算了。

2.統計邏輯複雜,相似業務場景 4,困難點在於統計每一天活躍的用戶次日是否還繼續活躍。

3.耗費集羣資源大,場景 4 和場景 5 都用到了 join 操做,場景 4 還不止一個 join,join 操做涉及 shuffle 操做,shuffle 操做須要大量的網絡 IO 操做,所以在集羣中是比較耗性能的,咱們應該儘可能避免執行這樣的操做。

4.以上統計邏輯可擴展性差,因爲數據分析常常進行探索性分析,上面傳統方案能解決上面幾個問題,可是數據分析稍微改變一下需求,就得從新開發,例如增長一個 15 天留存,或者統計最近 2 周的活躍天等。

5、bitmap 原理

上面的業務場景可否在一個模型表很簡單就能統計出,且不須要數據重複計算,也不須要 join 操做,還能知足數據分析更多指標探索分析呢?答案是確定的,能夠藉助 bitmap 思想。

何爲 bitmap?bitmap 就是用一個 bit 位來標記某個元素,而數組下標是該元素,該元素是否存在時用 bit 位的 1,0 表示。好比 10 億個 int 類型的數,若是用 int 數組存儲的話,那麼須要大約 4G 內存,當咱們用 int 類型來模擬 bitmap 時,一個 int 4 個字節共 4*8 = 32 位,能夠表示 32 個數,原來 10 億個 int 類型的數用 bitmap 只須要 4GB / 32 = 128 MB 的內存。

6、具體實現過程

大數據開發參考 bitmap 思想,就是參考其經過數組下標表示該元素的思想,將最近 31 天活躍用戶是否活躍用逗號分隔的 0 1 串存儲下來,將最近 31 天的播放 vv、贊轉評等消費數也用逗號分隔的具體數值存儲下來,造成一個字符數組,數組每個下標表示距離最新一天數據的天數差值,第一位下標爲 0,表示距離今天最新一天數據間隔爲 0 天,以下所示:

active_date_set 表示 31 天活躍集,0 表示對應下標(距離今天的 N 天前)不活躍,1 表示活躍;這個數據是 8 月 23 日統計的,1,0,0,1,…… 即用戶在 8 月 23 日,8 月 20 日有活躍,8 月 22 日,8 月 21 日並無活躍。play_vv_begin_set 表示 31 天播放 vv 集,0 表示對應下標(距離今天的 N 天前)沒有播放視頻,正整數表示當天的播放視頻次數;這裏用戶雖然在 8 月 23 日,8 月 20 日有活躍,可是該用戶一天只播放了一次視頻就離開微視了。這樣作的好處一方面也是大大壓縮了存儲,極端狀態下用戶 31 天都來,那麼就能夠將 31 行記錄壓縮在一行存儲。

假如 1 天活躍用戶 1 億,且這些用戶 31 天都活躍,那麼就能夠將 31 億行記錄壓縮在 1 億行裏,固然實際不會出現這樣的狀況,由於會有一部分老用戶流失,一部分新用戶加入,按照目前微視的統計能夠節省 80%多的存儲;另外一方面能夠更簡單快捷地統計每一個用戶最近一個月在微視的活躍與播放、消費(贊轉評)等狀況。

該模型表的詳細實現過程以下:

1.該模型表的前 31 天須要初始化一個集合,將第一天的數據寫到該表,而後一天一天滾動壘起來,累計 31 天以後就獲得這個可用的集合表了,也就能夠例行化跑下去。

2.最新一天須要統計時,須要拿前一天的集合表,剔除掉相對今天來講第 31 天前的數據,而後每一個集合字段將最後一位刪除掉 。

3.拿最新一天的增量數據(下面用 A 表替代) full join 第 2 步處理後的前一天表(下面用 B 表替代)關聯。

這裏有三種狀況須要處理:

a.既出如今 A 表,也出如今 B 表,這種狀況,只需直接拼接 A 表的最新值與 B 表的數組集便可(在微視裏就是最近 30 天用戶有活躍,且在最新一天有留存);

b.只出如今 B 表(在微視裏是最近 30 天活躍的用戶在最新一天沒留存),這時須要拿 「0,」 拼接一個 B 表的數組集,「0,」 放在第一位;

c.只出如今 A 表(在微視裏是新用戶或者 31 天前活躍的迴流用戶),這時須要拿 「1,」拼接一個 30 位長的默認數組集 「0,0,0,…,0,0」 ,「1,」 放在第一位。通過如此幾步,就能夠生成最新一天的集合表了,具體脫敏代碼以下:

select 
    20200823 imp_date 
    ,nvl(a.qimei,b.qimei) qimei 
    ,case 
       when a.qimei=b.qimei then concat(b.active_date_set,',',a.active_date_set) 
       when b.qimei is null then concat('0,',a.active_date_set) 
       when a.qimei is null then concat(b.active_date_set,',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') 
      end active_date_set 
    ,case 
       when a.qimei=b.qimei then concat(b.log_num_set,',',a.log_num_set) 
       when b.qimei is null then concat('0,',a.log_num_set) 
       when a.qimei is null then concat(b.log_num_set,',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') 
      end log_num_set 
    ,case 
       when a.qimei=b.qimei then concat(b.log_time_set,',',a.log_time_set) 
       when b.qimei is null then concat('0,',a.log_time_set) 
       when a.qimei is null then concat(b.log_time_set,',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') 
      end log_time_set 
    ,case 
       when a.qimei=b.qimei then concat(b.play_vv_begin_set,',',a.play_vv_begin_set) 
       when b.qimei is null then concat('0,',a.play_vv_begin_set) 
       when a.qimei is null then concat(b.play_vv_begin_set,',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') 
      end play_vv_begin_set 
  from 
  ( select 
        qimei 
        ,substr(active_date_set,1,instr(active_date_set,',',1,30)-1) active_date_set 
        ,substr(log_num_set,1,instr(log_num_set,',',1,30)-1) log_num_set 
        ,substr(log_time_set,1,instr(log_time_set,',',1,30)-1) log_time_set 
        ,substr(play_vv_begin_set,1,instr(play_vv_begin_set,',',1,30)-1) play_vv_begin_set 
      from 
        weishi_31d_active_set_table partition(p_20200822)a 
      where 
        last_time>=20200723 
   )a 
  full join 
  ( select 
        qimei 
        ,'1' active_date_set 
        ,cast(log_num as string) log_num_set 
        ,cast(log_time as string) log_time_set 
        ,cast(play_vv_begin as string) play_vv_begin_set 
      from 
        weishi_dau_active_table partition(p_20200823)a 
   )b 
   on 
    a.qimei=b.qimei

初始化集合代碼相對簡單,只需保留第一位爲實際數值,而後拼接一個 30 位的默認值 0 串,初始化脫敏代碼以下:

select 
    20200823 imp_date 
    ,qimei 
    ,'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' active_date_set 
    ,concat(cast(log_num as string),',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') log_num_set 
    ,concat(cast(log_time as string),',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') log_time_set 
    ,concat(cast(play_vv_begin as string),',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') play_vv_begin_set 
  from 
    weishi_dau_active_table partition(p_20200823)a

7、具體使用案例

在 hive 裏對這些 0 1 集合串的使用是比較困難的,爲了讓這個模型表的可用性更高,所以寫了幾個 UDF 函數來直接對數組集合進行簡單地運算,目前寫了以下幾個:str_sum()、str_count()、str_min()、str_max(),其中 str_sum、str_min、str_max 這幾個函數的參數同樣,第一個傳入一個數組集合字符串,第二位傳入一個整數,表明要計算最近 N 天的結果,第三個參數是傳入一個分隔符,在本模型裏分隔符均爲逗號「,」。

這幾個函數都是返回一個 int 值,str_sum 返回來的是最近 N 天的數值加總,str_min 返回該數組集合元素裏最小的值,str_max 返回該數組集合元素裏最大的值;str_count 前 3 個參數與前面三個函數同樣,第 4 個參數是傳入要統計的值,返回來的也是 int 值,返回傳入的統計值在數組集合出現的次數,具體使用方法以下,因爲是自定義函數,在 tdw 集羣跑的 sql 前面需加@pyspark:

以上函數的具體使用案例脫敏代碼以下:

@pysparkselect 
    qimei 
    ,str_sum(active_date_set,30,',') active_date_num  --每一個用戶最近30天活躍天數 
    ,str_sum(play_vv_begin_set,30,',') play_vv_begin  --每一個用戶最近30天播放視頻次數 
    ,30 - str_count(interact_num_set,30,',','0') interact_date_num  --每一個用戶最近30天有互動的天數,經過 30 - 互動天數爲0 統計獲得 
  from 
    weishi_31d_active_set_table partition(p_20200823)a 
  where 
    last_time>20200724

固然除了上面幾種 udf 統計所需指標以外,也能夠經過正則表達式進行使用,好比統計活躍天能夠這樣統計:

--將數組集合裏的'0'和','用正則表達式匹配去掉再來看剩下1的個數便可。 
select 
    count(qimei) --月活 
    ,sum(length(regexp_replace(substr(active_date_set,1,60),'0|,',''))) active_date_num  --月活躍天 
  from 
    weishi_31d_active_set_table partition(p_20200823)a 
  where 
    last_time>20200724

開篇前的幾個業務場景,也能夠經過該錶快速統計:1.統計最近 30 天用戶的累計活躍天?

@pyspark 
select 
    sum(active_date_num) active_date_num  --滾動月活躍天 
    ,count(1) uv  --滾動月活 
  from 
  ( select 
        qimei 
        ,str_sum(active_date_set,30,',') active_date_num 
      from 
        weishi_31d_active_set_table partition(p_20200823)a 
      where 
        last_time>20200724 
   )a

2.統計最近 7 天的用戶累計使用時長?

@pyspark 
select 
    sum(log_time) log_time  --滾動周活躍天 
    ,count(1) uv  --滾動周活 
  from 
  ( select 
        qimei 
        ,str_sum(log_time_set,7,',') log_time 
      from 
        weishi_31d_active_set_table partition(p_20200823)a 
      where 
        last_time>20200817 
   )a

3.統計最近 30 天有播放的累計用戶數?

@pyspark 
select 
    count(1) uv  --播放次數>0 
  from 
  ( select 
        qimei 
        ,str_sum(play_vv_begin_set,30,',','0') play_vv_begin 
      from 
        weishi_31d_active_set_table partition(p_20200823)a 
      where 
        last_time>20200724 
   )a 
  where 
    play_vv_begin>0

4.統計最近 30 天活躍用戶有多少在最近 30 天裏有連續 3 天及以上活躍?

--只是判斷活躍集合裏面有連續3位 1,1,1, 便可select 
    count(if(substr(active_date_set,1,60) like '%1,1,1,%',qimei,null)) active_date_num 
  from 
    weishi_31d_active_set_table partition(p_20200823)a 
  where 
    last_time>20200724

5.統計 28 天前活躍用戶的 一、三、七、1四、28 天留存率?

--不須要join操做,只需找到活躍日期集對應位是否1便可select 
    '20200723' imp_date 
    ,count(if(split(active_date_set,',')['29']='1',qimei,null))/count(1) 1d_retain_rate 
    ,count(if(split(active_date_set,',')['27']='1',qimei,null))/count(1) 3d_retain_rate 
    ,count(if(split(active_date_set,',')['23']='1',qimei,null))/count(1) 7d_retain_rate 
    ,count(if(split(active_date_set,',')['16']='1',qimei,null))/count(1) 14d_retain_rate 
    ,count(if(split(active_date_set,',')['2']='1',qimei,null))/count(1) 28d_retain_rate 
  from 
    weishi_31d_active_set_table partition(p_20200823)a 
  where 
    last_time>20200723 
    and split(active_date_set,',')['30']='1'

8、總結

從上面 5 個業務場景能夠看出來,只要有這樣一個藉助 bitmap 思想統計的模型表,無論統計最近一段時間的累計(月活躍天、月播放用戶等)與統計 1 個月內的留存,均可以一條簡單語句便可統計,不須要 join 操做,天天例行化跑時不須要重複跑接近一個月的分區,1 個月內能夠支持任意統計,好比只需最近 2 周的活躍天等,所以這樣的模型相對通用,另外若是業務須要用到 2 個月的數據,也能夠將模型從 31 位擴展到 61 位。

固然任何事情不可能只有優勢,而不存在缺點的狀況,這裏這個優化的模型只是參考了 bitmap 思想,並非 bitmap 方案實現,雖然能夠將 31 天活躍用戶壓縮 80%多存儲,可是天天都存儲 31 天活躍用戶的壓縮數據,所以相比以前只保留天增量表來講,仍是增長了實際存儲空間,可是這個以存儲換計算的方案是符合數倉設計原則的,由於計算是用成本昂貴的 cpu 和內存資源,存儲是用成本低廉的磁盤資源,所以有涉及最近 N 天累計或者留存計算需求的朋友能夠借鑑這樣的思路。

【本文爲51CTO專欄做者「騰訊技術工程」原創稿件,轉載請聯繫原做者(微信號:Tencent_TEG)】

戳這裏,看該做者更多好文

【編輯推薦】

  1. 大數據就是泡沫!除了報表和取數,它還能幹啥?

  2. 企業如何使用大數據和分析

  3. 大數據如何從新定義法律行業

  4. 在負面產品評論實際發生以前,如何利用大數據和人工智能對其進行 「捕捉」

  5. 物聯網和大數據:6個值得關注的零售和金融服務趨勢

【責任編輯:武曉燕 TEL:(010)68476606】

相關文章
相關標籤/搜索