【问题标题】:Percentage of Cross product purchase跨产品购买百分比
【发布时间】:2016-03-30 21:43:57
【问题描述】:

我试图确定有多少人从可能存在的所有不同的产品对中购买了一对商品。例如。我有三种产品,A、B、C,我想确定有多少百分比的客户购买了 A 和 B、B 和 C,以及 A 和 C,在每个国家/地区拥有每对产品的客户数量中。

我的表格如下所示。

 Customer | Country | Product
 1        |    US   |    A
 1        |    US   |    B
 2        |    CA   |    A
 2        |    CA   |    C
 3        |    US   |    A
 3        |    US   |    C
 4        |    US   |    B
 5        |    US   |    A

请注意,客户只能属于一个国家/地区。

我想要的输出是:

 Country | Pair |   %
 US      | A_B  |  25%    
 US      | B_C  |   0%
 US      | A_C  |  33%
 CA      | A_B  |   0%    
 CA      | B_C  |   0%
 CA      | A_C  | 100%

%本质上是比率

(# of unique customers who bought Product1 and Product2)/
(# of unique customers who bought Product1 or Product2)

按国家/地区。

例如,在美国,A_B 有 4 位客户购买了 AB,但其中只有 1 位同时购买了 AB,因此比例为 1/4

如果我有大量任意数量的对,是否有一个很好的解决方案可以扩展?

【问题讨论】:

  • 美国A_C值不应该是33%,而不是25%吗?三位客户(1、3、5)购买了 A 或 C,其中一位(3)同时购买了两者。
  • @APH 你是对的。应该是 33%

标签: sql sql-server


【解决方案1】:

迭代查询开发...

如果您没有product 表,而只有customer_country_product 表,则可以使用内联视图为每​​个国家/地区创建不同的产品列表。

按国家/地区获取产品...

  SELECT ccp.product_id
       , ccp.country_id
    FROM customer_country_product ccp
   GROUP
      BY ccp.product_id
       , ccp.country_id

我们可以将该查询用作行源,方法是将其设为内联视图。将该查询包装在括号中,分配一个别名,并在另一个查询的 FROM 子句中引用它。为了获得“对”产品,我们可以将内联视图加入到自身中(避免返回相同产品的对(A_A),并避免返回“重复”对(仅返回 A_C 和 @987654326 中的一个) @)。

  SELECT a.country_id
       , a.product_id AS a_product_id
       , b.product_id AS b_product_id
    FROM ( SELECT ccpa.product_id
                , ccpa.country_id
             FROM customer_country_product ccpa
            GROUP
               BY ccpa.product_id
                , ccpa.country_id
         ) a 
    JOIN ( SELECT ccpb.product_id
                , ccpb.country_id
             FROM customer_country_product ccpb
            GROUP
               BY ccpb.product_id
                , ccpb.country_id
         ) b
      ON b.country_id = a.country_id 
     AND b.product_id > a.product_id
   ORDER
      BY a.country_id
       , a.product_id
       , b.product_id

这应该为您提供每个国家/地区的所有产品“配对”。注意:这将省略没有客户拥有该产品的产品。如果我们想要所有可能的产品对,对于每个国家/地区,我们需要写得稍微不同......

  SELECT c.country_id
       , a.product_id AS a_product_id
       , b.product_id AS b_product_id
    FROM ( SELECT ccpa.product_id
             FROM customer_country_product ccpa
            GROUP BY ccpa.product_id
         ) a 
    JOIN ( SELECT ccpb.product_id
             FROM customer_country_product ccpb
            GROUP BY ccpb.product_id
         ) b
      ON b.product_id > a.product_id
   CROSS
    JOIN ( SELECT ccpc.country_id
             FROM customer_country_product ccpc
            GROUP BY ccpc.country_id
         ) c
    ORDER
      BY c.country_id
       , a.product_id
       , b.product_id

如果您有 productcountry 表,则可以将上述查询中的内联视图替换为对这些表的引用。

要获取客户的“计数”,我们可以使用 SELECT 列表中的相关子查询,也可以在 SELECT 列表中执行连接操作和聚合。 (对于连接,如果我们不小心,可能会生成和计算“重复项”。)

获取特定国家/地区拥有特定产品的不同客户的计数

SELECT COUNT(DISTINCT ccp.customer_id) AS cnt_cust
  FROM customer_country_product ccp
 WHERE ccp.country_id = ?
   AND ccp.product_id = ?

获取来自特定国家/地区至少拥有两种特定产品中的一种的不同客户的计数

SELECT COUNT(DISTINCT ccp.customer_id) AS cnt_cust_have_either
  FROM customer_country_product ccp
 WHERE ccp.country_id = ?
   AND ccp.product_id IN ( ? , ? )

要获得在特定国家/地区拥有两种特定产品的客户数量:

SELECT COUNT(DISTINCT ccp1.customer_id) AS cnt_cust_have_both
  FROM customer_country_product ccp1
  JOIN customer_country_product ccp2
    ON ccp2.country_id = ccp1.country_id
   AND ccp2.customer_id = ccp1.customer_id
 WHERE ccp1.country_id = ? 
   AND ccp1.product_id = ?
   AND ccp2.product_id = ?

由于这些查询返回包含单个列的单行,我们可以将它们用作另一个查询的 SELECT 列表中的表达式。我们从“产品对”查询开始,并添加到 SELECT 列表中。我们将这些问号占位符替换为对外部查询中列的引用:

  SELECT c.country_id
       , a.product_id AS a_product_id
       , b.product_id AS b_product_id
       , ( SELECT COUNT(DISTINCT ccp1.customer_id)
             FROM customer_country_product ccp1
             JOIN customer_country_product ccp2
               ON ccp2.country_id = ccp1.country_id
              AND ccp2.customer_id = ccp1.customer_id
            WHERE ccp1.country_id = c.country_id
              AND ccp1.product_id = a.product_id
              AND ccp2.product_id = b.product_id
         ) AS cnt_cust_have_both
       , ( SELECT COUNT(DISTINCT ccp.customer_id)
             FROM customer_country_product ccp
            WHERE ccp.country_id = c.country_id
              AND ccp.product_id IN (a.product_id,b.product_id)
         ) AS cnt_cust_have_either
    FROM ( SELECT ccpa.product_id
             FROM customer_country_product ccpa
            GROUP BY ccpa.product_id
         ) a 
    JOIN ( SELECT ccpb.product_id
             FROM customer_country_product ccpb
            GROUP BY ccpb.product_id
         ) b
      ON b.product_id > a.product_id
   CROSS
    JOIN ( SELECT ccpc.country_id
             FROM customer_country_product ccpc
            GROUP BY ccpc.country_id
         ) c
    ORDER
      BY c.country_id
       , a.product_id
       , b.product_id

现在,要计算“百分比”,我们只需要进行除法运算即可。对于 MySQL,“除以零”将返回 NULL。 (如果我们的外部查询只返回我们知道来自该国家/地区的客户拥有其中一种产品的行...即第一个查询返回的结果

  SELECT c.country_id
       , a.product_id AS a_product_id
       , b.product_id AS b_product_id
       , ( SELECT COUNT(DISTINCT ccp1.customer_id)
             FROM customer_country_product ccp1
             JOIN customer_country_product ccp2
               ON ccp2.country_id = ccp1.country_id
              AND ccp2.customer_id = ccp1.customer_id
            WHERE ccp1.country_id = c.country_id
              AND ccp1.product_id = a.product_id
              AND ccp2.product_id = b.product_id
         )
       / ( SELECT COUNT(DISTINCT ccp.customer_id)
             FROM customer_country_product ccp
            WHERE ccp.country_id = c.country_id
              AND ccp.product_id IN (a.product_id,b.product_id)
         ) 
       * 100.00 AS percent_cust_have_both
    FROM ( SELECT ccpa.product_id
             FROM customer_country_product ccpa
            GROUP BY ccpa.product_id
         ) a 
    JOIN ( SELECT ccpb.product_id
             FROM customer_country_product ccpb
            GROUP BY ccpb.product_id
         ) b
      ON b.product_id > a.product_id
   CROSS
    JOIN ( SELECT ccpc.country_id
             FROM customer_country_product ccpc
            GROUP BY ccpc.country_id
         ) c
    ORDER
      BY c.country_id
       , a.product_id
       , b.product_id

就“扩展”而言,对于任何重要的表,我们都需要有合适的索引可用。特别是对于相关的子查询。这些将为外部查询返回的每一行执行。

当分母中的计数为零时,最后一个查询有可能返回 NULL。我们可以通过将 while 除法运算包装在条件测试中来替换零

 IFNULL( <expr> , 0) * 100.00 AS 

(可能在这些查询中某处存在错误、缺少括号、无效引用、错误的限定符等。这些查询未经测试。我强烈建议您测试每一个,而不仅仅是抓住最后一个。)


跟进

用于测试的表...

CREATE TABLE customer_country_product
( customer_id INT
, country_id  VARCHAR(2)
, product_id  VARCHAR(2)
)
;
INSERT INTO customer_country_product (customer_id, country_id, product_id) VALUES
 ('1','US','A')
,('1','US','B')
,('2','CA','A')
,('2','CA','C')
,('3','US','A')
,('3','US','C')
,('4','US','B')
,('5','US','A')
;

最终查询返回:

country_id  a_product_id  b_product_id  percent_cust_have_both
----------  ------------  ------------  ----------------------
CA          A             B               0.000000
CA          A             C             100.000000
CA          B             C               0.000000
US          A             B              25.000000
US          A             C              33.333333
US          B             C               0.000000

a.product_idb.product_id 连接到一个列中将是一个微不足道的更改。 SELECT 列表中的第二列和第三列可以替换为 CONCAT(a.product_id,'_',b.product_id) AS a_b 之类的内容。

【讨论】:

  • 哎呀。当我写这个答案时,我想到的是 MySQL,而不是 SQL Server。我的错。某些语法可能特定于 MySQL。
  • 感谢您的帮助。这正是我所需要的,而且非常清楚。
  • @Black:这是一个相当长的查询。我试图演示一种逐步、增量的方法来构建该查询,并在此过程中进行测试。 (我不可能一举得出最后的查询;我只是不够聪明/没有才华做到这一点。)对于 SQL Server,如果它是“除以零”,则除法运算可能会引发错误手术。我建议将分母表达式包装在一个计算结果为零时返回 NULL 的函数中。
【解决方案2】:

您需要生成所有对产品以及国家/地区。然后,您需要计算购买其中任何一个的匹配客户数量以及同时购买两者的数量。

假设您有一个产品表和一个国家/地区表。那么,我认为子查询可能是最简单的解决方案:

select p1.product as product1, p2.product as p2,
       (select count(*)
        from (select cp.customer
              from customerproducts cp
              where cp.product in (p1.product, p2.product) and
                    cp.country = c.country
              group by cp.customer
              having count(distinct product) = 2
             ) cp
       ) as numWithBoth,
       (select count(*)
        from (select cp.customer
              from customerproducts cp
              where cp.product in (p1.product, p2.product) and
                    cp.country = c.country
              group by cp.customer
             ) cp
       ) as numWithEither
from countries c cross join
     products p1 cross join
     products p2 ;

最终的答案是两个值的比值。

【讨论】:

  • 这看起来不错。但是内联视图cp 不需要GROUP BY 子句吗? HAVING 子句中的聚合 (COUNT) 不会将其折叠成一行,只返回一个客户吗? (或者如果 sql_mode 包含 ONLY_FULL_GROUP_BY 则抛出错误?) MySQL 是否限制相关子查询可以引用外部查询中的列的深度? (也许这只是在旧版本中,或者是我正在考虑的不同数据库。)这不会返回重复的对,例如A_CC_A,还有 A_A ?
  • 很遗憾我只有这一张桌子。
【解决方案3】:

这是使用 CTE 的另一种方法,假设您只有发布的表格(因此需要做一些额外的工作来提取国家/地区/产品组合列表)。可能可以用更少的步骤来做到这一点,但我想详细说明解决方案,以便更容易看到正在发生的事情。

drop table #test

create table #test (customer int, country varchar(2), product char(1))
insert into #test values (1, 'US','A')
insert into #test values (1, 'US','B')
insert into #test values (2, 'CA','A')
insert into #test values (2, 'CA','C')
insert into #test values (3, 'US','A')
insert into #test values (3, 'US','C')
insert into #test values (4, 'US','B')
insert into #test values (5, 'US','A')

; with CTE as ( --Count the number of customers ordering each item
    select country, product, count(distinct customer) as TotalOrders
    from #test
    group by country, product
    )
, CTE2 as ( --Join the order counts back to the original data set (can do this in CTE as a windowed function if you don't have customers ordering the same product more than once)
    select a.*, b.TotalOrders from #test a
    left join cte b
    on a.country = b.country and a.product = b.product
    )
, combinations as ( --Generate all possible country/product combinations
    Select * from 
        (Select distinct Country from #test) a
    cross join
        (Select distinct a.product + '_' + b.product as ProductCombination from #test a
        left join #test b
        on a.product < b.product) b
    where b.ProductCombination is not null
    )
, calculations as ( --count purchasers of combinations, and use this combined with the earlier purchaser counts to generate an unduplicated total purchasers count
    select a.country, a.product + '_' + b.product as ProductCombination
        , cast(count(distinct a.customer)*100.0/(a.totalorders + b.totalorders - count(distinct a.customer)) as decimal(5,0)) as PctOfTotal
    from cte2 a
    inner join cte2 b
    on a.country = b.country 
        and a.customer = b.customer 
        and a.product < b.product
    group by a.country, a.product, b.product, a.totalorders + b.totalorders)

select a.*, isnull(b.PctOfTotal, 0) as PercentOfTotal from combinations a
left join calculations b
on a.country = b.country 
    and a.ProductCombination = b.ProductCombination
order by a.country, a.ProductCombination

【讨论】:

  • 感谢您的帮助。我试图运行它,但不幸的是我看不到结果,因为查询仍在运行。看起来很慢。
猜你喜欢
  • 2023-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多