啤酒和纸尿裤最搭? - 用HybridDB/PostgreSQL查询商品营销最佳组合

标签

PostgreSQL , 商品最佳组合 , 阿里云HybridDB


背景

购买早餐时,包子和豆浆、茶叶蛋是最佳搭档吗?

为什么纸尿裤和啤酒是最佳搭档?

这些问题在积累了一定的订单数据后,是可以挖掘出来的。这个问题实际上是4.8号PostgreSQL社区杭州区活动上,一位社区的朋友提出来的,如何使用PostgreSQL找出最佳搭配的商品。

实际上有一个专业的推荐数据库,支持多种推荐算法,也可以很好的解决这个问题。

《推荐系统分析 - 推荐算法, RecDB推荐数据库介绍》

但是本文不打算使用RecDB这个产品来解决这样的问题。而是使用统计的方法能得出结论。

本文统计方法限制

本文涉及的统计方法只能用于计算直接关联的商品(表现为在同一个订单中的数据)的最佳组合。

如果你要计算间接关联的商品组合,例如A用户买了1,2,B用户买了2,3,实际上1,3是存在间接关联关系的。那么你需要用到recDB中提到的推荐算法,或者使用类似图式搜索。

参考

《金融风控、公安刑侦、社会关系、人脉分析等需求分析与数据库实现 - PostgreSQL图数据库场景应用》

场景虚构

假设有10万商品ID,虚构一批用户的购买或购物车记录,每一条订单或购物车记录中包含5到15个商品。一共构建约1100万条这样的记录。

建表

postgres=# create unlogged table buy (pay_id int8, item_id int[]);
CREATE TABLE

造数据

创建一个函数,用于插入buy表,(5到15个商品的数组)

create or replace function f() returns void as $$
declare
begin
  for i in 5..15 loop
    insert into buy (item_id) select array_agg((100000*random())::int8) from generate_series(1,i);
  end loop;
end;
$$ language plpgsql strict;

使用pgbench,生成1100万记录

vi test.sql
select f();    

pgbench -M prepared -n -r -P 1 -f ./test.sql -c 100 -j 100 -t 10000    

transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 100
number of threads: 100
number of transactions per client: 10000
number of transactions actually processed: 1000000/1000000
latency average = 1.155 ms
latency stddev = 1.814 ms
tps = 85204.625725 (including connections establishing)
tps = 85411.351807 (excluding connections establishing)
script statistics:
 - statement latencies in milliseconds:
         1.158  select f();

确认数据已写入

postgres=# select count(*) from buy;
  count
----------
 11000000
(1 row)  

postgres=# select * from buy limit 10;
 pay_id |                           item_id
--------+--------------------------------------------------------------
        | {6537,76804,33612,75580,8021}
        | {72437,66015,2939,56128,7056}
        | {40983,79581,15954,21039,6702,90279}
        | {93626,8337,13416,69371,4366,75868}
        | {84611,56893,25201,74038,59337,62045,59178}
        | {97422,48801,69714,77056,17059,79714,21598}
        | {42997,50834,57214,52866,83656,76342,5639,93416}
        | {53543,24369,31552,28654,38516,63657,86564,11483}
        | {58873,23162,23369,55091,32046,29907,31895,65658,5487}
        | {39916,6641,85068,55870,27679,91770,46150,12290,48662,71350}
(10 rows)

GIN索引

postgres=# create index idx_buy_item on buy using gin(item_id);

分裂函数

分裂的目的是将一笔订单中的数组,分裂成若干个组合。例如5个商品的订单,拆分成4+3+2+1=10个2个商品的组合。

{6537,76804,33612,75580,8021}

拆分为如下组合

{6537,76804}  

{6537,33612}  

{6537,75580}  

{6537,8021}  

{76804,33612}  

{76804,75580}  

{76804,8021}  

{33612,75580}  

{33612,8021}  

{75580,8021}

创建一个函数来完成这样的拆分工作

使用递归查询可以满足重新组合的目的

例子

WITH RECURSIVE
t(i) AS (
  SELECT * FROM unnest('{A,B,C}'::char[])
),
cte AS (
     SELECT i AS combo, i, 1 AS ct
     FROM t
   UNION ALL
     SELECT cte.combo || t.i, t.i, ct + 1
     FROM cte, t
     WHERE ct <= 3  -- 组合3+1=4次
       AND position(t.i in cte.combo) = 0   -- 新加入的字符不在已有字符中
)
SELECT ARRAY(SELECT combo FROM cte ORDER BY ct, combo) AS result;  

                      result
---------------------------------------------------
 {A,B,C,AB,AC,BA,BC,CA,CB,ABC,ACB,BAC,BCA,CAB,CBA}
(1 row)

函数1,返回指定个数的组合

假设数组中没有重复元素

create or replace function array_regroup(
  i_arr int[],   -- 输入数组
  i_elems int    -- 打散成固定长度的组合
) returns setof int[] as $$
declare
  v_arr_len int := array_length(i_arr, 1);  -- 输入的数组长度
begin
  -- 保护
  if i_elems > v_arr_len then
    raise notice 'you cann''t return group len % more then %', i_elems, v_arr_len;
    return;
  elsif i_elems = v_arr_len then
    return next i_arr;
    return;
  elsif i_elems = 1 then
    return query select array(select i) from unnest(i_arr) t(i);
    return;
  end if;  

  return query
  WITH RECURSIVE
  t(i) AS (
      select array(select i) from unnest(i_arr) t(i)
  ),
  cte AS (
     SELECT i AS combo, i, 1 AS ct
     FROM t
   UNION ALL
     SELECT array(select i from (select unnest(array_cat(cte.combo, t.i)) order by 1) t(i)), t.i, ct + 1
     FROM cte, t
     WHERE cte.ct <= i_elems-1  -- 组合若干次
       AND (not cte.combo @> t.i)   -- 新加入的值不在已组合的值中
  )
  SELECT combo FROM cte where array_length(combo,1)=i_elems group by combo;   

  return;
end;
$$ language plpgsql strict;
postgres=# select array_regroup(array[1,2,3],2);
 array_regroup
---------------
 {2,3}
 {1,2}
 {1,3}
(3 rows)

函数2,返回所有个数的组合

create or replace function array_regroup(
  i_arr int[]   -- 输入数组
) returns setof int[] as $$
declare
  v_arr_len int := array_length(i_arr, 1);  -- 输入的数组长度
begin  

  return query
  WITH RECURSIVE
  t(i) AS (
      select array(select i) from unnest(i_arr) t(i)
  ),
  cte AS (
     SELECT i AS combo, i, 1 AS ct
     FROM t
   UNION ALL
     SELECT array(select i from (select unnest(array_cat(cte.combo, t.i)) order by 1) t(i)), t.i, ct + 1
     FROM cte, t
     WHERE cte.ct <= v_arr_len-1  -- 组合若干次
       AND (not cte.combo @> t.i)   -- 新加入的值不在已组合的值中
  )
  SELECT combo FROM cte group by combo;   

  return;
end;
$$ language plpgsql strict;
postgres=# select array_regroup(array[1,2,3]);
 array_regroup
---------------
 {2}
 {2,3}
 {1,2}
 {1}
 {1,2,3}
 {3}
 {1,3}
(7 rows)

函数3,返回指定个数的组合,仅输出包含了某些元素的组合(例如包含了面包ID的数组)

create or replace function array_regroup(
  i_arr int[],    -- 输入数组
  i_elems int,    -- 打散成固定长度的组合
  i_arr_contain int[]  -- 包含了这些商品ID的数组
) returns setof int[] as $$
declare
  v_arr_len int := array_length(i_arr, 1);  -- 输入的数组长度
begin
  -- 保护
  if i_elems > v_arr_len then
    raise notice 'you cann''t return group len % more then %', i_elems, v_arr_len;
    return;
  elsif i_elems = v_arr_len then
    return next i_arr;
    return;
  elsif i_elems = 1 then
    return query select array(select i) from unnest(i_arr) t(i);
    return;
  end if;  

  return query
  WITH RECURSIVE
  t(i) AS (
      select array(select i) from unnest(i_arr) t(i)
  ),
  cte AS (
     SELECT i AS combo, i, 1 AS ct
     FROM t
   UNION ALL
     SELECT array(select i from (select unnest(array_cat(cte.combo, t.i)) order by 1) t(i)), t.i, ct + 1
     FROM cte, t
     WHERE cte.ct <= i_elems-1  -- 组合若干次
       AND (not cte.combo @> t.i)   -- 新加入的值不在已组合的值中
       AND (cte.combo @> i_arr_contain)
  )
  SELECT combo FROM cte where array_length(combo,1)=i_elems group by combo;   

  return;
end;
$$ language plpgsql strict;
postgres=# select array_regroup(array[1,2,3,4,5],2,array[1]);
 array_regroup
---------------
 {1,2}
 {1,3}
 {1,4}
 {1,5}
(4 rows)  

Time: 1.150 ms

求单品的最佳一级组合

例如,找出面包的1个最佳搭档。

假设面包的商品ID=6537

postgres=# select item_id from buy where item_id @> array[6537];
......
 {60573,17248,6537,77857,43349,66208,13656}
 {97564,50031,79924,24255,6537,21174,39117}
 {24026,78667,99115,87856,64782,8344,73169,41478,63091,29609,6537,71982,75382}
 {53094,97465,26156,54181,6537}
(1101 rows)
Time: 5.791 ms  

postgres=# explain select item_id from buy where item_id @> array[6537];
                                   QUERY PLAN
---------------------------------------------------------------------------------
 Bitmap Heap Scan on buy  (cost=457.45..51909.51 rows=55000 width=60)
   Recheck Cond: (item_id @> '{6537}'::integer[])
   ->  Bitmap Index Scan on idx_buy_item  (cost=0.00..443.70 rows=55000 width=0)
         Index Cond: (item_id @> '{6537}'::integer[])
(4 rows)

组合,寻找出现次数最多的组合。

postgres=# select count(*), array_regroup(item_id,2,array[6537]) from buy where item_id @> array[6537] group by 2 order by 1 desc;
 count | array_regroup
-------+---------------
     3 | {6537,55286}
     3 | {6537,48661}
     3 | {6537,78337}
     3 | {6537,72623}
     3 | {6537,81442}
     3 | {6537,66414}
     3 | {6537,35346}
     3 | {6537,79565}
     3 | {3949,6537}
......  

Time: 286.859 ms

求单品的最佳二级组合

例如,找出面包的两个最佳搭档。

postgres=# select count(*), array_regroup(item_id,3,array[6537]) from buy where item_id @> array[6537] group by 2 order by 1 desc;
 count |   array_regroup
-------+--------------------
     1 | {32,999,6537}
     1 | {6537,49957,91533}
     1 | {6537,49957,88377}
     1 | {6537,49957,57887}
     1 | {6537,49957,55192}
     1 | {6537,49952,95266}
     1 | {6537,49952,56916}
     1 | {6537,49945,60492}
     1 | {6537,49940,92888}
......  

Time: 1055.414 ms

统计全网数据的最佳一级组合

可能需要很久

select count(*), array_regroup(item_id,2) from buy group by 2 order by 1 desc limit 10;

统计全网数据的最佳N级组合

可能需要很久

select count(*), array_regroup(item_id, n) from buy group by 2 order by 1 desc limit 10;

小结

1. 这个案例并没有什么高超的技术含量,仅仅是将数组按推荐级数进行分裂,统计出现的次数。

用到的数据库特性包括:

1.1. 数组类型的支持

1.2. plpgsql服务端编程

1.3. 数组元素的索引检索(包含某个元素)

1.4. MPP, 分布式数据库架构,提升运算速度。可以参考阿里云的HybridDB for PostgreSQL产品。

2. 注意, 本文统计方法限制

本文涉及的统计方法只能用于计算直接关联的商品(表现为在同一个订单中的数据)的最佳组合。

如果你要计算间接关联的商品组合,例如A用户买了1,2,B用户买了2,3,实际上1,3是存在间接关联关系的。那么你需要用到recDB中提到的推荐算法,或者使用类似图式搜索。

参考

《金融风控、公安刑侦、社会关系、人脉分析等需求分析与数据库实现 - PostgreSQL图数据库场景应用》

3. 阿里云HybridDB for PostgreSQL提供了MPP的能力,可以水平扩展,非常适合OLAP场景。例如本案涉及的大量group by的操作,可以得到很好的性能提升。

4. PostgreSQL 9.6开始加入了基于CPU的多核并行计算功能,对于OLAP场景也有非常大的性能提升,例如本案涉及的大量group by的操作,可以得到很好的性能提升。

参考

https://github.com/DataSystemsLab/recdb-postgresql

https://www.ibm.com/developerworks/cn/web/1103_zhaoct_recommstudy1/index.html

《推荐系统分析 - 推荐算法, RecDB推荐数据库介绍》

《分析加速引擎黑科技 - LLVM、列存、多核并行、算子复用 大联姻 - 一起来开启PostgreSQL的百宝箱》

《PostgreSQL 向量化执行插件(瓦片式实现) 10x提速OLAP》

《ApsaraDB的左右互搏(PgSQL+HybridDB+OSS) - 解决OLTP+OLAP混合需求》

《金融风控、公安刑侦、社会关系、人脉分析等需求分析与数据库实现 - PostgreSQL图数据库场景应用》

时间: 2024-10-01 04:33:18

啤酒和纸尿裤最搭? - 用HybridDB/PostgreSQL查询商品营销最佳组合的相关文章

HybridDB PostgreSQL &quot;Sort、Group、distinct 聚合、JOIN&quot; 不惧怕数据倾斜的黑科技和原理 - 多阶段聚合

标签 PostgreSQL , Greenplum , JOIN , group by , distinct , 聚合 , 非分布键 , 数据倾斜 , 多阶段聚合 背景 对于分布式系统,数据分布存储,例如随机.哈希分布. Greenplum数据库支持两种数据分布模式: 1.哈希(指定单个.或多个字段) 2.随机分布(无需指定任何字段) 数据分布存储后,面临一些挑战: JOIN,排序,group by,distinct. 1.JOIN涉及非分布键字段 2.排序,如何保证输出顺序全局有序 3.gro

postgresql 查询完整字段值

问题描述 字段值过大表里的普通查询字段值显示的都是一个引用id数字不知道要怎么才能看到真实的字段值?

PostgreSQL 空间独立事件相关性分析 二 - 人车拟合

标签 PostgreSQL , PostGIS , 人车拟合 背景 独立事件相关性分析是一件很有意思的事情,例如 探探软件的擦肩而过功能点,在不同时空与你擦肩而过的人. 舆情分析. 商品最佳销售组合. 安全系统中的人车拟合,对时空数据进行处理,用于司机.乘客.车辆的拟合. 人车拟合 1.建立表结构 人 create table u_pos ( id int8, uid int8, crt_time timestamp, pos geometry ); 车 create table c_pos (

如何检测、清理Greenplum垃圾 - 阿里云HybridDB for PG最佳实践

标签 PostgreSQL , Greenplum , HDB for PG 背景 Greenplum通过多版本支持数据的删除和更新的并发和回滚,在删除数据时(使用DELETE删除),对记录的头部xmax值进行标记.在删除记录时,对记录的头部进行标记,同时插入新的版本. 这一就会导致一个问题,如果用户经常删除和插入或更新数据,表和索引都会膨胀. PostgreSQL是通过HOT技术以及autovacuum来避免或减少垃圾的.但是Greenplum没有自动回收的worker进程,所以需要人为的触发

数据库案例集锦 - 开发者的《如来神掌》

背景 「剑魔独孤求败,纵横江湖三十馀载,杀尽仇寇,败尽英雄,天下更无抗手,无可柰何,惟隐居深谷,以雕为友.呜呼,生平求一敌手而不可得,诚寂寥难堪也.」 剑冢中,埋的是剑魔独孤求败毕生几个阶段中用过的几柄剑: 利剑无意:第一柄是青光闪闪的利剑,凌厉刚猛,无坚不摧,弱冠前以之与河朔群雄争锋. 软剑无常:第二柄是紫薇软剑,三十岁前所用,误伤义士不祥,悔恨不已,乃弃之深谷. 重剑无锋:第三柄是玄铁重剑,重剑无锋,大巧不工,四十岁之前恃之横行天下. 木剑无俦:第四柄是已腐朽的木剑. 无剑无招:四十岁后,不

高阶魔方与数据编排 - 数据库存储优化之路

标签 PostgreSQL , cluster , 预排 , 一维数据 , 多维数据 , 视觉编排 , 数据编排 , IO优化 背景 中华文化源远流长,比如这句古语"远水不救近火,远亲不如近邻",在数据库的优化中亦有体现.接下来我们来揭示这个道理. 大多数数据库的存储为块存储,一个块里面可能涉及到多条记录,当用户输入查询条件进行数据检索时,即使返回的结果集较小,也可能需要扫描多个数据块,因为你不知道你要的记录落在哪些数据块里面. 例子 随机写入一批数据 create table tbl

阿里云RDS PostgreSQL OSS 外部表 - 并行写提速案例

标签 PostgreSQL , oss对象存储 , 阿里云RDS PG , 并行写 , dblink , 异步调用 , 异步任务监控 , OSS外部表 , 数据传输 背景 阿里云RDS PostgreSQL.HybridDB for PostgreSQL提供了一个非常强大的功能,OSS对象存储外部表. 阿里云的RDS PostgreSQL用户可以利用OSS存储冷数据(OSS外部表的形态呈现),实现冷热分离:也可以利用OSS作为数据的中转桥梁,打通其他云端业务,例如HDB FOR PostgreS

PostgreSQL服务器管理:服务器配置

本文档为PostgreSQL 9.6.0文档,本转载已得到原译者彭煜玮授权. 1. 设置参数 1.1. 参数名称和值 所有参数名都是大小写不敏感的.每个参数都可以接受五种类型之一的值: 布尔.字符串.整数. 浮点数或枚举.该类型决定了设置该参数的语法: 布尔: 值可以被写成 on, off, true, false, yes, no, 1, 0 (都是大小写不敏感的)或者这些值的任何无歧义前缀. 字符串: 通常值被包括在单引号内,值内部的任何单引号都需要被双写.不过,如果值是一个简单数字或者 标

PostgreSQL 多字段任意组合搜索的性能

标签 PostgreSQL , 多字段搜索 , 任意字段搜索 背景 PostgreSQL 多字段,任意组合搜索,有三种优化手段: 1.gin索引(支持任意字段组合的查询) <宝剑赠英雄 - 任意组合字段等效查询, 探探PostgreSQL多列展开式B树 (GIN)> 2.bloom索引(支持任意只读组合的等值查询) <PostgreSQL 9.6 黑科技 bloom 算法索引,一个索引支撑任意列组合查询> 3.每个单列btree索引(支持任意字段组合的查询) <Postgre