SQL query to produce month report status - sql

I have a table like this:
TICKET ID | OPEN DATE | CLOSE DATE
----------+------------+------------
1 | 2018-12-30 | 2019-01-01
2 | 2019-01-30 | 2019-02-01
3 | 2019-01-20 | 2019-01-22
I have to produce a view that create for each ticket id several entry with a status (OPEN, CLOSED, BACKLOG), for each month of the current year. Status OPEN if OPEN DATE is in the month, status CLOSED if closed date is in the month, BACKLOG if the open date was in the previous month and close date was not in the previous month.
So in the example below the output table would be:
TICKET ID | MONTH | STATUS
----------+--------+---------
1 | JAN-19 | BACKLOG
1 | JAN-19 | CLOSED
2 | JAN-19 | OPEN
3 | JAN-19 | OPEN
3 | JAN-19 | CLOSED
2 | FEB-19 | CLOSED
Is there a way to do that in pure SQL in SQL Server?

Your description suggests:
select t.ticket_id, v.mon, v.status
from t cross apply
(values ('OPEN', datefromparts(year(open_date), month(open_date), 1)),
('CLOSED', datefromparts(year(open_date), month(open_date), 1)),
('BACKLOG', datefromparts(year(dateadd(month, 1, open_date)), month(dateadd(month, 1, open_date)), 1))
) v(status, mon)
where status <> 'BACKLOG' or
datediff(month, open_date, close_date) > 0;
You can add filtering in the where clause for a particular period of time, such as dates in 2019.
This should do exactly what you want for "BACKLOG":
BACKLOG if the open date was in the previous month and close date was not in the previous month.
However, you have not taken multiple months of backlog into account. If this is a possibility, ask a new question, with appropriate sample data and desired results.

Does this do what you want? You can adjust the period it runs over by tweaking the dates table.
declare #tickets table (ticketId int, openDate datetime, closeDate datetime)
insert into #tickets (ticketId, openDate, closeDate) values (1, '30 Dec 2018', '1 Jan 2019')
insert into #tickets (ticketId, openDate, closeDate) values (2, '30 Jan 2019', '1 Feb 2019')
insert into #tickets (ticketId, openDate, closeDate) values (3, '20 Jan 2019', '22 Jan 2019')
; with dates as (
select cast('1 Jan 2019' as datetime) startMon, cast('31 Jan 2019' as dateTime) endMon
union all
select dateAdd(mm, 1, startMon), DATEADD(dd, -1, dateAdd(mm, 2, startMon)) from dates where startMon < '01 Dec 2019'
)
select ticketId, startMon, 'BACKLOG' [Status] from #tickets t inner join dates d on t.openDate between dateAdd(m, -1, startMon) and startMon and closeDate < endMon and closeDate >= startMon
union
select ticketId, startMon, 'OPEN' from #tickets t inner join dates d on t.openDate between startMon and endMon
union
select ticketId, startMon, 'CLOSED' from #tickets t inner join dates d on t.closeDate between startMon and endMon
order by startMon, ticketId

Related

How to count number of people for each month, who was on internship, if we know dates of internship

First of all, sorry for bad title - I can't figure out how to write generalized formulation of my problem.
I have a table in PostgreSQL with users and dates of their internships. It looks like this:
user_id
start
end
1
December 22, 2019
June 29, 2020
2
March 8, 2020
September 8, 2020
3
May 21, 2020
November 21, 2020
From this I need to calculate for each month, how many people were on internship during this month. I only need to calculate full months (if internship actually started on December 22, 2019, I will calculate from January 2022. If internship were finished at June 29, 2020, I will calculate till May 2020.
Finally I need this table:
Month
Count
Jan-20
1
Feb-20
1
Mar-20
1
Apr-20
2
May-20
2
Jun-20
2
Jul-20
2
Aug-20
2
Sep-20
1
Oct-20
1
For making it absolutely clear, this is how I got it:
Month
user_1
user_2
user_3
Count
Jan-20
1
1
Feb-20
1
1
Mar-20
1
1
Apr-20
1
1
2
May-20
1
1
2
Jun-20
1
1
2
Jul-20
1
1
2
Aug-20
1
1
2
Sep-20
1
1
Oct-20
1
1
My idea is to:
Reshape my initial table, so it will look like this:
user_id
date
event
1
December 22, 2019
start
1
June 29, 2020
end
2
March 8, 2020
start
2
September 8, 2020
end
3
May 21, 2020
start
3
November 21, 2020
end
Generate series between each start and end event:
user_id
month
1
Jan-20
1
Feb-20
1
Mar-20
1
Apr-20
1
May-20
2
Apr-20
2
May-20
2
Jun-20
2
Jul-20
2
Aug-20
3
Jun-20
3
Jul-20
3
Aug-20
3
Sep-20
3
Oct-20
Using count() GROUP BY month
Unfortunately, I have problems with 1 and 2 clauses.
I don't know how to reshape the table in PostgreSQL. In Pandas I would use 'stack' function. For my case I can't find the appropriate function.
Even if I can reshape it, I don't understand how to make series of month for each user (shown above).
Please advise, what can be done here to solve my problem?
this query should do the job considering your table as test :
SELECT to_char(d.date, 'Mon-YY') AS month, count(*) AS count
FROM
( SELECT generate_series(date_trunc('month', min(start_date)), date_trunc('month', max(end_date)), interval '1 month') :: date AS date
FROM test
) AS d
INNER JOIN test AS t
ON daterange(t.start_date, t.end_date, '[]') && daterange(d.date, (d.date + interval '1 month') :: date)
WHERE daterange(t.start_date, t.end_date, '[]') #> daterange(d.date, (d.date + interval '1 month') :: date)
GROUP BY d.date
The first sub query calculate the months covered in table test.
The JOIN clause calculates the intersections between the months and the date interval for each user
The WHERE clause filters the rows where the date interval for a user corresponds to a full months.
Result :
month
count
Jan-20
1
Feb-20
1
Mar-20
1
Apr-20
2
May-20
2
Jun-20
2
Jul-20
2
Aug-20
2
Sep-20
1
Oct-20
1
see results in dbfiddle
Here is how I accomplished this:
Generated a calendar table using generate_series
Truncated the start and end dates to ensure we are only including instances where a full month of the internship was completed.
Performed a cross join to generate a cartesian product set.
Finally, add the WHERE predicate to include instances where the date_mm is between the truncated start and end dates.
SQL:
SELECT a.date_mm AS MONTH,
count(b.user_id) AS COUNT
FROM
(SELECT date_mm :: date
FROM generate_series('2020-01-01', '2023-01-01', '1 month' :: interval) date_mm) a
CROSS JOIN
(SELECT a.user_id,
a.start,
/* start_next_fom = first day of next month */
(date_trunc('month', a.start) + interval '1 month') AS start_next_fom,
a.end,
/* end_last_eom = last day of last month */
(date_trunc('month', a.end) - interval '1 day') AS end_last_eom
FROM users a) b
WHERE a.date_mm BETWEEN b.start_next_fom AND b.end_last_eom
GROUP BY a.date_mm
ORDER BY a.date_mm
Result:
| month | count |
|------------|-------|
| 2020-01-01 | 1 |
| 2020-02-01 | 1 |
| 2020-03-01 | 1 |
| 2020-04-01 | 2 |
| 2020-05-01 | 2 |
| 2020-06-01 | 2 |
| 2020-07-01 | 2 |
| 2020-08-01 | 2 |
| 2020-09-01 | 1 |
| 2020-10-01 | 1 |
SQL Fiddle:
http://sqlfiddle.com/#!17/9f9f3/37
One approach could be to
generate a calendar table using GENERATE_SERIES
joining the calendar table with your original table on date ranges
aggregating to count users for each month
WITH calendar AS (
SELECT DATE('2020-01-01') + (num_months::text || ' month')::interval AS months
FROM GENERATE_SERIES(0, 11) AS num_months
)
SELECT c.months, COUNT(user_id) AS cnt
FROM calendar c
INNER JOIN tab
ON c.months BETWEEN DATE_TRUNC('month', tab.start_) + INTERVAL '1 month' AND DATE_TRUNC('month', tab.end_) - INTERVAL '1 month'
GROUP BY c.months
ORDER BY c.months
Check the demo here.
If you're using a PostgreSQL legacy version, you can obtain the calendar table with a recursive query:
WITH RECURSIVE calendar AS (
SELECT '2020-01-01'::timestamp AS months,
0 AS num_months
UNION ALL
SELECT months + INTERVAL '1 month' AS months,
num_months + 1 AS num_months
FROM calendar
WHERE num_months +1 <= 12
)
SELECT c.months, COUNT(user_id) AS cnt
FROM calendar c
INNER JOIN tab
ON c.months BETWEEN DATE_TRUNC('month', tab.start_) + INTERVAL '1 month' AND DATE_TRUNC('month', tab.end_) - INTERVAL '1 month'
GROUP BY c.months
ORDER BY c.months
Check the demo here.
Example with date calendar as subquery - number sequence (10*12) - 10 year.
Test data:
create table test (user_id integer, start_date date, end_date date) ;
insert into test values
(1, 'December 22, 2019', 'June 29, 2020')
,(2, 'March 8, 2020', 'September 8, 2020')
,(3, 'May 21, 2020', 'November 21, 2020') --june-october
,(4, 'May 31, 2020', 'November 30, 2020') --june-november
,(5, 'May 01, 2020', 'May 31, 2020') -- 1 full month - may
,(6, 'May 01, 2020', 'May 30, 2020') -- part of month - null
,(7, 'May 05, 2020', 'May 30, 2020') -- part of month - null
,(8, 'May 05, 2020', 'May 31, 2020') -- part of month - null
,(9, 'May 05, 2020', 'June 6, 2020') -- part of month - null
,(10, 'May 05, 2020', 'July 6, 2020') -- part of 2 month -june
,(11, 'Jan 01, 2018', 'Dec 31, 2020') -- full 3 year
;
Main query:
SELECT user_id,start_date,end_date,mn monthNum
,to_char(case when date_trunc('month', start_date)=start_date
then (start_date+cast((mn-1) ||' month' as interval))
else (start_date+cast(mn ||' month' as interval))
end, 'YY-mm-Mon') AS month
,case when date_trunc('month', start_date)=start_date
then (start_date+cast((mn-1) ||' month' as interval))
else (start_date+cast(mn ||' month' as interval))
end date
FROM test AS t
left join --series of 120 numbers (month)
(select yn*12+mn as mn
from (select * from(values(1),(2),(3),(4),(5),(6),(7),(9),(10),(11),(12))tm(mn))tm -- month
,(select * from(values(0),(1),(2),(3),(4),(5),(6),(7),(9),(10))ty(yn)) ty --years
)mm
on case when date_trunc('month', start_date)=start_date -- first day of month
then (start_date+cast((mn-1) ||' month' as interval))
else (start_date+cast(mn ||' month' as interval))
end
<=
case when end_date
=(date_trunc('month', end_date)
+ interval '1 month' - interval '1 day') --eomonth
then end_date
else (end_date-cast(extract(day from end_date) ||' day' as interval))
end
ORDER BY user_id,mn
Somewhat complicated to check start_date is first day of month and end_date is last day of month
Fiddle here

Aggregate monthly rows created date and ended date

I need to adapt a graph from the current BI implementation to an SQL one. This graph reflects the amount of requests received and each one of these requests have 3 fields that are relevant for this query: the id, created date and the end date.
The graph looks like this https://i.stack.imgur.com/NRIjr.png:
+----+--------------+-------------+
| ID | CREATE_DATE | END_DATE |
+----+--------------+-------------+
| | | |
| 1 | 2022-01-01 | 2022-02-10 |
| | | |
| 2 | 2022-01-03 | 2022-03-01 |
| | | |
| 3 | 2022-02-01 | 2022-04-01 |
| | | |
| 4 | 2022-03-01 | null |
+----+--------------+-------------+
So for this particular example we'd have something like this:
January: active: 2 (requests 1 and 2), finished: 0;
February: active 2 (requests 2, 3), finished 1 (request 1);
March: active 2 (requests 3, 4) finished 1 (request 2)
So for each month I want the active requests for that particular month (those that their ended date goes after that particular month or is null) and the requests that finished during that month (this one might be split to another query, of course) I tried this query, but of course, it doesn't take into account the requests that ended in a particular month, and only gives me the cumulative sum
Edit: I forgot to mention that one of the requirements is that the beggining and end date of the graph might be set by the user. So maybe I want to see the months from April-2022 to April-2020 and see the 2 year behaviour!
WITH cte AS ( SELECT
date_trunc('month',
r.date_init) AS mon,
count(r.id) AS mon_sum
FROM
"FOLLOWUP"."CAT_REQUEST" r
GROUP BY
1 ) SELECT
to_char(mon,
'YYYY-mm') AS mon_text,
COALESCE(sum(c.mon_sum)
OVER (ORDER BY mon),
0) AS running_sum
FROM
generate_series('2022-01-01', '2023-12-25',
interval '1 month') mon
LEFT JOIN
cte c USING (mon)
ORDER BY
mon
I wrote query for you using some different business logic. But, result is will be same result which you needed. Sample query:
with month_list as (
select 1 as id, 'Yanuary' as mname union all
select 2 as id, 'Febriary' as mname union all
select 3 as id, 'Marth' as mname union all
select 4 as id, 'April' as mname union all
select 5 as id, 'May' as mname union all
select 6 as id, 'June' as mname union all
select 7 as id, 'Jule' as mname union all
select 8 as id, 'August' as mname union all
select 9 as id, 'September' as mname union all
select 10 as id, 'October' as mname union all
select 11 as id, 'November' as mname union all
select 12 as id, 'December' as mname
),
test_table as (
select
id,
create_date,
end_date,
extract(month from create_date) as month1,
extract(month from end_date) as month2
from
your_table
)
select
t1.mname,
count(*) as "actived"
from
month_list t1
inner join
test_table t2 on (t1.id >= t2.month1) and (t1.id < t2.month2)
group by
t1.id, t1.mname
order by
t1.id
/* --- Result:
mname actived
--------------------
Yanuary 2
Febriary 2
Marth 1
*/
PostgreSQL has many date & time functions and types.
I write some samples for you:
For example, in my samples function now() our chosen date.
-- get previos 12 month from date (return timestampt)
select now() - '12 month'::interval as newdate
-- Return:
2021-04-03 18:22:48.344 +0400
-- if you need only date, you can cast this to date
select (now() - '12 month'::interval)::date as newdate
-- Return:
2021-04-03
-- generate data from previous 12 month to selected date increase by month:
SELECT t1.datelist::date
from generate_series
(
now()-'12 month'::interval,
now(),
'1 month'
)
AS t1(datelist)
-- Return:
2021-04-03
2021-05-03
2021-06-03
2021-07-03
2021-08-03
2021-09-03
2021-10-03
2021-11-03
2021-12-03
2022-01-03
2022-02-03
2022-03-03
2022-04-03
-- generate data from previous 12 month to selected date increase by month with extracting month names and year:
-- this sample may be as you needed.
SELECT
extract(year from t1.datelist) as "year",
TO_CHAR(t1.datelist, 'Month') as "month",
trim(TO_CHAR(t1.datelist, 'Month')) || '-' || trim(to_char(t1.datelist, 'yyyy')) as "formatted_date"
from generate_series
(
now()-'12 month'::interval,
now(),
'1 month'
)
AS t1(datelist)
-- Return:
year month formatted_date
------------------------------------
2021 April April-2021
2021 May May-2021
2021 June June-2021
2021 July July-2021
2021 August August-2021
2021 September September-2021
2021 October October-2021
2021 November November-2021
2021 December December-2021
2022 January January-2022
2022 February February-2022
2022 March March-2022
2022 April April-2022

How to return records between a Start Date and End Date

I have the following query that returns records where the Start Date or End Date is between the date range specified.
DECLARE #ReportStartDate date = '01 Apr 2019'
DECLARE #ReportEndDate date = '01 May 2019'
select * from #temp
where
(StartDate between #ReportStartDate and #ReportEndDate) or
(EndDate between #ReportStartDate and #ReportEndDate) or
(EndDate is null)
The problem I have is where the Start Date is '01 Mar 2019' and the End Date is '04 Aug 2019', which means it was within the date range of 01 Apr 2019 and 01 May 2019, but my criteria doesn't meet that.
How could I include those type of records?
Sample data:
CREATE TABLE #temp
(
ID int,
StartDate datetime,
EndDate datetime NULL
)
insert into #temp
(
ID,
StartDate,
EndDate
)
select
1,
'01 Mar 2019',
NULL
union all
select
2,
'01 Mar 2019',
'04 Aug 2019'
union all
select
3,
'14 Jul 2019',
NULL
I would phrase your condition as follows:
(StartDate <= #ReportEndDate AND EndDate >= #ReportStartDate)
OR EndDate is null
This will actually check if the date ranges do overlap, which seems to be what you are looking for.
Demo on DB Fiddle:
ID | StartDate | EndDate
-: | :------------------ | :------------------
1 | 01/03/2019 00:00:00 | null
2 | 01/03/2019 00:00:00 | 04/08/2019 00:00:00
3 | 14/07/2019 00:00:00 | null

How to get Month, Year and Days in postgresql between two dates

I have two columns in the sql table which is startdate and enddate
Startdate Enddate
27-12-2015 22:30 03-01-2016 19:30
01-01-2016 12:45 09-02-2016 18:30
I want to get the resultant table like
Startdate Enddate Month year days
27-12-2015 22:30 03-01-2016 19:30 Dec 2015 5
27-12-2015 22:30 03-01-2016 19:30 Jan 2016 3
01-01-2016 12:45 09-02-2016 18:30 Jan 2016 31
01-01-2016 12:45 09-02-2016 18:30 Feb 2016 9
A rough solution would be to generate all the days and then aggregate (count) them. It works, but it's rough on memory. If it's not crucial, this solution would definitely work. The alternative is to generate a months series and make a day diff with a lot of conditions, if performance is critical.
SELECT
dates.startdate::DATE,
dates.enddate::DATE,
to_char(days.s, 'Mon') AS mon,
to_char(days.s, 'YYYY') AS yr,
count(1) AS d
FROM dates
CROSS JOIN LATERAL (
SELECT * FROM generate_series(dates.startdate, dates.enddate, INTERVAL '1 day') s
) days
GROUP BY 1, 2, 3, 4
In any case, here is the second variant, that loops through months instead (faster, harder to understand):
SELECT
dates.startdate::DATE,
dates.enddate::DATE,
to_char(months.startdate, 'Mon') AS mon,
to_char(months.startdate, 'YYYY') AS yr,
least(
months.enddate::DATE - dates.startdate::DATE + 1, -- takes care of first month
dates.enddate::DATE - months.startdate::DATE + 1, -- takes care of last month
months.enddate::DATE - months.startdate::DATE + 1 -- takes care of full months from the middle of the intervals
) AS "days"
FROM dates
-- get months as first day in that month
CROSS JOIN LATERAL (
SELECT * FROM generate_series(
(to_char(dates.startdate, 'YYYY-MM-') || '01')::DATE,
(to_char(dates.enddate + INTERVAL '1 month', 'YYYY-MM-') || '01')::DATE - 1, INTERVAL '1 month') m
) days
-- get months as start date and end date
CROSS JOIN LATERAL (
SELECT
days.m::DATE AS startdate,
(days.m + INTERVAL '1 month')::DATE - 1 AS enddate
) months
In this particular case a plpgsql function can provide better performance than a plain sql query.
create or replace function get_months(startdate date, enddate date)
returns table (mon text, year int, days int)
language plpgsql as $$
declare d date;
begin
d:= date_trunc('month', startdate);
while d < enddate loop
mon:= to_char(d, 'Mon');
year:= to_char(d, 'YYYY');
days:= case
when d+ '1month'::interval > enddate then enddate- d+ 1
when d < startdate then (d+ '1month'::interval)::date- startdate
else (d+ '1month'::interval)::date- d
end;
return next;
d:= d+ '1month'::interval;
end loop;
end
$$;
Test:
with my_table(startdate, enddate) as (
values
('2015-12-27 22:30', '2016-01-03 19:30'),
('2016-01-01 12:45', '2016-02-09 18:30')
)
select *
from my_table,
lateral get_months(startdate::date, enddate::date)
startdate | enddate | mon | year | days
------------------+------------------+-----+------+------
2015-12-27 22:30 | 2016-01-03 19:30 | Dec | 2015 | 5
2015-12-27 22:30 | 2016-01-03 19:30 | Jan | 2016 | 3
2016-01-01 12:45 | 2016-02-09 18:30 | Jan | 2016 | 31
2016-01-01 12:45 | 2016-02-09 18:30 | Feb | 2016 | 9
(4 rows)

I want to split a row into multiple row based on month and year of the date in SQL Server

For example- I have below rows in my table:
id StartDate EndDate
101 1/03/2017 15/03/2017
102 27/03/2017 10/04/2017
103 25/12/2017 5/02/2018
I want the following output:
id month year
101 03 2017
102 03 2017
102 04 2017
103 12 2017
103 01 2018
103 02 2018
I have tried my best to find a solution but couldn't get through it. Any kind of help is always appreciated.
Thanks in advance.
If i correctly understood your problem then below query will work for you, this is giving the exact output required by you :
DECLARE #SAMPLE_DATA TABLE(id INT, StartDate DATETIME, EndDate DATETIME)
INSERT INTO #SAMPLE_DATA VALUES
(101, '03/1/2017', '03/15/2017'),
(102, '03/27/2017', '04/10/2017'),
(103, '12/25/2017', '02/5/2018')
;WITH SAMPLE_DATA
AS
(
SELECT ID,StartDate FROM #SAMPLE_DATA
UNION ALL
SELECT S1.id,DATEADD(D,1,S.STARTDATE) FROM SAMPLE_DATA S JOIN #SAMPLE_DATA S1 ON S.id=S1.id WHERE
DATEADD(D,1,S.STARTDATE)<=S1.EndDate
)
SELECT DISTINCT ID,MONTH(StartDate)[MONTH],YEAR(StartDate)[YEAR] FROM SAMPLE_DATA ORDER BY ID,YEAR,MONTH
Output of query :
-------------------
ID MONTH YEAR
-------------------
101 3 2017
102 3 2017
102 4 2017
103 12 2017
103 1 2018
103 2 2018
-------------------
You could try it with CTE
DECLARE #SampleDate AS TABLE (
Id int, StartDate date, EndDate date
)
INSERT INTO #SampleDate
(
Id,
StartDate,
EndDate
)
VALUES (101, '2017-03-01', '2017-03-15') ,
(102, '2017-03-27', '2017-04-10'),
(103, '2017-12-25', '2018-02-05')
DECLARE #MinDate date = '2017-01-01'
DECLARE #MaxDate date = '2020-01-01'
;WITH temp AS
(
SELECT #MinDate AS StartMonth, EOMOnth(#MinDate) AS EndMonth
UNION ALL
SELECT Dateadd(month, 1, t.StartMonth), Dateadd(month, 1, t.EndMonth) AS CurrentDate
FROM temp t
WHERE t.EndMonth < #MaxDate
)
SELECT DISTINCT sd.Id, DATEPART(Month,t.StartMonth) AS Month,
DATEPART(Year,t.StartMonth) AS Year
FROM temp t
INNER JOIN #SampleDate sd ON t.StartMonth BETWEEN sd.StartDate AND sd.EndDate
OR t.EndMonth BETWEEN sd.StartDate AND sd.EndDate
OPTION (MAXRECURSION 0)
Demo link: RexTester