2017-02-08 9 views
3

Целью является выбор количества отдельных клиентов, которые не совершили покупку в течение 30-дневного периода, предшествующего каждому календарному 2016 году. Я создал календарь в моей базе данных.SQL-клиенты с отсроченными запросами с 30-дневной частотой по дням

Ниже приведен пример таблица для справки, скажем, у вас есть заказы клиентов нормированы следующим образом:

+-------------+------------+----------+ 
| 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  | 
+-------------+------------+----------+ 

Выход цели эффективно календарь с 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 году, поэтому это не возможно, за январь-01-2016 иметь счетчик истекших клиентов, потому что это первый возможный день когда-нибудь сделать покупка.

Так что для customer_id # 123 они приобрели 25.01.2012 и 27.04.2006. У них должно быть 2 счета, так как их покупки более чем на 30 дней. Один промах, произошедший 2/24/2016, и еще один промах на 27.05.2016.
Customer_id # 444 приобретается только один раз, поэтому у них должно быть одно количество граней в течение 30 дней после 02/02/2016 от 03/02/2016.
Customer_id # 521 сложно, так как они приобрели с частотой 1 день, мы не будем считать первую покупку 03/02/2016, так что есть только один промах, начиная с их последней покупки 03/03/2016. Счет на провал произойдет 04/02/2016 (+30 дней).

+0

Каким образом 2016-01-01 имеет значение 1? Были ли не все клиенты в этот день? –

+0

Я удалил ваш первый абзац, так как он вообще не помогает в понимании проблемы. (Кроме того, ваш литературный стиль не впечатляет, и вы делаете необоснованные предположения). Теперь: Как получается, что покупка на 1/25 означает дату истечения 2/25, но 02/02 означает провал 03/02? Каково ваше определение «30 дней» - это явно не основано на простом дневном счете. Также кажется, что клиенты «считают» совершить покупку на 12/31/2015 (вы не хотите, чтобы кто-то считался «истекшим» в течение большей части января) - это бессмысленно, но это ваша проблема - требование это то, что вы хотите. – mathguy

+0

Но это оставляет вопрос ... Если клиент совершает первую покупку в августе, они «угасают» с 30 или 31 января или 1 февраля (в зависимости от вашего определения «30 дней»). Это нормально, поскольку вы, по крайней мере, «видите» этого клиента в таблице. А как насчет клиентов, которые вообще не совершают покупку в 2016 году (но кто сделал покупки в прошлом, а также в 2017 году)? Разве это не нужно учитывать? И как мы ** знаем о них - есть ли у вас еще один стол со всеми клиентами, которых следует учитывать? – mathguy

ответ

1

Извинения, я не правильно прочитал ваш вопрос в первый раз. Этот запрос даст вам все упущения, которые у вас есть. Он принимает каждый заказ и использует аналитическую функцию, чтобы отработать следующую дату заказа - если зазор больше, чем 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 
; 
2

Если у вас есть таблица дат, вот один дорогой метод:

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; 

Для каждой пары даты/клиента, он определяет, последняя покупка клиент сделал до даты. Затем он использует эту информацию для подсчета истекшего времени.

Если быть честным, это, вероятно, будет работать хорошо на несколько дат, но не на весь год.

+0

Спасибо, Гордон, выглядит сложным. Я пытаюсь прочитать ваш код, и я не уверен, если вы поймаете край, когда клиенты размещают несколько заказов в тот же день. Кажется, вы считаете, что игнорируете покупки с частотой менее 30 дней, что хорошо. Я продолжу играть с этим до завершения ответа, спасибо! – barker

+0

@ баркер. , , Несколько заказов в тот же день не повлияют на этот запрос. Он ищет периоды в 30 дней с заказом * no *. Никакой заказ не является одинаковым, есть ли один заказ в день или более. –

1

Вот как я это сделать:

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_info к нему на основе lapsed_date, а затем выполнить подсчет прошедших дат для каждого дня.

+0

Я не знаю, что не так, но: я скопировал и вставил ваш запрос точно так, как есть, и я его запустил - и он возвращает 405 строк. Это не может быть правильно, должно быть 366 строк. :-) – mathguy

+0

Я пошел на начало 2016 года до сегодняшнего дня; достаточно просто изменить sysdate, на который ссылается запрос, до последнего дня в 2016 году, если этого хочет OP. – Boneist

+0

Я вижу! Вероятно, вы работали над этим после того, как он изменил требование ... – mathguy