SQLServerCentral Article

Performance Test of New Date Manipulation Functions (SQL Spackle)

,

Introduction

SQL Server 2012 introduces several new date/time functions that allow you to build a date/time from the individual parts of a date/time value. As I was experimenting with them, I was thinking that this is pretty neat, and bound to be very useful. And then I started wondering how well these functions performed.

Before I had time to perform a test, I was working on something else where I needed to truncate a date/time value to the first day of the month. I immediately whipped out my tool belt – it has the routine that I learned from Lynn Pettis a few years back on the forums here from his blog post on Common Date Routines. This routine does the DateAdd(MONTH, DateDiff(MONTH, '1900-01-01', @Date), '1900-01-01') trick to see how many month boundaries exist from a known date to the tested date, and then to add this number of month boundaries back to the known date. With the month datepart being used, the result ends up being the first of the month of the tested date. Using other dateparts gives us other dates with remainders being truncated. See Lynn’s blog for more uses.

As I was looking at that wonderfully efficient code, I realized that I could get the same results with the DateTimeFromParts function in SQL Server 2012. So I coded an example, and it worked beautifully.

And now, I had a burning question. Performance-wise, just how do these different methods of truncating dates compare against each other? So, stop everything that I’m doing, and code a test.

The Test Data

First things first. A performance test requires a bunch of test data. And I mean a bunch. So, I grabbed some code from an article that Jeff Moden wrote at the beginning of 2011. Sure enough, there’s a million row dataset of random dates… which is exactly what I’m looking for. So, by borrowing some code from Jeff, I have my test data:

SELECT TOP 1000000
    RowNum   = IDENTITY(INT,1,1),
    SomeDate = CAST(RAND(CHECKSUM(NEWID()))*3653.0+36524.0 AS DATETIME)
  INTO dbo.DateTest
  FROM sys.All_Columns t1,
       sys.All_Columns t2;

Next up is to generate a test and to produce timings. In the following code, I truncate each date to the first day of the year, month and quarter, and then I truncate the time down to the hour, minute, and second. I perform this conversion four times: the first time using Lynn’s DateAdd/DateDiff code; the second time by stripping out the various parts of the date, and putting it back together with the DateTimeFromParts function; the third time is using pre-extracted date/time information in this table; and the fourth time is using the new SQL Server 2012 Format function. All queries dump the results into temp tables. I run this block of code with SET STATISTICS TIME ON just before each query to capture the time to run each query, and that batch is performed 25 times.

The Test

First up, the code:

SET STATISTICS TIME OFF;
USE master;
GO
IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = 'SandBox')
   CREATE DATABASE SandBox;
GO
USE SandBox;
GO
-- Build a million-row test table with random dates
IF OBJECT_ID('dbo.DateTest','U') IS NULL
SELECT TOP 1000000
    RowNum   = IDENTITY(INT,1,1),
    SomeDate = CAST(RAND(CHECKSUM(NEWID()))*3653.0+36524.0 AS DATETIME)
  INTO dbo.DateTest
  FROM sys.All_Columns t1,
       sys.All_Columns t2;
GO
-- Add columns to have pre-defined the parts of this date
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE name = 'Yr' AND object_id = OBJECT_ID('dbo.DateTest','U'))
ALTER TABLE dbo.DateTest
  ADD Yr SMALLINT,
      Mth TINYINT,
      Dy TINYINT,
      Hr TINYINT,
      Mn TINYINT,
      Sec TINYINT,
      Qtr TINYINT;
GO
-- Populate the columns with the appropriate data from the date
UPDATE dbo.DateTest
   SET Yr = DATEPART(YEAR, SomeDate),
       Mth = DATEPART(MONTH, SomeDate),
       Dy = DATEPART(DAY, SomeDate),
       Hr = DATEPART(HOUR, SomeDate),
       Mn = DATEPART(MINUTE, SomeDate),
       Sec = DATEPART(SECOND, SomeDate),
       Qtr = ((CEILING(MONTH(SomeDate)/3.0)*3)-2);
-- Declare and populate variables for the date math.
DECLARE @Base   DATETIME = '1900-01-01T00:00:00',
        @Base2  DATETIME = '2000-01-01T00:00:00';
-- Ensure temp tables don't exist yet (for subsequent runs)
IF OBJECT_ID('tempdb..#test1','U') IS NOT NULL DROP TABLE #test1;
IF OBJECT_ID('tempdb..#test2','U') IS NOT NULL DROP TABLE #test2;
IF OBJECT_ID('tempdb..#test3','U') IS NOT NULL DROP TABLE #test3;
IF OBJECT_ID('tempdb..#test4','U') IS NOT NULL DROP TABLE #test4;
-- Test #1: Use the DateAdd/DateDiff method.
RAISERROR ('DateAdd/DateDiff', 10, 1) WITH NOWAIT;
SET STATISTICS TIME ON;
SELECT  SomeDate,
    DATEADD(YEAR, DATEDIFF(YEAR, @Base, SomeDate), @Base) AS [FirstDayOfYear],
    DATEADD(MONTH,   DATEDIFF(MONTH,   @Base,  SomeDate), @Base)  AS [FirstDayOfMonth],
    DATEADD(QUARTER, DATEDIFF(QUARTER, @Base,  SomeDate), @Base)  AS [FirstDayOfQuarter],
    DATEADD(HOUR,    DATEDIFF(HOUR,    @Base,  SomeDate), @Base)  AS [StartOfHour],
    DATEADD(MINUTE,  DATEDIFF(MINUTE,  @Base,  SomeDate), @Base)  AS [StartOfMinute],
    DATEADD(SECOND,  DATEDIFF(SECOND,  @Base2, SomeDate), @Base2) AS [StartOfSecond]
 INTO    #test1
 FROM    dbo.DateTest;
SET STATISTICS TIME OFF;
-- Test #2: Extract out the parts of the date, and put them back together in the DateTimeFromParts function.
RAISERROR ('DateTimeFromParts1', 10, 1) WITH NOWAIT;
SET STATISTICS TIME ON;
SELECT  SomeDate,
        DATETIMEFROMPARTS(ca.Yr, 1,      1,     0,     0,     0,      0) AS [FirstDayOfYear],
        DATETIMEFROMPARTS(ca.Yr, ca.Mth, 1,     0,     0,     0,      0) AS [FirstDayOfMonth],
        DATETIMEFROMPARTS(ca.Yr, ca.Qtr, 1,     0,     0,     0,      0) AS [FirstDayOfQuarter],
        DATETIMEFROMPARTS(ca.Yr, ca.Mth, ca.Dy, ca.Hr, 0,     0,      0) AS StartOfHour,
        DATETIMEFROMPARTS(ca.Yr, ca.Mth, ca.Dy, ca.Hr, ca.Mn, 0,      0) AS StartOfMinute,
        DATETIMEFROMPARTS(ca.Yr, ca.Mth, ca.Dy, ca.Hr, ca.Mn, ca.Sec, 0) AS StartOfSecond
 INTO    #test2
 FROM    dbo.DateTest dt
      CROSS APPLY (SELECT DATEPART(YEAR,   dt.SomeDate) AS Yr,
                         DATEPART(MONTH,  dt.SomeDate) AS Mth,
                         ((CEILING(MONTH(dt.SomeDate)/3.0)*3)-2) AS Qtr,
                         DATEPART(DAY,    dt.SomeDate) AS Dy,
                         DATEPART(HOUR,   dt.SomeDate) AS Hr,
                         DATEPART(MINUTE, dt.SomeDate) AS Mn,
                         DATEPART(SECOND, dt.SomeDate) AS Sec ) ca;
SET STATISTICS TIME OFF;
-- Test #3: Build the date from the pre-extracted appropriate parts with the DateTimeFromParts function.
RAISERROR ('DateTimeFromParts2', 10, 1) WITH NOWAIT;
SET STATISTICS TIME ON;
SELECT  SomeDate,
        DATETIMEFROMPARTS(dt.Yr, 1,      1,     0,     0,     0,      0) AS [FirstDayOfYear],
        DATETIMEFROMPARTS(dt.Yr, dt.Mth, 1,     0,     0,     0,      0) AS [FirstDayOfMonth],
        DATETIMEFROMPARTS(dt.Yr, dt.Qtr, 1,     0,     0,     0,      0) AS [FirstDayOfQuarter],
        DATETIMEFROMPARTS(dt.Yr, dt.Mth, dt.Dy, dt.Hr, 0,     0,      0) AS StartOfHour,
        DATETIMEFROMPARTS(dt.Yr, dt.Mth, dt.Dy, dt.Hr, dt.Mn, 0,      0) AS StartOfMinute,
        DATETIMEFROMPARTS(dt.Yr, dt.Mth, dt.Dy, dt.Hr, dt.Mn, dt.Sec, 0) AS StartOfSecond
 INTO    #test3
 FROM    dbo.DateTest dt;
SET STATISTICS TIME OFF;
-- Test #4: Utilize the new Format function.
-- Note: the FirstDayOfQuarter can't be done with Format, so DateAdd/DateDiff is used.
-- Just because I like it.
RAISERROR ('Format test', 10, 1) WITH NOWAIT;
SET STATISTICS TIME ON;
SELECT  SomeDate,
        CAST(FORMAT(SomeDate, 'yyyy-01-01') AS DATETIME) AS [FirstDayOfYear],
        CAST(FORMAT(SomeDate, 'yyyy-MM-01') AS DATETIME) AS [FirstDayOfMonth],
        DATEADD(QUARTER, DATEDIFF(QUARTER, @Base, SomeDate), @Base) AS [FirstDayOfQuarter],
        CAST(FORMAT(SomeDate, 'yyyy-MM-dd HH:00:00') AS DATETIME) AS StartOfHour,
        CAST(FORMAT(SomeDate, 'yyyy-MM-dd HH:mm:00') AS DATETIME) AS StartOfMinute,
        CAST(FORMAT(SomeDate, 'yyyy-MM-d HH:mm:ss') AS DATETIME) AS StartOfSecond
 INTO    #test4
 FROM    dbo.DateTest;
SET STATISTICS TIME OFF;
GO 25

The Results

I then took the results from the time statistics, and used Jeff’s DelimitedSplit function to get the timings from each query into a result set. This data was coped into Excel, where the following line graphs tell the rest of the story:

Well, those graphs don’t tell the whole story… what these line graphs tell us is that the Format function is so inefficient, that it skews the graph and we’re not able to really compare the remaining methods. So, let’s remove the Format from the graphs, and look at them again.

My Analysis

To perform the same task (take an existing date, and truncate it to different levels), the DateAdd/DateDiff method is about twice as fast as the DateTimeFromParts function. However, if the individual parts of the date are already available, then the two methods are an essential tie, with the DateTimeFromParts having a very slight advantage. And, obviously, the Format function takes about 100 times longer than the fastest method here.

I think I’ll continue to use Lynn’s DateAdd/DateDiff method when I need to truncate a date.

(Test environment: Windows 7 x64; SQL Server 2012 x64 RTM; 8GB ram)

Links

Common Date Routines: http://www.sqlservercentral.com/blogs/lynnpettis/2009/03/25/some-common-date-routines/

Formatting Dates with 3 Character Months (SQL Spackle): http://www.sqlservercentral.com/articles/formatting/72066/

Tally OH! An Improved SQL 8K “CSV Splitter” Function:  http://www.sqlservercentral.com/articles/Tally+Table/72993/

DateTimeFromParts: http://msdn.microsoft.com/en-us/library/hh213233(v=sql.110).aspx

Format: http://msdn.microsoft.com/en-us/library/hh213505(v=sql.110).aspx

Resources

Rate

4.75 (24)

You rated this post out of 5. Change rating

Share

Share

Rate

4.75 (24)

You rated this post out of 5. Change rating