SQL Count sickness days - sql

I have a SQL Server table which contains the list of all staff and their sickness.
I need to be able to calculate how many days they have had sick in the current quarter
The issue is, some people may have been sick for a year so, E.G the FROMDATE could be 2013-12-31 and the UNTILDATE could be 2014-12-31 (1 year sickness leave). However it should only count the days from that sickness that occur in the current quarter. So it should be around 90 days of sickness rather than count the entire year.
Current SQL
select SUM(a.WORKDAYS) as Total
from ABSENCE a
where a.FROMDATE < GETDATE() and
a.UNTILDATE > DATEADD(MONTH, -3, GETDATE())
and
a.ABS_REASON='SICK'
So at the moment, it takes from any fromdate which is correct as I need to account for people who were already sick before the quarter started but still sick going into the current quarter but should only count the number of days from when the quarter started until the end of the quarter.
Any help would be greatly appreciated.

With a table of dates, you could easily find the count of dates where the date is between your two dates of interest, and where there exists a leave period that surrounds it. You could also filter your dates to exclude non-business days and public holidays.
There are lots of ways to generate such a table of dates, and plenty described both on stackoverflow and dba.stackexchange.

Not sure about your columns.you should only provide sql that gives records between 2013-12-31 and 2014-12-31 and then ask your problem .
Try this,
select SUM(Case when datepart(MM, a.FROMDATE) IN (10,11,12) Then a.WORKDAYS Else End)
as Total
from ABSENCE a
where a.FROMDATE >= '2013-12-31' and
a.UNTILDATE <= '2014-12-31'
and
a.ABS_REASON='SICK'

SELECT SUM(a.WORKDAYS) as Total
FROM ABSENCE a
WHERE (a.FROMDATE >= DATEADD(MONTH, -3, GETDATE()) OR a.UNTILDATE >= DATEADD(MONTH, -3, GETDATE()))
AND a.ABS_REASON = 'SICK'
Quarter Specific
SELECT SUM(a.WORKDAYS) as Total
FROM ABSENCE a
WHERE (a.FROMDATE >= DATEADD(quarter, -1, GETDATE()) OR a.UNTILDATE >= DATEADD(quarter, -1, GETDATE()))
AND a.ABS_REASON = 'SICK'

Having a Calendar table with the list of all possible dates is handy, but in this case we can do without it.
I'll generalize your question a bit. Instead of looking just at the current quarter let's have two parameters that define the range of dates that you are interested in:
DECLARE #ParamStartDate date;
DECLARE #ParamEndDate date;
At first we need to get all rows from Absence that have a range from FromDate to UntilDate that intersects with the given period.
SELECT
...
FROM
Absence
WHERE
ABS_REASON='SICK'
-- all absence periods, which overlap with the given period
AND FromDate <= #ParamEndDate
AND UntilDate >= #ParamStartDate
Two periods A and B overlap when (StartA <= EndB) and (EndA >= StartB).
Then we need to calculate how many days are in the intersection of the two periods.
The intersection period can't be larger than the given range of dates (#ParamStartDate to #ParamEndDate).
The intersection period can't be larger than the duration of the sickness (FromDate to UntilDate).
So, the beginning of the intersection is the latest of FromDate and #ParamStartDate, i.e. MAX(FromDate, #ParamStartDate)
The ending of the intersection is the earliest of UntilDate and #ParamEndDate, i.e. MIN(UntilDate, #ParamEndDate)
Finally, the duration of the intersection in days is
DATEDIFF(day, MAX(FromDate, #ParamStartDate), MIN(UntilDate, #ParamEndDate))
But, only if it is positive. If it is negative, it means that sickness period ended before the given quarter started (or sickness started after the given quarter ended).
There is no built-in MIN, MAX functions that take two parameters as I need, so I use CROSS APPLY to calculate them. Also, I calculate the number of days in the given quarter, just for completeness. The final query looks like this:
SELECT
1+DATEDIFF(day, #ParamStartDate, #ParamEndDate) AS QuarterDays
,CASE WHEN 1+DATEDIFF(day, CTE_MaxStartDate.AbsenceStartDate, CTE_MinEndDate.AbsenceEndDate) > 0
THEN 1+DATEDIFF(day, CTE_MaxStartDate.AbsenceStartDate, CTE_MinEndDate.AbsenceEndDate)
ELSE 0 END AS AbsenceDays
FROM
Absence
CROSS APPLY
(
SELECT CASE WHEN UntilDate < #ParamEndDate THEN UntilDate ELSE #ParamEndDate END AS AbsenceEndDate
) AS CTE_MinEndDate
CROSS APPLY
(
SELECT CASE WHEN FromDate > #ParamStartDate THEN FromDate ELSE #ParamStartDate END AS AbsenceStartDate
) AS CTE_MaxStartDate
WHERE
ABS_REASON='SICK'
-- all absence periods, which overlap with the given period
AND FromDate <= #ParamEndDate
AND UntilDate >= #ParamStartDate
I add 1 to DATEDIFF to get a duration of one day if start and end dates of the period are the same.

Related

Calculate time from begin date in SQL

I'm trying to tally up the number of unique ID accounts that are active for more than a year as well as include how long each one is active. The problem with the code I have is it's not including accounts that are currently active (ones that don't have an end date). For example, if the begin date was May 01 2018 but has no end date since it's currently active, it should also be included in this query. Here's what I have so far..
SELECT UniqueID,
DATEDIFF(yy, Begin_Date,End_Date) as timeactive
FROM TABLE
WHERE DATEDIFF(yy, Begin_Date,End_Date) > 1
I want my output to look like...
Unique ID Time active
F000012 2.5
F000031 1.5
This is what ended up working:
SELECT UniqueID,
CAST(ROUND(DATEDIFF(day, Begin_Date, COALESCE(End_Date, getdate()))/365.0, 1, 0) AS NUMERIC (10,1)) as timeactive
FROM TABLE
WHERE DATEDIFF(day, Begin_Date, COALESCE (End_Date, getdate())) >= 365
If the EndDate is null then the output of the DateDiff function will be null, and any null compared to anything (even another null) is a result of null (usually then interpreted as false)
I suggest you use COALESCE to convert your end date to today if it is null:
SELECT
UniqueID,
DATEDIFF(yy, Begin_Date,COALESCE(End_Date, GetUtcDate()) as timeactive
FROM TABLE
WHERE DATEDIFF(yy, Begin_Date,COALESCE(End_Date, GetUtcDate()) > 1
You should bear in mind that the DATEDIFF function as used here, in SQLserver does NOT return the amount of time that has passed between the two dates. It returns the number of times the named interval has changed between the two dates
For example, DATEDIFF(year, 2000-01-01, 2000-12-31 23:59:59) will return 0 because these are both year 2000 even though they're just one second short of being a year apart. If you do DATEDIFF(year, 2000-12-31 23:59:59, 2001-01-01 00:00:01) even though these dates are only two seconds apart datediff will report them as 1 year apart because the year number has changed from 2000 to 2001.
DATEDIFF counts up by one every time the clock rolls past an interval change and in this case the interval is Year
To get your dates to report as 1.5 years etc you should consider to datediff by a smaller interval and divide, such as asking for the DAYS diff between two dates and then dividing by 365.25- the average number of days in a year. The smaller the interval you ask datediff for the more accurate the result will be but it'll never be 100%. If you're only after results to one decimal place of a year then days will be accurate enough
To get 1 decimal place, cast to a numeric with 1 DP:
SELECT
UniqueID,
CAST(DATEDIFF(day, Begin_Date,COALESCE(End_Date, GetUtcDate())/365.25 AS NUMERIC(5,1)) as timeactive
FROM TABLE
WHERE DATEDIFF(day, Begin_Date,COALESCE(End_Date, GetUtcDate()) >= 365
If you want time active as fractional years, then you need to use a smaller unit of time and divide. For instance:
SELECT UniqueID,
DATEDIFF(month, Begin_Date, COALESCE(End_Date, GETDATE())) / 12.0 as timeactive
FROM TABLE
WHERE Begin_Date < DATEADD(YEAR, -1, COALESCE(End_Date, GETDATE()))
Note the change in the WHERE clause. DATEDIFF() counts the number of year boundaries between dates. So the difference in years between 2019-01-01 and 2020-12-31 is the same as the difference between 2019-12-31 and 2020-01-01 -- exactly 1.
Consider:
SELECT
UniqueID,
DATEDIFF(yy, Begin_Date, COALESCE(End_Date, getdate()) as timeactive
FROM TABLE
WHERE DATEDIFF(yy, Begin_Date, COALESCE(End_Date, getdate()) > 1
This works by using the current date as default value for empty End_Dates. So this allows records with empty end date if their start date is more than one year ago.

SQL 'Round' Up a Date to a Given Day of the week

My company groups all tasks into individual weeks that end on a Thursday. Thus a task due on 3/20/19 would be grouped into the 3/21 week and tasks due on 3/22 group into the 3/28/19 week.
I'm looking to calculate this field (called duedate_Weekdue) based on an input duedate.
The following works but doesn't seem like the simplest way to do this. Anyone have more elegant methods?
Select
getdate() as duedate,
datepart(yy,getdate()) as duedate_yr,
datepart(ww,getdate()) as duedate_ww,
CASE
When datename(dw,Dateadd(day,1,getdate()))='Thursday' Then Dateadd(day,1,getdate())
When datename(dw,Dateadd(day,2,getdate()))='Thursday' Then Dateadd(day,2,getdate())
When datename(dw,Dateadd(day,3,getdate()))='Thursday' Then Dateadd(day,3,getdate())
When datename(dw,Dateadd(day,4,getdate()))='Thursday' Then Dateadd(day,4,getdate())
When datename(dw,Dateadd(day,5,getdate()))='Thursday' Then Dateadd(day,5,getdate())
When datename(dw,Dateadd(day,6,getdate()))='Thursday' Then Dateadd(day,6,getdate())
When datename(dw,Dateadd(day,0,getdate()))='Thursday' Then Dateadd(day,0,getdate())
END as duedate_Weekdue;
You can reduce that to one line of code that uses a little math, and some SQL Engine trivia.
The answers that depend on DATEPART return non-deterministic results, depending on the setting for DATEFIRST, which tells the SQL Engine what day of the week to treat as the first day of the week.
There's a way to do what you want without the risk of getting the wrong result based on a change to the DATEFIRST setting.
Inside SQL Server, day number 0 is January 1, 1900, which happens to have been a Monday. We've all used this little trick to strip the time off of GETDATE() by calculating the number of days since day 0 then adding that number to day 0 to get today's date at midnight:
SELECT DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()),0)
Similarly, day number 3 was January 4, 1900. That's relevant because that day was a Thursday. Applying a little math to the number of days, and relying on the DATEDIFF function to drop fractions, which it does, this calculation will always return the next Thursday for you:
SELECT DATEADD(DAY, (DATEDIFF(DAY, 3, GETDATE())/7)*7 + 7,3);
Credit to this answer for the assist.
So your final query comes down to this:
Select
getdate() as duedate,
datepart(yy,getdate()) as duedate_yr,
datepart(ww,getdate()) as duedate_ww,
DATEADD(DAY, (DATEDIFF(DAY, 3, GETDATE())/7)*7 + 7,3) as duedate_Weekdue;
If the first day of the week is Sunday, by using the modulo operator %:
cast(dateadd(day, (13 - datepart(dw, getdate())) % 7, getdate()) as date) as duedate_Weekdue
I also applied the casting of the result to date.
Try identifying number of day in week with DATEPART and then adding enough days to go to next thursday:
declare #dt date = '2019-03-22'
declare #weekDay int
SELECT #weekDay = DATEPART(dw, #dt)
if #weekDay <= 5
select DATEADD(day, 5 - #weekDay ,#dt)
else
select DATEADD(day, 12 - #weekDay ,#dt)

Counting working days between two dates Oracle

I'm trying to write a query which can count the number of working days between a payment being received and being processed, I started playing around with this for payments received in December 2017;
select unique trunc(date_received),
(case when trunc(date_received) in ('25-DEC-17','26-DEC-17') Then 0 when
to_char(date_received,'D') <6 Then 1 else 0 end) Working_day
from payments
where date_received between '01-DEC-17' and '31-dec-17'
order by trunc(date_received)
But to be honest, I'm at a loss as to how to take it further and add in date_processed and count the number of working days between date_processed and date_received... Any help would be much appreciated...
Maybe not the most optimal, but it works quite nicely, and it's easy to incorporate more complicated checks, like holidays. This query first generates all dates between the two dates, and then lets you filter out all the days that 'don't count'.
In this implementation I filtered out only weekend days, but it's quite easy to add checks for holidays and such.
with
-- YourQuery: I used a stub, but you can use your actual query here, which
-- returns a from date and to date. If you have multiple rows, you can also
-- output some id here, which can be used for grouping in the last step.
YourQuery as
(
select
trunc(sysdate - 7) as FromDate,
trunc(sysdate) as ToDate
from dual),
-- DaysBetween. This returns all the dates from the start date up to and
-- including the end date.
DaysBetween as
(
select
FromDate,
FromDate + level - 1 as DayBetween,
ToDate
from
YourQuery
connect by
FromDate + level - 1 <= ToDate)
-- As a last step, you can filter out all the days you want.
-- This default query only filters out Saturdays and Sundays, but you
-- could add a 'not exists' check that checks against a table with known
-- holidays.
select
count(*)
from
DaysBetween d
where
trim(to_char(DAYINBETWEEN, 'DAY', 'NLS_DATE_LANGUAGE=AMERICAN'))
not in ('SATURDAY', 'SUNDAY');

Get date for nth day of week in nth week of month

I have a column with values like '3rd-Wednesday', '2nd-Tuesday', 'Every-Thursday'.
I'd like to create a column that reads those strings, and determines if that date has already come this month, and if it has, then return that date of next month. If it has not passed yet for this month, then it would return the date for this month.
Expected results (on 4/22/16) from the above would be: '05-18-2016', '05-10-2016', '04-28-2016'.
I'd prefer to do it mathematically and avoid creating a calendar table if possible.
Thanks.
Partial answer, which is by no means bug free.
This doesn't cater for 'Every-' entries, but hopefully will give you some inspiration. I'm sure there are plenty of test cases this will fail on, and you might be better off writing a stored proc.
I did try to do this by calculating the day name and day number of the first day of the month, then calculating the next wanted day and applying an offset, but it got messy. I know you said no date table but the CTE simplifies things.
How it works
A CTE creates a calendar for the current month of date and dayname. Some rather suspect parsing code pulls the day name from the test data and joins to the CTE. The where clause filters to dates greater than the Nth occurrence, and the select adds 4 weeks if the date has passed. Or at least that's the theory :)
I'm using DATEFROMPARTS to simplify the code, which is a SQL 2012 function - there are alternatives on SO for 2008.
SELECT * INTO #TEST FROM (VALUES ('3rd-Wednesday'), ('2nd-Tuesday'), ('4th-Monday')) A(Value)
SET DATEFIRST 1
;WITH DAYS AS (
SELECT
CAST(DATEADD(MONTH,DATEDIFF(MONTH,0,GETDATE()),N.Number) AS DATE) Date,
DATENAME(WEEKDAY, DATEADD(MONTH,DATEDIFF(MONTH,0,GETDATE()),N.Number)) DayName
FROM master..spt_values N WHERE N.type = 'P' AND N.number BETWEEN 0 AND 31
)
SELECT
T.Value,
CASE WHEN MIN(D.Date) < GETDATE() THEN DATEADD(WEEK, 4, MIN(D.DATE)) ELSE MIN(D.DATE) END Date
FROM #TEST T
JOIN DAYS D ON REVERSE(SUBSTRING(REVERSE(T.VALUE), 1, CHARINDEX('-', REVERSE(T.VALUE)) -1)) = D.DayName
WHERE D.Date >=
DATEFROMPARTS(
YEAR(GETDATE()),
MONTH(GETDATE()),
1+ 7*(CAST(SUBSTRING(T.Value, 1,1) AS INT) -1)
)
GROUP BY T.Value
Value Date
------------- ----------
2nd-Tuesday 2016-05-10
3rd-Wednesday 2016-05-18
4th-Monday 2016-04-25

Createing a report using financial periods

I have created a report for management that will total everything up by month with in a date range. Management has now decided that rather than by month they would like to go by period. We have 13 periods in a year each is 28 days except the last one is 29 or 30 depending on if its a leap year. The beginning of the first period is always 1-1-YYYY. So now I will need to figure out what the beginning and end of each period is and total up each period. I am not really sure how to do this since every year the dates will change and they may want to look at periods from the previous year through the current period. The code and results I am currently using are enclosed
SELECT
DATEADD(MONTH, DATEDIFF(MONTH, 0, finspecteddate), 0) AS 'Date'
,COUNT(*) AS Lots
,sum(flotSize) as 'Lot Size'
,sum(LReject) 'Lots Rejected'
,sum(fnumreject) as Rejected
,sum(fsampleSize) as 'Sample Size'
,sum(BDueDate) as 'Before Due Date'
FROM
ReportData
WHERE
finspecteddate >= '01-01-2014'
AND finspecteddate <= '10-15-2014'
GROUP BY
DATEADD(MONTH, DATEDIFF(MONTH, 0, finspecteddate), 0)
ORDER BY
date
Modify the following queries to suit your needs:
;WITH Period AS (
SELECT 1 AS ReportingPeriod,
CAST('2013-01-01' AS datetime) AS PeriodStartDate,
CAST('2013-01-28' AS datetime) AS PeriodEndDate
UNION ALL
SELECT CASE
WHEN p.ReportingPeriod = 13 THEN 1
ELSE p.ReportingPeriod + 1
END,
CASE
WHEN p.ReportingPeriod = 13 THEN DATEADD(YEAR,YEAR(p.PeriodStartDate)-1899,'1900-01-01')
ELSE DATEADD(DAY,28,p.PeriodStartDate)
END,
CASE
WHEN p.ReportingPeriod = 12 THEN DATEADD(YEAR,YEAR(p.PeriodStartDate)-1900,'1900-12-31')
ELSE DATEADD(DAY,28,p.PeriodEndDate)
END
FROM Period p
WHERE p.PeriodStartDate < '2017-12-03'
)
SELECT
P.PeriodStartDate
,P.PeriodEndDate
,COUNT(*) AS Lots
,sum(flotSize) as 'Lot Size'
,sum(LReject) 'Lots Rejected'
,sum(fnumreject) as Rejected
,sum(fsampleSize) as 'Sample Size'
,sum(BDueDate) as 'Before Due Date'
FROM
ReportData R
INNER JOIN Period P ON R.finspecteddate >= P.PeriodStartDate AND R.finspecteddate <= P.PeriodEndDate
WHERE
finspecteddate >= '01-01-2014'
AND finspecteddate <= '10-15-2014'
GROUP BY
P.PeriodStartDate
,P.PeriodEndDate
ORDER BY
P.PeriodStartDate
It uses a recursive CTE to build a period table, which is then joined to ReportData to aggregate asccording to your requirements. I don't have SQL Server 2005 to test it on. It works with 2008. Post a SQL Fiddle if you need help in 2005.
If you haven't got one, create a period calendar table with a year, period number, start date and end date columns. Then when you need to refer to periods, you can refer to the table. When they change the definition of what a period is, you can change the table. When they decide that February 29 doesn't count as one of the 28 days, you can change the table. When they decide to use the first Monday instead of the first Thursday as the start of the year, you just change the table. And best of all, changing how next year works won't change how last year works.
Then you just join to the table to determine which period you're in.