Tuesday, June 22, 2010

Calculate DateTime difference in SQL Server with ITVF

If you work with Date types in SQL Server, calculating date difference is not simple because of the way DateDiff work. You need to write functions for calculating years difference between two dates, months difference, etc. You can do that creating like always scalar function that will do some calculations and return a value. If you use such a scalar function in a SELECT clause and that select returns large number of rows, you will soon realize that calculating scalar value will be significant part of the total execution time. Not just that, but also if you open profiler to see SQL batches, you will see all that calls to scalar functions for every row in the query as a row in the profiler, and with such a large number of rows in the profiler, you can miss the important ones. SQL Server since version 2000 have introduced a great feature called Inline Table Value Functions (iTVF). iTVF's are such an important feature because using them in combination with CROSS APPLY you can really have code reusability which is hard to achieve in any SQL language. If you do some complex columns calculations for example calculating some distributions, and you do that same calculations in many procedures, then creating ITVF and put the complex calculations can be your boost. After that changing the distribution and fixing bugs will be on one place only.
To return to the title and write functions that are very useful in any database. I will present two just to give you idea, from which you can create some others. First the function for calculating Years difference between two dates:

CREATE FUNCTION dbo.DateDiffYears
(
     @DateFrom AS DATETIME
     , @DateTo AS DATETIME
)
RETURNS TABLE
AS
     RETURN SELECT Years = DATEDIFF(yy, @DateFrom, @DateTo)
- CASE WHEN DATEPART(dy, @DateFrom) > DATEPART(dy, @DateTo) 
THEN 1 ELSE 0 END


Now you can use with CROSS APPLY in your queries. Imagine a table Persons that have a column BirthDate. You need to return their age in the result set. You will have something like this:
SELECT PersonId
       , Age = DY.Years
FROM   Persons
       CROSS APPLY dbo.DateDiffYears(BirthDate, GETDATE())

The second one is called DateSpan, not like .NET TimeSpan but if you need the same one, you can easely create one, which will return Years, Months and Days difference between two days:
CREATE FUNCTION dbo.DateSpan
(
       @DateFrom AS DATETIME
       , @DateTo AS DATETIME
)
RETURNS TABLE
AS
       RETURN SELECT Years, TotalMonths, Months = TotalMonths - Years * 12, TotalDays, [Days]
       FROM (
              Years = DATEDIFF(yy, @DateFrom, @DateTo) - CASE WHEN DATEPART(dy, @DateFrom) > DATEPART(dy, @DateTo) THEN 1 ELSE 0 END
              , TotalMonths = DATEDIFF(mm, @DateFrom, @DateTo) - CASE WHEN DATEPART(d, @DateFrom) > DATEPART(d, @DateTo) THEN 1 ELSE 0 END
              , TotalDays = DATEDIFF(dd, @DateFrom, @DateTo)
              , [Days] = ABS(DAY(@DateTo) - DAY(@DateFrom) + DAY(DATEADD(d, -DAY(@DateTo), @DateTo)))
       ) D
The performance difference between ITVF and scalar functions is big and lineary grows on number of rows, and you will not see any additional row in the profiler.