【问题标题】:SQL select lapsed customers with 30 day frequency by daySQL 以 30 天的频率每天选择失效的客户
【发布时间】:2017-06-25 13:14:56
【问题描述】:

目标是选择在 2016 日历年的每一天之前的滚动 30 天期间未进行购买的不同 customer_id 的计数。我在我的数据库中创建了一个日历表来加入。

这是一个示例表供参考,假设您将客户订单标准化如下:

+-------------+------------+----------+
| customer_id | date       | order_id |
+-------------+------------+----------+
| 123         | 01/25/2016 | 1000     |
+-------------+------------+----------+
| 123         | 04/27/2016 | 1025     |
+-------------+------------+----------+
| 444         | 02/02/2016 | 1010     |
+-------------+------------+----------+
| 521         | 01/23/2016 | 998      |
+-------------+------------+----------+
| 521         | 01/24/2016 | 999      |
+-------------+------------+----------+  

目标输出实际上是一个日历,其中 2016 年的每一天都有 1 行,每天计算当天有多少客户“失效”,这意味着他们最后一次购买是在 2016 年的那一天之前的 30 天或更长时间那一年。最终输出将如下所示:

+------------+--------------+
| date       | lapsed_count |
+------------+--------------+
| 01/01/2016 | 0            |
+------------+--------------+
| 01/02/2016 | 0            |
+------------+--------------+
| ...        | ...          |
+------------+--------------+
| 03/01/2016 | 12           |
+------------+--------------+
| 03/02/2016 | 9            |
+------------+--------------+
| 03/03/2016 | 7            |
+------------+--------------+  

此数据在 2015 年不存在,因此 2016 年 1 月 1 日不可能计算失效客户的数量,因为那是可能进行购买的第一天。

因此,对于 customer_id #123,他们在 2016 年 1 月 25 日和 2016 年 4 月 27 日购买。他们应该有 2 个失效计数,因为他们的购买间隔超过 30 天。一次失误发生在 2016 年 2 月 24 日,另一次失误发生在 2016 年 5 月 27 日。
Customer_id#444 只购买了一次,因此他们应该在 2016 年 2 月 2 日 2016 年 3 月 2 日之后的 30 天内有一次失效计数。
Customer_id#521 很棘手,因为他们以 1 天的频率购买我们不会计算 2016 年 3 月 2 日的第一次购买,因此从他们最后一次购买 03/03/2016 开始只有一个间隔。失效时间将在 2016 年 4 月 2 日(+30 天)发生。

【问题讨论】:

  • 2016-01-01 怎么会有值 1?那天不是所有客户都流失了吗?
  • 我删除了您的第一段,因为它对理解问题毫无帮助。 (此外,你的文学风格平淡无奇,而且你做出了毫无根据的假设)。现在: 1/25 的购买意味着 2/25 的失效日期,但 02/02 意味着 03/02 的失效?您对“30 天”的定义是什么——它显然不是基于简单的天数。客户似乎也被“视为”在 2015 年 12 月 31 日进行了购买(您不希望任何人在 1 月的大部分时间都被视为“失效”)——这很荒谬,但这是你的问题——要求是你想要的。
  • 但这留下了一个问题......如果客户在 8 月进行了第一次购买,他们将从 1 月 30 日或 31 日或 2 月 1 日“失效”(取决于您对“30 天”的定义) .这很好,因为您至少在表格中“看到”了这个客户。但是,那些在 2016 年根本没有进行任何购买的客户(但在过去和 2017 年都进行了购买)的客户呢?那些也不需要计算吗? 我们是如何知道他们的 - 您是否还有另一张桌子,其中包含所有应该计算在内的客户?
  • 感谢@mathguy 的努力,2016 年之前的数据不存在,因此 1 月失效没有意义。尽管客户可以在 1 月购买,但在 2 月失效,这仍然是有道理的。数据将持续到 2017 年,因此我可以查看是否有人在 2016 年 12 月 15 日失效。
  • @barker - 需要澄清。假设您有一位客户在 2016 年 3 月 25 日进行了一次购买。什么是“失效日期”: 2016 年 4 月 25 日,还是所有从 2016 年 4 月 25 日至今的日期?

标签: sql oracle datetime window-functions


【解决方案1】:

如果您有日期表,这是一种昂贵的方法:

select date,
       sum(case when prev_date < date - 30 then 1 else 0 end) as lapsed
from (select c.date, o.customer_id, max(o.date) as prev_date
      from calendar c cross join
           (select distinct customer_id from orders) c left join
           orders o
           on o.date <= c.date and o.customer_id = c.customer_id
      group by c.date, o.customer_id
     ) oc
group by date;

对于每个日期/客户对,它确定客户在该日期之前进行的最新购买。然后它使用此信息来计算已失效的时间。

老实说,这在少数几个日期可能会很有效,但不适用于一整年。

【讨论】:

  • 谢谢 Gordon,看起来很复杂。我正在尝试通读您的代码,但我不确定您是否遇到了客户在同一天下多个订单的极端情况。看来您忽略了频率低于 30 天的购买,这很好。在最终确定答案之前,我会继续玩这个,谢谢!
  • @barker。 . .同一天的多个订单不会影响此查询。它正在寻找具有 no 顺序的 30 天期间。无论每天有一个订单或更多订单,任何订单都不相同。
【解决方案2】:

抱歉,我第一次没有正确阅读您的问题。此查询将为您提供所有失误。它接受每个订单并使用分析函数计算下一个订单日期 - 如果间隔大于 30 天,则记录失效

WITH
 cust_orders (customer_id , order_date , order_id   )
 AS
  (SELECT 1, TO_DATE('01/01/2016','DD/MM/YYYY'), 1001 FROM dual UNION ALL
   SELECT 1, TO_DATE('29/01/2016','DD/MM/YYYY'), 1002 FROM dual UNION ALL
   SELECT 1, TO_DATE('01/03/2016','DD/MM/YYYY'), 1003 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/01/2016','DD/MM/YYYY'), 1004 FROM dual UNION ALL
   SELECT 2, TO_DATE('29/01/2016','DD/MM/YYYY'), 1005 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/04/2016','DD/MM/YYYY'), 1006 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/06/2016','DD/MM/YYYY'), 1007 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/08/2016','DD/MM/YYYY'), 1008 FROM dual UNION ALL
   SELECT 3, TO_DATE('01/09/2016','DD/MM/YYYY'), 1009 FROM dual UNION ALL
   SELECT 3, TO_DATE('01/12/2016','DD/MM/YYYY'), 1010 FROM dual UNION ALL
   SELECT 3, TO_DATE('02/12/2016','DD/MM/YYYY'), 1011 FROM dual UNION ALL
   SELECT 3, TO_DATE('03/12/2016','DD/MM/YYYY'), 1012 FROM dual UNION ALL
   SELECT 3, TO_DATE('04/12/2016','DD/MM/YYYY'), 1013 FROM dual UNION ALL
   SELECT 3, TO_DATE('05/12/2016','DD/MM/YYYY'), 1014 FROM dual UNION ALL
   SELECT 3, TO_DATE('06/12/2016','DD/MM/YYYY'), 1015 FROM dual UNION ALL
   SELECT 3, TO_DATE('07/12/2016','DD/MM/YYYY'), 1016 FROM dual 
  )
SELECT
 customer_id
,order_date
,order_id
,next_order_date
,order_date + 30   lapse_date
FROM
 (SELECT
   customer_id
  ,order_date
  ,order_id
  ,LEAD(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) next_order_date
  FROM
   cust_orders
 )
WHERE NVL(next_order_date,sysdate) - order_date > 30
;

现在将其加入一组日期并运行 COUNT 函数(输入年份参数为 YYYY):

WITH
 cust_orders (customer_id , order_date , order_id   )
 AS
  (SELECT 1, TO_DATE('01/01/2016','DD/MM/YYYY'), 1001 FROM dual UNION ALL
   SELECT 1, TO_DATE('29/01/2016','DD/MM/YYYY'), 1002 FROM dual UNION ALL
   SELECT 1, TO_DATE('01/03/2016','DD/MM/YYYY'), 1003 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/01/2016','DD/MM/YYYY'), 1004 FROM dual UNION ALL
   SELECT 2, TO_DATE('29/01/2016','DD/MM/YYYY'), 1005 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/04/2016','DD/MM/YYYY'), 1006 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/06/2016','DD/MM/YYYY'), 1007 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/08/2016','DD/MM/YYYY'), 1008 FROM dual UNION ALL
   SELECT 3, TO_DATE('01/09/2016','DD/MM/YYYY'), 1009 FROM dual UNION ALL
   SELECT 3, TO_DATE('01/12/2016','DD/MM/YYYY'), 1010 FROM dual UNION ALL
   SELECT 3, TO_DATE('02/12/2016','DD/MM/YYYY'), 1011 FROM dual UNION ALL
   SELECT 3, TO_DATE('03/12/2016','DD/MM/YYYY'), 1012 FROM dual UNION ALL
   SELECT 3, TO_DATE('04/12/2016','DD/MM/YYYY'), 1013 FROM dual UNION ALL
   SELECT 3, TO_DATE('05/12/2016','DD/MM/YYYY'), 1014 FROM dual UNION ALL
   SELECT 3, TO_DATE('06/12/2016','DD/MM/YYYY'), 1015 FROM dual UNION ALL
   SELECT 3, TO_DATE('07/12/2016','DD/MM/YYYY'), 1016 FROM dual 
  )
,calendar (date_value)
 AS
 (SELECT TO_DATE('01/01/'||:P_year,'DD/MM/YYYY') + (rownum -1) 
  FROM all_tables
  WHERE rownum < (TO_DATE('31/12/'||:P_year,'DD/MM/YYYY') - TO_DATE('01/01/'||:P_year,'DD/MM/YYYY')) + 2
 )
SELECT
 calendar.date_value
,COUNT(*)
FROM
 (
  SELECT
   customer_id
  ,order_date
  ,order_id
  ,next_order_date
  ,order_date + 30   lapse_date
  FROM
   (SELECT
     customer_id
    ,order_date
    ,order_id
    ,LEAD(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) next_order_date
    FROM
     cust_orders
   )
  WHERE NVL(next_order_date,sysdate) - order_date > 30
 )  lapses
,calendar
WHERE 1=1
AND calendar.date_value = TRUNC(lapses.lapse_date)
GROUP BY
 calendar.date_value
;

或者如果你真的想打印出每个日期,那么使用这个:

WITH
 cust_orders (customer_id , order_date , order_id   )
 AS
  (SELECT 1, TO_DATE('01/01/2016','DD/MM/YYYY'), 1001 FROM dual UNION ALL
   SELECT 1, TO_DATE('29/01/2016','DD/MM/YYYY'), 1002 FROM dual UNION ALL
   SELECT 1, TO_DATE('01/03/2016','DD/MM/YYYY'), 1003 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/01/2016','DD/MM/YYYY'), 1004 FROM dual UNION ALL
   SELECT 2, TO_DATE('29/01/2016','DD/MM/YYYY'), 1005 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/04/2016','DD/MM/YYYY'), 1006 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/06/2016','DD/MM/YYYY'), 1007 FROM dual UNION ALL
   SELECT 2, TO_DATE('01/08/2016','DD/MM/YYYY'), 1008 FROM dual UNION ALL
   SELECT 3, TO_DATE('01/09/2016','DD/MM/YYYY'), 1009 FROM dual UNION ALL
   SELECT 3, TO_DATE('01/12/2016','DD/MM/YYYY'), 1010 FROM dual UNION ALL
   SELECT 3, TO_DATE('02/12/2016','DD/MM/YYYY'), 1011 FROM dual UNION ALL
   SELECT 3, TO_DATE('03/12/2016','DD/MM/YYYY'), 1012 FROM dual UNION ALL
   SELECT 3, TO_DATE('04/12/2016','DD/MM/YYYY'), 1013 FROM dual UNION ALL
   SELECT 3, TO_DATE('05/12/2016','DD/MM/YYYY'), 1014 FROM dual UNION ALL
   SELECT 3, TO_DATE('06/12/2016','DD/MM/YYYY'), 1015 FROM dual UNION ALL
   SELECT 3, TO_DATE('07/12/2016','DD/MM/YYYY'), 1016 FROM dual 
  )
,lapses
 AS
  (SELECT
    customer_id
   ,order_date
   ,order_id
   ,next_order_date
   ,order_date + 30   lapse_date
   FROM
    (SELECT
      customer_id
     ,order_date
     ,order_id
     ,LEAD(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) next_order_date
     FROM
      cust_orders
    )
   WHERE NVL(next_order_date,sysdate) - order_date > 30
  )  
,calendar (date_value)
 AS
 (SELECT TO_DATE('01/01/'||:P_year,'DD/MM/YYYY') + (rownum -1) 
  FROM all_tables
  WHERE rownum < (TO_DATE('31/12/'||:P_year,'DD/MM/YYYY') - TO_DATE('01/01/'||:P_year,'DD/MM/YYYY')) + 2
 )
SELECT
 calendar.date_value
,(SELECT COUNT(*)
  FROM lapses
  WHERE calendar.date_value = lapses.lapse_date
 )
FROM
 calendar
WHERE 1=1
ORDER BY
 calendar.date_value
;

【讨论】:

    【解决方案3】:

    我会这样做:

    WITH your_table AS (SELECT 123 customer_id, to_date('24/01/2016', 'dd/mm/yyyy') order_date, 12345 order_id FROM dual UNION ALL
                        SELECT 123 customer_id, to_date('24/01/2016', 'dd/mm/yyyy') order_date, 12346 order_id FROM dual UNION ALL
                        SELECT 123 customer_id, to_date('25/01/2016', 'dd/mm/yyyy') order_date, 12347 order_id FROM dual UNION ALL
                        SELECT 123 customer_id, to_date('24/02/2016', 'dd/mm/yyyy') order_date, 12347 order_id FROM dual UNION ALL
                        SELECT 123 customer_id, to_date('16/03/2016', 'dd/mm/yyyy') order_date, 12348 order_id FROM dual UNION ALL
                        SELECT 123 customer_id, to_date('18/04/2016', 'dd/mm/yyyy') order_date, 12349 order_id FROM dual UNION ALL
                        SELECT 456 customer_id, to_date('20/02/2016', 'dd/mm/yyyy') order_date, 12350 order_id FROM dual UNION ALL
                        SELECT 456 customer_id, to_date('01/03/2016', 'dd/mm/yyyy') order_date, 12351 order_id FROM dual UNION ALL
                        SELECT 456 customer_id, to_date('03/03/2016', 'dd/mm/yyyy') order_date, 12352 order_id FROM dual UNION ALL
                        SELECT 456 customer_id, to_date('18/04/2016', 'dd/mm/yyyy') order_date, 12353 order_id FROM dual UNION ALL
                        SELECT 456 customer_id, to_date('20/05/2016', 'dd/mm/yyyy') order_date, 12354 order_id FROM dual UNION ALL
                        SELECT 456 customer_id, to_date('23/06/2016', 'dd/mm/yyyy') order_date, 12355 order_id FROM dual UNION ALL
                        SELECT 456 customer_id, to_date('19/01/2017', 'dd/mm/yyyy') order_date, 12356 order_id FROM dual),
    -- end of mimicking your_table with data in it
        lapsed_info AS (SELECT customer_id,
                               order_date,
                               CASE WHEN TRUNC(SYSDATE) - order_date <= 30 THEN NULL
                                    WHEN COUNT(*) OVER (PARTITION BY customer_id ORDER BY order_date RANGE BETWEEN 1 FOLLOWING AND 30 FOLLOWING) = 0 THEN order_date+30
                                    ELSE NULL
                               END lapsed_date
                        FROM   your_table),
              dates AS (SELECT to_date('01/01/2016', 'dd/mm/yyyy') + LEVEL -1 dt
                        FROM   dual
                        CONNECT BY to_date('01/01/2016', 'dd/mm/yyyy') + LEVEL -1 <= TRUNC(SYSDATE))
    SELECT dates.dt,
           COUNT(li.lapsed_date) lapsed_count
    FROM   dates
           LEFT OUTER JOIN lapsed_info li ON dates.dt = li.lapsed_date
    GROUP BY dates.dt
    ORDER BY dates.dt;
    

    结果:

    DT         LAPSED_COUNT
    ---------- ------------
    01/01/2016            0
    <snip>
    23/01/2016            0
    24/01/2016            0
    25/01/2016            0
    26/01/2016            0
    <snip>
    19/02/2016            0
    20/02/2016            0
    21/02/2016            0
    22/02/2016            0
    23/02/2016            0
    24/02/2016            1
    25/02/2016            0
    <snip>
    29/02/2016            0
    01/03/2016            0
    02/03/2016            0
    03/03/2016            0
    04/03/2016            0
    <snip>
    15/03/2016            0
    16/03/2016            0
    17/03/2016            0
    <snip>
    20/03/2016            0
    21/03/2016            0
    22/03/2016            0
    <snip>
    30/03/2016            0
    31/03/2016            0
    01/04/2016            0
    02/04/2016            1
    03/04/2016            0
    <snip>
    14/04/2016            0
    15/04/2016            1
    16/04/2016            0
    17/04/2016            0
    18/04/2016            0
    19/04/2016            0
    <snip>
    17/05/2016            0
    18/05/2016            2
    19/05/2016            0
    20/05/2016            0
    21/05/2016            0
    <snip>
    18/06/2016            0
    19/06/2016            1
    20/06/2016            0
    21/06/2016            0
    22/06/2016            0
    23/06/2016            0
    24/06/2016            0
    <snip>
    22/07/2016            0
    23/07/2016            1
    24/07/2016            0
    <snip>
    18/01/2017            0
    19/01/2017            0
    20/01/2017            0
    <snip>
    08/02/2017            0
    

    这会获取您的数据,并使用分析计数函数来计算在当前行日期的 30 天内(但不包括)具有值的行数。

    然后我们应用一个 case 表达式来确定如果该行的日期在今天日期的 30 天内,我们会将这些日期视为未失效。如果返回的计数为 0,则该行被视为失效,我们将输出失效日期作为 order_date 加上 30 天。任何其他计数结果表示该行没有失效。

    以上都是在lapsed_info子查询中解决的。

    然后我们需要做的就是列出日期(请参阅dates 子查询)并根据 lapsed_date 将 lapsed_info 子查询外连接到它,然后计算每天的失效日期。

    【讨论】:

    • 我不知道出了什么问题,但是:我完全按原样复制并粘贴了您的查询并运行它 - 它返回 405 行。那不可能,应该有 366 行。 :-)
    • 我从2016年初到今天;如果 OP 想要的话,将查询中引用的 sysdate 更改为 2016 年的最后一天很简单。
    • 当我阅读它时,我更有可能错过了那个问题! *{;-)
    • 如果我没看错的话,如果只有一位客户在 2016 年 3 月 15 日只购买了一次,那么您计算的是 2016 年 4 月 15 日的一个“失效日期”。这不是要求;从 2016 年 4 月 15 日开始并延长到现在的所有天数都是该客户的“失效状态”天数。
    • 要求不明确。 OP 似乎自相矛盾 - 对于客户 123,他们说有两个失效计数,这就是我认为他们是在客户失效的日子之后的意思。嘿嘿,我想我需要等待 OP 澄清。
    猜你喜欢
    • 2016-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-24
    • 1970-01-01
    • 2022-01-01
    • 1970-01-01
    • 2021-10-13
    相关资源
    最近更新 更多