This is the solution post for – The Case of the Time it takes to Convert Time Zones
So did you figure it out? The case asked 2 questions:
- What was the problem?
- How did it happen?
What was the problem?
So lets walk through how it would convert a datetime from CT to UTC. We’d issue a SELECT dbo.GetDateOffset(@MyDate, 1); Next the function builds 2 in memory table variables – 1 for getting the start date of DST for the given year and 1 for getting the end date of DST for the given year. It then compares @MyDate to see if it’s within the DST range and then offsets accordingly. Now increase that to 50,000 calls, and we have some serious performance issues.
How did it happen?
It happened because of 2 reasons. First, the mantra of “I’ll get it to work and then worry about performance”. Secondly, and more importantly, I didn’t spend time thinking about the problem – you know creating a design. I figured it’s just a small function, not a huge system and therefore no big deal. However what is a huge system made up of? Oh yeah, that’s right, small functions and other pieces of code. Had I taken the time to actually design it I would have written it better.
The Solution
So let’s actually think about the problem again, and let’s begin by looking at Daylight Savings, but look at it a different way:
2005 and Before:
- Begins the first Sunday in April clocks are set ahead one hour, so 2:00 a.m. becomes 3:00 a.m.
- Ends the last Sunday in October clocks are set back one hour, so 2:00 a.m. becomes 1:00 a.m.
2006 and After:
- Ends the second Sunday in March clocks are set ahead one hour, so 2:00 a.m. becomes 3:00 a.m.
- Ends the first Sunday in November clocks are set back one hour, so 2:00 a.m. becomes 1:00 a.m.
That means for 10 months out of the year we know exactly what to do without any other investigation! For 2005 and before November, December, January, February, March I need to offset by 6, while May, June, July, August, September I need to offset by 5. For 2006 and after I shift accordingly. Awesome! However while that drastically reduces the amount of times the GetStartDST and GetEndDST is called, those functions are still called to decide the start and end date for April and October of 2005 and before as well as March and November 2006 and after.
We’re doing so well, let’s continue to break it down, and start with the first one. How can we figure out the first Sunday of a April in 2005. We know the date and we know the month, how about if we just get the weekday of the first of the month and then just offset? The idea being that if the first of the month is a Monday, then we know the first Sunday of the month is the 7th, also if the first of the month is a Saturday we know the first Sunday of the month is the 2nd. We don’t need to use an in memory table at all!!
So without further adieu, here was my final function:
CREATE FUNCTION dbo.GetOffsetDate
(
@Date datetime
, @Sign bit
--0 = UTC to CT
--1 = CT to UTC
)
RETURNS datetime
AS
BEGIN
IF @Date = CAST('1753-01-01 00:00:00.000' as datetime) AND @Sign = 0 RETURN @Date;
IF @Date = CAST('9999-12-31 23:59:59.997' as datetime) AND @Sign = 1 RETURN @Date;
DECLARE
@Time tinyint
, @Datetimeoffset datetimeoffset
, @BeginDST datetime
, @EndDST datetime
, @ReturnDate datetime;
SET @Time = 0;
SELECT @Datetimeoffset = CAST(@Date as datetimeoffset);
IF YEAR(@Date) >= 2006
BEGIN
IF MONTH(@Date) IN (1,2,12) SET @Time = 1; --Not in DST
IF MONTH(@Date) IN (4,5,6,7,8,9,10) SET @Time = 2; --In DST
END;
ELSE
BEGIN
IF MONTH(@Date) IN (1,2,3,11,12) SET @Time = 1; --Not in DST
IF MONTH(@Date) IN (5,6,7,8,9) SET @Time = 2; --In DST
END;
--Still Need to determin if DST or Not, either a starting or ending month
IF @Time = 0
BEGIN
DECLARE
@DSTDate date
, @WeekDay int;
IF YEAR(@Date) >= 2006
BEGIN
--2006 and forward second Sunday in March, clocks are set ahead - 2:00 a.m. becomes 3:00 a.m.
SET @DSTDate = CAST(CAST(YEAR(@Date) as varchar(4)) + '-03-01' as Date);
SET
@WeekDay =
CASE DATEPART(WEEKDAY, @DSTDate)
WHEN 1 THEN 8
WHEN 2 THEN 14
WHEN 3 THEN 13
WHEN 4 THEN 12
WHEN 5 THEN 11
WHEN 6 THEN 10
WHEN 7 THEN 9
END;
SELECT @BeginDST = CAST(CAST(@DSTDate as varchar) + ' 02:00:00' as datetime);
--2005 and before first Sunday in April, clocks are set ahead - 2:00 a.m. becomes 3:00 a.m.
SET @DSTDate = CAST(CAST(YEAR(@Date) as varchar(4)) + '-04-01' as Date);
SET
@WeekDay =
CASE DATEPART(WEEKDAY, @DSTDate)
WHEN 1 THEN 1
WHEN 2 THEN 7
WHEN 3 THEN 6
WHEN 4 THEN 5
WHEN 5 THEN 4
WHEN 6 THEN 3
WHEN 7 THEN 2
END;
SELECT @EndDST = CAST(CAST(@DSTDate as varchar) + ' 02:00:00' as datetime);
END;
ELSE
BEGIN
--2006 first Sunday in November, clocks are set back - 2:00 a.m. becomes 1:00 a.m.
SET @DSTDate = CAST(CAST(YEAR(@Date) as varchar(4)) + '-11-01' as Date);
SET
@WeekDay =
CASE DATEPART(WEEKDAY, @DSTDate)
WHEN 1 THEN 1
WHEN 2 THEN 7
WHEN 3 THEN 6
WHEN 4 THEN 5
WHEN 5 THEN 4
WHEN 6 THEN 3
WHEN 7 THEN 2
END;
SELECT @BeginDST = CAST(CAST(@DSTDate as varchar) + ' 02:00:00' as datetime);
--2005 last Sunday in October, clocks are set back - 2:00 a.m. becomes 1:00 a.m.
SET @DSTDate = CAST(CAST(YEAR(@Date) as varchar(4)) + '-10-31' as Date);
SET
@WeekDay =
Case DATEPART(WEEKDAY, @DSTDate)
WHEN 1 THEN 31
WHEN 2 THEN 30
WHEN 3 THEN 29
WHEN 4 THEN 28
WHEN 5 THEN 27
WHEN 6 THEN 26
WHEN 7 THEN 25
END;
SELECT @EndDST = CAST(CAST(@DSTDate as varchar) + ' 02:00:00' as datetime);
END;
END;
SELECT
@ReturnDate =
CASE
--UTC to CT
WHEN @Sign = 0
THEN
CASE
WHEN (@Datetimeoffset BETWEEN @BeginDST AND @EndDST) OR (@Time = 2)
--UTC to CDT
THEN
SWITCHOFFSET(@Datetimeoffset, '-05:00')
--UTC to CST
ELSE
SWITCHOFFSET(@Datetimeoffset, '-06:00')
END
--CT to UTC
ELSE
CASE
WHEN (@Datetimeoffset BETWEEN @BeginDST AND @EndDST) OR (@Time = 2)
--CDT to UTC
THEN
SWITCHOFFSET (@Datetimeoffset, '+05:00')
--CST to UTC
ELSE
SWITCHOFFSET (@Datetimeoffset, '+06:00')
END
END;
RETURN @ReturnDate;
END
As you’ll see it really was a rewrite, not just “fixed” for performance.
So what did we learn? Performance is not just a bolt-on at the end and always, always, always, think about the problem before you try code a solution.