• stephen.lightfoot - Tuesday, March 28, 2017 3:48 AM

    I too have struggled with this in the past.  The easiest and fastest way I have found to do this is by using the function below.  It works every time, even for leap years and is only 1 line of code!

    <<snip>>
     DECLARE @Age INT

     SET @Age = (0+Convert(Char(8),@AsAtDate,112) - Convert(Char(8),@DOB,112)) / 10000

    Using this little gem (thanks Stephen), here's a function which appears to calculate age in years, months, days, hours, minutes, seconds and milliseconds. Not only that: if you use DATEADD to add all of these units to the DOB, the result is the "date as of":
    ALTER FUNCTION [dbo].[il_ElapsedTime]
    /*
    Calculate the elapsed time between two datetimes
    as year, month, day, hour, minute, second, millisecond
    such that adding these values using DATEADD to the earlier
    value will yield the later value.
    This also calculates AGE.
    */
       (@Then DATETIME, @Now DATETIME)
    RETURNS TABLE WITH SCHEMABINDING AS
    RETURN
    SELECT
       ds.Years,
       mo.Months,
       ds.[Days],
       r.[Hours],
       r.[Minutes],
       r.Seconds,
       r.Milliseconds
    FROM (
     SELECT
      [Days] = CASE
       WHEN DAY(@Now) >= DAY(@Then) THEN DAY(@Now) - DAY(@Then)
       ELSE DAY(@Now) + (DATEDIFF(DAY,@Then,EOMONTH(@Then)))
       END,
      [Years] = (0 + CONVERT(CHAR(8),@Now,112) - CONVERT(CHAR(8),@Then,112)) / 10000
    ) ds
    CROSS APPLY (SELECT [Months] = DATEDIFF(month,DATEADD(YEAR,[Years],DATEADD(day,[Days],@Then)),@Now)) mo
    CROSS APPLY (
       SELECT
          [Hours] = DATEPART(HOUR,@Now-@Then),
          [Minutes] = DATEPART(MINUTE,@Now-@Then),
          [Seconds] = DATEPART(SECOND,@Now-@Then),
          [Milliseconds] = DATEDIFF(MILLISECOND,DATEADD(SECOND,(CASE WHEN DATEPART(MILLISECOND,@Then) > DATEPART(MILLISECOND,@Now) THEN -1 ELSE 0 END),DATEADD(MILLISECOND,DATEPART(MILLISECOND,@Then),DATEADD(MILLISECOND,0-DATEPART(MILLISECOND,@Now),@Now))),@Now)
    ) rWHERE @Now > @Then

    Here's a test harness too:
    SET NOCOUNT ON
    DECLARE @Today DATETIME = GETDATE()-13
    IF OBJECT_ID('TempDB..#DOBs') IS NULL BEGIN
     SELECT DOB = DATEADD(DAY,1-ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),CAST(@Today AS DATETIME))
     INTO #DOBs
     FROM
      (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) d1 (n),
      (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) d2 (n),
      (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) d3 (n),
      (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) d4 (n),
      (VALUES (0),(0),(0),(0),(0)) d5 (n)
    CREATE UNIQUE CLUSTERED INDEX ucx_Stuff ON #DOBs (DOB)
    END

    SELECT
     d.DOB,
     Today = @Today,
     x1.*,
     NewDate =
      DATEADD(YEAR,x1.Years,
      DATEADD(MONTH,x1.Months,
      DATEADD(DAY,x1.[Days],
      DATEADD(HOUR,x1.[Hours],
      DATEADD(MINUTE,x1.[Minutes],
      DATEADD(SECOND,x1.[Seconds],
      DATEADD(MILLISECOND,x1.[Milliseconds],d.DOB)))))))
    FROM #DOBs d
    CROSS APPLY [dbo].[il_ElapsedTime] (d.DOB, @Today) x1
    -- Without the NewDate calculation and with rows blackholed
    -- into variables; 50,000 rows in about half a second (SET STATISTICS TIME)

    Edit: Sorry, but the code display seems to be completely haywire today. If I enter EDIT mode, the code tags look fine.

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden