行業 :
幾乎所有行業 , 如互聯網、新零售、教育、遊戲等 .
應用場景
根據目標群體的特徵 , 快速提取目標群體. 例如 ,
- 在電商行業中, 商家在搞運營活動前, 根據活動的目標群體的特徵, 圈選出一批目標用戶進行廣告推送或活動條件的命中.
- 在遊戲行業中, 運營經常會根據遊戲玩家的某些特徵圈選, 針對性的發放大禮包, 激活遊戲活躍度.
- 在教育行業中, 根據學生不同的特徵, 推送不同的有針對性的習題, 提升學生的弱項.
- 在搜索、門戶、視頻網站等業務中, 根據用戶的關注熱點, 近期行為的不同, 根據群體推送內容.
場景痛點
業務特點 :
- 數據量龐大, 運算量大
- 用戶標籤多, 字段多, 存儲佔用空間多
- 字段多, 可能超過數據庫的字段數限制, 一般數據庫最多支持1000 多個字段.
- 使用數組替代多字段存儲標籤, 需要數據庫支持倒排索引, 不是所有數據庫都支持倒排索引
- 使用數組代替多字段存儲標籤, 加上倒排索引, 存儲空間會暴增
- 圈選條件組合多樣化, 沒有固定索引可以優化, 每個字段一個索引, 存儲空間暴增
- 性能要求高, 實時營銷, 要求秒級響應
- 數據更新時效要求高, 用戶畫像要求近實時的更新, 如果不能準實時更新很可能圈選到的用戶不精準( 例如用戶昨天在瀏覽手機. 昨晚已下單, 但是數據未更新, 那麼作為手機賣家圈選時這個用戶就會被選中, 但是實際上已經不是目標群體)
業務痛點 : 在常見的產品如 MySQL 中, 無法在有限的資源下, 滿足實時圈選目標群體的需求.
技術方案
方案 1
表結構 :
KEY: 用戶 ID
標籤 1:
標籤 2:
…
標籤 N:
Copy
索引 :
每個標籤字段一個索引
Copy
搜索方法 :
and , or , not 組合
where 標籤 a and 標籤 b and …
Copy
缺陷 :
- 用戶標籤多, 字段多, 存儲佔用空間多
- 字段多, 可能超過數據庫的字段數限制, 一般數據庫最多支持1000多個字段.
- 圈選條件組合多樣化, 沒有固定索引可以優化, 每個字段一個索引, 存儲空間暴增
- 新增一個新多用戶群體(標籤)時, 需要更新大量數據
- 查詢性能差
- 方案1也可以是多對多的結構, 每個標籤一條記錄, 解決字段數限制的問題.
- 當然了, 字段數限制還可以通過拆表來解決, 但是查詢的時候就可能需要跨表JOIN了
方案 2
表結構 :
KEY :用戶 ID
VALUES :標籤數組
Copy
索引 :
標籤數組字段 : GIN 倒排索引
Copy
搜索方法 :
與、或、非
where VALUES @> array[标签s] — 与
where VALUES && array[标签s] — 或
where not VALUES @> array[标签s] — 非
Copy
缺陷 :
- 使用數組替代多字段存儲標籤, 需要數據庫支持倒排索引, 不是所有數據庫都支持倒排索引
- 使用數組代替多字段存儲標籤, 加上倒排索引, 存儲空間會暴增
- 新增一個新多用戶群體(標籤)時, 需要更新大量數據
方案 3
表結構 :
KEY :標籤 ID
VALUES: 用戶 bitmap
Copy
索引 :
標籤 ID 字段 : Btree 索引
Copy
搜索方法 :
聚合bitmap: 与、或、非
and_agg(bitmaps) where KEY in (标签s) -- 与
or_agg(bitmaps) where KEY in (标签s) -- 或
except(bitmap1,bitmap2) -- 非
Copy
缺陷
- bitmap最大長度為1GB, 用戶數超過長度需要使用offset, 方法如下:
- o offset0_bitmap, offset1gb_bitmap, …
- 用戶ID需要是數字(建議連續數值), 如果沒有數值型UID, 需要治理, 建立映射表.
優勢 :
- 表存儲佔用空間小
- 索引存儲佔用空間小
- 僅需一個Btree索引, 索引記錄數少(有多少標籤, 就有多少條記錄, 通常標籤數在百萬以內)
- 新增一個新多用戶群體(標籤)時, 不需要更新大量數據, 僅需新增一條新人群的bitmap記錄
- 查詢性能極好
DEMO 介紹
通用操作
- 購買RDS PG 12
- 購買RDS MySQL 8.0
- 配置白名單
- 創建用戶
- 創建數據庫
方案 1 DEMO
MySQL 不支持數組類型、倒排索引、位圖功能 , 所以僅支持方案 1.
-
MySQL 8.0
- PG 12
1 、創建人群表 , 每條記錄代表一個人群 .
create table t_tag_dict (
tag int primary key, -- 标签(人群)id
info text, -- 人群描述
crt_time timestamp -- 时间
);
Copy
2 、生成 10 萬個人群 ( 即標籤 )
insert into t_tag_dict values (1, '男', now());
insert into t_tag_dict values (2, '女', now());
insert into t_tag_dict values (3, '大于24岁', now());
-- ...
insert into t_tag_dict
select generate_series(4,100000), md5(random()::text), clock_timestamp();
Copy
3 、創建用戶畫像表 ( 每個用戶 N 條記錄 , 每條記錄代表這個用戶貼的某個標籤 )
create table t_user_tag (
uid int8, -- 用户id
tag int, -- 用户对应标签(人群)
mod_time timestamp, -- 时间
primary key (tag,uid)
);
Copy
4 、給 2000 萬個用戶打標 , 每個用戶 64 個隨機標籤 , 其中男、女各一半 , 一共 12.8 億條記錄
create or replace function gen_rand_tag(int,int) returns setof int as
$$
select case when random() > 0.5 then 1::int else 2::int end as tag
union all
select ceil(random()*$1)::int as tag from generate_series(1,$2);
$$ language sql strict volatile;
insert into t_user_tag
select uid, gen_rand_tag(100000,63) as tag, clock_timestamp()
from generate_series(1,20000000) as uid on conflict (uid,tag) do nothing;
— 或使用如下方法加速導入
create sequence seq;
vi test.sql
insert into t_user_tag
select uid, gen_rand_tag(100000,63) as tag, clock_timestamp()
from nextval('seq'::regclass) as uid
on conflict(tag,uid) do nothing;
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 50 -j 50 -t 400000
Copy
5 、查詢包含 1,3 標籤的人群
1 、人群數量
select count(*) from
(
select uid from t_user_tag where tag=1
intersect
select uid from t_user_tag where tag=3
) t;
-- Time: 1494.789 ms (00:01.495)
2 、提取人群 ID
select uid from t_user_tag where tag=1
intersect
select uid from t_user_tag where tag=3;
-- Time: 3246.184 ms (00:03.246)
Copy
6 、查詢包含 1 或 3 或 10 或 200 標籤的人群
1 、人群數量
select count(*) from
(
select uid from t_user_tag where tag=1
union
select uid from t_user_tag where tag=3
union
select uid from t_user_tag where tag=10
union
select uid from t_user_tag where tag=200
) t;
-- Time: 3577.714 ms (00:03.578)
2 、提取人群 ID
select uid from t_user_tag where tag=1
union
select uid from t_user_tag where tag=3
union
select uid from t_user_tag where tag=10
union
select uid from t_user_tag where tag=200;
-- Time: 5682.458 ms (00:05.682)
Copy
7 、空間佔用情況 :
public | t_user_tag | table | postgres | 62 GB |
public | t_user_tag_pkey | index | postgres | t_user_tag | 61 GB |
Copy
方案 2 DEMO
- PG 12
1 、創建人群表 , 每條記錄代表一個人群 .
create table t_tag_dict (
tag int primary key, -- 标签(人群)id
info text, -- 人群描述
crt_time timestamp -- 时间
);
Copy
2 、生成 10 萬個人群 ( 即標籤 )
insert into t_tag_dict values (1, '男', now());
insert into t_tag_dict values (2, '女', now());
insert into t_tag_dict values (3, '大于24岁', now());
-- ...
insert into t_tag_dict
select generate_series(4,100000), md5(random()::text), clock_timestamp();
Copy
3 、創建用戶畫像表 ( 每個用戶一條記錄 , 用數組表示這個用戶歸屬哪些標籤 )
create table t_user_tags (
uid int8 primary key, -- 用户id
tags int[], -- 用户标签(人群)数组
mod_time timestamp -- 时间
);
Copy
4 、創建生成隨機打標數組的函數
create or replace function gen_rand_tags(int,int) returns int[] as $$
select array_agg(ceil(random()*$1)::int) from generate_series(1,$2);
$$ language sql strict;
Copy
4.1 、在 10 萬個標籤內隨機提取 8 個標籤 :
select gen_rand_tags(100000, 8);
en_rand_tags
---------------------------------------------------
{43494,46038,74102,25308,99129,40893,33653,29690}
(1 row)
Copy
5 、給 2000 萬個用戶打標 , 每個用戶 64 個隨機標籤 , 其中男、女各一半
insert into t_user_tags
select generate_series(1,10000000),
array_append(gen_rand_tags(100000, 63),1), now();
insert into t_user_tags
select generate_series(10000001,20000000),
array_append(gen_rand_tags(100000, 63),2), now();
Copy
6 、創建標籤(人群)字段倒排索引
create index idx_t_user_tags_1 on t_user_tags using gin (tags);
Copy
7 、查詢包含 1,3 標籤的人群
1 、人群數量
select count(uid) from t_user_tags where tags @> array[1,3];
2 、提取人群 ID
select uid from t_user_tags where tags @> array[1,3];
Copy
8 、查詢包含 1 或 3 或 10 或 200 標籤的人群
1 、人群數量
select count(uid) from t_user_tags where tags && array[1,3,10,200];
2 、提取人群 ID
select uid from t_user_tags where tags && array[1,3,10,200];
Copy
方案 3 DEMO
- PG 12
RDS PG 12 已支持位圖功能 , 常用說明:
安裝插件 – create extension roaringbitmap;
bitmap 輸出格式 – set roaringbitmap.output_format='bytea|array';
bitmap 取值範圍 – 40 億( int4 )
構造 bitmap – rb_build(int4[])
bitmap 轉換為數組或多條記錄 - rb_to_array(rb) – rb_iterate(rb)
bitmap 內包含對像個數 – rb_cardinality(rb)
邏輯運算 : 與、或、異或、差
SELECT roaringbitmap('{1,2,3}') | roaringbitmap('{3,4,5}');
SELECT roaringbitmap('{1,2,3}') & roaringbitmap('{3,4,5}');
SELECT roaringbitmap('{1,2,3}') # roaringbitmap('{3,4,5}');
SELECT roaringbitmap('{1,2,3}') - roaringbitmap('{3,4,5}');
聚合運算 : build rb 、與、或、異或
SELECT rb_build_agg(e) FROM generate_series(1,100) e;
SELECT rb_or_agg(bitmap) FROM t1;
SELECT rb_and_agg(bitmap) FROM t1;
SELECT rb_xor_agg(bitmap) FROM t1;
聚合併統計對像數 ( 與、或、異或 )
rb_or_cardinality_agg
rb_and_cardinality_agg
rb_xor_cardinality_agg
邏輯判斷 : 包含、相交、相等、不相等
Opperator Input Output Desc Example Result
@> roaringbitmap,roaringbitmap bool contains roaringbitmap('{1,2,3}') @> roaringbitmap('{3,4,5}') f
@> roaringbitmap,integer bool contains roaringbitmap('{1,2,3,4,5}') @> 3 t
<@ roaringbitmap,roaringbitmap bool is contained by roaringbitmap('{1,2,3}') f
<@ integer,roaringbitmap bool is contained by 3 t
&& roaringbitmap,roaringbitmap bool overlap (have elements in common) roaringbitmap('{1,2,3}') && roaringbitmap('{3,4,5}') t
= roaringbitmap,roaringbitmap bool equal roaringbitmap('{1,2,3}') = roaringbitmap('{3,4,5}') f
<> roaringbitmap,roaringbitmap bool not equal roaringbitmap('{1,2,3}') <> roaringbitmap('{3,4,5}') t
Copy
當 uid 超過 int4(40 億 ) 時 , 使用 offset 轉換 , 轉換方法如下 :
- https://github.com/digoal/blog/blob/master/202001/20200110_03.md
其他使用方法參考 :
- https://pgxn.org/dist/pg_roaringbitmap/
1 、安裝插件
create extension roaringbitmap;
Copy
2 、創建標籤,用戶 bitmap 表
create table t_tag_users ( tagid int primary key, -- 用户标签(人群)id
uid_offset int, -- 由于userid是int8类型,roaringbitmap内部使用int4
存儲,需要轉換一下。
userbits roaringbitmap, -- 用户id聚合的 bitmap mod_time timestamp -- 时间
);
Copy
3 、生成標籤, uid bitmap
insert into t_tag_users
select tagid, uid_offset, rb_build_agg(uid::int) as userbits from
(
select
unnest(tags) as tagid,
(uid / (2^31)::int8) as uid_offset,
mod(uid, (2^31)::int8) as uid
from t_user_tags
) t
group by tagid, uid_offset;
Copy
4 、查詢包含 1,3 標籤的人群
1 、人群數量
select sum(ub) from
(
select uid_offset,
rb_and_cardinality_agg(userbits) as ub
from t_tag_users
where tagid in (1,3)
group by uid_offset
) t;
2 、提取人群 ID
select uid_offset,rb_and_agg(userbits) as ub from t_tag_users
where tagid in (1,3)
group by uid_offset;
Copy
5 、查詢包含 1 或 3 或 10 或 200 標籤的人群
1 、人群數量
select sum(ub) from(
select uid_offset,rb_or_cardinality_agg(userbits) as ub
from t_tag_users
where tagid in (1,3,10,200)
group by uid_offset
) t;
2 、提取人群 ID
select uid_offset,rb_or_agg(userbits) as ub
from t_tag_users
where tagid in (1,3,10,200)
group by uid_offset;
Copy
方案對比
環境 :
數據庫 |
計算規格 |
存儲規格 |
MySQL 8.0 |
8C 32G |
1500GB ESSD |
PG 12 |
8C 32G |
1500GB ESSD |
性能對比 :
CASE (12.8億user/tags) (2000萬, 64 tags/user) |
方案 1 (MySQL、PG) 多對多 :常規方案 |
方案 2 (PG) 一對多 :數組、倒排索引 |
方案 3 (PG) 一對多 :位圖 |
方案 3 vs方案1 提升 % |
與查詢圈選用戶速度 |
1.5秒 |
42毫秒 |
1.5毫秒 |
99900%
|
或查詢圈選用戶速度 |
3.6秒 |
3秒 |
1.7毫秒 |
211665% |
空間佔用(表) |
62GB |
3126MB |
1390MB |
4467% |
空間佔用(索引) |
61GB |
3139MB |
2MB |
3123100% |
build索引速度 |
- |
20分鐘 |
0 |
- |
RDS PG 方案價值:
1 、RDS PG 支持了位圖功能(roaringbitmap), 可以非常高效率的生成、壓縮、解析位圖數據, 支持最常見的與、或、非、異或等位圖聚合操作, 提取位圖的ID 、選擇性, 判斷ID 是否存在等操作. 2 、使用RDS PG 數據庫, 滿足了用戶在億級以上用戶, 百萬~ 千萬量級標籤的大數據量下實時精準營銷、快速圈選用戶的需求. 3 、對比MySQL 的方案, RDS PG 方案優勢非常明顯, 是一個低成本, 高效率的解決方案.
· 節約存儲空間8948%,
· 平均性能提升155782.5%,
· 最高性能提升211665%.