SQL: Getting a row for each passing month up to date - sql

I have a table that stores defects.
Each defect has a detection date and a closing date.
I want to extract the info for each month up to current date, about the number of open defects in that month.
For example, lets assume these are all the defects I have in my DB:
defect 1 - Detected in (01-01-2014) , Closed in (04-03-2014)
defect 2 - Detected in (07-02-2014) , Closed in (null) (still open)
I'd like to extract the info as follows:
01-01-2014 - 1 open defect (defect 1 was created)
01-02-2014 - 2 open defects (defect 2 was created)
01-03-2014 - 1 open defect (defect 1 was closed this month)
01-04-2014 - 1 open defect (defect 2 was never closed so I would get entries up to today)
...
...
01-02-2015 - 1 open
Is there a way to get this info with a single query without using functions?

Avoiding the question of "why no functions", it is possible to go N months back (limits apply) with the query below. Just replace "16" in "WHERE n < 16" with the number of months back you want to evaluate
; WITH Numbers (n) AS (
SELECT 1
UNION ALL
SELECT n + 1
FROM Numbers
WHERE n < 16
)
, NextMonth (dt) AS (
SELECT CAST( CAST( DATEPART(YEAR,GETDATE()) AS VARCHAR ) + '-' + CAST( DATEPART(MONTH,GETDATE())+1 AS VARCHAR ) + '-1' AS DATETIME )
)
, PriorMonths (dt) AS (
SELECT DATEADD( MONTH, -1 * n, dt )
FROM NextMonth
CROSS JOIN Numbers
), RawData ( y, m, DateOpened ) AS (
SELECT DATEPART( YEAR, dt )
, DATEPART( MONTH, dt )
, t.DateOpened
FROM PriorMonths AS pm
LEFT JOIN #t AS t
ON DATEADD(MONTH,-1,t.DateOpened) < dt
AND (DATEADD(MONTH,-1,t.DateClosed) >= dt OR t.DateClosed IS NULL)
)
SELECT y, m, COUNT(DateOpened)
FROM RawData
GROUP BY y, m
ORDER BY y DESC, m DESC

First I want to point out that I've used MS SQL Server T-SQL functions DATEADD(), DATEDIFF(), and GETDATE() - there should be equivalents in whatever flavour of SQL you want to use.
The variable #startDate should be set to the start of the range on which you want to report.
DECLARE #startDate DATE;
DECLARE #endDate DATE;
SET #startDate = '20140101';
SET #endDate = DATEADD(m, DATEDIFF(m, 0, GETDATE()), 0);
WITH Dates (dEnd, dStart) AS (
SELECT DATEADD(M, 1, #endDate), #endDate
UNION ALL
SELECT DATEADD(M, -1, dEnd), DATEADD(M, -1, dStart)
FROM Dates
WHERE dEnd > #startDate
)
SELECT dStart AS 'Month start', COUNT(Defects.reportedDate) AS 'Open defects'
FROM Dates
LEFT OUTER JOIN Defects ON
Defects.reportedDate < dEnd AND (Defects.resolvedDate IS NULL OR Defects.resolvedDate > dStart)
GROUP BY dStart
ORDER BY dStart;

Related

Merging two SELECT queries with same date fields

I have a table of Tasks where I have records for a particular date. I want to have all dates in one month displayed with numbers of tasks per date. If on some date there were no record of a task it should be written 0.
I have results with duplicating records from the same date when there were tasks on a given day.
Table:
Date Tasks
2021-08-01 0
2021-08-02 0
2021-08-03 0
2021-08-03 25
2021-08-04 0
2021-08-04 18
2021-08-05 0
2021-08-05 31
2021-08-06 0
SQL code I am using:
Declare #year int = 2021, #month int = 8;
WITH numbers
as
(
Select 1 as value
UNion ALL
Select value +1 from numbers
where value + 1 <= Day(EOMONTH(datefromparts(#year, #month, 1)))
)
SELECT datefromparts(#year, #month, numbers.value) AS 'Datum', 0 AS 'Tasks' FROM numbers
UNION
SELECT CONVERT(date, added_d) AS 'Datum', COUNT(*) AS 'Tasks' FROM Crm.Task
WHERE YEAR(added_d) = '2021' AND MONTH(added_d) = '8' GROUP BY CONVERT(date, added_d)
How can I remove duplicates that I will have only one date record 21-08-03 with 25 tasks?
Thank you for your help
You requires OUTER JOIN :
WITH numbers as (
Select datefromparts(#year, #month, 1) as value
UNION ALL
Select DATEADD(DAY, 1, value) as value
from numbers
where value < EOMONTH(value)
)
select num.value, COUNT(tsk.added_d) AS Tasks
from numbers num left join
Crm.Task tsk
on CONVERT(date, tsk.added_d) = num.value
GROUP BY num.value;
If you want all dates for one month, you can do:
with dates as (
select datefromparts(#year, #month, 1) as dte
union all
select dateadd(day, 1, dte)
from dates
where dte < eomonth(dte)
)
You can then incorporate this into the logic using an outer join or subquery:
with dates as (
select datefromparts(#year, #month, 1) as dte
union all
select dateadd(day, 1, dte)
from dates
where dte < eomonth(dte)
)
select d.dte, count(t.added_d)
from dates d left join
Crm.Task t
on convert(date, t.added_d) = d.dte
group by d.dte
order by d.dte;
You can easily extend the logic for the CTE for more than one month, by adjusting the where clause in the second clause.

How can I select the 1st of every month for the last 5 years in sql?

I am trying to get a list of the 1st of the Month for the last 5 years. How can i do that ?? I have a select statement:
select convert(varchar(10), dateadd(mm,Datediff(mm,0,getdate()),0),111) as StartDate
but i am not sure how to get a list for every month.
with dates
as (
select dateadd(month, datediff(month, 0, getdate()), 0) as date
union all
select dateadd(month, - 1, date)
from dates
)
select top 60 *
from dates
with cte as (
select DATEFROMPARTS ( datepart(yyyy,getdate()), datepart(mm,getdate()), 1 ) as startdate
union all
select dateadd(month,-1,startdate) from dates
where datediff(year,startdate,getdate()) <> 5 )
select CONVERT ( varchar(12), startdate , 107 ) from cte;

Calculating days of therapy in a sql query

I'm attempting to calculate days of therapy by month from an oracle database. The (vastly simplified) data is as follows:
Therapies
+-----------+-----------+----------+
| Rx Number | StartDate | StopDate |
|-----------+-----------+----------|
| 1 | 12-29-14 | 1-10-15 |
| 2 | 1-2-15 | 1-14-15 |
| 3 | 1-29-15 | 2-15-15 |
+-----------+-----------+----------+
For the purposes of this example, all times are assumed to be midnight. The total days of therapy in this table is (10-1 + 32-29) + (14-2) + (15-1 + 32-29) = 41. The total days of therapy in January in this table is (10-1) + (14-2) + (32-29) = 24.
If I wanted to calculate days of therapy for the month of January , my best effort is the following query:
SELECT SUM(stopdate - startdate)
FROM therapies
WHERE startdate > to_date('01-JAN-15')
AND stopdate < to_date ('01-FEB-15');
However, rx's 1 and 3 are not captured at all. I could try the following instead:
SELECT SUM(stopdate - startdate)
FROM therapies
WHERE stopdate > to_date('01-JAN-15')
AND startdate < to_date ('01-FEB-15');
But that would include the full duration of the first and third therapies, not just the portion in January. To make the matter more complex, I need these monthly summaries over a period of two years. So my questions are:
How do I include overhanging therapies such that only the portion within the target time period is included, and
How do I automatically generate these monthly summaries over a two year period?
How do I include overhanging therapies such that only the portion
within the target time period is included?
select sum(
greatest(least(stopdate, date '2015-01-31' + 1)
- greatest(startdate, date '2015-01-01'), 0)) suma
from therapies
How do I automatically generate these monthly summaries over a two
year period?
with period as (select date '2014-01-01' d1, date '2015-12-31' d2 from dual),
months as (select trunc(add_months(d1, level-1), 'Month') dt
from period connect by add_months(d1, level-1)<d2)
select to_char(dt, 'yyyy-mm') mth,
sum(greatest(least(stopdate, add_months(dt, 1)) - greatest(startdate, dt), 0)) suma
from therapies, months
group by to_char(dt, 'yyyy-mm') order by mth
Above queries produced desired output. Please insert your dates in proper places to change analyzed periods.
In second SQL inner subquery months gives 24 dates, one for each month. The rest is only maneuvering
with functions greatest(),least() and some math.
Use a case statement to set the start date and stop date. Like the below:
select sum(
Stopdate -
(case Startdate
when startdate < to_date(#YourBeginingDate) then To_date(#YourBeginingDate)
else startdate
end)
FROM therapies
WHERE stopdate > to_date(#YourBeginingDate)
AND StartDate < to_date(#YourEndingDate)
I would do something like the following:
WITH t1 AS (
SELECT 1 AS rx, DATE'2014-12-29' AS start_date
, DATE'2015-01-10' AS stop_date
FROM dual
UNION ALL
SELECT 2, DATE'2015-01-02', DATE'2015-01-14'
FROM dual
UNION ALL
SELECT 3, DATE'2015-01-29', DATE'2015-02-15'
FROM dual
)
SELECT TRUNC(rx_dt, 'MONTH') AS rx_month, SUM(rx_cnt) AS rx_day_cnt
FROM (
SELECT rx_dt, COUNT(*) AS rx_cnt
FROM (
SELECT rx, start_date + LEVEL - 1 AS rx_dt
FROM t1
CONNECT BY start_date + LEVEL - 1 < stop_date
AND PRIOR rx = rx
AND PRIOR DBMS_RANDOM.VALUE IS NOT NULL
) GROUP BY rx_dt
) GROUP BY TRUNC(rx_dt, 'MONTH')
ORDER BY rx_month
Results:
12/1/2014 12:00:00 AM 2
1/1/2015 12:00:00 AM 24
2/1/2015 12:00:00 AM 15
See SQL Fiddle here.
What I am doing is using LEVEL and CONNECT BY to get all the days of therapy based on start_date and stop_date (not inclusive). I then GROUP BY the therapy date (rx_dt) to handle the overlapping therapies. Then I GROUP BY the month of the therapy using the TRUNC() function.
This should work just fine over a two-year period (or more); just add that filter before the last GROUP BY:
WHERE rx_dt >= DATE'2014-01-01'
AND rx_dt < DATE'2016-01-01'
GROUP BY TRUNC(rx_dt, 'MONTH')
Note that if your primary key is composite, you should include all the columns in the CONNECT BY clause:
CONNECT BY start_date + LEVEL - 1 < stop_date
AND PRIOR rx = rx
AND PRIOR patient_id = patient_id
--etc.
This is a bit tricky, as you need to capture days from sessions that:
Begin before the month and end after the month
Begin before the month and end during the month
Begin during the month and end after the month
Begin during the month and end during the month
To get those sessions, you can use a WHERE statement like this (the # symbol means that those are variables being passed in):
*examples are in TSQL, PLSQL might have somewhat different syntax
WHERE startdate < #endDate AND stopdate > #startDate
That should capture all four of those scenarios that I listed.
Then you only need to capture days that occurred during the month. I do this with a query that replaces the startdate/enddate with the date range limits if they exceed the range, like this:
SELECT
CASE WHEN enddate > #endDate then #endDate ELSE enddate END -
CASE WHEN startdate < #startDate THEN #startDate ELSE startdate END
So your whole query should look like this:
SELECT
SUM(
CASE WHEN enddate > #endDate then #endDate ELSE enddate END -
CASE WHEN startdate < #startDate THEN #startDate ELSE startdate END
)
FROM therapies
WHERE startdate < #endDate AND stopdate > #startDate
If you want to run that for two years, toss that code in a function that accepts #startDate and #endDate parameters, then call it from a query that gives you two years worth of months, like this:
WITH dateCTE AS (
SELECT
GETDATE() AS StartDate,
DATEADD(Month, 1, GETDATE()) AS EndDate
UNION ALL
SELECT
DATEADD(MONTH, -1, StartDate),
DATEADD(MONTH, -1, EndDate)
FROM dateCTE
WHERE StartDate > DATEADD(YEAR, -2, GETDATE())
)
SELECT
StartDate,
EndDate,
SomeFunction(StartDate, EndDate)
FROM dateCTE

calculate 3 working days before in stored proc using Holidays lookup table

I have a tricky issue I am struggling with on a mental level.
In our db we have a table showing the UK Holidays for the next few years, and a stored function returns a recordset to my front end.
I have a flag in my recordset called 'deletable' which allows the frontend to decide if a context menu can be shown in the data grid, thus allowing that record to be deleted.
Currently the test (in my stored proc) just checks if the date column has a date from three days ago or more.
case when DATEDIFF(d,a.[date],GETDATE()) > 3 then 1 else 0 end as [deletable]
how can I modify that to find the previous working date by checking weekends and the Holidays table 'Holiday' column (which is a Datetime) and see if the [date] column in my recordset row is 3 working days before, taking into account Holidays from the Holidays table and weekends?
so if the [date] column is 23th May, and todays's date is 28th May, then that column returns 0, as the 27th was a bank holiday, whereas the next day it would return 1 because there would be more than 3 working days difference.
Is there an elegent way to do that?
thanks
Philip
Okay I'm totally refactoring this.
declare
#DeletablePeriodStart datetime,
#BusinessDays int
set #DeletablePeriodStart = dateadd(d,0,datediff(d,0,getdate()))
set #BusinessDays = 0
while #BusinessDays < 3
begin
set #DeletablePeriodStart = dateadd(d,-1,#DeletablePeriodStart)
if datepart(dw,#DeletablePeriodStart) not in (1,7) and
not exists (select * from HolidayTable where Holiday = #DeletablePeriodStart)
begin
set #BusinessDays = #BusinessDays + 1
end
end
This time it doesn't make any assumptions. It runs a quick loop checking whether each day is a valid business day and doesn't stop till it counts three of them. Then later just check whether a.[date] >= #DeletablePeriodStart
You should substract the number of holidays between a.[date] and GETDATE() from the DATEDIFF. Try something like this:
case when DATEDIFF(d,a.[date],GETDATE())-(
SELECT COUNT(*) FROM Holidays
WHERE HolidayDate BETWEEN a.[date] AND GETDATE()
)>3 then 1 else 0 end as [deletable]
Razvan
I am assuming that you don't have a Calendar table, although I'd highly recommend creating one, you can still achieve this without one:
The following will just get you a list of 2047 dates from yesterday going backwards (using the system table Master..spt_values):
WITH Dates AS
( SELECT Date = DATEADD(DAY, -number, CAST(GETDATE() AS DATE))
FROM Master..spt_values
WHERE type = 'P'
AND number > 0
)
SELECT Dates.Date
FROM Dates
ORDER BY Dates.Date DESC;
You then need to exclude weekends, and holidays from your table using this:
SET DATEFIRST 1;
WITH Dates AS
( SELECT Date = DATEADD(DAY, -number, CAST(GETDATE() AS DATE))
FROM Master..spt_values
WHERE type = 'P'
AND number > 0
)
SELECT Dates.Date
FROM Dates
WHERE DATEPART(WEEKDAY, Dates.Date) <= 5
AND NOT EXISTS
( SELECT 1
FROM HolidayTable h
WHERE Dates.Date = h.HolidayDate
)
ORDER BY Dates.Date DESC;
N.B. You should explicitly set your DATEFIRST and not rely on server defaults
The above gives you a list of working days prior to today, you can then use the ROW_NUMBER() function, get the 3rd occurance in the list, giving a final query:
WITH Dates AS
( SELECT Date = DATEADD(DAY, -number, CAST(GETDATE() AS DATE))
FROM Master..spt_values
WHERE type = 'P'
AND number > 0
), WorkingDays AS
( SELECT Dates.Date, RN = ROW_NUMBER() OVER(ORDER BY Dates.Date DESC)
FROM Dates
WHERE DATEPART(WEEKDAY, Dates.Date) <= 5
AND NOT EXISTS
( SELECT 1
FROM HolidayTable h
WHERE Dates.Date = h.HolidayDate
)
)
SELECT WorkingDays.Date
FROM WorkingDays
WHERE RN = 3;
Or if you prefer this can be done with one query (exact same principle as above):
SELECT d.Date
FROM ( SELECT Date = DATEADD(DAY, -number, CAST(GETDATE() AS DATE)), RN = ROW_NUMBER() OVER(ORDER BY number)
FROM Master..spt_values
WHERE type = 'P'
AND number > 0
AND DATEPART(WEEKDAY, DATEADD(DAY, -number, CAST(GETDATE() AS DATE))) <= 5
AND NOT EXISTS
( SELECT 1
FROM HolidayTable h
WHERE DATEADD(DAY, -number, CAST(GETDATE() AS DATE)) = h.HolidayDate
)
) d
WHERE rn = 3;

Get a table of dates of last 14 days (2 weeks) starting from previous day

I want to write a query which gives a coloumn of DATES of last 14 days
starting from yesterday.
Example:
Dates
2012-06-21
2012-06-20
2012-06-19
--
-
;WITH n(n) AS
(
SELECT TOP (14) ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.objects ORDER BY [object_id]
)
SELECT Dates = DATEADD(DAY, -n, DATEDIFF(DAY, 0, GETDATE())) FROM n
ORDER BY n;
Another way;
;with days(day) as
(
select getdate() - 1 as day
union all
select day - 1
from days
where day > dateadd(day, -14, getdate())
)
select cast(day as date) from days