Skip to content

Latest commit

 

History

History
907 lines (740 loc) · 65.1 KB

20230824_02.md

File metadata and controls

907 lines (740 loc) · 65.1 KB

沉浸式学习PostgreSQL|PolarDB 3: 营销场景, 根据用户画像的相似度进行目标人群圈选, 实现精准营销

作者

digoal

日期

2023-08-24

标签

PostgreSQL , PolarDB , 数据库 , 教学


背景

非常欢迎数据库用户提出场景给我, 在此issue回复即可, 一起来建设沉浸式数据库学习教学素材库, 帮助开发者用好数据库, 提升开发者职业竞争力, 同时为企业降本提效.

本文的实验可以使用永久免费的阿里云云起实验室来完成.

如果你本地有docker环境也可以把镜像拉到本地来做实验:

x86_64机器使用以下docker image:

Apple Chip机器使用以下docker image:

业务场景1 介绍: 营销场景, 根据用户画像的相似度进行目标人群圈选, 实现精准营销

在营销场景中, 通常会对用户的属性、行为等数据进行统计分析, 生成用户的标签, 也就是常说的用户画像.

标签举例: 男性、女性、年轻人、大学生、90后、司机、白领、健身达人、博士、技术达人、科技产品爱好者、2胎妈妈、老师、浙江省、15天内逛过手机电商店铺、... ...

有了用户画像, 在营销场景中一个重要的营销手段是根据条件选中目标人群, 进行精准营销.

例如圈选出包含这些标签的人群: 白领、科技产品爱好者、浙江省、技术达人、15天内逛过手机电商店铺 .

这个实验的目的是在有画像的基础上, 如何快速根据标签组合进行人群圈选 .

实现和对照

设计1张标签元数据表, 后面的用户画像表从这张标签表随机抽取标签. 业务查询时也从这里搜索存在的标签并进行圈选条件的组合, 得到对应的标签ID组合.

drop table if exists tbl_tag;  
  
create table tbl_tag (  
  tid int primary key,  -- 标签id  
  tag text,  -- 标签名  
  info text  -- 标签描述  
);  

假设有1万个标签, 写入标签元数据表.

insert into tbl_tag select id, md5(id::text), md5(random()::text) from generate_series(1, 10000) id;  

创建2个函数, 产生若干的标签. 用来模拟产生每个用户对应的标签数据. 分别返回字符串和数组类型.

第一个函数, 随机提取若干个标签, 始终包含1-100的热门标签8个, 返回用户标签字符串:

create or replace function get_tags_text(int) returns text as $$  
  with a as (select string_agg(tid::text, ',') s from tbl_tag where tid = any (array(select ceil(random()*100)::int from generate_series(1,8) group by 1)))  
  , b as (select string_agg(tid::text, ',') s from tbl_tag where tid = any (array(select ceil(100+random()*9900)::int from generate_series(1,$1) group by 1)))  
  select ','||a.s||','||b.s||',' from a,b;  
$$ language sql strict;  

得到类似这样的结果:

postgres=# select get_tags_text(10);  
                            get_tags_text  
----------------------------------------------------------------------  
 ,11,12,39,44,45,59,272,1001,1322,1402,2514,6888,7404,8922,9200,9409,  
(1 row)  
  
postgres=# select get_tags_text(10);  
                             get_tags_text  
------------------------------------------------------------------------  
 ,12,34,52,55,71,79,88,302,582,1847,3056,5156,8231,8542,8572,8747,9727,  
(1 row)  

第二个函数, 随机提取若干个标签, 始终包含1-100的热门标签8个, 返回用户标签数组:

create or replace function get_tags_arr(int) returns int[] as $$  
  with a as (select array_agg(tid) s from tbl_tag where tid = any (array(select ceil(random()*100)::int from generate_series(1,8) group by 1)))  
  , b as (select array_agg(tid) s from tbl_tag where tid = any (array(select ceil(100+random()*9900)::int from generate_series(1,$1) group by 1)))  
  select a.s||b.s from a,b;  
$$ language sql strict;  

得到类似这样的结果:

postgres=# select * from get_tags_arr(10);  
                                get_tags_arr  
----------------------------------------------------------------------------  
 {13,35,42,61,67,69,76,78,396,2696,3906,4356,5064,5711,7363,9417,9444,9892}  
(1 row)  
  
postgres=# select * from get_tags_arr(10);  
                              get_tags_arr  
-------------------------------------------------------------------------  
 {2,10,20,80,84,85,89,3410,3515,4159,4182,5217,6549,6775,7289,9141,9431}  
(1 row)  

传统方法 设计和实验

传统数据库没有数组类型, 所以需要用字符串存储标签.

创建用户画像表

drop table if exists tbl_users;  
  
create unlogged table tbl_users (  -- 为便于加速生成测试数据, 使用unlogged table  
  uid int primary key,   -- 用户id  
  tags text   -- 该用户拥有的标签 , 使用字符串类型  
);  

创建100万个用户, 用户被贴的标签数从32到256个, 随机产生, 其中8个为热门标签(例如性别、年龄段等都属于热门标签).

insert into tbl_users select id, get_tags_text(ceil(24+random()*224)::int) from generate_series(1,1000000) id;  

测试如下, 分别搜索包含如下标签组合的用户:

  • 2
  • 2,8
  • 2,2696
  • 2,4356,5064,5711,7363,9417,9444
  • 4356,5064,5711,7363,9417,9444

使用如下SQL:

select uid from tbl_users where tags like '%,2,%';  
  
select uid from tbl_users where tags like '%,2,%' or tags like '%,8,%';  
  
select uid from tbl_users where tags like '%,2,%' or tags like '%,2696,%';  
  
select uid from tbl_users where tags like '%,2,%' or tags like '%,4356,%' or tags like '%,5064,%' or tags like '%,5711,%' or tags like '%,7363,%' or tags like '%,9417,%' or tags like '%,9444,%' ;  
  
select uid from tbl_users where tags like '%,4356,%' or tags like '%,5064,%' or tags like '%,5711,%' or tags like '%,7363,%' or tags like '%,9417,%' or tags like '%,9444,%' ;  

查看以上SQL运行的执行计划和耗时如下:

postgres=# explain analyze select uid from tbl_users where tags like '%,2,%';  
                                                    QUERY PLAN  
-------------------------------------------------------------------------------------------------------------------  
 Seq Scan on tbl_users  (cost=0.00..103268.00 rows=80808 width=4) (actual time=0.018..1108.805 rows=77454 loops=1)  
   Filter: (tags ~~ '%,2,%'::text)  
   Rows Removed by Filter: 922546  
 Planning Time: 1.095 ms  
 Execution Time: 1110.267 ms  
(5 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags like '%,2,%' or tags like '%,8,%';  
                                                     QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------  
 Seq Scan on tbl_users  (cost=0.00..105768.00 rows=127232 width=4) (actual time=0.029..2001.379 rows=149132 loops=1)  
   Filter: ((tags ~~ '%,2,%'::text) OR (tags ~~ '%,8,%'::text))  
   Rows Removed by Filter: 850868  
 Planning Time: 1.209 ms  
 Execution Time: 2004.062 ms  
(5 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags like '%,2,%' or tags like '%,2696,%';  
                                                    QUERY PLAN  
-------------------------------------------------------------------------------------------------------------------  
 Seq Scan on tbl_users  (cost=0.00..105768.00 rows=90093 width=4) (actual time=0.035..2058.797 rows=90084 loops=1)  
   Filter: ((tags ~~ '%,2,%'::text) OR (tags ~~ '%,2696,%'::text))  
   Rows Removed by Filter: 909916  
 Planning Time: 1.190 ms  
 Execution Time: 2060.434 ms  
(5 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags like '%,2,%' or tags like '%,4356,%' or tags like '%,5064,%' or tags like '%,5711,%' or tags like '%,7363,%' or tags like '%,9417,%' or tags like '%,9444,%' ;  
                                                                                                       QUERY PLAN  
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Seq Scan on tbl_users  (cost=0.00..118268.00 rows=135482 width=4) (actual time=0.024..6765.315 rows=150218 loops=1)  
   Filter: ((tags ~~ '%,2,%'::text) OR (tags ~~ '%,4356,%'::text) OR (tags ~~ '%,5064,%'::text) OR (tags ~~ '%,5711,%'::text) OR (tags ~~ '%,7363,%'::text) OR (tags ~~ '%,9417,%'::text) OR (tags ~~ '%,9444,%'::text))  
   Rows Removed by Filter: 849782  
 Planning Time: 4.344 ms  
 Execution Time: 6767.990 ms  
(5 rows)  
  
postgres=#  explain analyze select uid from tbl_users where tags like '%,4356,%' or tags like '%,5064,%' or tags like '%,5711,%' or tags like '%,7363,%' or tags like '%,9417,%' or tags like '%,9444,%' ;  
                                                                                          QUERY PLAN  
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Seq Scan on tbl_users  (cost=0.00..115768.00 rows=59480 width=4) (actual time=0.112..6206.775 rows=78827 loops=1)  
   Filter: ((tags ~~ '%,4356,%'::text) OR (tags ~~ '%,5064,%'::text) OR (tags ~~ '%,5711,%'::text) OR (tags ~~ '%,7363,%'::text) OR (tags ~~ '%,9417,%'::text) OR (tags ~~ '%,9444,%'::text))  
   Rows Removed by Filter: 921173  
 Planning Time: 4.223 ms  
 Execution Time: 6208.191 ms  
(5 rows)  

PolarDB|PG新方法1 设计和实验

第一种方法没有用到任何的索引, 每次请求都要扫描用户画像表的所有记录, 计算每一个LIKE的算子, 性能比较差.

为了提升查询性能, 我们可以使用gin索引和pg_trgm插件, 支持字符串内的模糊查询索引加速.

复用方法1的数据, 创建gin索引, 支持索引加速模糊查询.

create extension pg_trgm;  
  
create index on tbl_users using gin (tags gin_trgm_ops);  

使用索引后, 查看执行计划和耗时如下:

postgres=# explain analyze select uid from tbl_users where tags like '%,2,%';  
                                                              QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=555.93..58686.88 rows=80808 width=4) (actual time=30.315..76.314 rows=77454 loops=1)  
   Recheck Cond: (tags ~~ '%,2,%'::text)  
   Heap Blocks: exact=53210  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..535.73 rows=80808 width=0) (actual time=22.967..22.967 rows=77454 loops=1)  
         Index Cond: (tags ~~ '%,2,%'::text)  
 Planning Time: 0.991 ms  
 Execution Time: 78.163 ms  
(7 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags like '%,2,%' or tags like '%,8,%';  
                                                                 QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=983.56..87215.27 rows=127232 width=4) (actual time=48.651..811.842 rows=149132 loops=1)  
   Recheck Cond: ((tags ~~ '%,2,%'::text) OR (tags ~~ '%,8,%'::text))  
   Rows Removed by Index Recheck: 299658  
   Heap Blocks: exact=41915 lossy=33158  
   ->  BitmapOr  (cost=983.56..983.56 rows=131313 width=0) (actual time=43.554..43.554 rows=0 loops=1)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..535.73 rows=80808 width=0) (actual time=24.923..24.923 rows=77454 loops=1)  
               Index Cond: (tags ~~ '%,2,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..384.22 rows=50505 width=0) (actual time=18.629..18.629 rows=77054 loops=1)  
               Index Cond: (tags ~~ '%,8,%'::text)  
 Planning Time: 1.496 ms  
 Execution Time: 814.748 ms  
(11 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags like '%,2,%' or tags like '%,2696,%';  
                                                                 QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=958.67..64006.30 rows=90093 width=4) (actual time=75.859..900.779 rows=90084 loops=1)  
   Recheck Cond: ((tags ~~ '%,2,%'::text) OR (tags ~~ '%,2696,%'::text))  
   Rows Removed by Index Recheck: 348263  
   Heap Blocks: exact=39411 lossy=33155  
   ->  BitmapOr  (cost=958.67..958.67 rows=90909 width=0) (actual time=71.980..71.981 rows=0 loops=1)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..535.73 rows=80808 width=0) (actual time=26.486..26.487 rows=77454 loops=1)  
               Index Cond: (tags ~~ '%,2,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..377.89 rows=10101 width=0) (actual time=45.492..45.492 rows=62326 loops=1)  
               Index Cond: (tags ~~ '%,2696,%'::text)  
 Planning Time: 1.479 ms  
 Execution Time: 902.637 ms  
(11 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags like '%,2,%' or tags like '%,4356,%' or tags like '%,5064,%' or tags like '%,5711,%' or tags like '%,7363,%' or tags like '%,9417,%' or tags like '%,9444,%' ;  
                                                                                                          QUERY PLAN  
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=3041.18..100880.75 rows=135482 width=4) (actual time=210.772..4047.148 rows=150218 loops=1)  
   Recheck Cond: ((tags ~~ '%,2,%'::text) OR (tags ~~ '%,4356,%'::text) OR (tags ~~ '%,5064,%'::text) OR (tags ~~ '%,5711,%'::text) OR (tags ~~ '%,7363,%'::text) OR (tags ~~ '%,9417,%'::text) OR (tags ~~ '%,9444,%'::text))  
   Rows Removed by Index Recheck: 422706  
   Heap Blocks: exact=56868 lossy=33226  
   ->  BitmapOr  (cost=3041.18..3041.18 rows=141614 width=0) (actual time=205.898..205.899 rows=0 loops=1)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..535.73 rows=80808 width=0) (actual time=24.656..24.656 rows=77454 loops=1)  
               Index Cond: (tags ~~ '%,2,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..428.40 rows=20202 width=0) (actual time=45.014..45.014 rows=62615 loops=1)  
               Index Cond: (tags ~~ '%,4356,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..377.89 rows=10101 width=0) (actual time=22.680..22.680 rows=39025 loops=1)  
               Index Cond: (tags ~~ '%,5064,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..428.40 rows=20202 width=0) (actual time=28.809..28.809 rows=62697 loops=1)  
               Index Cond: (tags ~~ '%,5711,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..377.89 rows=10101 width=0) (actual time=28.646..28.646 rows=62647 loops=1)  
               Index Cond: (tags ~~ '%,7363,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..327.89 rows=100 width=0) (actual time=28.361..28.361 rows=62172 loops=1)  
               Index Cond: (tags ~~ '%,9417,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..327.89 rows=100 width=0) (actual time=27.729..27.730 rows=62821 loops=1)  
               Index Cond: (tags ~~ '%,9444,%'::text)  
 Planning Time: 4.517 ms  
 Execution Time: 4050.040 ms  
(21 rows)  
  
postgres=#  explain analyze select uid from tbl_users where tags like '%,4356,%' or tags like '%,5064,%' or tags like '%,5711,%' or tags like '%,7363,%' or tags like '%,9417,%' or tags like '%,9444,%' ;  
                                                                                             QUERY PLAN  
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=2357.58..50755.97 rows=59480 width=4) (actual time=209.115..3689.534 rows=78827 loops=1)  
   Recheck Cond: ((tags ~~ '%,4356,%'::text) OR (tags ~~ '%,5064,%'::text) OR (tags ~~ '%,5711,%'::text) OR (tags ~~ '%,7363,%'::text) OR (tags ~~ '%,9417,%'::text) OR (tags ~~ '%,9444,%'::text))  
   Rows Removed by Index Recheck: 455241  
   Heap Blocks: exact=55903 lossy=33218  
   ->  BitmapOr  (cost=2357.58..2357.58 rows=60806 width=0) (actual time=204.235..204.236 rows=0 loops=1)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..428.40 rows=20202 width=0) (actual time=57.485..57.485 rows=62615 loops=1)  
               Index Cond: (tags ~~ '%,4356,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..377.89 rows=10101 width=0) (actual time=26.156..26.157 rows=39025 loops=1)  
               Index Cond: (tags ~~ '%,5064,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..428.40 rows=20202 width=0) (actual time=33.539..33.539 rows=62697 loops=1)  
               Index Cond: (tags ~~ '%,5711,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..377.89 rows=10101 width=0) (actual time=30.136..30.136 rows=62647 loops=1)  
               Index Cond: (tags ~~ '%,7363,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..327.89 rows=100 width=0) (actual time=28.794..28.794 rows=62172 loops=1)  
               Index Cond: (tags ~~ '%,9417,%'::text)  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..327.89 rows=100 width=0) (actual time=28.122..28.122 rows=62821 loops=1)  
               Index Cond: (tags ~~ '%,9444,%'::text)  
 Planning Time: 3.860 ms  
 Execution Time: 3691.329 ms  
(19 rows)  

PolarDB|PG新方法2 设计和实验

很显然你不能满足于前面的模糊查询索引带来的性能提升, 特别是当and条件非常多时, 模糊查询的索引也要被多次扫描并使用bitmap进行合并, 性能不好. (以上方法对于一个模糊查询条件性能提升是非常明显的.)

PolarDB和PostgreSQL都支持数组类型, 用数组存储标签, 支持gin索引可以加速数组的包含查询.

创建用户画像表, 使用数组存储标签字段.

drop table if exists tbl_users;  
  
create unlogged table tbl_users (  -- 为便于加速生成测试数据, 使用unlogged table  
  uid int primary key,   -- 用户id  
  tags int[]   -- 该用户拥有的标签 , 使用数组类型  
);  

创建100万个用户, 用户被贴的标签数从32到256个, 随机产生, 其中8个为热门标签(例如性别、年龄段等都属于热门标签).

insert into tbl_users select id, get_tags_arr(ceil(24+random()*224)::int) from generate_series(1,1000000) id;  
  
create index on tbl_users using gin (tags);  

搜索包含如下标签组合的用户:

  • 2
  • 2,8
  • 2,2696
  • 2,4356,5064,5711,7363,9417,9444
  • 4356,5064,5711,7363,9417,9444

数组匹配的 SQL 语句如下:

select uid from tbl_users where tags @> array[2];  
  
select uid from tbl_users where tags @> array[2,8];  
  
select uid from tbl_users where tags @> array[2,2696];  
  
select uid from tbl_users where tags @> array[2,4356,5064,5711,7363,9417,9444];  
  
select uid from tbl_users where tags @> array[4356,5064,5711,7363,9417,9444];  

使用数组类型和gin索引后, 查看执行计划和耗时如下:

postgres=# explain analyze select uid from tbl_users where tags @> array[2];  
                                                              QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=437.95..53717.07 rows=76333 width=4) (actual time=24.031..69.706 rows=77641 loops=1)  
   Recheck Cond: (tags @> '{2}'::integer[])  
   Heap Blocks: exact=50231  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..418.86 rows=76333 width=0) (actual time=15.026..15.026 rows=77641 loops=1)  
         Index Cond: (tags @> '{2}'::integer[])  
 Planning Time: 1.137 ms  
 Execution Time: 74.015 ms  
(7 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags @> array[2,8];  
                                                             QUERY PLAN  
------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=49.97..6172.63 rows=5847 width=4) (actual time=10.745..18.272 rows=5303 loops=1)  
   Recheck Cond: (tags @> '{2,8}'::integer[])  
   Heap Blocks: exact=5133  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..48.51 rows=5847 width=0) (actual time=10.081..10.081 rows=5303 loops=1)  
         Index Cond: (tags @> '{2,8}'::integer[])  
 Planning Time: 0.256 ms  
 Execution Time: 18.561 ms  
(7 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags @> array[2,2696];  
                                                           QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=21.27..443.58 rows=382 width=4) (actual time=2.872..4.662 rows=1003 loops=1)  
   Recheck Cond: (tags @> '{2,2696}'::integer[])  
   Heap Blocks: exact=999  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..21.18 rows=382 width=0) (actual time=2.729..2.729 rows=1003 loops=1)  
         Index Cond: (tags @> '{2,2696}'::integer[])  
 Planning Time: 0.246 ms  
 Execution Time: 4.750 ms  
(7 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags @> array[2,4356,5064,5711,7363,9417,9444];  
                                                         QUERY PLAN  
----------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=64.38..65.50 rows=1 width=4) (actual time=5.476..5.478 rows=0 loops=1)  
   Recheck Cond: (tags @> '{2,4356,5064,5711,7363,9417,9444}'::integer[])  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..64.38 rows=1 width=0) (actual time=5.471..5.472 rows=0 loops=1)  
         Index Cond: (tags @> '{2,4356,5064,5711,7363,9417,9444}'::integer[])  
 Planning Time: 0.223 ms  
 Execution Time: 5.523 ms  
(6 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags @> array[4356,5064,5711,7363,9417,9444];  
                                                         QUERY PLAN  
----------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=55.36..56.47 rows=1 width=4) (actual time=4.476..4.477 rows=0 loops=1)  
   Recheck Cond: (tags @> '{4356,5064,5711,7363,9417,9444}'::integer[])  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..55.36 rows=1 width=0) (actual time=4.471..4.472 rows=0 loops=1)  
         Index Cond: (tags @> '{4356,5064,5711,7363,9417,9444}'::integer[])  
 Planning Time: 0.275 ms  
 Execution Time: 4.528 ms  
(6 rows)  

PolarDB|PG新方法3 设计和实验

当我们输入一组标签, 如果想放宽圈选条件, 而不仅仅是以上精确包含, 怎么实现? 例如:

  • 包含多少个以上的标签
  • 有百分之多少以上的标签重合

复用上面的数据, 换上smlar插件和索引来实现以上功能.

创建smlar插件

postgres=# create extension smlar ;  
CREATE EXTENSION  

换上smlar索引

drop index tbl_users_tags_idx;  
  
create index on tbl_users using gin (tags _int4_sml_ops);  

smlar插件的%操作符用来表达数组近似度过滤.

postgres=# explain analyze select count(*) from tbl_users where tags % array[1,2,3];  
                                                                QUERY PLAN  
-------------------------------------------------------------------------------------------------------------------------------------------  
 Aggregate  (cost=1132.46..1132.47 rows=1 width=8) (actual time=75.613..75.614 rows=1 loops=1)  
   ->  Bitmap Heap Scan on tbl_users  (cost=35.25..1129.96 rows=1000 width=0) (actual time=75.609..75.610 rows=0 loops=1)  
         Recheck Cond: (tags % '{1,2,3}'::integer[])  
         Rows Removed by Index Recheck: 15059  
         Heap Blocks: exact=13734  
         ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..35.00 rows=1000 width=0) (actual time=31.466..31.466 rows=15059 loops=1)  
               Index Cond: (tags % '{1,2,3}'::integer[])  
 Planning Time: 0.408 ms  
 Execution Time: 75.687 ms  
(9 rows)  

smlar插件支持的参数配置如下, 通过配置这些参数, 我们可以控制按什么算法来计算相似度, 相似度的过滤阈值是多少?

postgres=# select name,setting,enumvals,extra_desc from pg_settings where name ~ 'smlar';  
          name          | setting |        enumvals        |                                 extra_desc  
------------------------+---------+------------------------+-----------------------------------------------------------------------------  
 smlar.idf_plus_one     | off     |                        | Calculate idf by log(1+d/df)  
 smlar.persistent_cache | off     |                        | Cache of global stat is stored in transaction-independent memory  
 smlar.stattable        |         |                        | Named table stores global frequencies of array's elements  
 smlar.tf_method        | n       | {n,log,const}          | TF method: n => number of entries, log => 1+log(n), const => constant value  
 smlar.threshold        | 0.6     |                        | Array's with similarity lower than threshold are not similar by % operation  
 smlar.type             | cosine  | {cosine,tfidf,overlap} | Type of similarity formula: cosine(default), tfidf, overlap  
(6 rows)  

接下来我们来实现上述两种近似搜索:

  • 包含多少个以上的标签
  • 有百分之多少以上的标签重合

包含多少个以上的标签, smlar.type = overlap , smlar.threshold = INT

set smlar.type = overlap;  
set smlar.threshold = 1;  -- 精确匹配  
select uid from tbl_users where tags % array[2];  
  
set smlar.type = overlap;  
set smlar.threshold = 1;  -- 匹配到1个以上标签  
select uid from tbl_users where tags % array[2,8];  
  
set smlar.type = overlap;  
set smlar.threshold = 2;  -- 精确匹配  
select uid from tbl_users where tags % array[2,2696];  
  
set smlar.type = overlap;  
set smlar.threshold = 5;  -- 匹配到5个以上标签  
select uid from tbl_users where tags % array[2,4356,5064,5711,7363,9417,9444];  
  
set smlar.type = overlap;  
set smlar.threshold = 6;  -- 精确匹配  
select uid from tbl_users where tags % array[4356,5064,5711,7363,9417,9444];  

使用smlar插件, 数组类型和gin索引后, 查看执行计划和耗时如下:

postgres=# explain analyze select uid from tbl_users where tags % array[2];  
                                                             QUERY PLAN  
-------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=17.65..1112.36 rows=1000 width=4) (actual time=38.272..306.985 rows=77129 loops=1)  
   Recheck Cond: (tags % '{2}'::integer[])  
   Heap Blocks: exact=50082  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..17.40 rows=1000 width=0) (actual time=26.498..26.498 rows=77129 loops=1)  
         Index Cond: (tags % '{2}'::integer[])  
 Planning Time: 0.414 ms  
 Execution Time: 309.182 ms  
(7 rows)  
  
  
postgres=# explain analyze select uid from tbl_users where tags % array[2,8];  
                                                              QUERY PLAN  
--------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=26.45..1121.16 rows=1000 width=4) (actual time=33.378..790.183 rows=149118 loops=1)  
   Recheck Cond: (tags % '{2,8}'::integer[])  
   Rows Removed by Index Recheck: 351146  
   Heap Blocks: exact=35117 lossy=33064  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..26.20 rows=1000 width=0) (actual time=29.934..29.934 rows=149118 loops=1)  
         Index Cond: (tags % '{2,8}'::integer[])  
 Planning Time: 0.924 ms  
 Execution Time: 794.029 ms  
(8 rows)  
  
  
postgres=# explain analyze select uid from tbl_users where tags % array[2,2696];  
                                                            QUERY PLAN  
----------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=26.45..1121.16 rows=1000 width=4) (actual time=6.287..26.042 rows=1028 loops=1)  
   Recheck Cond: (tags % '{2,2696}'::integer[])  
   Heap Blocks: exact=1019  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..26.20 rows=1000 width=0) (actual time=5.956..5.956 rows=1028 loops=1)  
         Index Cond: (tags % '{2,2696}'::integer[])  
 Planning Time: 0.439 ms  
 Execution Time: 26.218 ms  
(7 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags % array[2,4356,5064,5711,7363,9417,9444];  
                                                           QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=70.45..1165.16 rows=1000 width=4) (actual time=13.211..13.212 rows=0 loops=1)  
   Recheck Cond: (tags % '{2,4356,5064,5711,7363,9417,9444}'::integer[])  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..70.20 rows=1000 width=0) (actual time=13.204..13.205 rows=0 loops=1)  
         Index Cond: (tags % '{2,4356,5064,5711,7363,9417,9444}'::integer[])  
 Planning Time: 0.204 ms  
 Execution Time: 13.264 ms  
(6 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags % array[4356,5064,5711,7363,9417,9444];  
                                                           QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=61.65..1156.36 rows=1000 width=4) (actual time=11.364..11.366 rows=0 loops=1)  
   Recheck Cond: (tags % '{4356,5064,5711,7363,9417,9444}'::integer[])  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..61.40 rows=1000 width=0) (actual time=11.357..11.358 rows=0 loops=1)  
         Index Cond: (tags % '{4356,5064,5711,7363,9417,9444}'::integer[])  
 Planning Time: 0.264 ms  
 Execution Time: 11.447 ms  
(6 rows)  

有百分之多少以上的标签重合, smlar.type = cosine , smlar.threshold = FLOAT

set smlar.type = cosine;  
set smlar.threshold = 1;  -- 精确匹配, 目标也必须只包含2, 相当于相等  
select uid from tbl_users where tags % array[2];  
  
set smlar.type = cosine;  
set smlar.threshold = 0.5;  -- 两组标签的交集(重叠标签)占两组标签叠加(并集)后的50%以上  
select uid from tbl_users where tags % array[2,8];  
  
set smlar.type = cosine;  
set smlar.threshold = 1;  -- 精确匹配, 两组标签的交集(重叠标签)占两组标签叠加(并集)后的100%以上  
select uid from tbl_users where tags % array[2,2696];  
  
set smlar.type = cosine;  
set smlar.threshold = 0.7;  -- 两组标签的交集(重叠标签)占两组标签叠加(并集)后的70%以上  
select uid from tbl_users where tags % array[2,4356,5064,5711,7363,9417,9444];  
  
set smlar.type = cosine;  
set smlar.threshold = 0.9;  -- 两组标签的交集(重叠标签)占两组标签叠加(并集)后的90%以上  
select uid from tbl_users where tags % array[4356,5064,5711,7363,9417,9444];  

使用smlar插件, 数组类型和gin索引后, 查看执行计划和耗时如下:

postgres=# explain analyze select uid from tbl_users where tags % array[2];  
                                                             QUERY PLAN  
-------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=17.65..1112.36 rows=1000 width=4) (actual time=301.094..301.094 rows=0 loops=1)  
   Recheck Cond: (tags % '{2}'::integer[])  
   Rows Removed by Index Recheck: 77129  
   Heap Blocks: exact=50082  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..17.40 rows=1000 width=0) (actual time=25.659..25.659 rows=77129 loops=1)  
         Index Cond: (tags % '{2}'::integer[])  
 Planning Time: 0.252 ms  
 Execution Time: 301.135 ms  
(8 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags % array[2,8];  
                                                              QUERY PLAN  
--------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=26.45..1121.16 rows=1000 width=4) (actual time=799.554..799.554 rows=0 loops=1)  
   Recheck Cond: (tags % '{2,8}'::integer[])  
   Rows Removed by Index Recheck: 500264  
   Heap Blocks: exact=35117 lossy=33064  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..26.20 rows=1000 width=0) (actual time=43.356..43.356 rows=149118 loops=1)  
         Index Cond: (tags % '{2,8}'::integer[])  
 Planning Time: 0.379 ms  
 Execution Time: 799.611 ms  
(8 rows)  
  
postgres=# explain analyze select uid from tbl_users where tags % array[2,2696];  
                                                            QUERY PLAN  
----------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=26.45..1121.16 rows=1000 width=4) (actual time=26.476..26.478 rows=0 loops=1)  
   Recheck Cond: (tags % '{2,2696}'::integer[])  
   Rows Removed by Index Recheck: 1028  
   Heap Blocks: exact=1019  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..26.20 rows=1000 width=0) (actual time=5.242..5.242 rows=1028 loops=1)  
         Index Cond: (tags % '{2,2696}'::integer[])  
 Planning Time: 0.570 ms  
 Execution Time: 26.570 ms  
(8 rows)  
  
  
postgres=# explain analyze select uid from tbl_users where tags % array[2,4356,5064,5711,7363,9417,9444];  
                                                           QUERY PLAN  
---------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=70.45..1165.16 rows=1000 width=4) (actual time=16.722..16.723 rows=0 loops=1)  
   Recheck Cond: (tags % '{2,4356,5064,5711,7363,9417,9444}'::integer[])  
   Rows Removed by Index Recheck: 8  
   Heap Blocks: exact=8  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..70.20 rows=1000 width=0) (actual time=16.586..16.587 rows=8 loops=1)  
         Index Cond: (tags % '{2,4356,5064,5711,7363,9417,9444}'::integer[])  
 Planning Time: 0.276 ms  
 Execution Time: 16.795 ms  
(8 rows)  
  
  
postgres=# explain analyze select uid from tbl_users where tags % array[4356,5064,5711,7363,9417,9444];  
                                                          QUERY PLAN  
-------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tbl_users  (cost=61.65..1156.36 rows=1000 width=4) (actual time=9.755..9.757 rows=0 loops=1)  
   Recheck Cond: (tags % '{4356,5064,5711,7363,9417,9444}'::integer[])  
   ->  Bitmap Index Scan on tbl_users_tags_idx  (cost=0.00..61.40 rows=1000 width=0) (actual time=9.748..9.749 rows=0 loops=1)  
         Index Cond: (tags % '{4356,5064,5711,7363,9417,9444}'::integer[])  
 Planning Time: 0.294 ms  
 Execution Time: 9.811 ms  
(6 rows)  

对照

方法 SQL1 耗时 ms SQL2 耗时 ms SQL3 耗时 ms SQL4 耗时 ms SQL5 耗时 ms
传统字符串 + 全表扫描 1110.267 2004.062 2060.434 6767.990 6208.191
传统字符串 + 模糊搜索 + gin索引加速 78.163 814.748 902.637 4050.040 3691.329
数组 + gin索引加速 74.015 18.561 4.750 5.523 4.528
数组(重叠个数)相似度搜索 + gin索引加速 309.182 794.029 26.218 13.264 11.447
数组(重叠占比)相似度搜索 + gin索引加速 301.135 799.611 26.570 16.795 9.811

知识点

1、数组类型

2、gin索引

3、smlar 插件

更多算法参考: https://github.com/jirutka/smlar

4、pg_trgm 插件

思考

pg_trgm插件对字符串做了什么处理, 可以利用gin索引加速模糊查询加速?

smlar插件是如何通过索引快速判断两个数组的相似性达到阈值的?

为什么多个模糊匹配条件使用and条件后, 性能下降严重?

为什么使用数组类型后, 标签条件越多性能越好?

如果多个模糊匹配条件是or 条件呢? 性能会下降还是提升?

还有什么业务场景会用到数组?

还有哪些业务场景会用到字符串模糊匹配?

还有什么业务场景非常适合使用数组相似的功能?

除了使用标签匹配来圈选相似目标人群, 还可不可以使用其他方式圈选? 例如向量距离?

使用标签匹配时, 如果我们要排除某些标签, 而不是包含某些标签, 应该如何写sql, 性能又会怎么样呢?

为什么使用smlar进行相似度过滤时, 相似度越高性能越好?

SQL圈选性能和返回符合条件的用户记录数有没有关系? 是什么关系?

当使用pg_trgm进行模糊搜索加速时, 如果字符串中包含wchar(例如中文)时性能如果很差要怎么办? 如果需要模糊搜索的字符只有1个或2个字符时性能如果很差要怎么办?

参考

202109/20210906_02.md 《B2B营销的第一性原理》

digoal's wechat