Difficulty in getting Shift Value in SQL Query - sql

Currently my table structire for table SHIFT is as follow:
ID Name Start End
1 Shift1 06:00 14:00
2 Shift2 14:00 22:00
3 Shift3 22:00 06:00
Now I pass parameter to this query in hour like 11 or 15 or 22 or 03
For that parameter, I would like to get the result that in which shift the passed hour will reside.
So if I pass 11, it shoud give me Shift1. If I pass 23, it should give me Shift3.
Following query that I wrote works fine for any value from 07 to 21, it is giving me blank value and for obvious reasons.
select * from MII_SHIFT
where '[Param.1]' >= left(START,2) and '[Param.1]' < left(END,2)
Can anyone help me how can I change the query so that I can get proper response for 22,23,00,01,02,03,04,05.
Thanks

SELECT *
FROM shift
WHERE
( left(START,2) > left(END,2)
AND ('[Param.1]' >= left(START,2) OR '[Param.1]' < left(END,2))
)
OR ( left(START,2) < left(END,2)
AND '[Param.1]' >= left(START,2) AND '[Param.1]' < left(END,2)
)
I answer a similar answer a litle time ago.
Shorts start < end (5-9): the value need be between start and end
Jacket start > end (10-4): the value is < start or > end

Assuming the values are stored as strings, then this is pretty easy:
select s.*
from shifts s
where (start < end and right('00' + #param1, 2) >= start and right('00' + #param1, 2) < end) or
(start > end and (right('00' + #param1, 2) >= start or right('00' + #param1, 2) < end))
This assumes that #param1 is a string. The right() is used to left pad the string with zeroes. If that is already true, then the code would be even simpler.
EDIT:
With padding, this simplifies to:
select s.*
from shifts s
where (start < end and #param1 >= start and #param1< end) or
(start > end and (#param1 >= start or #param1 < end))

Simplest way is most likely to convert the times into dates, and if the end date is earlier than start, then add one day. You could use time datatype as input too, instead of just hour, but this is now an example with int:
declare #hour int, #date datetime
set #hour = 3
set #date = convert(datetime, convert(varchar(2), #hour) + ':00', 108)
select Name
from (
select Name,
[Start] as Start1,
case when [End] < [Start] then dateadd(day, 1, [End]) else [End] End as End1,
case when [End] < [Start] then dateadd(day, -1, [Start]) else [Start] End as Start2,
[End] as End2
from (
select Name, convert(datetime, [Start], 108) as [Start], convert(datetime, [End], 108) as [End]
from Table1
) X
) Y
where ((Start1 <= #date and #date < End1) or (Start2 <= #date and #date < End2))
Edit: added 2nd start / end columns to the derived table to handle second part of the shift.
Example in SQL Fiddle

Thankk you all. With the hep from all of your refrences, I was able to build the query which gave me appropriate results.
Query is as foolow:
SELECT Name FROM SHIFT WHERE
(LEFT(START,2) < LEFT(END,2) AND '[Param.1]' >= LEFT(START,2) AND '[Param.1]' < LEFT(END,2))
OR
(LEFT(START,2) > LEFT(END,2) AND ('[Param.1]' >= LEFT(START,2) OR '[Param.1]' < LEFT(END,2)))

Related

SQL Count with zero values

I want to create a graph for my dataset for the last 24 hours.
I found a solution that works but this is pretty bad since the table I am outer joining cotains every single row in the DB since I am using the (now deprecated) "all" parameter in the group by.
Here is the solution that currently kind of works.
First I declare the date intervals that is 24 hours back in time from now. I declare it twice so I can use it later in the procedure aswell.
Declare #StartDate datetime = dateadd(hour, -24, getdate())
Declare #StartDateProc datetime = dateadd(hour, -24, getdate())
Declare #EndDate datetime = getdate()
I populate the dates into a temp table including a special formated datetsring.
create table #tempTable
(
Date datetime,
DateString varchar(11)
)
while #StartDate <= #EndDate
begin
insert into #tempTable (Date, DateString)
values (#StartDate, convert(varchar(8), #StartDate, 5) + '-' + convert(varchar(2), #StartDate, 108));
SET #StartDate = dateadd(hour,1, #StartDate);
end
This gives me data that looks like this:
Date DateString
---------------------------------------------
2015-12-09 13:59:01.970 09-12-15-13
2015-12-09 14:59:01.970 09-12-15-14
2015-12-09 15:59:01.970 09-12-15-15
2015-12-09 16:59:01.970 09-12-15-16
So what I want is to join my dataset on the matching date string and show the date even if the matching rows is zero.
Here is the rest of the query
select
Date = c.Date,
Amount = sum(c.Amount)
from
DbTable a
outer apply
(select
Date = b.DateString,
Amount = count(*)
from
#tempTable b
where
convert(varchar(8), a.DateColumn, 5) + '-' + convert(varchar(2), a.DateColumn, 108) = b.DateString
group by all
b.DateString) c
where
a.SomeParameter = 'test' and
a.DateColumn >= #StartDateProc and
a.DateColumn <= #EndDate
group by
c.Date
drop table #tempTable
Test to show actual data:
Declare #StartDate datetime = dateadd(hour, -24, getdate())
Declare #EndDate datetime = getdate()
select
dateString = convert(varchar(8),a.DateColumn,5) + '-' + convert(varchar(2),a.DateColumn, 108),
Amount = COUNT(*)
from
DbTable a
where
a.someParameter = 'test' and
a.DateColumn>= dateadd(hour, -24, getdate()) and
a.DateColumn<= getdate()
group by
convert(varchar(8),a.DateColumn,5) + '-' + convert(varchar(2),a.DateColumn, 108)
First output rows:
dateString Amount
09-12-15-14 1
09-12-15-15 1
09-12-15-16 1
09-12-15-17 3
09-12-15-18 1
09-12-15-22 3
09-12-15-23 2
As you can see here there is no data for the times from 19.00 to 21.00. This is how I want the data to be displayed:
dateString Amount
09-12-15-14 1
09-12-15-15 1
09-12-15-16 1
09-12-15-17 3
09-12-15-18 1
09-12-15-19 0
09-12-15-20 0
09-12-15-21 0
09-12-15-22 3
09-12-15-23 2
Normally, this would be approached with left join rather than outer apply. The logic is simple: keep all rows in the first table along with any matching information from the second. This means put the dates table first:
select tt.DateString, count(t.DateColumn) as Amount
from #tempTable tt left join
DbTable t
on convert(varchar(8), t.DateColumn, 5) + '-' + convert(varchar(2), t.DateColumn, 108) = tt.DateString and
t.SomeParameter = 'test'
where tt.Date >= #StartDateProc and
tt.Date <= #EndDate
group by tt.DateString;
In addition, your comparison for the dates seems overly complex, but if it works for you, it works.
The best bet here would be to use DATETIME type itself and not to lose the opportunity to use indexes:
Declare #d datetime = GETDATE()
;WITH cte1 AS(SELECT TOP 25 -1 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) h
FROM master..spt_values),
cte2 AS(SELECT DATEADD(hh, -h, #d) AS startdate,
DATEADD(hh, -h + 1, #d) AS enddate
FROM cte1)
SELECT c.startdate, c.enddate, count(*) as amount
FROM cte2 c
LEFT JOIN DbTable a ON a.DateColumn >= c.startdate AND
a.DateColumn < c.enddate AND
a.SomeParameter = 'test'
GROUP BY c.startdate, c.enddate

Update datetime fields date only

Years ago a conversion from MSSQL 6.5 to MSSQL 2000 has been done and they realized just this week that the conversion failed to convert some datetime columns. It is now my task to fix that and I've been scratching my head on how I could preserve some pieces of information I know is accurate. Here is the structure of one of the table I need to fix.
DateTimeField1 DateTimeField2 DateTimeField3
01/01/1900 5:50:00 PM 01/01/1900 5:52:00 PM 15/02/2005 12:00:00 AM
This is one sample of the many records that are corrupted, unfortunatly I don't have access of any backup from before the conversion. As you can see, the date part is the default value for a DateTime field and is the part I need to fix. I have the following select, which gives me the rows I need to fix.
SELECT DateTimeField1, DateTimeField2, DateTimeField3
FROM Table1
WHERE (DateTimeField1 < '20000101') OR (DateTimeField2 < '20000101')
Now assume I have 60 records resulting from the select. I need to update those records based on the DateTimeField3 DATE part only. The sample above would look like;
DateTimeField1 DateTimeField2 DateTimeField3
15/02/2005 5:50:00 PM 15/02/2005 5:52:00 PM 15/02/2005 12:00:00 AM
Any idea on how to achieve this?
If one field is always 1900-01-01 but with the correct time, and the other field is 12:00:00 AM but with the correct date, you can just add them together.
UPDATE Test
SET
DateTimeField1 = DateTimeField1 + DateTimeField3,
DateTimeField2 = DateTimeField2 + DateTimeField3
WHERE (DateTimeField1 < '20000101') OR (DateTimeField2 < '20000101')
See this SQL Fiddle.
1900-01-01 is the "zero" date, so if you add it to something else, you get that same value. 12:00:00 AM is the "zero" time.
If there are cases where DateTimeField1 has the correct date but DateTimeField2 doesn't, you might want to do this as two separate queries.
I believe you want to only update DateTimeField1 & DateTimeField2 when they are less than '20000101'. CASE Statement will take care of not updating wrong field.
Try single query UPDATE -
SQL SERVER 2008 AND LATER -
UPDATE Table1
SET DateTimeField1 = (CASE WHEN (DateTimeField1 < '20000101')
THEN CAST(CAST (DateTimeField3 AS DATE) AS DATETIME)
+ CAST (DateTimeField1 AS TIME)
ELSE DateTimeField1
END)
, DateTimeField2 = (CASE WHEN (DateTimeField2 < '20000101')
THEN CAST(CAST (DateTimeField3 AS DATE) AS DATETIME)
+ CAST (DateTimeField2 AS TIME)
ELSE DateTimeField2
END)
WHERE (DateTimeField1 < '20000101') OR (DateTimeField2 < '20000101');
EARLIER THAN SQL SERVER 2008 -
UPDATE Table1
SET DateTimeField1 = (CASE WHEN (DateTimeField1 < '20000101')
THEN DATEADD(DAY, 0, DATEDIFF(day, 0, DateTimeField3))
+ DATEADD(DAY, 0 - DATEDIFF(day, 0, DateTimeField1), DateTimeField1)
ELSE DateTimeField1
END)
, DateTimeField2 = (CASE WHEN (DateTimeField2 < '20000101')
THEN DATEADD(DAY, 0, DATEDIFF(day, 0, DateTimeField3))
+ DATEADD(DAY, 0 - DATEDIFF(day, 0, DateTimeField2), DateTimeField2)
ELSE DateTimeField2
END)
WHERE (DateTimeField1 < '20000101') OR (DateTimeField2 < '20000101');
Use this query:
SELECT DateTimeField1 =
convert(datetime,convert(int,convert(float,t.DateTimeField3))
+ convert(float,t.DateTimeField1)),
DateTimeField2 =
convert(datetime,convert(int,convert(float,t.DateTimeField3))
+ convert(float,t.DateTimeField2)),
FROM Table1 t
WHERE (DateTimeField1 < '20000101') OR (DateTimeField2 < '20000101')
SQL Server stores datetime as a float, where a right side is a time and a left side is a date. This query replaces the left side of the wrong datetime by the left side of the correct datetime
Try something like this, it should work on MSSQL 2000
UPDATE tab SET DateTimeField1 =
ltrim(str(datepart(year, DateTimeField3))) + '-' +
ltrim(str(datepart(month, DateTimeField3))) + '-' +
ltrim(str(datepart(day, DateTimeField3))) + ' ' +
ltrim(str(datepart(hour, DateTimeField1))) + ':' +
ltrim(str(datepart(minute, DateTimeField1))) + ':' +
ltrim(str(datepart(second, DateTimeField1))) + '.' +
ltrim(str(datepart(millisecond, DateTimeField1))) ,
DateTimeField2 = ltrim(str(datepart(year, DateTimeField3))) + '-' +
ltrim(str(datepart(month, DateTimeField3))) + '-' +
ltrim(str(datepart(day, DateTimeField3))) + ' ' +
ltrim(str(datepart(hour, DateTimeField2))) + ':' +
ltrim(str(datepart(minute, DateTimeField2))) + ':' +
ltrim(str(datepart(second, DateTimeField2))) + '.' +
ltrim(str(datepart(millisecond, DateTimeField2)))
WHERE (DateTimeField1 < '20000101') OR (DateTimeField2 < '20000101')
This should do what you need:
update <yourtable>
set
DateTimeField1 = case when cast('1 jan 1900' as datetime) = cast(floor(cast(DateTimeField1 as float)) as datetime) then DateTimeField1 + DateTimeField3 else date1 end,
DateTimeField2 = case when cast('1 jan 1900' as datetime) = cast(floor(cast(DateTimeField2 as float)) as datetime) then DateTimeField2 + DateTimeField3 else date2 end
where DateTimeField1 < '2 jan 1900' or DateTimeField2 < '2 jan 1900'
This works by checking to see if the date and time, converted to a float and then floored (which removes the time part) equals 1st Jan 1900.
As the date part of DateTimeField1 or DateTimeField2 are essentially 0 and the time part of DateTimeField3 is 0, you can simply add the two together.
The simplest way to do this would be:
update Table1
set
DateTimeField1 = cast(cast(DateTimeField1 as float)-floor(cast(DateTimeField1 as float)) + floor(cast(DateTimeField3 as float)) as datetime
WHERE DateTimeField1 < '20000101'
update Table1
set
DateTimeField2 = cast(cast(DateTimeField2 as float)-floor(cast(DateTimeField2 as float)) + floor(cast(DateTimeField3 as float)) as datetime
WHERE DateTimeField2 < '20000101'
I know it'd work for SQL Server 2005 and 2008, but I'm not sure about 2000 edition, so test this first.
The explanation is this: datetime is stored as float value, where int part is date, and decimal part is time. So by floor(cast(DateTimeField3 as float)) you get date part, and you can simply add this to DateTimeField1 and DateTimeField2, after you subtracted date part from them.
For 1900-01-01 date part would be zero: select cast(0 as datetime), but it'll still work.

MySQL group by intervals in a date range

I am going to be graphing netflow data stored in a MySQL database, and I need an efficient way to get the relevant data points. They records are stored with the date as an int for seconds since epoch. I Would like to be able to something like:
Select SUM(bytes) from table where stime > x and stime < Y
group by (10 second intervals)
Is there anyway to do this? or, would it be faster to handle it locally in python? even for a 500K row table?
EDIT
My Mistake, the time is stored as an unsigned double instead of an INT.
I'm currently using GROUP BY (FLOOR(stime / I)) where I is the desired interval.
You may be able to do this using integer division. Not sure of the performance.
Let I be your desired interval in seconds.
SELECT SUM(bytes), ((stime - X) DIV I) as interval
FROM table
WHERE (stime > X) and (stime < Y)
GROUP BY interval
Example, let X = 1500 and I = 10
stime = 1503 -> (1503 - 1500) DIV 10 = 0
stime = 1507 -> (1507 - 1500) DIV 10 = 0
stime = 1514 -> (1514 - 1500) DIV 10 = 1
stime = 1523 -> (1523 - 1500) DIV 10 = 2
Have you tried the following? Just devide the tyiem column by 10 and round the result down.
SELECT SUM(bytes)
FROM table
WHERE stime > x
AND stime < Y
GROUP BY ROUND(stime/10, -1)
I don't know wether the ROUND() function and grouping with function calls works in MySQL though, the above is T-SQL.
FLOOR in group by sometimes fails. it sometimes groups different times as one value for example when you divide the value with 3 but it doesn't do the same when you divide with 4, although the difference between these two values is far bigger than 3 or 4 which it should group as two different groups. Better cast it to unsigned after floor which works like:
CAST(FLOOR(UNIX_TIMESTAMP(time_field)/I) AS UNSIGNED INT)
The problem:
Sometimes GROUP BY FLOOR(UNIX_TIMESTAMP(time_field)/3) gives less groups compared to GROUP BY FLOOR(UNIX_TIMESTAMP(time_field)/4) which is mathematically shouldn't be possible.
SELECT sec_to_time(time_to_sec(datefield)- time_to_sec(datefield)%(10)) as intervals,SUM(bytes)
FROM table
WHERE where stime > x and stime < Y
group by intervals
I used suggestions from both answers and a coworker. End result is as follows:
Select FROM_UNIXTIME(stime), bytes
from argusTable_2009_10_22
where stime > (UNIX_TIMESTAMP()-600)
group by floor(stime /10)
I tried the rounding solution as well, but the results were inconsistent.
Chance
I did this a few time ago, so i created some function (with sql server, but i assume it's nearly the same) :
First I created a scalar function that return me the ID of a date depending on an interval and a date part (minute,hour,day,moth,year):
CREATE FUNCTION [dbo].[GetIDDate]
(
#date datetime,
#part nvarchar(10),
#intervalle int
)
RETURNS int
AS
BEGIN
-- Declare the return variable here
DECLARE #res int
DECLARE #date_base datetime
SET #date_base = convert(datetime,'01/01/1970',103)
set #res = case #part
WHEN 'minute' THEN datediff(minute,#date_base,#date)/#intervalle
WHEN 'hour' THEN datediff(hour,#date_base,#date)/#intervalle
WHEN 'day' THEN datediff(day,#date_base,#date)/#intervalle
WHEN 'month' THEN datediff(month,#date_base,#date)/#intervalle
WHEN 'year' THEN datediff(year,#date_base,#date)/#intervalle
ELSE datediff(minute,#date_base,#date)/#intervalle END
-- Return the result of the function
RETURN #res
END
Then I created a table function that returns me all the id betweend a date range :
CREATE FUNCTION [dbo].[GetTableDate]
(
-- Add the parameters for the function here
#start_date datetime,
#end_date datetime,
#interval int,
#unite varchar(10)
)
RETURNS #res TABLE (StartDate datetime,TxtStartDate nvarchar(50),EndDate datetime,TxtEndDate nvarchar(50),IdDate int)
AS
begin
declare #current_date datetime
declare #end_date_courante datetime
declare #txt_start_date nvarchar(50)
declare #txt_end_date nvarchar(50)
set #current_date = case #unite
WHEN 'minute' THEN dateadd(minute, datediff(minute,0,#start_date),0)
WHEN 'hour' THEN dateadd(hour, datediff(hour,0,#start_date),0)
WHEN 'day' THEN dateadd(day, datediff(day,0,#start_date),0)
WHEN 'month' THEN dateadd(month, datediff(month,0,#start_date),0)
WHEN 'year' THEN dateadd(year, datediff(year,0,dateadd(year,#interval,#start_date)),0)
ELSE dateadd(minute, datediff(minute,0,#start_date),0) END
while #current_date < #end_date
begin
set #end_date_courante =
case #unite
WHEN 'minute' THEN dateadd(minute, datediff(minute,0,dateadd(minute,#interval,#current_date)),0)
WHEN 'hour' THEN dateadd(hour, datediff(hour,0,dateadd(hour,#interval,#current_date)),0)
WHEN 'day' THEN dateadd(day, datediff(day,0,dateadd(day,#interval,#current_date)),0)
WHEN 'month' THEN dateadd(month, datediff(month,0,dateadd(month,#interval,#current_date)),0)
WHEN 'year' THEN dateadd(year, datediff(year,0,dateadd(year,#interval,#current_date)),0)
ELSE dateadd(minute, datediff(minute,0,dateadd(minute,#interval,#current_date)),0) END
SET #txt_start_date = case #unite
WHEN 'minute' THEN CONVERT(VARCHAR(20), #current_date, 100)
WHEN 'hour' THEN CONVERT(VARCHAR(20), #current_date, 100)
WHEN 'day' THEN REPLACE(CONVERT(VARCHAR(11), #current_date, 106), ' ', '-')
WHEN 'month' THEN REPLACE(RIGHT(CONVERT(VARCHAR(11), #current_date, 106), 8), ' ', '-')
WHEN 'year' THEN CONVERT(VARCHAR(20), datepart(year,#current_date))
ELSE CONVERT(VARCHAR(20), #current_date, 100) END
SET #txt_end_date = case #unite
WHEN 'minute' THEN CONVERT(VARCHAR(20), #end_date_courante, 100)
WHEN 'hour' THEN CONVERT(VARCHAR(20), #end_date_courante, 100)
WHEN 'day' THEN REPLACE(CONVERT(VARCHAR(11), #end_date_courante, 106), ' ', '-')
WHEN 'month' THEN REPLACE(RIGHT(CONVERT(VARCHAR(11), #end_date_courante, 106), 8), ' ', '-')
WHEN 'year' THEN CONVERT(VARCHAR(20), datepart(year,#end_date_courante))
ELSE CONVERT(VARCHAR(20), #end_date_courante, 100) END
INSERT INTO #res (
StartDate,
EndDate,
TxtStartDate,
TxtEndDate,
IdDate) values(
#current_date,
#end_date_courante,
#txt_start_date,
#txt_end_date,
dbo.GetIDDate(#current_date,#unite,#interval)
)
set #current_date = #end_date_courante
end
return
end
So if I want to count all the user added for each interval of 33 minutes :
SELECT count(id_user) , timeTable.StartDate
FROM user
INNER JOIn dbo.[GetTableDate]('1970-01-01',datedate(),33,'minute') as timeTable
ON dbo.getIDDate(user.creation_date,'minute',33) = timeTable.IDDate
GROUP BY dbo.getIDDate(user.creation_date,'minute',33)
ORDER BY timeTable.StartDate
:)

Calculating number of full months between two dates in SQL

I need to calculate the number of FULL month in SQL, i.e.
2009-04-16 to 2009-05-15 => 0 full month
2009-04-16 to 2009-05-16 => 1 full month
2009-04-16 to 2009-06-16 => 2 full months
I tried to use DATEDIFF, i.e.
SELECT DATEDIFF(MONTH, '2009-04-16', '2009-05-15')
but instead of giving me full months between the two date, it gives me the difference of the month part, i.e.
1
anyone know how to calculate the number of full months in SQL Server?
The original post had some bugs... so I re-wrote and packaged it as a UDF.
CREATE FUNCTION FullMonthsSeparation
(
#DateA DATETIME,
#DateB DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Result INT
DECLARE #DateX DATETIME
DECLARE #DateY DATETIME
IF(#DateA < #DateB)
BEGIN
SET #DateX = #DateA
SET #DateY = #DateB
END
ELSE
BEGIN
SET #DateX = #DateB
SET #DateY = #DateA
END
SET #Result = (
SELECT
CASE
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY)
THEN DATEDIFF(MONTH, #DateX, #DateY) - 1
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
)
RETURN #Result
END
GO
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-05-15') as MonthSep -- =0
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-05-16') as MonthSep -- =1
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-06-16') as MonthSep -- =2
select case when DATEPART(D,End_dATE) >=DATEPART(D,sTAR_dATE)
THEN ( case when DATEPART(M,End_dATE) = DATEPART(M,sTAR_dATE) AND DATEPART(YYYY,End_dATE) = DATEPART(YYYY,sTAR_dATE)
THEN 0 ELSE DATEDIFF(M,sTAR_dATE,End_dATE)END )
ELSE DATEDIFF(M,sTAR_dATE,End_dATE)-1 END
What's your definition of a month? Technically a month can be 28,29,30 or 31 days depending on the month and leap years.
It seems you're considering a month to be 30 days since in your example you disregarded that May has 31 days, so why not just do the following?
SELECT DATEDIFF(DAY, '2009-04-16', '2009-05-15')/30
, DATEDIFF(DAY, '2009-04-16', '2009-05-16')/30
, DATEDIFF(DAY, '2009-04-16', '2009-06-16')/30
The dateadd function can be used to offset to the beginning of the month. If the endDate has a day part less than startDate, it will get pushed to the previous month, thus datediff will give the correct number of months.
DATEDIFF(MONTH, DATEADD(DAY,-DAY(startDate)+1,startDate),DATEADD(DAY,-DAY(startDate)+1,endDate))
This is for ORACLE only and not for SQL-Server:
months_between(to_date ('2009/05/15', 'yyyy/mm/dd'),
to_date ('2009/04/16', 'yyyy/mm/dd'))
And for full month:
round(months_between(to_date ('2009/05/15', 'yyyy/mm/dd'),
to_date ('2009/04/16', 'yyyy/mm/dd')))
Can be used in Oracle 8i and above.
I know this is an old question, but as long as the dates are >= 01-Jan-1753 I use:
DATEDIFF(MONTH, DATEADD(DAY,-DAY(#Start)+1,#Start),DATEADD(DAY,-DAY(#Start)+1,#End))
DATEDIFF() is designed to return the number boundaries crossed between the two dates for the span specified. To get it to do what you want, you need to make an additional adjustment to account for when the dates cross a boundary but don't complete the full span.
WITH
-- Count how many months must be added to #StartDate to exceed #DueDate
MONTHS_SINCE(n, [Month_hence], [IsFull], [RemainingDays] ) AS (
SELECT
1 as n,
DATEADD(Day, -1, DATEADD(Month, 1, #StartDate)) AS Month_hence
,CASE WHEN (DATEADD(Day, -1, DATEADD(Month, 1, #StartDate)) <= #LastDueDate)
THEN 1
ELSE 0
END AS [IsFull]
,DATEDIFF(day, #StartDate, #LastDueDate) as [RemainingDays]
UNION ALL
SELECT
n+1,
--DateAdd(Month, 1, Month_hence) as Month_hence -- No, causes propagation of short month discounted days
DATEADD(Day, -1, DATEADD(Month, n+1, #StartDate)) as Month_hence
,CASE WHEN (DATEADD(Day, -1, DATEADD(Month, n+1, #StartDate)) <= #LastDueDate)
THEN 1
ELSE 0
END AS [IsFull]
,DATEDIFF(day, DATEADD(Day, -1, DATEADD(Month, n, #StartDate)), #LastDueDate)
FROM MONTHS_SINCE
WHERE Month_hence<( #LastDueDate --WHERE Period= 1
)
), --SELECT * FROM MONTHS_SINCE
MONTH_TALLY (full_months_over_all_terms, months_over_all_terms, days_in_incomplete_month ) AS (
SELECT
COALESCE((SELECT MAX(n) FROM MONTHS_SINCE WHERE isFull = 1),1) as full_months_over_all_terms,
(SELECT MAX(n) FROM MONTHS_SINCE ) as months_over_all_terms,
COALESCE((SELECT [RemainingDays] FROM MONTHS_SINCE WHERE isFull = 0),0) as days_in_incomplete_month
) SELECT * FROM MONTH_TALLY;
Is not necesary to create the function only the #result part. For example:
Select Name,
(SELECT CASE WHEN
DATEPART(DAY, '2016-08-28') > DATEPART(DAY, '2016-09-29')
THEN DATEDIFF(MONTH, '2016-08-28', '2016-09-29') - 1
ELSE DATEDIFF(MONTH, '2016-08-28', '2016-09-29') END) as NumberOfMonths
FROM
tableExample;
This answer follows T-SQL format. I conceptualize this problem as one of a linear-time distance between two date points in datetime format, call them Time1 and Time2; Time1 should be aligned to the 'older in time' value you are dealing with (say a Birth date or a widget Creation date or a journey Start date) and Time2 should be aligned with the 'newer in time' value (say a snapshot date or a widget completion date or a journey checkpoint-reached date).
DECLARE #Time1 DATETIME
SET #Time1 = '12/14/2015'
DECLARE #Time2 DATETIME
SET #Time2 = '12/15/2016'
The solution leverages simple measurement, conversion and calculations of the serial intersections of multiple cycles of different lengths; here: Century,Decade,Year,Month,Day (Thanks Mayan Calendar for the concept!). A quick note of thanks: I thank other contributors to Stack Overflow for showing me some of the component functions in this process that I've stitched together. I've positively rated these in my time on this forum.
First, construct a horizon that is the linear set of the intersections of the Century,Decade,Year,Month cycles, incremental by month. Use the cross join Cartesian function for this. (Think of this as creating the cloth from which we will cut a length between two 'yyyy-mm' points in order to measure distance):
SELECT
Linear_YearMonths = (centuries.century + decades.decade + years.[year] + months.[Month]),
1 AS value
INTO #linear_months
FROM
(SELECT '18' [century] UNION ALL
SELECT '19' UNION ALL
SELECT '20') centuries
CROSS JOIN
(SELECT '0' [decade] UNION ALL
SELECT '1' UNION ALL
SELECT '2' UNION ALL
SELECT '3' UNION ALL
SELECT '4' UNION ALL
SELECT '5' UNION ALL
SELECT '6' UNION ALL
SELECT '7' UNION ALL
SELECT '8' UNION ALL
SELECT '9') decades
CROSS JOIN
(SELECT '1' [year] UNION ALL
SELECT '2' UNION ALL
SELECT '3' UNION ALL
SELECT '4' UNION ALL
SELECT '5' UNION ALL
SELECT '6' UNION ALL
SELECT '7' UNION ALL
SELECT '8' UNION ALL
SELECT '9' UNION ALL
SELECT '0') years
CROSS JOIN
(SELECT '-01' [month] UNION ALL
SELECT '-02' UNION ALL
SELECT '-03' UNION ALL
SELECT '-04' UNION ALL
SELECT '-05' UNION ALL
SELECT '-06' UNION ALL
SELECT '-07' UNION ALL
SELECT '-08' UNION ALL
SELECT '-09' UNION ALL
SELECT '-10' UNION ALL
SELECT '-11' UNION ALL
SELECT '-12') [months]
ORDER BY 1
Then, convert your Time1 and Time2 date points into the 'yyyy-mm' format (Think of these as the coordinate cut points on the whole cloth). Retain the original datetime versions of the points as well:
SELECT
Time1 = #Time1,
[YYYY-MM of Time1] = CASE
WHEN LEFT(MONTH(#Time1),1) <> '1' OR MONTH(#Time1) = '1'
THEN (CAST(YEAR(#Time1) AS VARCHAR) + '-' + '0' + CAST(MONTH(#Time1) AS VARCHAR))
ELSE (CAST(YEAR(#Time1) AS VARCHAR) + '-' + CAST(MONTH(#Time1) AS VARCHAR))
END,
Time2 = #Time2,
[YYYY-MM of Time2] = CASE
WHEN LEFT(MONTH(#Time2),1) <> '1' OR MONTH(#Time2) = '1'
THEN (CAST(YEAR(#Time2) AS VARCHAR) + '-' + '0' + CAST(MONTH(#Time2) AS VARCHAR))
ELSE (CAST(YEAR(#Time2) AS VARCHAR) + '-' + CAST(MONTH(#Time2) AS VARCHAR))
END
INTO #datepoints
Then, Select the ordinal distance of 'yyyy-mm' units, less one to convert to cardinal distance (i.e. cut a piece of cloth from the whole cloth at the identified cut points and get its raw measurement):
SELECT
d.*,
Months_Between = (SELECT (SUM(l.value) - 1) FROM #linear_months l
WHERE l.[Linear_YearMonths] BETWEEN d.[YYYY-MM of Time1] AND d.[YYYY-MM of Time2])
FROM #datepoints d
Raw Output:
I call this a 'raw distance' because the month component of the 'yyyy-mm' cardinal distance may be one too many; the day cycle components within the month need to be compared to see if this last month value should count. In this example specifically, the raw output distance is '12'. But this wrong as 12/14 is before 12/15, so therefore only 11 full months have lapsed--its just one day shy of lapsing through the 12th month. We therefore have to bring in the intra-month day cycle to get to a final answer. Insert a 'month,day' position comparison between the to determine if the latest date point month counts nominally, or not:
SELECT
d.*,
Months_Between = (SELECT (SUM(l.value) - 1) FROM AZ_VBP.[MY].[edg_Linear_YearMonths] l
WHERE l.[Linear_YearMonths] BETWEEN d.[YYYY-MM of Time1] AND d.[YYYY-MM of Time2])
+ (CASE WHEN DAY(Time1) < DAY(Time2)
THEN -1
ELSE 0
END)
FROM #datepoints d
Final Output:
The correct answer of '11' is now our output. And so, I hope this helps. Thanks!
select CAST(DATEDIFF(MONTH, StartDate, EndDate) AS float) -
(DATEPART(dd,StartDate) - 1.0) / DATEDIFF(DAY, StartDate, DATEADD(MONTH, 1, StartDate)) +
(DATEPART(dd,EndDate)*1.0 ) / DATEDIFF(DAY, EndDate, DATEADD(MONTH, 1, EndDate))
I realize this is an old post, but I created this interesting solution that I think is easy to implement using a CASE statement.
Estimate the difference using DATEDIFF, and then test the months before and after using DATEADD to find the best date. This assumes Jan 31 to Feb 28 is 1 month (because it is).
DECLARE #First date = '2015-08-31'
DECLARE #Last date = '2016-02-28'
SELECT
#First as [First],
#Last as [Last],
DateDiff(Month, #First, #Last) as [DateDiff Thinks],
CASE
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) +1, #First) <= #Last Then DATEDIFF(Month, #First, #Last) +1
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) , #First) <= #Last Then DATEDIFF(Month, #First, #Last)
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) -1, #First) <= #Last Then DATEDIFF(Month, #First, #Last) -1
END as [Actual Months Apart]
SIMPLE AND EASY WAY, Just Copy and Paste this FULL code to MS SQL and Execute :
declare #StartDate date='2019-01-31'
declare #EndDate date='2019-02-28'
SELECT
DATEDIFF(MONTH, #StartDate, #EndDate)+
(
case
when format(#StartDate,'yyyy-MM') != format(#EndDate,'yyyy-MM') AND DATEPART(DAY,#StartDate) > DATEPART(DAY,#EndDate) AND DATEPART(DAY,#EndDate) = DATEPART(DAY,EOMONTH(#EndDate)) then 0
when format(#StartDate,'yyyy-MM') != format(#EndDate,'yyyy-MM') AND DATEPART(DAY,#StartDate) > DATEPART(DAY,#EndDate) then -1
else 0
end
)
as NumberOfMonths
All you need to do is deduct the additional month if the end date has not yet passed the day of the month in the start date.
DECLARE #StartDate AS DATE = '2019-07-17'
DECLARE #EndDate AS DATE = '2019-09-15'
DECLARE #MonthDiff AS INT = DATEDIFF(MONTH,#StartDate,#EndDate)
SELECT #MonthDiff -
CASE
WHEN FORMAT(#StartDate,'dd') > FORMAT(#EndDate,'dd') THEN 1
ELSE 0
END
You can create this function to calculate absolute difference between two dates.
As I found using DATEDIFF inbuilt system function we will get the difference only in months, days and years. For example : Let say there are two dates 18-Jan-2018 and 15-Jan-2019. So the difference between those dates will be given by DATEDIFF in month as 12 months where as it is actually 11 Months 28 Days. So using the function given below, we can find absolute difference between two dates.
CREATE FUNCTION GetDurationInMonthAndDays(#First_Date DateTime,#Second_Date DateTime)
RETURNS VARCHAR(500)
AS
BEGIN
DECLARE #RESULT VARCHAR(500)=''
DECLARE #MONTHS TABLE(MONTH_ID INT,MONTH_NAME VARCHAR(100),MONTH_DAYS INT)
INSERT INTO #MONTHS
SELECT 1,'Jan',31
union SELECT 2,'Feb',28
union SELECT 3,'Mar',31
union SELECT 4,'Apr',30
union SELECT 5,'May',31
union SELECT 6,'Jun',30
union SELECT 7,'Jul',31
union SELECT 8,'Aug',31
union SELECT 9,'Sep',30
union SELECT 10,'Oct',31
union SELECT 11,'Nov',30
union SELECT 12,'Jan',31
IF(#Second_Date>#First_Date)
BEGIN
declare #month int=0
declare #days int=0
declare #first_year int
declare #second_year int
SELECT #first_year=Year(#First_Date)
SELECT #second_year=Year(#Second_Date)+1
declare #first_month int
declare #second_month int
SELECT #first_month=Month(#First_Date)
SELECT #second_month=Month(#Second_Date)
if(#first_month=2)
begin
IF((#first_year%100<>0) AND (#first_year%4=0) OR (#first_year%400=0))
BEGIN
SELECT #days=29-day(#First_Date)
END
else
begin
SELECT #days=28-day(#First_Date)
end
end
else
begin
SELECT #days=(SELECT MONTH_DAYS FROM #MONTHS WHERE MONTH_ID=#first_month)-day(#First_Date)
end
SELECT #first_month=#first_month+1
WHILE #first_year<#second_year
BEGIN
if(#first_month=13)
begin
set #first_month=1
end
WHILE #first_month<13
BEGIN
if(#first_year=Year(#Second_Date))
begin
if(#first_month=#second_month)
begin
SELECT #days=#days+DAY(#Second_Date)
break;
end
else
begin
SELECT #month=#month+1
end
end
ELSE
BEGIN
SELECT #month=#month+1
END
SET #first_month=#first_month+1
END
SET #first_year = #first_year + 1
END
select #month=#month+(#days/30)
select #days=#days%30
if(#days>0)
begin
SELECT #RESULT=CAST(#month AS VARCHAR)+' Month '+CAST(#days AS VARCHAR)+' Days '
end
else
begin
SELECT #RESULT=CAST(#month AS VARCHAR)+' Month '
end
END
ELSE
BEGIN
SELECT #RESULT='ERROR'
END
RETURN #RESULT
END
SELECT dateadd(dd,number,DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) AS gun FROM master..spt_values
WHERE type = 'p'
AND year(dateadd(dd,number,DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)))=year(DATEADD(yy, DATEDIFF(yy,0,getdate()), 0))
CREATE FUNCTION ufFullMonthDif (#dStart DATE, #dEnd DATE)
RETURNS INT
AS
BEGIN
DECLARE #dif INT,
#dEnd2 DATE
SET #dif = DATEDIFF(MONTH, #dStart, #dEnd)
SET #dEnd2 = DATEADD (MONTH, #dif, #dStart)
IF #dEnd2 > #dEnd
SET #dif = #dif - 1
RETURN #dif
END
GO
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-01')
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-29')
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-30')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-05-15')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-05-16')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-06-16')
SELECT dbo.ufFullMonthDif ('2019-01-31', '2019-02-28')
Making Some changes to the Above function worked for me.
CREATE FUNCTION [dbo].[FullMonthsSeparation]
(
#DateA DATETIME,
#DateB DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Result INT
DECLARE #DateX DATETIME
DECLARE #DateY DATETIME
IF(#DateA < #DateB)
BEGIN
SET #DateX = #DateA
SET #DateY = #DateB
END
ELSE
BEGIN
SET #DateX = #DateB
SET #DateY = #DateA
END
SET #Result = (
SELECT
CASE
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY)
THEN DATEDIFF(MONTH, #DateX, #DateY) - iif(EOMONTH(#DateY) = #DateY, 0, 1)
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
)
RETURN #Result
END
Declare #FromDate datetime, #ToDate datetime,
#TotalMonth int ='2021-10-01', #TotalDay='2021-12-31' int,
#Month int = 0
WHILE #ToDate > DATEADD(MONTH,#Month,#FromDate)
BEGIN
SET #Month = #Month +1
END
SET #TotalMonth = #Month -1
SET #TotalDay = DATEDIFF(DAY, DATEADD(MONTH,#TotalMonth, #FromDate),#ToDate) +1
IF(#TotalDay = DAY(EOMONTH(#ToDate)))
BEGIN
SET #TotalMonth = #TotalMonth +1
SET #TotalDay =0
END
Result #TotalMonth = 3, #TotalDay=0
if you are using PostGres only --
SELECT (DATE_PART('year', '2012-01-01'::date) - DATE_PART('year', '2011-10-02'::date)) * 12 +
(DATE_PART('month', '2012-01-01'::date) - DATE_PART('month', '2011-10-02'::date));
There are a lot of answers here that did not satisfy all the corner cases so I set about to fix them. This handles:
01/05/2021 - 02/04/2021 = 0 months
01/31/2021 - 02/28/2021 = 1 months
09/01/2021 - 10/31/2021 = 2 months
I think this generally handles all the cases needed.
declare #dateX date = '01/1/2022'
declare #datey date = '02/28/2022'
-- select datediff(month, #dateX, #datey) --Here for comparison
SELECT
CASE
WHEN DATEPART(DAY, #DateX) = 1 and DATEPART(DAY, #DateY) = DATEPART(DAY, eomonth(#DateY))
THEN DATEDIFF(MONTH, #DateX, #DateY) + 1
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY) and DATEPART(DAY, #DateY) != DATEPART(DAY, eomonth(#DateY))
THEN DATEDIFF(MONTH, #DateX, #DateY) - 1
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
I believe it is important to note that the question specifically asks for "full months between" AND that in the examples given each date is treated as "the START point of that date". This latter item is important because some comments state that year-01-31 to year-02-28 is a result of zero. This is correct. 1 complete day in January, plus 27 complete days in February (02-28 is the start of that day, so incomplete) is zero "full" months.
With that in mind I believe the following would meet the requirements IF StartDate is <= EndDate
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
To accommodate the possibility that the dates may be in any order then:
, CASE WHEN StartDate <= EndDate THEN
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
ELSE
(DATEPART(YEAR, StartDate) - DATEPART(YEAR, EndDate)) * 12
+ (DATEPART(MONTH, StartDate) - DATEPART(MONTH, EndDate))
- CASE WHEN DATEPART(DAY,StartDate) < DATEPART(DAY,EndDate) THEN 1 ELSE 0 END
END AS FullMnthsBtwn
For this sample:
select
StartDate, EndDate
into mytable
from (
values
(cast(getdate() as date),cast(getdate() as date)) -- both same date
-- original
,('2009-04-16','2009-05-15') -- > 0 full month
,('2009-04-16','2009-05-16') -- > 1 full month
,('2009-04-16','2009-06-16') -- > 2 full months
-- '1/31/2018' and endDate of '3/1/2018', I get a 0 – Eugene
, ('2018-01-31','2018-03-01')
-- some extras mentioned in comments, both of these should return 0 (in my opinion)
,('2009-01-31','2009-02-28')
,('2012-12-31','2013-02-28')
,('2022-05-15','2022-04-16') -- > 0 full month
,('2022-05-16','2022-04-16') -- > 1 full month
,('2021-06-16','2022-04-16') -- > 10 full months
) d (StartDate, EndDate)
query
select
StartDate
, EndDate
, CASE WHEN StartDate <= EndDate THEN
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
ELSE
(DATEPART(YEAR, StartDate) - DATEPART(YEAR, EndDate)) * 12
+ (DATEPART(MONTH, StartDate) - DATEPART(MONTH, EndDate))
- CASE WHEN DATEPART(DAY,StartDate) < DATEPART(DAY,EndDate) THEN 1 ELSE 0 END
END AS FullMnthsBtwn
from mytable
order by 1
result
+------------+------------+---------------+
| StartDate | EndDate | FullMnthsBtwn |
+------------+------------+---------------+
| 2009-01-31 | 2009-02-28 | 0 |
| 2009-04-16 | 2009-05-15 | 0 |
| 2009-04-16 | 2009-05-16 | 1 |
| 2009-04-16 | 2009-06-16 | 2 |
| 2012-12-31 | 2013-02-28 | 1 |
| 2018-01-31 | 2018-03-01 | 1 |
| 2021-06-16 | 2022-04-16 | 10 |
| 2022-05-15 | 2022-04-16 | 0 |
| 2022-05-16 | 2022-04-16 | 1 |
| 2022-07-09 | 2022-07-09 | 0 |
+------------+------------+---------------+
See db<>fiddle here (compares some other responses as well)
I got some ideas from the other answers, but none of them gave me exactly what I wanted.
The problem boils down to what I perceive a "month between" to be, which may be what others are also looking for also.
For example 25th February to 25th March would be one month to me, even though it is only 28 days. I would also consider 25th March to 25th April as one month at 31 days.
Also, I would consider 31st January to 2nd March as 1 month and 2 days even though it is 30 days between.
Also, fractions of a month are a bit meaningless as it depends on the length of a month and which month in the range do you choose to take a fraction of.
So, with that in mind, I came up with this function. It returns a decimal, the integer part is the number of months and the decimal part is the number of days, so a return value of 3.07 would mean 3 months and 7 days.
CREATE FUNCTION MonthsAndDaysBetween (#fromDt date, #toDt date)
RETURNS decimal(10,2)
AS
BEGIN
DECLARE #d1 date, #d2 date, #numM int, #numD int, #trc varchar(10);
IF(#fromDt < #toDt)
BEGIN
SET #d1 = #fromDt;
SET #d2 = #toDt;
END
ELSE
BEGIN
SET #d1 = #toDt;
SET #d2 = #fromDt;
END
IF DAY(#d1)>DAY(#d2)
SET #numM = year(#d2)*12+month(#d2)-year(#d1)*12-month(#d1)-1;
ELSE
SET #numM = year(#d2)*12+month(#d2)-year(#d1)*12-month(#d1);
IF YEAR(#d1) < YEAR(#d2) OR (YEAR(#d1) = YEAR(#d2) AND MONTH(#d1) < MONTH(#d2))
BEGIN
IF DAY(#d2) < DAY(#d1)
SET #numD = DAY(#d2) + DAY(EOMONTH(DATEADD(month,-1,#d2))) - DAY(#d1);
ELSE
SET #numD = DAY(#d2)-DAY(#d1);
END
ELSE
SET #numD = DAY(#d2)-DAY(#d1);
RETURN #numM + ABS(#numD) / 100.0;
END
In sql server, this formula works for going backward and forward in time.
DATEDIFF(month,#startdate, #enddate) + iif(#startdate <=#enddate,IIF(DAY(#startdate) > DAY(#enddate),-1,0),IIF(DAY(#startdate) < DAY(#enddate),+1, 0)))
SELECT 12 * (YEAR(end_date) - YEAR(start_date)) +
((MONTH(end_date) - MONTH(start_date))) +
SIGN(DAY(end_date) / DAY(start_date));
This works fine for me on SQL SERVER 2000.
Try:
trunc(Months_Between(date2, date1))
UPDATED
Right now, I just use
SELECT DATEDIFF(MONTH, '2019-01-31', '2019-02-28')
and SQL server returns the exact result (1).
I googled over internet.
And suggestion I found is to add +1 to the end.
Try do it like this:
Declare #Start DateTime
Declare #End DateTime
Set #Start = '11/1/07'
Set #End = '2/29/08'
Select DateDiff(Month, #Start, #End + 1)

SQL DateDiff advanced usage?

I need to calculate the DateDiff (hours) between two dates, but only during business-hours (8:30 - 16:00, no weekends). This result will then be put into the Reaction_Time column as per the example below.
ID Date Reaction_Time Overdue
1 29.04.2003 15:00:00
1 30.04.2003 11:00:00 3:30
2 30.04.2003 14:00:00
2 01.05.2003 14:00:00 7:30 YES
*Note: I didn't check to see if the dates in example were holidays.
I'm using SQL Server 2005
This will be combined with a bigger query, but for now all I need is this to get started, I'll try to figure out how to put it all together on my own. Thanks for the help!
Edit: Hey, thanks everyone for the replies. But due to the obvious complexity of a solution on SQL side, it was decided we would do this in Excel instead as that's where the report will be moved anyway. Sorry for the trouble, but I really figured it would be simpler than this. As it is, we just don't have the time.
I would recommend building a user defined function that calculates the date difference in business hours according to your rules.
SELECT
Id,
MIN(Date) DateStarted,
MAX(Date) DateCompleted,
dbo.udfDateDiffBusinessHours(MIN(Date), MAX(Date)) ReactionTime
FROM
Incident
GROUP BY
Id
I'm not sure where your Overdue value comes from, so I left it off in my example.
In a function you can write way more expressive SQL than in a query, and you don't clog your query with business rules, making it hard to maintain.
Also a function can easily be reused. Extending it to include support for holidays (I'm thinking of a Holidays table here) would not be too hard. Further refinements are possible without the need to change hard to read nested SELECT/CASE WHEN constructs, which would be the alternative.
If I have time today, I'll look into writing an example function.
EDIT: Here is something with bells and whistles, calculating around weekends transparently:
ALTER FUNCTION dbo.udfDateDiffBusinessHours (
#date1 DATETIME,
#date2 DATETIME
) RETURNS DATETIME AS
BEGIN
DECLARE #sat INT
DECLARE #sun INT
DECLARE #workday_s INT
DECLARE #workday_e INT
DECLARE #basedate1 DATETIME
DECLARE #basedate2 DATETIME
DECLARE #calcdate1 DATETIME
DECLARE #calcdate2 DATETIME
DECLARE #cworkdays INT
DECLARE #cweekends INT
DECLARE #returnval INT
SET #workday_s = 510 -- work day start: 8.5 hours
SET #workday_e = 960 -- work day end: 16.0 hours
-- calculate Saturday and Sunday dependent on SET DATEFIRST option
SET #sat = CASE ##DATEFIRST WHEN 7 THEN 7 ELSE 7 - ##DATEFIRST END
SET #sun = CASE ##DATEFIRST WHEN 7 THEN 1 ELSE #sat + 1 END
SET #calcdate1 = #date1
SET #calcdate2 = #date2
-- #date1: assume next day if start was after end of workday
SET #basedate1 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate1))
SET #calcdate1 = CASE WHEN DATEDIFF(mi, #basedate1, #calcdate1) > #workday_e
THEN #basedate1 + 1
ELSE #calcdate1
END
-- #date1: if Saturday or Sunday, make it next Monday
SET #basedate1 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate1))
SET #calcdate1 = CASE DATEPART(dw, #basedate1)
WHEN #sat THEN #basedate1 + 2
WHEN #sun THEN #basedate1 + 1
ELSE #calcdate1
END
-- #date1: assume #workday_s as the minimum start time
SET #basedate1 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate1))
SET #calcdate1 = CASE WHEN DATEDIFF(mi, #basedate1, #calcdate1) < #workday_s
THEN DATEADD(mi, #workday_s, #basedate1)
ELSE #calcdate1
END
-- #date2: assume previous day if end was before start of workday
SET #basedate2 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate2))
SET #calcdate2 = CASE WHEN DATEDIFF(mi, #basedate2, #calcdate2) < #workday_s
THEN #basedate2 - 1
ELSE #calcdate2
END
-- #date2: if Saturday or Sunday, make it previous Friday
SET #basedate2 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate2))
SET #calcdate2 = CASE DATEPART(dw, #calcdate2)
WHEN #sat THEN #basedate2 - 0.00001
WHEN #sun THEN #basedate2 - 1.00001
ELSE #date2
END
-- #date2: assume #workday_e as the maximum end time
SET #basedate2 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate2))
SET #calcdate2 = CASE WHEN DATEDIFF(mi, #basedate2, #calcdate2) > #workday_e
THEN DATEADD(mi, #workday_e, #basedate2)
ELSE #calcdate2
END
-- count full work days (subtract Saturdays and Sundays)
SET #cworkdays = DATEDIFF(dd, #basedate1, #basedate2)
SET #cweekends = #cworkdays / 7
SET #cworkdays = #cworkdays - #cweekends * 2
-- calculate effective duration in minutes
SET #returnval = #cworkdays * (#workday_e - #workday_s)
+ #workday_e - DATEDIFF(mi, #basedate1, #calcdate1)
+ DATEDIFF(mi, #basedate2, #calcdate2) - #workday_e
-- return duration as an offset in minutes from date 0
RETURN DATEADD(mi, #returnval, 0)
END
The function returns a DATETIME value meant as an offset from date 0 (which is "1900-01-01 00:00:00"). So for example a timespan of 8:00 hours would be "1900-01-01 08:00:00" and 25 hours would be "1900-01-02 01:00:00". The function result is the time difference in business hours between two dates. No special handling/support for overtime.
SELECT dbo.udfDateDiffBusinessHours('2003-04-29 15:00:00', '2003-04-30 11:00:00')
--> 1900-01-01 03:30:00.000
SELECT dbo.udfDateDiffBusinessHours('2003-04-30 14:00:00', '2003-05-01 14:00:00')
--> 1900-01-01 07:30:00.000
The function assumes the start of the next available work day (08:30 h) when the #date1 is off-hours, and the end of the previous available work day (16:00 h) when #date2 is off-hours.
"next/previous available" means:
if #date1 is '2009-02-06 07:00:00' (Fri), it will become '2009-02-06 08:30:00' (Fri)
if #date1 is '2009-02-06 19:00:00' (Fri), it will become '2009-02-09 08:30:00' (Mon)
if #date2 is '2009-02-09 07:00:00' (Mon), it will become '2009-02-06 16:00:00' (Fri)
if #date2 is '2009-02-09 19:00:00' (Mon), it will become '2009-02-09 16:00:00' (Mon)
DECLARE #BusHourStart DATETIME, #BusHourEnd DATETIME
SELECT #BusHourStart = '08:30:00', #BusHourEnd = '16:00:00'
DECLARE #BusMinutesStart INT, #BusMinutesEnd INT
SELECT #BusMinutesStart = DATEPART(minute,#BusHourStart)+DATEPART(hour,#BusHourStart)*60,
#BusMinutesEnd = DATEPART(minute,#BusHourEnd)+DATEPART(hour,#BusHourEnd)*60
DECLARE #Dates2 TABLE (ID INT, DateStart DATETIME, DateEnd DATETIME)
INSERT INTO #Dates2
SELECT 1, '15:00:00 04/29/2003', '11:00:00 04/30/2003' UNION
SELECT 2, '14:00:00 04/30/2003', '14:00:00 05/01/2003' UNION
SELECT 3, '14:00:00 05/02/2003', '14:00:00 05/06/2003' UNION
SELECT 4, '14:00:00 05/02/2003', '14:00:00 05/04/2003' UNION
SELECT 5, '07:00:00 05/02/2003', '14:00:00 05/02/2003' UNION
SELECT 6, '14:00:00 05/02/2003', '23:00:00 05/02/2003' UNION
SELECT 7, '07:00:00 05/02/2003', '08:00:00 05/02/2003' UNION
SELECT 8, '22:00:00 05/02/2003', '23:00:00 05/03/2003' UNION
SELECT 9, '08:00:00 05/03/2003', '23:00:00 05/04/2003' UNION
SELECT 10, '07:00:00 05/02/2003', '23:00:00 05/02/2003'
-- SET DATEFIRST to U.S. English default value of 7.
SET DATEFIRST 7
SELECT ID, DateStart, DateEnd, CONVERT(VARCHAR, Minutes/60) +':'+ CONVERT(VARCHAR, Minutes % 60) AS ReactionTime
FROM (
SELECT ID, DateStart, DateEnd, Overtime,
CASE
WHEN DayDiff = 0 THEN
CASE
WHEN (MinutesEnd - MinutesStart - Overtime) > 0 THEN (MinutesEnd - MinutesStart - Overtime)
ELSE 0
END
WHEN DayDiff > 0 THEN
CASE
WHEN (StartPart + EndPart - Overtime) > 0 THEN (StartPart + EndPart - Overtime)
ELSE 0
END + DayPart
ELSE 0
END AS Minutes
FROM(
SELECT ID, DateStart, DateEnd, DayDiff, MinutesStart, MinutesEnd,
CASE WHEN(#BusMinutesStart - MinutesStart) > 0 THEN (#BusMinutesStart - MinutesStart) ELSE 0 END +
CASE WHEN(MinutesEnd - #BusMinutesEnd) > 0 THEN (MinutesEnd - #BusMinutesEnd) ELSE 0 END AS Overtime,
CASE WHEN(#BusMinutesEnd - MinutesStart) > 0 THEN (#BusMinutesEnd - MinutesStart) ELSE 0 END AS StartPart,
CASE WHEN(MinutesEnd - #BusMinutesStart) > 0 THEN (MinutesEnd - #BusMinutesStart) ELSE 0 END AS EndPart,
CASE WHEN DayDiff > 1 THEN (#BusMinutesEnd - #BusMinutesStart)*(DayDiff - 1) ELSE 0 END AS DayPart
FROM (
SELECT DATEDIFF(d,DateStart, DateEnd) AS DayDiff, ID, DateStart, DateEnd,
DATEPART(minute,DateStart)+DATEPART(hour,DateStart)*60 AS MinutesStart,
DATEPART(minute,DateEnd)+DATEPART(hour,DateEnd)*60 AS MinutesEnd
FROM (
SELECT ID,
CASE
WHEN DATEPART(dw, DateStart) = 7
THEN DATEADD(SECOND, 1, DATEADD(DAY, DATEDIFF(DAY, 0, DateStart), 2))
WHEN DATEPART(dw, DateStart) = 1
THEN DATEADD(SECOND, 1, DATEADD(DAY, DATEDIFF(DAY, 0, DateStart), 1))
ELSE DateStart END AS DateStart,
CASE
WHEN DATEPART(dw, DateEnd) = 7
THEN DATEADD(SECOND, -1, DATEADD(DAY, DATEDIFF(DAY, 0, DateEnd), 0))
WHEN DATEPART(dw, DateEnd) = 1
THEN DATEADD(SECOND, -1, DATEADD(DAY, DATEDIFF(DAY, 0, DateEnd), -1))
ELSE DateEnd END AS DateEnd FROM #Dates2
)Weekends
)InMinutes
)Overtime
)Calculation
select datediff(hh,#date1,#date2) - 16.5*(datediff(dd,#date1,#date2))
The only catch is that it will give you 3:30 as 3.5 hours but you can fix that easily.
Use this code : to find out weekend in between dates
(
DATEDIFF(dd, open_date, zassignment_date) + 1
- ( (DATEDIFF(dd, open_date, zassignment_date) + 1)
-(DATEDIFF(wk, open_date, zassignment_date) * 2)
-(CASE WHEN DATENAME(dw, open_date) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, zassignment_date) = 'Saturday' THEN 1 ELSE 0 END) )) wk_end
Assuming you have a reference-table of the working days (and their hours), then I would use a 3 stage approach (pseudo-sql)
(first preclude the "all in one day" trivial example, since that simplifies the logic)
-- days that are neither the start nor end (full days)
SELECT #FullDayHours = SUM(day start to day end)
FROM reference-calendar
WHERE Start >= midnight-after-start and End <= midnight-before-end
-- time after the [query start] to the end of the first working day
SELECT #FirstDayHours = [query start] to day end
FROM reference-calandar
WHERE start day
-- time from the start of the last working day to the [query end]
SELECT #LastDayHours = day start to [query end]
FROM reference-calendar
WHERE end-day
IF #FirstDayHours < 0 SET #FirstDayHours = 0 -- starts outside working time
IF #LastDayHours < 0 SET #LastDayHours = 0 -- ends outside working time
PRINT #FirstDayHours + #FullDayHours + #LastDayHours
Obviously it is a bit hard to do properly without more context...
This function will give you the difference in business hours between two given times. This will return the difference in minutes or hours based on the date part parameter.
CREATE FUNCTION [dbo].[fnBusinessHoursDateDiff] (#StartTime SmallDatetime, #EndTime SmallDateTime, #DatePart varchar(2)) RETURNS DECIMAL (10,2)
AS
BEGIN
DECLARE #Minutes bigint
, #FinalNumber Decimal(10,2)
-- // Create Minute By minute table for CTE
-- ===========================================================
;WITH cteInputHours (StartTime, EndTime, NextTime) AS (
SELECT #StartTime
, #EndTime
, dateadd(mi, 1, #StartTime)
),
cteBusinessMinutes (TimeOfDay, [isBusHour], NextTime) AS(
SELECT StartTime [TimeOfDay]
, case when datepart(dw, StartTime) between 2 and 6 and convert(time,StartTime) between '08:30' and '15:59' then 1 else 0 end [isBusHour]
, dateadd(mi, 1, #StartTime) [NextTime]
FROM cteInputHours
UNION ALL
SELECT dateadd(mi, 1, (a.TimeOfDay)) [TimeOfDay]
, case when datepart(dw, a.TimeOfDay) between 2 and 6 and convert(time,dateadd(mi, 1, (a.TimeOfDay)) ) between '08:30' and '15:59' then 1 else 0 end [isBusHour]
, dateadd(mi, 2, (a.TimeOfDay)) NextTime
FROM cteBusinessMinutes a
WHERE dateadd(mi, 1, (a.TimeOfDay)) < #EndTime
)
SELECT #Minutes = count(*)
FROM cteBusinessMinutes
WHERE isBusHour = 1
OPTION (MAXRECURSION 0);
-- // Final Select
-- ===========================================================
SELECT #FinalNumber = #Minutes / (case when #DatePart = 'hh' then 60.00 else 1 end)
RETURN #FinalNumber
END