SQL select lapsed customers with 30 day frequency by day - sql

The goal is to select the count of distinct customer_id's who have not made a purchase in the rolling 30 day period prior to every day in the calendar year 2016. I have created a calendar table in my database to join to.
Here is an example table for reference, let's say you have customers orders normalized as follows:
+-------------+------------+----------+
| 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 |
+-------------+------------+----------+
The goal output is effectively a calendar with 1 row for every single day of 2016 with a count on each day of how many customers "lapsed" on that day, meaning their last purchase was 30 days or more prior from that day of the year. The final output will look like this:
+------------+--------------+
| date | lapsed_count |
+------------+--------------+
| 01/01/2016 | 0 |
+------------+--------------+
| 01/02/2016 | 0 |
+------------+--------------+
| ... | ... |
+------------+--------------+
| 03/01/2016 | 12 |
+------------+--------------+
| 03/02/2016 | 9 |
+------------+--------------+
| 03/03/2016 | 7 |
+------------+--------------+
This data does not exist in 2015, therefore it's not possible for Jan-01-2016 to have a count of lapsed customers because that is the first possible day to ever make a purchase.
So for customer_id #123, they purchased on 01/25/2016 and 04/27/2016. They should have 2 lapse counts because their purchases are more than 30 days apart. One lapse occurring on 2/24/2016 and another lapse on 05/27/2016.
Customer_id#444 only purchased once, so they should have one lapse count for 30 days after 02/02/2016 on 03/02/2016.
Customer_id#521 is tricky, since they purchased with a frequency of 1 day we will not count the first purchase on 03/02/2016, so there is only one lapse starting from their last purchase of 03/03/2016. The count for the lapse will occur on 04/02/2016 (+30 days).

If you have a table of dates, here is one expensive method:
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;
For each date/customer pair, it determines the latest purchase the customer made before the date. It then uses this information to count the lapsed.
To be honest, this will probably work well on a handful of dates, but not for a full year's worth.

Apologies, I didn't read your question properly the first time around. This query will give you all the lapses you have. It takes each order and uses an analytic function to work out the next order date - if the gap is greater than 30 days then a lapse is recorded
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
;
Now join that to a set of dates and run a COUNT function (enter the year parameter as 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
;
Or if you really want every date printed out then use this :
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
;

Here's how I'd do it:
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;
Results:
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
This takes your data, and uses an the analytic count function to work out the number of rows that have a value within 30 days of (but excluding) the current row's date.
Then we apply a case expression to determine that if the row has a date within 30 days of today's date, we'll count those as not lapsed. If a count of 0 was returned, then the row is considered lapsed and we'll output the lapsed date as the order_date plus 30 days. Any other count result means the row has not lapsed.
The above is all worked out in the lapsed_info subquery.
Then all we need to do is list the dates (see the dates subquery) and outer join the lapsed_info subquery to it based on the lapsed_date and then do a count of the lapsed dates for each day.

Related

SQL Query to fetch the latest payment method and invoice date

There are four tables
First - Customer
Second - Invoice
Third - Supplier
Fourth - Supplier_Remit
Tables details are mentioned below
Customer_id
Customer_Account_number
Customer_Status
Supplier_id
Supplier_Remit_id
1
1501
Active
11
111
2
1502
Inactive
12
112
3
1503
Active
13
113
4
1504
Active
14
114
5
1505
Inactive
15
115
Invoice_Date
Invoice_Amount
Invoice_Number
Payment Method
Customer_id
01/01/2023
100
1000001
Cash
1
12/01/2022
150
1000002
Credit Card
1
11/09/2022
200
1000003
Credit Card
1
12/09/2022
300
1000004
Cash
2
04/15/2022
1000
1000005
Cash
2
04/15/2022
1000
1000006
Credit Card
3
10/31/2022
250
1000007
Cash
4
10/25/2022
250
1000008
Cash
4
09/20/2022
130
1000009
Credit Card
5
05/20/2022
120
10000010
Credit Card
5
Supplier_Name
Supplier_id
ABC
11
ACCC
12
ADEF
13
AJKL
14
AFLR
15
City
Country
Supplier_Remit_id
Supplier_id
Boston
US
111
11
Oak
US
112
12
Albany
US
113
13
Madison
US
114
14
Los Ang
US
115
15
I need help in finding the most recent payment method, most recent invoice amount, no of count of invoices missing for current year (2023) and no of count of invoices missing for previous year(2022)
I have written query to find first few columns but unable to write further to get the above mentioned details
select c.customer_id,c.customer_account_number,c.customer_status,sr.country,max(i.invoice_date) as Latest receieved_Invoice_date
from
customer c,
invoice i,
supplier s,
supplier_Remit sr
where
c.customer_status='Active' and
sr.supplier_id=s.supplier_id and
c.supplier_remit_id=sr.supplier_remit_id and
c.customer_id=i.customer_id
group by
c.customer_id,c.customer_account_number,c.customer_status,sr.country;
My expected output would be as below
Customer_id
Cust_Acct_Num
Cust_Status
Country
Last_Inv_Rec_Date
1
1501
Active
US
01/01/2023
3
1503
Active
US
04/15/2022
4
1504
Active
US
10/31/2022
Latest_Paym_Method
Lastest_Inv_Amt
Count of Missing Inv for Curr Yr
Cash
100
0
Credit card
1000
1
Cash
250
1
Count of Missing Invoices for Prev Year
10
11
11
You can use MAX(...) KEEP (DENSE_RANK LAST ORDER BY invoice_date) to get values for the latest invoice and conditional aggregation to count the number of months where there are invoices and then subtract from the total number of months to find the missing invoices:
SELECT c.Customer_id,
c.Customer_Account_number,
c.Customer_Status,
r.country,
i.last_invoice_date,
i.latest_payment_method,
i.latest_invoice_amount,
EXTRACT(MONTH FROM SYSDATE) - COALESCE(i.missing_invoices_this_year, 0)
AS missing_invoices_this_year,
12 - COALESCE(i.missing_invoices_last_year, 0)
AS missing_invoices_last_year
FROM customer c
INNER JOIN supplier_remit r
ON (c.supplier_id = r.supplier_id)
LEFT OUTER JOIN (
SELECT customer_id,
MAX(invoice_date) AS last_invoice_date,
MAX(payment_method) KEEP (DENSE_RANK LAST ORDER BY invoice_date)
AS latest_payment_method,
MAX(invoice_amount) KEEP (DENSE_RANK LAST ORDER BY invoice_date)
AS latest_invoice_amount,
COUNT(
DISTINCT
CASE
WHEN invoice_date < SYSDATE
AND invoice_date >= TRUNC(SYSDATE, 'YY')
THEN TRUNC(invoice_date, 'MM')
END
) AS missing_invoices_this_year,
COUNT(
DISTINCT
CASE
WHEN invoice_date < TRUNC(SYSDATE, 'YY')
AND invoice_date >= ADD_MONTHS(TRUNC(SYSDATE, 'YY'), -12)
THEN TRUNC(invoice_date, 'MM')
END
) AS missing_invoices_last_year
FROM invoice
GROUP BY customer_id
) i
ON (c.customer_id = i.customer_id)
WHERE c.customer_status = 'Active';
Which, for the sample data:
CREATE TABLE customer (Customer_id, Customer_Account_number, Customer_Status, Supplier_id, Supplier_Remit_id) AS
SELECT 1, 1501, 'Active', 11, 111 FROM DUAL UNION ALL
SELECT 2, 1502, 'Inactive', 12, 112 FROM DUAL UNION ALL
SELECT 3, 1503, 'Active', 13, 113 FROM DUAL UNION ALL
SELECT 4, 1504, 'Active', 14, 114 FROM DUAL UNION ALL
SELECT 5, 1505, 'Inactive', 15, 115 FROM DUAL;
CREATE TABLE invoice (Invoice_Date, Invoice_Amount, Invoice_Number, Payment_Method, Customer_id) AS
SELECT DATE '2023-01-01', 100, 1000001, 'Cash', 1 FROM DUAL UNION ALL
SELECT DATE '2022-12-01', 150, 1000002, 'Credit Card', 1 FROM DUAL UNION ALL
SELECT DATE '2022-11-09', 200, 1000003, 'Credit Card', 1 FROM DUAL UNION ALL
SELECT DATE '2022-12-09', 300, 1000004, 'Cash', 2 FROM DUAL UNION ALL
SELECT DATE '2022-04-15', 1000, 1000005, 'Cash', 2 FROM DUAL UNION ALL
SELECT DATE '2022-04-15', 1000, 1000006, 'Credit Card', 3 FROM DUAL UNION ALL
SELECT DATE '2022-10-31', 250, 1000007, 'Cash', 4 FROM DUAL UNION ALL
SELECT DATE '2022-10-25', 250, 1000008, 'Cash', 4 FROM DUAL UNION ALL
SELECT DATE '2022-09-20', 130, 1000009, 'Credit Card', 5 FROM DUAL UNION ALL
SELECT DATE '2022-05-20', 120, 10000010, 'Credit Card', 5 FROM DUAL;
CREATE TABLE supplier (Supplier_Name, Supplier_id) AS
SELECT 'ABC', 11 FROM DUAL UNION ALL
SELECT 'ACCC', 12 FROM DUAL UNION ALL
SELECT 'ADEF', 13 FROM DUAL UNION ALL
SELECT 'AJKL', 14 FROM DUAL UNION ALL
SELECT 'AFLR', 15 FROM DUAL;
CREATE TABLE supplier_remit (City, Country, Supplier_Remit_id, Supplier_id) AS
SELECT 'Boston', 'US', 111, 11 FROM DUAL UNION ALL
SELECT 'Oak', 'US', 112, 12 FROM DUAL UNION ALL
SELECT 'Albany', 'US', 113, 13 FROM DUAL UNION ALL
SELECT 'Madison', 'US', 114, 14 FROM DUAL UNION ALL
SELECT 'Los Ang', 'US', 115, 15 FROM DUAL;
Outputs:
CUSTOMER_ID
CUSTOMER_ACCOUNT_NUMBER
CUSTOMER_STATUS
COUNTRY
LAST_INVOICE_DATE
LATEST_PAYMENT_METHOD
LATEST_INVOICE_AMOUNT
MISSING_INVOICES_THIS_YEAR
MISSING_INVOICES_LAST_YEAR
1
1501
Active
US
2023-01-01 00:00:00
Cash
100
0
10
3
1503
Active
US
2022-04-15 00:00:00
Credit Card
1000
1
11
4
1504
Active
US
2022-10-31 00:00:00
Cash
250
1
11
fiddle
In order to find what's missing, you have to first define what should be there, so you need to create a calendar of every month. Then you can use outer joins to the invoice table to find where there aren't any records for that month for that customer. There are lots of ways to write SQL to do this. Here's one:
WITH months AS(SELECT /*+ MATERIALIZE */ *
FROM (SELECT 'Current' year,
ADD_MONTHS(TRUNC(SYSDATE,'YYYY'),ROWNUM-1) month_start
FROM [any table with at least 12 rows]
WHERE ROWNUM <= 12)
WHERE month_start < SYSDATE
UNION ALL
SELECT 'Previous' year,
ADD_MONTHS(TRUNC(ADD_MONTHS(SYSDATE,-12),'YYYY'),ROWNUM-1)
FROM [any table with at least 12 rows]
WHERE ROWNUM <= 12)
SELECT customer.*,
inv.invoice_amount most_recent_invoice_amount,
inv.payment_method most_recent_payment_method,
(SELECT COUNT(*)
FROM months,
invoice
WHERE months.year = 'Current'
AND months.month_start = TRUNC(invoice_date(+),'MM')
AND invoice.customer_id(+) = customer.customer_id
AND invoice.customer_id IS NULL) missed_current_year_months,
(SELECT COUNT(*)
FROM months,
invoice
WHERE months.year = 'Previous'
AND months.month_start = TRUNC(invoice_date(+),'MM')
AND invoice.customer_id(+) = customer.customer_id
AND invoice.customer_id IS NULL) missed_previous_year_months
FROM customer
OUTER APPLY (SELECT invoice_amount,
payment_method
FROM (SELECT invoice_amount,
payment_method,
ROW_NUMBER() OVER (ORDER BY invoice_date DESC) seq
FROM invoice
WHERE invoice.customer_id = customer.customer_id)
WHERE seq = 1) inv

With Oracle SQL how can I find 3 days where total sum >= 150

I have a report that needs to list activity where total is >= 150 over 3 consecutive days.
Let's say I've created a temp table foo, to summarize daily totals.
| ID | Day | Total |
| -- | ---------- | ----- |
| 01 | 2020-01-01 | 10 |
| 01 | 2020-01-02 | 50 |
| 01 | 2020-01-03 | 50 |
| 01 | 2020-01-04 | 50 |
| 01 | 2020-01-05 | 20 |
| 02 | 2020-01-01 | 10 |
| 02 | 2020-01-02 | 10 |
| 02 | 2020-01-03 | 10 |
| 02 | 2020-01-04 | 10 |
| 02 | 2020-01-05 | 10 |
How Would I write SQL to return ID 01, but not 02?
Example Result:
| ID |
| -- |
| 01 |
I suspect that you want window functions here:
select distinct id
from (
select
t.*,
sum(total) over(partition by id order by day rows between 2 preceding and current row) sum_total,
count(*) over(partition by id order by day rows between 2 preceding and current row) cnt
from mytable t
) t
where cnt = 3 and sum_total >= 150
This gives you the ids that have a total greater than the given threshold over 3 consecutive days - which is how I understood your question.
If you just want to output the rows that have 3 consecutive days with a sum >= 150, you can use an analytic function to determine the moving total across each 3 day period per id, and then find the aggregate max value of the moving total per id, returning the id where it's >= 150.
E.g.:
WITH your_table AS (SELECT 1 ID, to_date('01/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 1 ID, to_date('02/01/2020', 'dd/mm/yyyy') dy, 50 total FROM dual UNION ALL
SELECT 1 ID, to_date('03/01/2020', 'dd/mm/yyyy') dy, 50 total FROM dual UNION ALL
SELECT 1 ID, to_date('04/01/2020', 'dd/mm/yyyy') dy, 50 total FROM dual UNION ALL
SELECT 1 ID, to_date('05/01/2020', 'dd/mm/yyyy') dy, 20 total FROM dual UNION ALL
SELECT 2 ID, to_date('01/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 2 ID, to_date('02/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 2 ID, to_date('03/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 2 ID, to_date('04/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual UNION ALL
SELECT 2 ID, to_date('05/01/2020', 'dd/mm/yyyy') dy, 10 total FROM dual),
moving_sums AS (SELECT ID,
dy,
total,
SUM(total) OVER (PARTITION BY ID ORDER BY dy RANGE BETWEEN 2 PRECEDING AND CURRENT ROW) moving_sum
FROM your_table)
SELECT ID
FROM moving_sums
GROUP BY ID
HAVING MAX(moving_sum) >= 150;
ID
----------
1
You can use a HAVING Clause GROUPED BY ID to list the desired ID values
SELECT ID
FROM foo
GROUP BY ID
HAVING COUNT( distinct day )>=3 AND SUM( NVL(Total,0) ) >= 150
Demo
Use this if you are to specify dates
WITH foo( ID, Day, Total ) AS
(
SELECT '01', date'2020-01-01' , 10 FROM dual
UNION ALL SELECT '01', date'2020-01-02' , 50 FROM dual
UNION ALL SELECT '01', date'2020-01-03' , 50 FROM dual
UNION ALL SELECT '01', date'2020-01-04' , 50 FROM dual
UNION ALL SELECT '01', date'2020-01-05' , 20 FROM dual
UNION ALL SELECT '02', date'2020-01-01' , 10 FROM dual
UNION ALL SELECT '02', date'2020-01-02' , 10 FROM dual
UNION ALL SELECT '02', date'2020-01-03' , 10 FROM dual
UNION ALL SELECT '02', date'2020-01-04' , 10 FROM dual
UNION ALL SELECT '02', date'2020-01-05' , 10 FROM dual
)SELECT
ID
FROM foo
WHERE day BETWEEN TO_DATE('2020-01-01', 'YYYY-MM-DD' ) AND TO_DATE('2020-01-04', 'YYYY-MM-DD' )
GROUP BY ID HAVING SUM(Total) >= 150;
RESULT:
ID|
--|
01|
Maybe you can try something like this :
SELECT
*
FROM foo
WHERE day BETWEEN 2020-01-01 AND 2020-01-04
AND total > 150

Oracle - Assigning the correct Date from a Set

I have a table A like below
REGID | PKG_DESC | EVENT_DATE | IS_CON | IS_REN
-----------------------------------------------------
1234 | cc | 27-MAR-14 | 0 | 0
1234 | cc | 27-JUN-14 | 1 | 0
1234 | GUI | 27-MAR-14 | 0 | 0
1234 | GUI | 27-JUN-14 | 1 | 0
1234 | GUI | 27-SEPT-14 | 0 | 1
1234 | GUI | 27-SEPT-15 | 0 | 1
1234 | REMOTE | 27-MAR-14 | 0 | 0
1234 | REMOTE | 27-JUN-14 | 1 | 0
1234 | REMOTE | 27-SEPT-14 | 0 | 1
2431 | cc | 27-MAR-14 | 0 | 0
2431 | cc | 27-JUN-14 | 1 | 0
I have a query like below
select a.reg_id, b.sess_start_dt,
case when TRUNC(A.EVENT_DATE) - B.SESS_START_DT BETWEEN 0-30 THEN 'DAYS 0_30'
WHEN TRUNC(A.EVENT_DATE) - B.SESS_START_DT BETWEEN 31-60 THEN 'DAYS 31-60'
from tab a inner join tab b on a.reg_id = b.reg_id and a.is_ren = 1
union
select a.reg_id, b.sess_start_dt,
case when TRUNC(A.EVENT_DATE) - B.SESS_START_DT BETWEEN 0-30 THEN 'DAYS 0_30'
WHEN TRUNC(A.EVENT_DATE) - B.SESS_START_DT BETWEEN 31-60 THEN 'DAYS 31-60'
from tab a inner join tab b on a.reg_id = b.reg_id and a.is_con = 1
Tab B contains all the usage for each reg_id there will be 100's of records.. Sample of few are
REGID | SESS_START_DT
1234 | 27-Jan-14
1234 | 20-MAR-12
1234 | 27-MAR-12
1234 | 01-sept-14
1234 | 07-sept-14
1234 | 29-JUL-14
1234 | 03-AUG-14
1234 | 27-MAR-13
1234 | 27-MAR-12
1234 | 27-MAR-12
1234 | 27-MAR-12
1234 | 27-MAR-12
1234 | 27-MAR-12
1234 | 27-MAR-12
2431 | 20-JUN-14
The Above query needs to be corrected in a way like,
1) If the REG_ID is having at least one is_ren = 1 then that subscription should be considered as renewal subscription and needs to get the 30 days and 60 days usage from table B from his is_ren = 1 event_date. (for REGID 1234 only is_ren query should execute)
2) If multiple IS_REN = 1 are existing for each REGID then the usage needs to be taken 30 days and 60 days from table B with the MIN(event_date). in this case the usage should be taken from 27-SEPT-14 instead of 27-SEPT-15
3) If there is no IS_REN = 1 and there is IS_CON = 1 then it's considered as conversion and usage should be taken before 60 days from the converted date (for REGID 2431, usage needs to get 60 days back from 27-JUN-14{this is my event_date in the query})
The O/P should be like
REGID | EVENT_DATE | DAYS 0_30 | DAYS 31-60 | CODE
1234 | 27-SEPT-14 | 2 | 2 | REN
2431 | 27-JUL-14 | 1 | 0 | CON
If my assumptions in my Comment are correct, this may be what you need. Notice the order by clause in row_number() - first the rows with is_ren = 1, then the rows with is_ren = 0 and is_con = 1, then all the other rows, and within each group order by event_date ascending. This way, the top row (rn = 1), which is the only one I use in the outer query, will have is_ren = 1 with the earliest possible date, or if no is_ren = 1 then the row with is_con = 1 and the earliest date, or else just the earliest date. (In the last case, the CODE will be null: this means there were no is_ren = 1 and no is_con = 1 for that regid.
Not sure why you have 27-JUL-14 in the output for regid = 2431, that should be 27-JUN-14. Also, there are no four-letter months in Oracle ("SEPT"). The output shows dates using my session parameters; if you need to format the dates, use to_date(event_date, .....) with the desired date format model. Also, since the data you provided is just dates (with no time-of-day component), I didn't truncate anything; you may need to, if your real data has time-of-day components.
with
table_a ( regid, pkg_desc, event_date, is_con, is_ren ) as (
select 1234, 'cc' , to_date ('27-MAR-14', 'dd-MON-rr'), 0, 0 from dual union all
select 1234, 'cc' , to_date ('27-JUN-14', 'dd-MON-rr'), 1, 0 from dual union all
select 1234, 'GUI' , to_date ('27-MAR-14', 'dd-MON-rr'), 0, 0 from dual union all
select 1234, 'GUI' , to_date ('27-JUN-14', 'dd-MON-rr'), 1, 0 from dual union all
select 1234, 'GUI' , to_date ('27-SEP-14', 'dd-MON-rr'), 0, 1 from dual union all
select 1234, 'GUI' , to_date ('27-SEP-15', 'dd-MON-rr'), 0, 1 from dual union all
select 1234, 'REMOTE', to_date ('27-MAR-14', 'dd-MON-rr'), 0, 0 from dual union all
select 1234, 'REMOTE', to_date ('27-JUN-14', 'dd-MON-rr'), 1, 0 from dual union all
select 1234, 'REMOTE', to_date ('27-SEP-14', 'dd-MON-rr'), 0, 1 from dual union all
select 2431, 'cc' , to_date ('27-MAR-14', 'dd-MON-rr'), 0, 0 from dual union all
select 2431, 'cc' , to_date ('27-JUN-14', 'dd-MON-rr'), 1, 0 from dual
),
table_b ( regid, sess_start_dt ) as (
select 1234, to_date ('27-JAN-14', 'dd-MON-rr') from dual union all
select 1234, to_date ('20-MAR-12', 'dd-MON-rr') from dual union all
select 1234, to_date ('27-MAR-12', 'dd-MON-rr') from dual union all
select 1234, to_date ('01-SEP-14', 'dd-MON-rr') from dual union all
select 1234, to_date ('07-SEP-14', 'dd-MON-rr') from dual union all
select 1234, to_date ('29-JUL-14', 'dd-MON-rr') from dual union all
select 1234, to_date ('03-AUG-14', 'dd-MON-rr') from dual union all
select 1234, to_date ('27-MAR-13', 'dd-MON-rr') from dual union all
select 1234, to_date ('27-MAR-12', 'dd-MON-rr') from dual union all
select 1234, to_date ('27-MAR-12', 'dd-MON-rr') from dual union all
select 1234, to_date ('27-MAR-12', 'dd-MON-rr') from dual union all
select 1234, to_date ('27-MAR-12', 'dd-MON-rr') from dual union all
select 1234, to_date ('27-MAR-12', 'dd-MON-rr') from dual union all
select 1234, to_date ('27-MAR-12', 'dd-MON-rr') from dual union all
select 2431, to_date ('20-JUN-14', 'dd-MON-rr') from dual
),
prep ( regid, event_date, code, rn ) as (
select regid, event_date,
case when is_ren = 1 then 'REN' when is_con = 1 then 'CON' else null end,
row_number() over (partition by regid
order by case when is_ren = 1 then 0
when is_con = 1 then 1 else 2 end,
event_date)
from table_a
)
select p.regid, p.event_date,
count(case when b.sess_start_dt between p.event_date - 30 and p.event_date
then 1 end) as days_0_30,
count(case when b.sess_start_dt between p.event_date - 60 and p.event_date - 31
then 1 end) as days_31_60,
p.code
from prep p inner join table_b b on p.regid = b.regid
where rn = 1
group by p.regid, p.event_date, p.code
;
Output:
REGID EVENT_DATE DAYS_0_30 DAYS_31_60 COD
---------- ------------------- ---------- ---------- ---
1234 2014-09-27 00:00:00 2 2 REN
2431 2014-06-27 00:00:00 1 0 CON

Count running total in Oracle

I want to make a query, which shows the progress of the number of users on my webpage by week.
I use following query to run the users database and get the number, grouped by a week:
SELECT TRUNC(FAB.LICENSE_DATE, 'IW'),
COUNT(DISTINCT FAB.STATEMENT_NUMBER) AS "Number of account statements"
FROM USERS FAB
GROUP BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW');
This gives following output:
Date | Users
----------------------
2015/09/07 | 5
2015/09/14 | 4
2015/09/21 | 6
But this is actually not what I want to achieve. I want to have the following output:
Date | Users
----------------------
2015/09/07 | 5
2015/09/14 | 9 (5 + 4)
2015/09/21 | 15 (5 + 4 + 6)
How to modify the query so I get all the results?
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE USERS (
LICENSE_DATE,
LAST_UPDATED_TIME,
STATEMENT_NUMBER
) AS
SELECT DATE '2015-09-07', DATE '2015-09-07', 1 FROM DUAL
UNION ALL SELECT DATE '2015-09-08', DATE '2015-09-08', 2 FROM DUAL
UNION ALL SELECT DATE '2015-09-08', DATE '2015-09-08', 3 FROM DUAL
UNION ALL SELECT DATE '2015-09-09', DATE '2015-09-09', 4 FROM DUAL
UNION ALL SELECT DATE '2015-09-12', DATE '2015-09-12', 5 FROM DUAL
UNION ALL SELECT DATE '2015-09-14', DATE '2015-09-15', 6 FROM DUAL
UNION ALL SELECT DATE '2015-09-15', DATE '2015-09-16', 7 FROM DUAL
UNION ALL SELECT DATE '2015-09-16', DATE '2015-09-16', 8 FROM DUAL
UNION ALL SELECT DATE '2015-09-17', DATE '2015-09-18', 9 FROM DUAL
UNION ALL SELECT DATE '2015-09-21', DATE '2015-09-21', 10 FROM DUAL
UNION ALL SELECT DATE '2015-09-21', DATE '2015-09-26', 11 FROM DUAL
UNION ALL SELECT DATE '2015-09-22', DATE '2015-09-22', 12 FROM DUAL
UNION ALL SELECT DATE '2015-09-23', DATE '2015-09-25', 13 FROM DUAL
UNION ALL SELECT DATE '2015-09-24', DATE '2015-09-24', 14 FROM DUAL
UNION ALL SELECT DATE '2015-09-27', DATE '2015-09-27', 15 FROM DUAL;
Query 1:
SELECT LAST_UPDATED_WEEK,
SUM( NUM_STATEMENTS ) OVER ( ORDER BY LAST_UPDATED_WEEK ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS "Number of account statements"
FROM (
SELECT TRUNC(LAST_UPDATED_TIME, 'IW') AS LAST_UPDATED_WEEK,
COUNT(DISTINCT STATEMENT_NUMBER) AS NUM_STATEMENTS
FROM USERS
GROUP BY
TRUNC( LAST_UPDATED_TIME, 'IW')
)
Results:
| LAST_UPDATED_WEEK | Number of account statements |
|-----------------------------|------------------------------|
| September, 07 2015 00:00:00 | 5 |
| September, 14 2015 00:00:00 | 9 |
| September, 21 2015 00:00:00 | 15 |
SELECT TRUNC(FAB.LICENSE_DATE, 'IW'),
SUM(COUNT(DISTINCT FAB.STATEMENT_NUMBER)) OVER (ORDER BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW')) as "Number of account statements"
FROM USERS FAB
GROUP BY TRUNC(FAB.LAST_UPDATED_TIME, 'IW');
You can use this code block for your problem :
select u.date
,(select sum(u1.users)
from users u1
where u1.ddate <= u.date) as users
from users u;
It gives this output :
07.09.2015 5
14.09.2015 9
21.09.2015 15
Good luck
Hello you can try this code too.
WITH t1 AS
( SELECT to_date('01/01/2015','mm/dd/yyyy') rn, 5 usrs FROM dual
UNION ALL
SELECT to_date('02/01/2015','mm/dd/yyyy') rn, 4 usrs FROM dual
UNION ALL
SELECT to_date('03/01/2015','mm/dd/yyyy') rn, 8 usrs FROM dual
UNION ALL
SELECT to_date('04/01/2015','mm/dd/yyyy') rn, 2 usrs FROM dual
)
SELECT rn,
usrs,
sum(usrs) over (order by rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) cumm_usrs
FROM t1
GROUP BY rn,
usrs;

Oracle: Identifying peak values in a time series

I have following values in a column of table. there are two columns in table. The other column is having distinct dates in descending order.
3
4
3
21
4
4
-1
3
21
-1
4
4
8
3
3
-1
21
-1
4
The graph will be
I need only peaks higlighted in graph with circles in output
4
21
21
8
21
4
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TEST ( datetime, value ) AS
SELECT DATE '2015-01-01', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-02', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-03', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-04', 21 FROM DUAL
UNION ALL SELECT DATE '2015-01-05', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-06', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-07', -1 FROM DUAL
UNION ALL SELECT DATE '2015-01-08', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-09', 21 FROM DUAL
UNION ALL SELECT DATE '2015-01-10', -1 FROM DUAL
UNION ALL SELECT DATE '2015-01-11', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-12', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-13', 8 FROM DUAL
UNION ALL SELECT DATE '2015-01-14', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-15', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-16', -1 FROM DUAL
UNION ALL SELECT DATE '2015-01-17', 21 FROM DUAL
UNION ALL SELECT DATE '2015-01-18', -1 FROM DUAL
UNION ALL SELECT DATE '2015-01-19', 4 FROM DUAL
Query 1:
SELECT datetime, value
FROM (
SELECT datetime,
LAG( value ) OVER ( ORDER BY datetime ) AS prv,
value,
LEAD( value ) OVER ( ORDER BY datetime ) AS nxt
FROM test
)
WHERE ( prv IS NULL OR prv < value )
AND ( nxt IS NULL OR nxt < value )
Results:
| DATETIME | VALUE |
|---------------------------|-------|
| January, 02 2015 00:00:00 | 4 |
| January, 04 2015 00:00:00 | 21 |
| January, 09 2015 00:00:00 | 21 |
| January, 13 2015 00:00:00 | 8 |
| January, 17 2015 00:00:00 | 21 |
| January, 19 2015 00:00:00 | 4 |
So the peak is defined as the previous value and next value being less than the current value, and you can retrieve the previous an next using LAG() and LEAD() functions.
You really need some other column (e.g. my_date) to define the order of the rows, then you can:
select my_date,
value
from (select value,
lag(value ) over (order by my_date) lag_value,
lead(value) over (order by my_date) lead_value
from my_table)
where value > coalesce(lag_value , value - 1) and
value > coalesce(lead_value, value - 1);
This would not allow for a "double-peak" such as:
1,
15,
15,
4
... for which much more complex logic would be needed.
Just for completeness the row pattern matching example:
WITH source_data(datetime, value) AS (
SELECT DATE '2015-01-01', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-02', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-03', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-04', 21 FROM DUAL UNION ALL
SELECT DATE '2015-01-05', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-06', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-07', -1 FROM DUAL UNION ALL
SELECT DATE '2015-01-08', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-09', 21 FROM DUAL UNION ALL
SELECT DATE '2015-01-10', -1 FROM DUAL UNION ALL
SELECT DATE '2015-01-11', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-12', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-13', 8 FROM DUAL UNION ALL
SELECT DATE '2015-01-14', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-15', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-16', -1 FROM DUAL UNION ALL
SELECT DATE '2015-01-17', 21 FROM DUAL UNION ALL
SELECT DATE '2015-01-18', -1 FROM DUAL UNION ALL
SELECT DATE '2015-01-19', 4 FROM DUAL
)
SELECT *
FROM
source_data MATCH_RECOGNIZE (
ORDER BY datetime
MEASURES
LAST(UP.datetime) AS datetime,
LAST(UP.value) AS value
ONE ROW PER MATCH
PATTERN ((UP DOWN) | UP$)
DEFINE
DOWN AS DOWN.value < PREV(DOWN.value),
UP AS UP.value > PREV(UP.value)
)
ORDER BY
datetime
There is a much more sophisticated method available in Oracle 12c, which is to use pattern matching SQL.
http://docs.oracle.com/database/121/DWHSG/pattern.htm#DWHSG8966
It would be overkill for a situation like this, but if you needed more complex patterns matched, such as W shaped patterns, then it would be worth investigating.
Using LAG function you can compare values from different rows. I assume the resultset you showed is ordered by another column named position.
select value
from
(select value,
lag(value,-1) over (order by position) prev,
lag(value,1) over (order by position) next
from table)
where value > prev
and value > next