SQL how to count how many months have passed since certain status - sql

I have a table with project, period and status with their periodic values. My goal is to show how many months have passed since certain project was last in 'Approved status' (or show what period this status was changed) in the period_leaving_approved_status column. for example, for periods 201005 to 201008 - i would need to show '201005' values, but for periods 201011-201012 - '201011' values. I managed to mark the line where status changes, but i don't know how to apply the condition for the following rows. My example query :
with subq as (
select 123 as project, 201002 as period, 'Approved' as status from dual
union all
select 123 as project, 201003 as period, 'Approved' as status from dual
union all
select 123 as project, 201004 as period, 'Approved' as status from dual
union all
select 123 as project, 201005 as period, 'Pending Close' as status from dual
union all
select 123 as project, 201006 as period, 'Pending Close' as status from dual
union all
select 123 as project, 201007 as period, 'Closed' as status from dual
union all
select 123 as project, 201008 as period, 'Closed' as status from dual
union all
select 123 as project, 201009 as period, 'Approved' as status from dual
union all
select 123 as project, 201010 as period, 'Approved' as status from dual
union all
select 123 as project, 201011 as period, 'Closed' as status from dual
union all
select 123 as project, 201012 as period, 'Closed' as status from dual
union all
select 123 as project, 201101 as period, 'Approved' as status from dual
union all
select 123 as project, 201102 as period, 'Approved' as status from dual
union all
select 123 as project, 201112 as period, 'Approved' as status from dual
union all
select 123 as project, 201301 as period, 'Pending Close' as status from dual
union all
select 123 as project, 201302 as period, 'Closed' as status from dual
union all
select 123 as project, 201203 as period, 'Closed' as status from dual
)
select project,
period,
status,
case when lag(status, 1, null) OVER (ORDER BY period)='Approved'
AND lag(status, 1, null) OVER (ORDER BY period) NOT IN(status) then period end as period_leaving_approved_status
from subq

I would approach this using aggregation:
select project,
max(case when status = 'Approved' then period end) as ApprovedPeriod
from subq;
If you want the time span then something like this:
select project,
months_between(max(case when status = 'Approved' then period end),
sysdate) as monthsSinceApproved,
max(case when status = 'Approved' then period end) as ApprovedPeriod
from subq
group by project;
EDIT:
If you want this information cumulatively on all rows, then use window functions:
select s.*,
max(case when status = 'Approved' then period end) over
(partition b project order by period) as LastApprovedPeriod
from subq s;

Related

How to get date difference and date from the below table

I have a table like this
Emplid
REQUEST_ID
Status
Status_Dttm
1234
1
New
02-Jun-2022 12.35.00.AM
1231
5
Draft
02-Jun-2022 12.30.00.AM
1234
1
In Progress
02-Jun-2022 12.47.00.AM
1234
1
Cancelled
02-Jun-2022 12.50.00.AM
1234
2
New
03-Jun-2022 12.47.00.AM
Trying to create a view with the fields as EMPLID,REQUEST_ID,REQUEST_DATE,NO_OF_DAYS_IN_NEW,IN_PROGRESS_SINCE,COMPLETED/CANCELLED DATE
Tried using this
SELECT MIN(emplid) AS emplid, request_id, MIN(status_dttm) AS request_date,
MAX(status) KEEP (DENSE_RANK LAST ORDER BY status_dttm) AS current_status
FROM sts_tbl
GROUP BY request_id
CASE WHEN (MAX(status) KEEP (DENSE_RANK LAST ORDER BY status_dttm)) = 'NEW' THEN
TRUNC(SYSDATE) - MIN(status_dttm)
END AS "No of Days in New"
CASE WHEN (MAX(status) KEEP (DENSE_RANK LAST ORDER BY status_dttm)) = 'In Progree' THEN
status_dttm
END AS "In Progress Since"
FROM sts_tbl
GROUP BY emplid,request_id
getting error as "Not a Group By expression"
You can use the LEAD analytic function to find the time of the next status change and then aggregate:
SELECT MIN(emplid) AS emplid,
request_id,
MIN(status_dttm) AS request_date,
SUM(CASE status WHEN 'New' THEN duration ELSE 0 END) AS no_of_days_in_new,
MIN(CASE status WHEN 'In Progress' THEN status_dttm END)
AS in_progress_since,
MAX(status) KEEP (DENSE_RANK LAST ORDER BY status_dttm)
AS current_status,
MIN(CASE WHEN status IN ('Completed', 'Cancelled') THEN status_dttm END)
AS completed_cancelled_date
FROM (
SELECT t.*,
LEAD(status_dttm, 1, SYSDATE) OVER (
PARTITION BY request_id ORDER BY status_dttm
) - status_dttm AS duration
FROM sts_tbl t
)
GROUP BY request_id
Which, for the sample data:
CREATE TABLE sts_tbl (Emplid, REQUEST_ID, Status, Status_Dttm) AS
SELECT 1234, 1, 'Open', DATE '2022-06-02' + INTERVAL '35' MINUTE FROM DUAL UNION ALL
SELECT 1231, 5, 'Draft', DATE '2022-06-02' + INTERVAL '30' MINUTE FROM DUAL UNION ALL
SELECT 1234, 1, 'In Progress', DATE '2022-06-02' + INTERVAL '47' MINUTE FROM DUAL UNION ALL
SELECT 1234, 1, 'Cancelled', DATE '2022-06-02' + INTERVAL '50' MINUTE FROM DUAL UNION ALL
SELECT 1234, 2, 'New', DATE '2022-06-03' + INTERVAL '47' MINUTE FROM DUAL;
Outputs:
EMPLID
REQUEST_ID
REQUEST_DATE
NO_OF_DAYS_IN_NEW
IN_PROGRESS_SINCE
CURRENT_STATUS
COMPLETED_CANCELLED_DATE
1234
1
2022-06-02 00:35:00
0
2022-06-02 00:47:00
Cancelled
2022-06-02 00:50:00
1234
2
2022-06-03 00:47:00
80.54319444444444444444444444444444444444
null
New
null
1231
5
2022-06-02 00:30:00
0
null
Draft
null
Note: there is not a next status for request_id 2 so it is assumed that the number of days in the New status is from the start of the new status until the current date.
db<>fiddle here

Select with two counts same column different where

I have a table with
Logdate,Status
20190101 ok
20190101 notok
20190101 ok
20190102 ok
20190102 notok
I would like to get a query result like these:
date ok notok
20190101 2 1
20190102 1 1
I don't know hot make a query of same column agreggate with 2 different where's
Please any help?
Thanks!
edit--- mi querys
SELECT LOGDATE AS EXECUTION_DATE, COUNT(1) AS TOTAL_OK FROM CMR_IOALOG WHERE UPPER(STATUS) LIKE upper('% OK %') group by logdate ORDER BY LOGDATE DESC;
SELECT LOGDATE AS EXECUTION_DATE COUNT(1) AS TOTAL_NOTOK FROM CMR_IOALOG WHERE UPPER(STATUS) LIKE upper('%NOTOK%') group by logdate ORDER BY LOGDATE DESC;
You can use conditional aggregation, via a case expression inside the count() call:
select logdate,
count(case when status = 'ok' then status end) as ok,
count(case when status = 'notok' then status end) as notok
from your_table
group by logdate;
The count() function ignores nulls, so the case expression gives a not-null value for the status you want to count, and defaults to null for anything else.
Demo with your sample data as a CTE:
-- CTE for sample data
with your_table (logdate, status) as (
select 20190101, 'ok' from dual
union all select 20190101, 'notok' from dual
union all select 20190101, 'ok' from dual
union all select 20190102, 'ok' from dual
union all select 20190102, 'notok' from dual
)
-- actual query
select logdate,
count(case when status = 'ok' then status end) as ok,
count(case when status = 'notok' then status end) as notok
from your_table
group by logdate;
LOGDATE OK NOTOK
---------- ---------- ----------
20190102 1 1
20190101 2 1
Hopefully your logdate is actually a date rather than a number; I've just used a number to match the value you showed. If it is a date and has non-midnight times then you can trunc(logdate) to count values across the whole day:
with your_table (logdate, status) as (
select to_date('20190101 00:01', 'YYYYMMDD HH24:MI'), 'ok' from dual
union all select to_date('20190101 00:02', 'YYYYMMDD HH24:MI'), 'notok' from dual
union all select to_date('20190101 00:03', 'YYYYMMDD HH24:MI'), 'ok' from dual
union all select to_date('20190102 00:01', 'YYYYMMDD HH24:MI'), 'ok' from dual
union all select to_date('20190102 00:02', 'YYYYMMDD HH24:MI'), 'notok' from dual
)
select trunc(logdate) as logdate,
count(case when status = 'ok' then status end) as ok,
count(case when status = 'notok' then status end) as notok
from your_table
group by trunc(logdate);
LOGDATE OK NOTOK
---------- ---------- ----------
2019-01-02 1 1
2019-01-01 2 1
You could use sum() instead, and make the case expression evaluate to either zero or one, but the effect is the same - and I prefer to use count() when the overall aim is to count things.
You could also use an explicit pivot, but it does the same thing under the hood, and is probably overkill for this simple scenario.
-- Oracle 11+
with s (Logdate,Status) as (
select 20190101, 'ok' from dual union all
select 20190101, 'notok' from dual union all
select 20190101, 'ok' from dual union all
select 20190102, 'ok' from dual union all
select 20190102, 'notok' from dual)
select *
from s
pivot (count(*) for status in ('ok' as ok, 'notok' as notok))
order by Logdate;
LOGDATE OK NOTOK
---------- ---------- ----------
20190101 2 1
20190102 1 1
Try this:
select logdate, ok, notok from your_table
pivot (count(status) for status in ('ok' as ok, 'notok' as notok));

Finding missing dates in a sequence

I have following table with ID and DATE
ID DATE
123 7/1/2015
123 6/1/2015
123 5/1/2015
123 4/1/2015
123 9/1/2014
123 8/1/2014
123 7/1/2014
123 6/1/2014
456 11/1/2014
456 10/1/2014
456 9/1/2014
456 8/1/2014
456 5/1/2014
456 4/1/2014
456 3/1/2014
789 9/1/2014
789 8/1/2014
789 7/1/2014
789 6/1/2014
789 5/1/2014
789 4/1/2014
789 3/1/2014
In this table, I have three customer ids, 123, 456, 789 and date column which shows which month they worked.
I want to find out which of the customers have gap in their work.
Our customers work record is kept per month...so, dates are monthly..
and each customer have different start and end dates.
Expected results:
ID First_Absent_date
123 10/01/2014
456 06/01/2014
To get a simple list of the IDs with gaps, with no further details, you need to look at each ID separately, and as #mikey suggested you can count the number of months and look at the first and last date to see if how many months that spans.
If your table has a column called month (since date isn't allowed unless it's a quoted identifier) you could start with:
select id, count(month), min(month), max(month),
months_between(max(month), min(month)) + 1 as diff
from your_table
group by id
order by id;
ID COUNT(MONTH) MIN(MONTH) MAX(MONTH) DIFF
---------- ------------ ---------- ---------- ----------
123 8 01-JUN-14 01-JUL-15 14
456 7 01-MAR-14 01-NOV-14 9
789 7 01-MAR-14 01-SEP-14 7
Then compare the count with the month span, in a having clause:
select id
from your_table
group by id
having count(month) != months_between(max(month), min(month)) + 1
order by id;
ID
----------
123
456
If you can actually have multiple records in a month for an ID, and/or the date recorded might not be the start of the month, you can do a bit more work to normalise the dates:
select id,
count(distinct trunc(month, 'MM')),
min(trunc(month, 'MM')),
max(trunc(month, 'MM')),
months_between(max(trunc(month, 'MM')), min(trunc(month, 'MM'))) + 1 as diff
from your_table
group by id
order by id;
select id
from your_table
group by id
having count(distinct trunc(month, 'MM')) !=
months_between(max(trunc(month, 'MM')), min(trunc(month, 'MM'))) + 1
order by id;
Oracle Setup:
CREATE TABLE your_table ( ID, "DATE" ) AS
SELECT 123, DATE '2015-07-01' FROM DUAL UNION ALL
SELECT 123, DATE '2015-06-01' FROM DUAL UNION ALL
SELECT 123, DATE '2015-05-01' FROM DUAL UNION ALL
SELECT 123, DATE '2015-04-01' FROM DUAL UNION ALL
SELECT 123, DATE '2014-09-01' FROM DUAL UNION ALL
SELECT 123, DATE '2014-08-01' FROM DUAL UNION ALL
SELECT 123, DATE '2014-07-01' FROM DUAL UNION ALL
SELECT 123, DATE '2014-06-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-11-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-10-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-09-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-08-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-05-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-04-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-03-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-09-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-08-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-07-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-06-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-05-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-04-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-03-01' FROM DUAL;
Query:
SELECT ID,
MIN( missing_date )
FROM (
SELECT ID,
CASE WHEN LEAD( "DATE" ) OVER ( PARTITION BY ID ORDER BY "DATE" )
= ADD_MONTHS( "DATE", 1 ) THEN NULL
WHEN LEAD( "DATE" ) OVER ( PARTITION BY ID ORDER BY "DATE" )
IS NULL THEN NULL
ELSE ADD_MONTHS( "DATE", 1 )
END AS missing_date
FROM your_table
)
GROUP BY ID
HAVING COUNT( missing_date ) > 0;
Output:
ID MIN(MISSING_DATE)
---------- -------------------
123 2014-10-01 00:00:00
456 2014-06-01 00:00:00
You could use a Lag() function to see if records have been skipped for a particular date or not.Lag() basically helps in comparing the data in current row with previous row. So if we order by DATE, we could easily compare and find any gaps.
select * from
(
select ID,DATE_, case when DATE_DIFF>1 then 1 else 0 end comparison from
(
select ID, DATE_ ,DATE_-LAG(DATE_, 1) OVER (PARTITION BY ID ORDER BY DATE_) date_diff from trial
)
)
where comparison=1 order by ID,DATE_;
This groups all the entries by id, and then arranges the records by date. If a customer is always present, there would not be a gap in his date. So anyone who has a date difference greater than 1 had a gap. You could tweak this as per your requirement.
EDIT : Just observed that you are storing data in mm/dd/yyyy format, when I closely observed above answers.You are storing only first date of every month. So, the above query can be tweaked as :
select * from
(
select ID,DATE_,PREV_DATE,last_day(PREV_DATE)+1 ABSENT_DATE, case when DATE_DIFF>31 then 1 else 0 end comparison from
(
select ID, DATE_ ,LAG(DATE_,1) OVER (PARTITION BY ID ORDER BY DATE_) PREV_DATE,DATE_-LAG(DATE_, 1) OVER (PARTITION BY ID ORDER BY DATE_) date_diff from trial
)
)
where comparison=1 order by ID,DATE_;

How do I write an SQL to get a cumulative value and a monthly total in one row?

Say, I have the following data:
select 1 id, date '2007-01-16' date_created, 5 sales, 'Bob' name from dual union all
select 2 id, date '2007-04-16' date_created, 2 sales, 'Bob' name from dual union all
select 3 id, date '2007-05-16' date_created, 6 sales, 'Bob' name from dual union all
select 4 id, date '2007-05-21' date_created, 4 sales, 'Bob' name from dual union all
select 5 id, date '2013-07-16' date_created, 24 sales, 'Bob' name from dual union all
select 6 id, date '2007-01-17' date_created, 15 sales, 'Ann' name from dual union all
select 7 id, date '2007-04-17' date_created, 12 sales, 'Ann' name from dual union all
select 8 id, date '2007-05-17' date_created, 16 sales, 'Ann' name from dual union all
select 9 id, date '2007-05-22' date_created, 14 sales, 'Ann' name from dual union all
select 10 id, date '2013-07-17' date_created, 34 sales, 'Ann' name from dual
I want to get results like the following:
Name Total_cumulative_sales Total_sales_current_month
Bob 41 24
Ann 91 34
In this table, for Bob, his total sales is 41 starting from the beginning. And for this month which is July, his sales for this entire month is 24. Same goes for Ann.
How do I write an SQL to get this result?
Try this way:
select name, sum(sales) as Total_cumulative_sales ,
sum(
case trunc(to_date(date_created), 'MM')
when trunc(sysdate, 'MM') then sales
else 0
end
) as Total_sales_current_month
from tab
group by name
SQL Fiddle Demo
More information
Trunc
Case Statement
SELECT Name,
SUM(Sales) Total_sales,
SUM(CASE WHEN MONTH(date_created) = MONTH(GetDate()) AND YEAR(date_created) = YEAR(GetDate()) THEN Sales END) Total_sales_current_month
GROUP BY Name
Should work, but there's probably a more elegant way to specify "in the current month".
This should work for sales over a number of years. It will get the cumulative sales over any number of years. It won't produce a record if there are no sales in the latest month.
WITH sales AS
(select 1 id, date '2007-01-16' date_created, 5 sales, 'Bob' sales_name from dual union all
select 2 id, date '2007-04-16' date_created, 2 sales, 'Bob' sales_name from dual union all
select 3 id, date '2007-05-16' date_created, 6 sales, 'Bob' sales_name from dual union all
select 4 id, date '2007-05-21' date_created, 4 sales, 'Bob' sales_name from dual union all
select 5 id, date '2013-07-16' date_created, 24 sales, 'Bob' sales_name from dual union all
select 6 id, date '2007-01-17' date_created, 15 sales, 'Ann' sales_name from dual union all
select 7 id, date '2007-04-17' date_created, 12 sales, 'Ann' sales_name from dual union all
select 8 id, date '2007-05-17' date_created, 16 sales, 'Ann' sales_name from dual union all
select 9 id, date '2007-05-22' date_created, 14 sales, 'Ann' sales_name from dual union all
select 10 id, date '2013-07-17' date_created, 34 sales, 'Ann' sales_name from dual)
SELECT sales_name
,total_sales
,monthly_sales
,mon
FROM (SELECT sales_name
,SUM(sales) OVER (PARTITION BY sales_name ORDER BY mon) total_sales
,SUM(sales) OVER (PARTITION BY sales_name,mon ORDER BY mon) monthly_sales
,mon
,max_mon
FROM ( SELECT sales_name
,sum(sales) sales
,mon
,max_mon
FROM (SELECT sales_name
,to_number(to_char(date_created,'YYYYMM')) mon
,sales
,MAX(to_number(to_char(date_created,'YYYYMM'))) OVER (PARTITION BY sales_name) max_mon
FROM sales
ORDER BY 2)
GROUP BY sales_name
,max_mon
,mon
)
)
WHERE max_mon = mon
;

Ranking over consecutive dates

I need to get the amount of consecutive unpayments:
with payments as
(
select '1' as ID, '20130331' as DateR, 'Not_paid' as Status from dual
union
select '1' as ID, '20130430' as DateR, 'Paid' as Status from dual
union
select '1' as ID, '20130531' as DateR, 'Not_paid' as Status from dual
union
select '2' as ID, '20130331' as DateR, 'Not_paid' as Status from dual
union
select '2' as ID, '20130430' as DateR, 'Not_paid' as Status from dual
union
select '3' as ID, '20130331' as DateR, 'Paid' as Status from dual
union
select '3' as ID, '20130430' as DateR, 'Paid' as Status from dual
union
select '3' as ID, '20130531' as DateR, 'Paid' as Status from dual
)
select ID, dater, status, dense_rank() over (partition by ID, status order by dater asc) rnk from payments
As you see from this I get the right number of unpayments from id 2: his first unpayment was in March, and the second in April. Id 3 is ok too, because I would exclude him out later on, but for id 1 it says the second unpayment was in May, while I want to make it to be the first because he unpaid in March, but paid again in April so it should start ranking from there. Once he paid his last payment the process starts again.
The idea is to keep it simple without complex queries. I just need to do the same as the dense rank but only when the dates are consecutive
I hope the example is clear enough.
Edit:
This is what I get from the current query:
ID DATER STATUS RNK
1 20130331 Not_paid 1
1 20130531 Not_paid 2
1 20130430 Paid 1
2 20130331 Not_paid 1
2 20130430 Not_paid 2
3 20130331 Paid 1
3 20130430 Paid 2
3 20130531 Paid 3
And what I would like to get is this:
ID DATER STATUS RNK
1 20130331 Not_paid 1
1 20130430 Paid 1
1 20130531 Not_Paid 1
2 20130331 Not_paid 1
2 20130430 Not_paid 2
3 20130331 Paid 1
3 20130430 Paid 2
3 20130531 Paid 3
Such that if I want to get the max(rank) to check how many unpayments a user currently has I get that ID has 1 unpayment, ID 2 two consecutive unpayments, and ID 3 has 0 unpayments. This is because on the forth consecutive unpayment I have to consider the user as churned.
Edit:29/06/2013
Someone gave me a perfect solution in another forum:
https://forums.oracle.com/thread/2555552
This isn't a complete answer to your question, but it's a possible solution to get the number of outstanding non-payments for each id. It assigns a value of 1 for a Not_paid status and a -1 for a Paid status. So we can then group the query by ID and sum the value column to get the number of outstanding payments. For sums that are negative, we assign to zero as they have no outstanding payments.
with payments as
(
select '1' as ID, '20130331' as DateR, 'Not_paid' as Status from dual
union
select '1' as ID, '20130430' as DateR, 'Paid' as Status from dual
union
select '1' as ID, '20130531' as DateR, 'Not_paid' as Status from dual
union
select '2' as ID, '20130331' as DateR, 'Not_paid' as Status from dual
union
select '2' as ID, '20130430' as DateR, 'Not_paid' as Status from dual
union
select '3' as ID, '20130331' as DateR, 'Paid' as Status from dual
union
select '3' as ID, '20130430' as DateR, 'Paid' as Status from dual
union
select '3' as ID, '20130531' as DateR, 'Paid' as Status from dual
)
SELECT id,
DECODE(SIGN(SUM(value)), -1, 0, SUM(value))
FROM (SELECT id,
dater,
status,
DECODE(status, 'Paid', -1, 1) value
FROM payments
)
GROUP BY id
ORDER BY id;
Now this query works for the data set in your example, but may not work for bigger data sets. Also it won't work if there aren't at least an equal number of Paid statuses for the Not_paid status. For instance, in your example for ID = 2, if the account is paid in full in May, there would need to be 2 Paid entries loaded into your table in order for my solution to work. If only 1 Paid entry was loaded, then my solution would still show an outstanding payment required for this ID.