sql timezone calculation - sql

I have a table which stores the storecodes and their timezone. Now based on a given local date, I need to know if that date converted to stores local date was a in a weekend or not. Now I already know how to get the weekend part. I am struggling with the conversion. I am actually confused. My table has for example the following two values:
Store / TimeZone(Standard)
100 / 1 (This is frankfurt)
200 / 2 (This is tel aviv)
Our sql server is located in LA. I used the following code to get the UTC date:
DECLARE #LocalDate DATETIME, #UTCDate DATETIME
SET #LocalDate = GetDate()
-- convert local date to utc date
SET #UTCDate = DATEADD(Hour, DATEDIFF(Hour, GETUTCDATE(), GETDATE()), #LocalDate)
If I understood everything correct, I can now simply add the required hours to the #UTCDate to get the #UTCDate of that local timezone, correct?
For frankfurt it would be:
print DATEADD(HOUR, 1, #UTCDate)
Now this returns me the UTCDate for Frankfurt. How would I get the local date of Frankfurt though?
Edit: I am using Sql 2005.
Edit2: Complete example that is still confusing to me:
DECLARE #LocalDate DATETIME, #UTCDate DATETIME
SET #LocalDate = GetDate()
-- convert local date to utc date
SET #UTCDate = DATEADD(Hour, DATEDIFF(Hour, GETUTCDATE(), GetDate()), #LocalDate)
print GetDate()
print #UTCDate
print DATEADD(HOUR, 1, #UTCDate)
Output:
Jan 11 2010 12:32PM
Jan 11 2010 4:32AM
Jan 11 2010 5:32AM
Now does this mean, that if its 12:32PM in LA, then its 5:32AM in Franfurt? That seems be incorrect though. It should be 9:32PM in Franfurt.

You shouldn't start with local time. Start directly with UTC time:
DECLARE #UTCDate DATETIME
SET #UTCDate = GETUTCDATE();
Frankfurt is one hour ahead of UTC (UTC + 1) when summer time is not in effect so you add one hour:
print DATEADD(HOUR, 1, #UTCDate);
Remember that time zones are not on 60 minutes intervals, Mumbai is UTC + 5:30 and Nepal is UTC + 5:45. You must also account for daylight savings, and those change regularly. Argentine for instance opts to use the daylight on a year-by-year basis based on the ammount of water stored in its hydro power plants.
To sum up: always use UTC and leave the localisation of time to the client display and reporting.

If you have a UTCDate, it is the same for all timezones... I.e., when it's 1 am UTC in New York, it is also 1 am UTC in Frankfort. To get local time for any timnezone just add the offset (that's the value you have in your table) from the UTC DateTime... i.e., when it's 1 AM UTC, it's 2 am local in Frankfort. To remember whether to add or subtract, just remember that its always Earlier East.

Here is a SQL-Only implementation I recently put together you can use (Forums suggest that CLR is the only method since TSQL is needlessly complicated to achieve this in - not really afaik). I implemented via an inline function which avoids RBAR (You can profile and test this to confirm).
Performance is great even over old-school Distributed Partitioned Views too.
Make sure your indexing is good for it, even on the string manipulations on the DateTime Fields (To bypass the Year DatePart dependencies) I get the desired seeks. Some of the underlying partitioned tables are over 80GB in size.
Of course, you will need to add your timezone rows as you need and remember to keep the daylight savings start and end dates updated (They can change).
In both cases of timezone and daylight savings, offsets are in minutes so this works for all scenarios I have bumped into so far.
Lastly, the Daylight savings offset is always a positive number, note the function caters for this to suite the rule of thumb (Spring Forward, Fall Back)
If Not Exists (Select Name from sys.objects where name = 'tblTimeZones' and type = 'U')
Begin
Create Table tblTimeZones(
[ID] Int Identity (0,1) NOT NULL,
[UserID] Int NOT NULL,
[Description] NVarchar(128) NOT NULL,
[TZ_OffSet_Mins] Int NOT NULL,
[Use_DST] Bit NOT NULL,
[DST_AddOffSet] Int NOT NULL,
[DST_StartDate] DateTime NOT NULL Constraint DF_DST_StartDate Default ('1900-01-01 00:00:00.000'),
[DST_EndDate] DateTime NOT NULL Constraint DF_DST_EndDate Default ('1900-01-01 00:00:00.000'),
Constraint PK_tblTimeZones Primary Key NonClustered (ID),
Constraint UQ_tblTimeZones_Description Unique Clustered ([Description])
)
End
Go
If Exists (Select Name from sys.objects where name = 'fncV1_iCalcDateInTimeZone' and type = 'IF')
Begin
Drop Function fncV1_iCalcDateInTimeZone
End
Go
Create Function fncV1_iCalcDateInTimeZone
(
#UserID Int, #DateAndTime DateTime, #EntID Int
)
Returns Table
With SchemaBinding
As
Return (
Select TZDateAndTime =
DateAdd(
mi,
tz.TZ_OffSet_Mins +
-- Daylight Savings STARTS earlier in the Year than Ends (So, Northern Hemisphere), In Daylight Savings Time Period and Daylight Savings In Use
Case when
tz.Use_DST = 1
And SubString(Convert(Varchar(23),tz.DST_StartDate,21), 6, 18) < SubString(Convert(Varchar(23),tz.DST_EndDate,21), 6, 18)
And SubString(Convert(Varchar(23),#DateAndTime,21), 6, 18) >= SubString(Convert(Varchar(23),tz.DST_StartDate,21), 6, 18)
And SubString(Convert(Varchar(23),#DateAndTime,21), 6, 18) < SubString(Convert(Varchar(23),tz.DST_EndDate,21), 6, 18)
then tz.DST_AddOffSet
Else 0
End
+
-- Daylight Savings STARTS later in the Year than Ends (So, Southern Hemisphere), In Daylight Savings Surround Period
Case when
tz.Use_DST = 1
And SubString(Convert(Varchar(23),tz.DST_StartDate,21), 6, 18) > SubString(Convert(Varchar(23),tz.DST_EndDate,21), 6, 18)
And
(
SubString(Convert(Varchar(23),#DateAndTime,21), 6, 18) >= SubString(Convert(Varchar(23),tz.DST_StartDate,21), 6, 18)
Or
SubString(Convert(Varchar(23),#DateAndTime,21), 6, 18) < SubString(Convert(Varchar(23),tz.DST_EndDate,21), 6, 18)
)
then tz.DST_AddOffSet
Else 0
End
,#DateAndTime
)
From dbo.tblSomeEntityTable rd
Inner Join dbo.tblBranch b on rd.BranchID = b.ID
Inner Join dbo.tblUsers u on u.ID = #UserID
Inner Join dbo.tblTimeZones tz on tz.ID = case when u.UserTZOverBranchTZ = 1 then u.TimeZoneID else b.TimeZoneID End
Where
rd.ID = Case when ISNULL(#EntID, -1) = -1 then rd.ID else #EntID End
)
Go

Related

Convert to my time zone - SQL Server

Could somebody give me a help?
I have a source data where it is in another server in another country, when I get it to my B.I Staging area, I want to convert it to my time zone, but my SQL Server versions is 2014.
I can't use AT TIME ZONE SQL Server function.
Is there any other way to do this without fixing a hard coded value like below?
The below result doesn't solve my problem, because its hard coded values:
SELECT SWITCHOFFSET (DATETIMEFIELD, '-03:00')
SELECT TODATETIMEOFFSET(DATETIMEFIELD,'-03:00')
Ps: Not forgetting about DAYLIGHT SAVING
Without any sample data, this is very much a stab in the dark, that at least tries to explain it. Note, that this isn't fool proof. For example, when the clocks go back then the times 01:00:00 - 01:59:59 will occur twice. These times will always be assumed to be before DST, regardless of if they really were.
Anyway, firstly I created a small table with the dates that DST occurred both in the UK (GMT: UTC + 0) and in Eastern America (EST: UTC - 5).
Then, I created a query as below, using that data.
USE Sandbox;
GO
--SAmple Daylight Savings Table
CREATE TABLE DaylightSaving (Timezone char(3),
ClocksChangeDate datetime,
Change smallint,
UTCDiff smallint);
--Dates for UK
INSERT INTO DaylightSaving
VALUES ('GMT','20170326 02:00:00',1,0),
('GMT','20171029 02:00:00',0,0),
('GMT','20180325 02:00:00',1,0),
('GMT','20181028 02:00:00',0,0),
('GMT','20190331 02:00:00',1,0),
('GMT','20191027 02:00:00',0,0);
--Dates for Eastern America
INSERT INTO DaylightSaving
VALUES ('EST','20170312',1,-5),
('EST','20171105',0,-5),
('EST','20180311',1,-5),
('EST','20181104',0,-5),
('EST','20190310',1,-5),
('EST','20191103',0,-5);
GO
--Made up sample data
CREATE TABLE SampleData (DateAndTime datetime, Timezone char(3));
INSERT INTO SampleData
VALUES ('20180123 12:28:00.000','EST'),
('20180523 19:58:00.000','EST'),
('20181101 07:19:00.000','EST'),
('20190330 20:50:00.000','EST'),
('20190330 21:05:00.000','EST');
GO
SELECT DateAndTime AS ESTTime,
DATEADD(HOUR, 5 + DT.Change - ST.Change, DateAndTime) AS GMTTime,
DATEDIFF(HOUR, DateAndTime, DATEADD(HOUR, DT.UTCDiff - ST.UTCDiff + DT.Change - ST.Change, DateAndTime)) AS TimeZoneDifference,
ST.ClocksChangeDate, DT.ClocksChangeDate
FROM SampleData SD
CROSS APPLY (SELECT TOP 1 *
FROM DaylightSaving sq
WHERE sq.ClocksChangeDate <= SD.DateAndTime
AND Timezone = SD.Timezone
ORDER BY sq.ClocksChangeDate DESC) ST
CROSS APPLY (SELECT TOP 1 *
FROM DaylightSaving sq
WHERE sq.ClocksChangeDate <= DATEADD(HOUR, sq.UTCDiff - ST.UTCDiff,SD.DateAndTime)
AND sq.Timezone = 'GMT' --your destination Timezone
ORDER BY sq.ClocksChangeDate DESC) DT;
GO
DROP TABLE DaylightSaving;
DROP TABLE SampleData;
Edit: Note that that the difference between the 2 zones was hard coded (I have a DATEADD with a hard coded value of 5), as this is an example. You will need to extrapolate this for your own system, but as we have no concept of what that looks like, you'll need to do that work.
Edit 2:Felt like adding the timezone diffs in. Only hard coded value now is the value of the local timezone ('GMT' in this case in the second CROSS APPLY)
As several people have said, datetime has no concept of timezones. If this is paramount to your data, then don't use datetime, use datetimeoffset. That stores the UTC +/- value as part of the value; making things like this trivial.

Sql Server get current date time in Europe

SELECT GETDATE()
The above query in SQL Server will return current date and time in USA because server is located in USA. How can I modify it to retrieve current date and time in Europe?
try this: Set #Offset =
0 for Greenwich Mean Time (GMT) - Great Britain,
1 for Central European Time (CET) -Netherlands, Germnany, France
etc,
2 for Eastern European Time (EET) Czechoslovakia, Hungary, etc.
Set #Offset = 0, 1, 2 ...
Declare #offset tinyInt = 0
Select GetUtcDate() + #offset/24.0
Use SwitchDateTimeOffset function with the datetimeoffset data type (requires SQL Server 2008 or higher).
Demo query:
CREATE TABLE TimeZone (ID int identity,
LocalTime datetimeoffset);
INSERT INTO TimeZone values ('2008-05-21 17:50:01.1234567 -08:00'),
('2008-05-21 18:50:01.1234567 -08:00');
SELECT * FROM TimeZone;
SELECT ID, SWITCHOFFSET(LocalTime,'+01:00') [time for new time zone]
FROM TimeZone;
DROP TABLE TimeZone;
In your scenario this becomes:
SELECT SWITCHOFFSET(SYSDATETIMEOFFSET(),'+01:00')
As pointed out by p.campbell, you still have to decide which timezone represents 'Europe' for your purposes.

How to Insert varchar data into a datetime field (SQL Server 2005)?

I have this data I want inserted to a table. Sample data MM/DD/YYYY + 1 day and 12:00:00 PM
Basically what I need is to insert the current date + 1 day and specific time of 12:00:00 PM.
My code is this:
DECLARE #MyEstimatedDate as varchar(100) ---TEMPORARY CONTAINER
DECLARE #MyEstimatedDate1 as varchar(100) ---TEMPORARY CONTAINER
DECLARE #MyEstimatedDate2 as varchar(100) ---TEMPORARY CONTAINER
DECLARE #MyEstimatedDate3 as DATETIME ---FINAL DATA NEEDED. This is the data I want inserted.
SET #MyEstimatedDate = DATEADD(day,1,GETDATE())
SET #MyEstimatedDate1 = CONVERT(VARCHAR(100),#MyEstimatedDate,101)
SET #MyEstimatedDate2 = #MyEstimatedDate1 + ' 12:00:00 PM'
SET #MyEstimatedDate3 = cast(#MyEstimatedDate2 as datetime) ---I believe this is the error
Error message I get:
The conversion of a char data type to a datetime data type resulted in an out-of-range datetime value.
Just don't use varchar when manipulating datetime data. SQL Server 2005 offers enough tools for you to be able to avoid conversion.
The following is a more or less known method of dropping the time part from a datetime value:
SELECT DATEADD(DAY, DATEDIFF(DAY, 0, #InputDateTime), 0);
In the above example, DATEDIFF calculates the number of days between a date specified as 0 and the given date. The number of days is then added by the DATEADD function to the 0 date. The final result is a datetime value with the time of 00:00:00 and the same date as #InputDateTime. This is because the 0 date is an integer representation of 1900-01-01 00:00:00: its time part is zero and, since we have incremented it by a whole number of days, so is the result's time part.
Now, if instead of the DATEDIFF days you add DATEDIFF+1, you will get the next day. Furthermore, if instead of 0 as the date to be incremented you use 12:00, you will get the next day's noon, which appears to be what you want. So, the final expression will look like this:
SELECT DATEADD(DAY, DATEDIFF(DAY, 0, #InputDateTime) + 1, '12:00');
Since your input timestamp is supposed to be the current date & time, just replace #InputDateTime with GETDATE():
SELECT DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()) + 1, '12:00');
Why not simply do
cast(dateadd(day, 1, getdate()) as date)
that was for midnight. For noon, do this
dateadd(hour, 12, cast(cast(dateadd(day, 1, getdate()) as date) as datetime))
forget the above, it's wrong.
Correct answer is
dateadd(hour, 12, cast(cast(dateadd(day, 1, getdate()) as date) as datetime))
This time I even tested it.
datetime type contains date + time. In your case for #MyEstimatedDate1 need just a date
DECLARE #MyEstimatedDate varchar(100) ---TEMPORARY CONTAINER
DECLARE #MyEstimatedDate1 varchar(100) ---TEMPORARY CONTAINER
DECLARE #MyEstimatedDate2 varchar(100) ---TEMPORARY CONTAINER
DECLARE #MyEstimatedDate3 DATETIME ---FINAL DATA NEEDED. This is the data I want inserted.
SET #MyEstimatedDate = DATEADD(day, 1, GETDATE())
SET #MyEstimatedDate1 = CONVERT(VARCHAR(100), CAST(#MyEstimatedDate AS date), 101)
SET #MyEstimatedDate2 = #MyEstimatedDate1 + ' 12:00:00 PM'
SET #MyEstimatedDate3 = cast(#MyEstimatedDate2 as datetime) ---I believ
OR simple to use it
SELECT DATEADD(hour, 36, GETDATE() - CAST(GETDATE() AS time))

How can I compare time in SQL Server?

I'm trying to compare time in a datetime field in a SQL query, but I don't know if it's right. I don't want to compare the date part, just the time part.
I'm doing this:
SELECT timeEvent
FROM tbEvents
WHERE convert(datetime, startHour, 8) >= convert(datetime, #startHour, 8)
Is it correct?
I'm asking this because I need to know if 08:00:00 is less or greater than 07:30:00 and I don't want to compare the date, just the time part.
Thanks!
Your compare will work, but it will be slow because the dates are converted to a string for each row. To efficiently compare two time parts, try:
declare #first datetime
set #first = '2009-04-30 19:47:16.123'
declare #second datetime
set #second = '2009-04-10 19:47:16.123'
select (cast(#first as float) - floor(cast(#first as float))) -
(cast(#second as float) - floor(cast(#second as float)))
as Difference
Long explanation: a date in SQL server is stored as a floating point number. The digits before the decimal point represent the date. The digits after the decimal point represent the time.
So here's an example date:
declare #mydate datetime
set #mydate = '2009-04-30 19:47:16.123'
Let's convert it to a float:
declare #myfloat float
set #myfloat = cast(#mydate as float)
select #myfloat
-- Shows 39931,8244921682
Now take the part after the comma character, i.e. the time:
set #myfloat = #myfloat - floor(#myfloat)
select #myfloat
-- Shows 0,824492168212601
Convert it back to a datetime:
declare #mytime datetime
set #mytime = convert(datetime,#myfloat)
select #mytime
-- Shows 1900-01-01 19:47:16.123
The 1900-01-01 is just the "zero" date; you can display the time part with convert, specifying for example format 108, which is just the time:
select convert(varchar(32),#mytime,108)
-- Shows 19:47:16
Conversions between datetime and float are pretty fast, because they're basically stored in the same way.
convert(varchar(5), thedate, 108) between #leftTime and #rightTime
Explanation:
if you have varchar(5) you will obtain HH:mm
if you have varchar(8) you obtain HH:mm ss
108 obtains only the time from the SQL date
#leftTime and #rightTime are two variables to compare
If you're using SQL Server 2008, you can do this:
WHERE CONVERT(time(0), startHour) >= CONVERT(time(0), #startTime)
Here's a full test:
DECLARE #tbEvents TABLE (
timeEvent int IDENTITY,
startHour datetime
)
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 0, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 1, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 2, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 3, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 4, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 5, GETDATE())
--SELECT * FROM #tbEvents
DECLARE #startTime datetime
SET #startTime = DATEADD(mi, 65, GETDATE())
SELECT
timeEvent,
CONVERT(time(0), startHour) AS 'startHour',
CONVERT(time(0), #startTime) AS '#startTime'
FROM #tbEvents
WHERE CONVERT(time(0), startHour) >= CONVERT(time(0), #startTime)
Just change convert datetime to time that should do the trick:
SELECT timeEvent
FROM tbEvents
WHERE convert(time, startHour) >= convert(time, #startHour)
if (cast('2012-06-20 23:49:14.363' as time) between
cast('2012-06-20 23:49:14.363' as time) and
cast('2012-06-20 23:49:14.363' as time))
One (possibly small) issue I have noted with the solutions so far is that they all seem to require a function call to process the comparison. This means that the query engine will need to do a full table scan to seek the rows you are after - and be unable to use an index. If the table is not going to get particularly large, this probably won't have any adverse affects (and you can happily ignore this answer).
If, on the other hand, the table could get quite large, the performance of the query could suffer.
I know you stated that you do not wish to compare the date part - but is there an actual date being stored in the datetime column, or are you using it to store only the time? If the latter, you can use a simple comparison operator, and this will reduce both CPU usage, and allow the query engine to use statistics and indexes (if present) to optimise the query.
If, however, the datetime column is being used to store both the date and time of the event, this obviously won't work. In this case if you can modify the app and the table structure, separate the date and time into two separate datetime columns, or create a indexed view that selects all the (relevant) columns of the source table, and a further column that contains the time element you wish to search for (use any of the previous answers to compute this) - and alter the app to query the view instead.
Using float does not work.
DECLARE #t1 datetime, #t2 datetime
SELECT #t1 = '19000101 23:55:00', #t2 = '20001102 23:55:00'
SELECT CAST(#t1 as float) - floor(CAST(#t1 as float)), CAST(#t2 as float) - floor(CAST(#t2 as float))
You'll see that the values are not the same (SQL Server 2005). I wanted to use this method to check for times around midnight (the full method has more detail) in which I was comparing the current time for being between 23:55:00 and 00:05:00.
Adding to the other answers:
you can create a function for trimming the date from a datetime
CREATE FUNCTION dbo.f_trimdate (#dat datetime) RETURNS DATETIME AS BEGIN
RETURN CONVERT(DATETIME, CONVERT(FLOAT, #dat) - CONVERT(INT, #dat))
END
So this:
DECLARE #dat DATETIME
SELECT #dat = '20080201 02:25:46.000'
SELECT dbo.f_trimdate(#dat)
Will return
1900-01-01 02:25:46.000
Use Datepart function: DATEPART(datepart, date)
E.g#
SELECT DatePart(#YourVar, hh)*60) +
DatePart(#YourVar, mi)*60)
This will give you total time of day in minutes allowing you to compare more easily.
You can use DateDiff if your dates are going to be the same, otherwise you'll need to strip out the date as above
You can create a two variables of datetime, and set only hour of date that your need to compare.
declare #date1 datetime;
declare #date2 datetime;
select #date1 = CONVERT(varchar(20),CONVERT(datetime, '2011-02-11 08:00:00'), 114)
select #date2 = CONVERT(varchar(20),GETDATE(), 114)
The date will be "1900-01-01" you can compare it
if #date1 <= #date2
print '#date1 less then #date2'
else
print '#date1 more then #date2'
SELECT timeEvent
FROM tbEvents
WHERE CONVERT(VARCHAR,startHour,108) >= '01:01:01'
This tells SQL Server to convert the current date/time into a varchar using style 108, which is "hh:mm:ss". You can also replace '01:01:01' which another convert if necessary.
I believe you want to use DATEPART('hour', datetime).
Reference is here:
http://msdn.microsoft.com/en-us/library/ms174420.aspx
I don't love relying on storage internals (that datetime is a float with whole number = day and fractional = time), but I do the same thing as the answer Jhonny D. Cano. This is the way all of the db devs I know do it. Definitely do not convert to string. If you must avoid processing as float/int, then the best option is to pull out hour/minute/second/milliseconds with DatePart()
I am assuming your startHour column and #startHour variable are both DATETIME; In that case, you should be converting to a string:
SELECT timeEvent
FROM tbEvents
WHERE convert(VARCHAR(8), startHour, 8) >= convert(VARCHAR(8), #startHour, 8)
below query gives you time of the date
select DateAdd(day,-DateDiff(day,0,YourDateTime),YourDateTime) As NewTime from Table
#ronmurp raises a valid concern - the cast/floor approach returns different values for the same time. Along the lines of the answer by #littlechris and for a more general solution that solves for times that have a minute, seconds, milliseconds component, you could use this function to count the number of milliseconds from the start of the day.
Create Function [dbo].[MsFromStartOfDay] ( #DateTime datetime )
Returns int
As
Begin
Return (
( Datepart( ms , #DateTime ) ) +
( Datepart( ss , #DateTime ) * 1000 ) +
( Datepart( mi , #DateTime ) * 1000 * 60 ) +
( Datepart( hh , #DateTime ) * 1000 * 60 * 60 )
)
End
I've verified that it returns the same int for two different dates with the same time
declare #first datetime
set #first = '1900-01-01 23:59:39.090'
declare #second datetime
set #second = '2000-11-02 23:56:39.090'
Select dbo.MsFromStartOfDay( #first )
Select dbo.MsFromStartOfDay( #second )
This solution doesn't always return the int you would expect. For example, try the below in SQL 2005, it returns an int ending in '557' instead of '556'.
set #first = '1900-01-01 23:59:39.556'
set #second = '2000-11-02 23:56:39.556'
I think this has to do with the nature of DateTime stored as float. You can still compare the two number, though. And when I used this approach on a "real" dataset of DateTime captured in .NET using DateTime.Now() and stored in SQL, I found that the calculations were accurate.
TL;DR
Separate the time value from the date value if you want to use indexes in your search (you probably should, for performance). You can: (1) use function-based indexes or (2) create a new column for time only, index this column and use it in you SELECT clause.
Keep in mind you will lose any index performance boost if you use functions in a SQL's WHERE clause, the engine has to do a scan search. Just run your query with EXPLAIN SELECT... to confirm this. This happens because the engine has to process EVERY value in the field for EACH comparison, and the converted value is not indexed.
Most answers say to use float(), convert(), cast(), addtime(), etc.. Again, your database won't use indexes if you do this. For small tables that may be OK.
It is OK to use functions in WHERE params though (where field = func(value)), because you won't be changing EACH field's value in the table.
In case you want to keep use of indexes, you can create a function-based index for the time value. The proper way to do this (and support for it) may depend on your database engine. Another option is adding a column to store only the time value and index this column, but try the former approach first.
Edit 06-02
Do some performance tests before updating your database to have a new time column or whatever to make use of indexes. In my tests, I found out the performance boost was minimal (when I could see some improvement) and wouldn't be worth the trouble and overhead of adding a new index.

What's a good way to check if two datetimes are on the same calendar day in TSQL?

Here is the issue I am having: I have a large query that needs to compare datetimes in the where clause to see if two dates are on the same day. My current solution, which sucks, is to send the datetimes into a UDF to convert them to midnight of the same day, and then check those dates for equality. When it comes to the query plan, this is a disaster, as are almost all UDFs in joins or where clauses. This is one of the only places in my application that I haven't been able to root out the functions and give the query optimizer something it can actually use to locate the best index.
In this case, merging the function code back into the query seems impractical.
I think I am missing something simple here.
Here's the function for reference.
if not exists (select * from dbo.sysobjects
where id = object_id(N'dbo.f_MakeDate') and
type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
exec('create function dbo.f_MakeDate() returns int as
begin declare #retval int return #retval end')
go
alter function dbo.f_MakeDate
(
#Day datetime,
#Hour int,
#Minute int
)
returns datetime
as
/*
Creates a datetime using the year-month-day portion of #Day, and the
#Hour and #Minute provided
*/
begin
declare #retval datetime
set #retval = cast(
cast(datepart(m, #Day) as varchar(2)) +
'/' +
cast(datepart(d, #Day) as varchar(2)) +
'/' +
cast(datepart(yyyy, #Day) as varchar(4)) +
' ' +
cast(#Hour as varchar(2)) +
':' +
cast(#Minute as varchar(2)) as datetime)
return #retval
end
go
To complicate matters, I am joining on time zone tables to check the date against the local time, which could be different for every row:
where
dbo.f_MakeDate(dateadd(hh, tz.Offset +
case when ds.LocalTimeZone is not null
then 1 else 0 end, t.TheDateINeedToCheck), 0, 0) = #activityDateMidnight
[Edit]
I'm incorporating #Todd's suggestion:
where datediff(day, dateadd(hh, tz.Offset +
case when ds.LocalTimeZone is not null
then 1 else 0 end, t.TheDateINeedToCheck), #ActivityDate) = 0
My misconception about how datediff works (the same day of year in consecutive years yields 366, not 0 as I expected) caused me to waste a lot of effort.
But the query plan didn't change. I think I need to go back to the drawing board with the whole thing.
This is much more concise:
where
datediff(day, date1, date2) = 0
You pretty much have to keep the left side of your where clause clean. So, normally, you'd do something like:
WHERE MyDateTime >= #activityDateMidnight
AND MyDateTime < (#activityDateMidnight + 1)
(Some folks prefer DATEADD(d, 1, #activityDateMidnight) instead - but it's the same thing).
The TimeZone table complicates matter a bit though. It's a little unclear from your snippet, but it looks like t.TheDateInTable is in GMT with a Time Zone identifier, and that you're then adding the offset to compare against #activityDateMidnight - which is in local time. I'm not sure what ds.LocalTimeZone is, though.
If that's the case, then you need to get #activityDateMidnight into GMT instead.
where
year(date1) = year(date2)
and month(date1) = month(date2)
and day(date1) = day(date2)
Make sure to read Only In A Database Can You Get 1000% + Improvement By Changing A Few Lines Of Code so that you are sure that the optimizer can utilize the index effectively when messing with dates
this will remove time component from a date for you:
select dateadd(d, datediff(d, 0, current_timestamp), 0)
Eric Z Beard:
I do store all dates in GMT. Here's the use case: something happened at 11:00 PM EST on the 1st, which is the 2nd GMT. I want to see activity for the 1st, and I am in EST so I will want to see the 11PM activity. If I just compared raw GMT datetimes, I would miss things. Each row in the report can represent an activity from a different time zone.
Right, but when you say you're interested in activity for Jan 1st 2008 EST:
SELECT #activityDateMidnight = '1/1/2008', #activityDateTZ = 'EST'
you just need to convert that to GMT (I'm ignoring the complication of querying for the day before EST goes to EDT, or vice versa):
Table: TimeZone
Fields: TimeZone, Offset
Values: EST, -4
--Multiply by -1, since we're converting EST to GMT.
--Offsets are to go from GMT to EST.
SELECT #activityGmtBegin = DATEADD(hh, Offset * -1, #activityDateMidnight)
FROM TimeZone
WHERE TimeZone = #activityDateTZ
which should give you '1/1/2008 4:00 AM'. Then, you can just search in GMT:
SELECT * FROM EventTable
WHERE
EventTime >= #activityGmtBegin --1/1/2008 4:00 AM
AND EventTime < (#activityGmtBegin + 1) --1/2/2008 4:00 AM
The event in question is stored with a GMT EventTime of 1/2/2008 3:00 AM. You don't even need the TimeZone in the EventTable (for this purpose, at least).
Since EventTime is not in a function, this is a straight index scan - which should be pretty efficient. Make EventTime your clustered index, and it'll fly. ;)
Personally, I'd have the app convert the search time into GMT before running the query.
You're spoilt for choice in terms of options here. If you are using Sybase or SQL Server 2008 you can create variables of type date and assign them your datetime values. The database engine gets rid of the time for you. Here's a quick and dirty test to illustrate (Code is in Sybase dialect):
declare #date1 date
declare #date2 date
set #date1='2008-1-1 10:00'
set #date2='2008-1-1 22:00'
if #date1=#date2
print 'Equal'
else
print 'Not equal'
For SQL 2005 and earlier what you can do is convert the date to a varchar in a format that does not have the time component. For instance the following returns 2008.08.22
select convert(varchar,'2008-08-22 18:11:14.133',102)
The 102 part specifies the formatting (Books online can list for you all the available formats)
So, what you can do is write a function that takes a datetime and extracts the date element and discards the time. Like so:
create function MakeDate (#InputDate datetime) returns datetime as
begin
return cast(convert(varchar,#InputDate,102) as datetime);
end
You can then use the function for companions
Select * from Orders where dbo.MakeDate(OrderDate) = dbo.MakeDate(DeliveryDate)
Eric Z Beard:
the activity date is meant to indicate the local time zone, but not a specific one
Okay - back to the drawing board. Try this:
where t.TheDateINeedToCheck BETWEEN (
dateadd(hh, (tz.Offset + ISNULL(ds.LocalTimeZone, 0)) * -1, #ActivityDate)
AND
dateadd(hh, (tz.Offset + ISNULL(ds.LocalTimeZone, 0)) * -1, (#ActivityDate + 1))
)
which will translate the #ActivityDate to local time, and compare against that. That's your best chance for using an index, though I'm not sure it'll work - you should try it and check the query plan.
The next option would be an indexed view, with an indexed, computed TimeINeedToCheck in local time. Then you just go back to:
where v.TheLocalDateINeedToCheck BETWEEN #ActivityDate AND (#ActivityDate + 1)
which would definitely use the index - though you have a slight overhead on INSERT and UPDATE then.
I would use the dayofyear function of datepart:
Select *
from mytable
where datepart(dy,date1) = datepart(dy,date2)
and
year(date1) = year(date2) --assuming you want the same year too
See the datepart reference here.
Regarding timezones, yet one more reason to store all dates in a single timezone (preferably UTC). Anyway, I think the answers using datediff, datepart and the different built-in date functions are your best bet.