What is the most accurate way of using DATEDIFF in SQL Server? - sql-server

I have two computed columns (MonthsInService and YearsInService) with the following expressions.
MonthsInService = (datediff(month,[DateEngaged],getdate()))
YearsInService = (datediff(month,[DateEngaged],getdate())/(12))
Now if for example DateEngaged = 2012-April-09 and getdate() is 2013-April-08, MonthsInService returns 12 and YearsInService is 1.
My application requires that YearsInService be Zero since there is still one day to go before the employees first Anniversary.
Am not even sure how to best handle the MonthsInService column since months have varying number of days.

Unfortunately, DATEDIFF computes the number of transitions of the element, rather than the usual, human intuition of the difference between two dates (e.g. DATEDIFF(year,'20121231','20130101') is 1, even though not many people would say that there's a difference of a year).
The solution I'd use is a bit repetitive, but doesn't need a separate function, and always gets e.g. leap years correct:
declare #T table (
DateEngaged datetime not null,
MonthsInService as CASE
WHEN DATEADD(month,DATEDIFF(month,DateEngaged,GETDATE()),DateEngaged) > GETDATE()
THEN DATEDIFF(month,DateEngaged,GETDATE()) - 1
ELSE DATEDIFF(month,DateEngaged,GETDATE())
END,
YearsInService as CASE
WHEN DATEADD(year,DATEDIFF(year,DateEngaged,GETDATE()),DateEngaged) > GETDATE()
THEN DATEDIFF(year,DateEngaged,GETDATE()) - 1
ELSE DATEDIFF(year,DateEngaged,GETDATE())
END
)
insert into #T (DateEngaged) values ('20120409'),('20120408')
select * from #T
Produces:
DateEngaged MonthsInService YearsInService
----------------------- --------------- --------------
2012-04-09 00:00:00.000 11 0
2012-04-08 00:00:00.000 12 1
It works by asking "If we take the naive answer produced by DATEDIFF, does it given an answer that's too high by 1?" - and if so, we just subtract one from the answer it gives. DATEDIFF should only ever be over by 1.

Via using day you can reach the result:
select
datediff(month,'2012-April-09','2013-April-08') MonthsInService
,datediff(day,'2012-April-09','2013-April-08')/365 YearsInService
Output:
12 0
or use function for maximum precision:
CREATE FUNCTION [dbo].[getFullYears]
(
#dateX datetime,
#dateY datetime
)
RETURNS int
AS
BEGIN
DECLARE #y int
SET #y =DATEDIFF(year,#dateX,#dateY)
IF (#dateY < DATEADD(year, #y, #dateX)) SET #y = #y -1
RETURN #y
END
select dbo.getFullYears('2012-April-09','2013-April-09') --1
select dbo.getFullYears('2012-April-09','2013-April-08') --0
For months calculation you can refer here: Calculating number of full months between two dates in SQL

Try this query :
DATEDIFF(DAY, CONVERT(date, dtmDOB),
CONVERT(date, GETDATE()))*(12.0/365.25)),1))
AS TotalMonths,

Related

How to find out Date/Month interval between #StartDate and #EndDate passed in stored procedure parameters

I am working on a stored procedure where I am dividing the number of rows by interval of month and day repeated in the specified date range.
Interval Month and Day = 7th April and 8th October
Example
For Date range 2014/01/01 and 2014/12/31, 7th April and 8th October are repeated 2 times so I will divide my statement by 2.
For Date range 2014/01/01 and 2015/09/01, 7th April came 2 and 8th October 1 so I will divide my statement by 3.
As others have stated, the question is a bit unclear, but I believe I know what you're trying to do. You are trying to find the number of times that a set of dates (only taking Month/Day into account) happen over a range of dates (set by #StartDate and #EndDate). I think the select count(*) from TableName part of the question is a distraction, as you already know how to do that. Below is an answer on how to get the denominator, which is what you are trying to figure out how to do.
declare #StartDate date = '2014-01-01'
, #EndDate date = '2014-12-31'
, #DenVal int --Denominator Value
create table #dates_of_interest
(
month_nbr tinyint not null
, day_nbr tinyint not null
)
insert into #dates_of_interest
values (4, 7) --7th of April
, (10, 8) --8th of October
; with date_list as
(
--use a Recursive CTE to generate a list of all the dates in the given range.
select #StartDate as dt
union all
select dateadd(d,1,dt) as dt
from date_list
where 1=1
and dt < #EndDate
)
--Get the output of the Recursive CTE along with Month/Day numbes
select dt
, datepart(m,dt) as month_nbr
, datepart(d,dt) as day_nbr
into #list_of_dates
from date_list as dl
option (maxrecursion 32767) --set to max possible levels of recursion (might want to lower this number)
--Set the Denominator to the results of the sum(case/when) AKA countif
set #DenVal =
(
select sum(case when di.month_nbr is null and di.day_nbr is null then 0 else 1 end)
from #list_of_dates as ld
left join #dates_of_interest as di on ld.month_nbr = di.month_nbr
and ld.day_nbr = di.day_nbr
)
Print #DenVal
Both examples of 1/1/2014 - 12/31/2014 and 1/1/2014 - 9/1/2015 come up with the desired results of 2 and 3 respectively. There may be other ways of accomplishing this, but I thought that a Recursive CTE was the best option.

Can any one please tell me logic behind select DATENAME(month,29*5)

select DATENAME(month,29*5)
Can any one please tell me logic behind the above query.
How it always returns correct month name when provided month number as integer.
Datetime values in Sql server are stored on 8 bytes.
The first 4 bytes represents the date and the last 4 byte represents the time.
On the date part, date is stored as the number of days since 1900-01-01.
On the time part, it's the number of clock ticks since midnight.
There are 300 clock ticks per second, so a tick is 3.33333 milliseconds.
That's also the reason why datetime is only accurate to .003 of a second.
This query will hopefully help to explain:
SELECT CAST(0 As datetime) As Date_0,
29*5 As NumberOfDays,
CAST(29*5 as datetime) As TheDate,
DATENAME(month,29*5) As TheMonthName
Results:
Date_0 NumberOfDays TheDate TheMonthName
----------------------- ------------ ----------------------- ------------
1900-01-01 00:00:00.000 145 1900-05-26 00:00:00.000 May
As for the last part of your question, 29 (28 would work as well) is the magic number here - 30 is too big (May would be returned for 4 and 5) and 27 is too small - (September would be returned for 9 and 10).
Basically i'ts just math - get the number correctly so that each time you double it with any number between 1 and 12 will give you a number of days that sums up to a day that belongs to the correct month.
You can test it yourself using this script:
DECLARE #MagicNumber int = 28
;With cte as
(
select 1 as num
union all
select num + 1
from cte
where num < 12
)
SELECT num, DATENAME(month, #MagicNumber * num ) As TheMonthName
from cte
Just change the value of #MagicNumber and see the results you get.
I think I will able to explain.
The default year-month-day for any date data type is 1900-01-01. If we consider above select query, it add 29*5 days into default date and gives the MONTHNAME.
Select DATENAME(month,29*5)
Now understand the DATENAME
DateName - Returns a character string that represents the specified datepart of the specified date. Its have different -2 argument and give the different-2 result as per datepart.
Argument 1 - Is the part of the date to return.
Argument 2 - Is a any date (Is an expression that can be resolved to a
time, date, smalldatetime, datetime, datetime2, or datetimeoffset
value.)
Here we given month as a first argument. Which means it return monthname.
The calculation of 29*5 gives 145 answer and if we simply cast into date it consider as a days and calculate as 1900-01-01 + 145 and gives the date 1900-05-26 00:00:00.000.
Means if we get the month of this will give the 5 - MAY as a answer.
Execute this query and check the answer for the above logic.
Select DATENAME(month,29*5), (29*5) , DATENAME(month, '12:10:30.123'), DATENAME(month, getdate())
select cast (145 as datetime)
DECLARE #t datetime = '12:10:30.123';
SELECT DATENAME(month, 29*5), 145/30.00;
Check for further.
MSDN Link
Convert Month Number to Month Name Function in SQL (check the #user275683 answer)
If you are simply want to show the month corresponding to month number then you should have to use like this.
declare #intMonth as int
set #intMonth = 5
Select DateName( month , DateAdd( month , #intMonth , -1 ))

T-SQL Count days between two days (datediff not quite working)

We have a requirement to bill our customers per day. We bill for an asset's existence in our system on that day. So, I started with datediff...
select datediff(dd ,'2015-04-24 12:59:32.050' ,'2015-05-01 00:59:59.000');
Returns this:
7
But I need to count the following dates: 4/24,4/25,4/26,4/27,4/28,4/29, 4/30, 5/1, which are 8 days. So datediff isn't quite working right. I tried these variations below
--too simple, returns 7, i need it to return 8
select datediff(dd ,'2015-04-24 12:59:32.050', '2015-05-01 23:59:59.000');
--looking better, this returns the 8 i need
select ceiling(datediff(hh,'2015-04-24 12:59:32.050', '2015-05-01 23:59:59.000')/24.0);
-- returns 7, even though the answer still needs to be 8. (changed enddate)
select ceiling(datediff(hh,'2015-04-24 12:59:32.050', '2015-05-01 00:59:59.000')/24.0);
So, my question... How, in SQL, would I derive the date count like i described, since I believe datediff counts the number of day boundaries crossed.... My current best approach is loop through each day in a cursor and count. Ick.
Use CONVERT to get rid of the time part, add 1 to get the desired result:
SELECT DATEDIFF(dd,
CONVERT(DATE, '2015-04-24 12:59:32.050'),
CONVERT(DATE, '2015-05-01 00:59:59.000')) + 1;
It turns out the time part does not play any significant role in DATEDIFF when dd is used as the datepart argument. Hence, CONVERT is redundant. This:
SELECT DATEDIFF(dd, '2015-04-24 23:59:59.59','2015-05-01 00:00:00.000') + 1
will return 8 as well.
You could try this which would return 8 days.
select datediff(dd ,'2015-04-24 12:59:32.050' ,CASE DATEDIFF(Second,'2015-05-01 00:00:00.000','2015-05-01 23:59:59.000') WHEN 0 THEN '2015-05-01 23:59:59.000' ELSE DATEADD(dd,+1,'2015-05-01 23:59:59.000') END)
If you want to use variables for your dates then something like this would work.
BEGIN
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
DECLARE #EndDateOnly DATE
SET #StartDate = '2015-04-24 12:59:32.050'
SET #EndDate = '2015-05-01 23:59:59.000'
SET #EndDateOnly = CAST(#EndDate AS DATE)
SELECT datediff(dd ,#StartDate ,CASE DATEDIFF(Second,CAST(#EndDateOnly||' 00:00:00.000' AS DATETIME),#EndDate) WHEN 0 THEN #EndDate ELSE DATEADD(dd,+1,#EndDate) END)
END

DATEADD with part days

I'm having a little trouble getting a count of dates in SQL SERVER. I require the number of calender days between 2 dates start and ends dates included. The problem with the example below is that it always returns 10 when I believe it should be 11.
DECLARE #FROM DATETIME, #TO DATETIME
SET #FROM = '18/12/2011 00:00:00'
SET #TO = '28/12/2011 00:00:00'
SELECT
DATEDIFF(MINUTE,#FROM,#TO), -- Returns 14459
DATEDIFF(HOUR,#FROM,#TO), -- Returns 241
DATEDIFF(DAY,#FROM,#TO), -- Returns 10
CEILING(CAST((DATEDIFF(HOUR,#FROM,#TO) / 24) as DECIMAL(9,5))) --Returns 10
CEILING(CAST(CEILING(CEILING(CAST(DATEDIFF(SECOND,#FROM,#TO) as DECIMAL(18,5))) / 60) / 60 as DECIMAL(9,5)) / 24) --Returns 10
The bottom line works if there is at least 1 second between the times but I must account for all scenarios.
My only other thought was to simply add one to the date diff to account for the part days? Is that reliable?
DATEDIFF(DAY,#FROM,#TO) + 1
I came across when answering this question How to find the total between the dates for each values
Is an expression that can be resolved to a time, date, smalldatetime,
datetime, datetime2, or datetimeoffset value. date can be an
expression, column expression, user-defined variable or string
literal. startdate is subtracted from end date.
This is taken from MSDN here.
28-18 = 10. I think you will always have to add 1 in the scenario you have because of the definition for DATEDIFF.
You need to set the #TO date to:
SET #TO = '28/12/2011 23:59:59'
To get the number of days between two dates (ignoring the time of day), including the start and end date, try;
SELECT FLOOR(CONVERT(FLOAT, #TO))-FLOOR(CONVERT(FLOAT, #FROM))+1
Edit:
SELECT DATEDIFF(d, #FROM, #TO)+1
seems to return the exact same results, which would indeed make it a more elegant way of doing it. Always thought DATEDIFF timeparts were about truncating after the calculation (which would give the wrong result if the start time was later in the day than the end time) and not truncating before the calculation which gives the correct result for your case. You learn something new every day :)
If you want a close equivalent of the C# DateTime.TotalDays() function (i.e. to know fractional days) you can use the following:
DECLARE #start DATETIME = '10 Apr 2012 15:00'
DECLARE #end DATETIME = '12 Apr 2012 16:00'
SELECT CONVERT(FLOAT, DATEDIFF(SECOND, #start, #end)) / 86400
*Note: 86400 = seconds in a day = 24 hours x 60 mins x 60 seconds

Deterministic scalar function to get day of week for a date

SQL Server, trying to get day of week via a deterministic UDF.
Im sure this must be possible, but cant figure it out.
UPDATE: SAMPLE CODE..
CREATE VIEW V_Stuff WITH SCHEMABINDING AS
SELECT
MD.ID,
MD.[DateTime]
...
dbo.FN_DayNumeric_DateTime(MD.DateTime) AS [Day],
dbo.FN_TimeNumeric_DateTime(MD.DateTime) AS [Time],
...
FROM {SOMEWHERE}
GO
CREATE UNIQUE CLUSTERED INDEX V_Stuff_Index ON V_Stuff (ID, [DateTime])
GO
Ok, i figured it..
CREATE FUNCTION [dbo].[FN_DayNumeric_DateTime]
(#DT DateTime)
RETURNS INT WITH SCHEMABINDING
AS
BEGIN
DECLARE #Result int
DECLARE #FIRST_DATE DATETIME
SELECT #FIRST_DATE = convert(DATETIME,-53690+((7+5)%7),112)
SET #Result = datediff(dd,dateadd(dd,(datediff(dd,#FIRST_DATE,#DT)/7)*7,#FIRST_DATE), #DT)
RETURN (#Result)
END
GO
Slightly similar approach to aforementioned solution, but just a one-liner that could be used inside a function or inline for computed column.
Assumptions:
You don't have dates before
1899-12-31 (which is a Sunday)
You want to imitate ##datefirst = 7
#dt is smalldatetime, datetime,
date, or datetime2 data type
If you'd rather it be different, change the date '18991231' to a date with the weekday that you'd like to equal 1. The convert() function is key to making the whole thing work - cast does NOT do the trick:
((datediff(day, convert(datetime,
'18991231', 112), #dt) % 7)
+ 1)
I know this post is way-super-old, but I was trying to do a similar thing and came up with a different solution and figured I'd post for posterity. Plus I did some searching around and did not find much content on this question.
In my case, I was trying to use a computed column PERSISTED, which requires the calculation to be deterministic. The calculation I used is:
datediff(dd,'2010-01-03',[DateColumn]) % 7 + 1
The idea is to figure out a known Sunday that you know will occur before any possible date in your table (in this case, Jan 3 2010), then calculate the modulo 7 + 1 of the number of days since that Sunday.
The problem is that including a literal date in the function call is enough to mark it as non-deterministic. You can work around that by using the integer 0 to represent the epoch, which for SQL Server is Jan 1st, 1900, a Sunday.
datediff(dd,0,[DateColumn]) % 7 + 1
The +1 just makes the result work the same as datepart(dw,[datecolumn]) when datefirst is set to 7 (default for US), which sets Sunday to 1, Monday to 2, etc
I can also use this in conjunction with case [thatComputedColumn] when 1 then 'Sunday' when 2 then 'Monday' ... etc. Wordier, but deterministic, which was a requirement in my environs.
Taken from Deterministic scalar function to get week of year for a date
;
with
Dates(DateValue) as
(
select cast('2000-01-01' as date)
union all
select dateadd(day, 1, DateValue) from Dates where DateValue < '2050-01-01'
)
select
year(DateValue) * 10000 + month(DateValue) * 100 + day(DateValue) as DateKey, DateValue,
datediff(day, dateadd(week, datediff(week, 0, DateValue), 0), DateValue) + 2 as DayOfWeek,
datediff(week, dateadd(month, datediff(month, 0, DateValue), 0), DateValue) + 1 as WeekOfMonth,
datediff(week, dateadd(year, datediff(year, 0, DateValue), 0), DateValue) + 1 as WeekOfYear
from Dates option (maxrecursion 0)
There is an already built-in function in sql to do it:
SELECT DATEPART(weekday, '2009-11-11')
EDIT:
If you really need deterministic UDF:
CREATE FUNCTION DayOfWeek(#myDate DATETIME )
RETURNS int
AS
BEGIN
RETURN DATEPART(weekday, #myDate)
END
GO
SELECT dbo.DayOfWeek('2009-11-11')
EDIT again: this is actually wrong, as DATEPART(weekday) is not deterministic.
UPDATE:
DATEPART(weekday) is non-deterministic because it relies on DATEFIRST (source).
You can change it with SET DATEFIRST but you can't call it inside a stored function.
I think the next step is to make your own implementation, using your preferred DATEFIRST inside it (and not considering it at all, using for example Monday as first day).
The proposed solution has one problem - it returns 0 for Saturdays. Assuming that we're looking for something compatible with DATEPART(WEEKDAY) this is an issue.
Nothing a simple CASE statement won't fix, though.
Make a function, and have #dbdate varchar(8) as your input variable.
Have it return the following:
RETURN (DATEDIFF(dd, -1, convert(datetime, #dbdate, 112)) % 7)+1;
The value 112 is the sql style YYYYMMDD.
This is deterministic because the datediff does not receive a string input, if it were to receive a string it would no longer work because it internally converts it to a datetime object. Which is not deterministic.
Not sure what you are looking for, but if this is part of a website, try this php function from http://php.net/manual/en/function.date.php
function weekday($fyear, $fmonth, $fday) //0 is monday
{
return (((mktime ( 0, 0, 0, $fmonth, $fday, $fyear) - mktime ( 0, 0, 0, 7, 17, 2006))/(60*60*24))+700000) % 7;
}
The day of the week? Why don't you just use DATEPART?
DATEPART(weekday, YEAR_DATE)
Can't you just select it with something like:
SELECT DATENAME(dw, GETDATE());

Resources