I have a string value as 07052018080504623
It represents MM DD YYYY HH MM SS MS
means
MM = 07, DD = 05, YYYY=2018, HH = 08, MM=05 , SS= 04, MS = 623
Now I have a table defined as
declare #t table (WorkRequestId varchar(100))
insert into #t values
('07052018080504623'),('07062018012756663'),('07062018020148130'),('07062018095201231'),
('07062018102203805'),('07062018103718059'),('07062018110304836'),('07062018115356135'),
('07062018120624983'),('07062018124035480'),('07062018080504623'),('07062018070504623')
select
*
from #t
The records should be sorted in ascending order such that
WorkRequestId
07052018080504623
07062018095201231
07062018102203805
07062018103718059
07062018110304836
07062018115356135
07062018120624983
07062018124035480
07062018012756663
07062018020148130
The cutoff time is 8PM of previous day to 19:59:59 PM of next day.
In our example, 05 is previous day while 06 is next day.
Also no transaction happens between 1AM - 7:59:59 AM of next day. It starts again from 8AM of next day and continues till
19:59:59 PM.
So when we encounter 07062018012756663, the DD=06 and HH = 01. It means 13HRs (i.e. 1PM) of 6th. Same for 07062018020148130 where DD=06 and HH = 02 (i.e. 2PM or 14Hrs).
But 07062018095201231 where DD=06 and HH = 09 means 9AM of 6th.
That is why
07062018095201231 comes before 07062018012756663 and 07062018020148130
while ordering
My attempt so far (not correct yet)
select
*
,DY=SUBSTRING([WorkRequestId],3,2)
,HH = SUBSTRING([WorkRequestId],9,2)
,CurrentDY=CONVERT(varchar(2), getdate(), 103)
from #t
order by left([WorkRequestId],8) +
cast(iif(
SUBSTRING([WorkRequestId],3,2) = '6',--CONVERT(varchar(2), getdate(), 103),
iif(cast(SUBSTRING([WorkRequestId],9,2) as int) between 1 and 7,
cast(SUBSTRING([WorkRequestId],9,2) as int)+12,SUBSTRING([WorkRequestId],9,2)),
cast(SUBSTRING([WorkRequestId],9,2)as varchar(4)))as varchar(20))
+right([WorkRequestId],7)
So, I guess you should realize by now that storing dates as strings (and in fact, storing anything in the wrong data type) is bad practice.
The correct solution is to change the database structure to hold that data as DateTime2 instead of a string. However, assuming this can't be done for some reason, you can get the results you want by converting the string values to datetime2, adding 12 hours where the hour is between 1 a.m. and 8 a.m., and sort by that date.
I've written my suggestion in a cumbersome way because I wanted to show every part of the process - I've used 3 common table expression though if can be done in a single query - again, that's just to illustrate every step of the solution:
;WITH CTEDateParts AS -- break down the string to it's parts
(
SELECT WorkRequestId,
SUBSTRING(WorkRequestId, 5, 4) As Year,
SUBSTRING(WorkRequestId, 1, 2) As Month,
SUBSTRING(WorkRequestId, 3, 2) As Day,
SUBSTRING(WorkRequestId, 9, 2) As Hour,
SUBSTRING(WorkRequestId, 11, 2) As Minute,
SUBSTRING(WorkRequestId, 13, 2) As Second,
SUBSTRING(WorkRequestId, 15, 3) As Millisecond
FROM #t
), CTEDates AS -- create datetime values from the string parts
(
SELECT WorkRequestId,
CAST(Year +'-'+ Month +'-'+ Day +'T'+
Hour +':'+ Minute +':'+ Second +'.'+ Millisecond As DateTime2(7)) As DateValue
FROM CTEDateParts
), CTEFixedDates AS -- add 12 hours for hours between 1 and 8 a.m.
(
SELECT WorkRequestId,
DateValue,
CASE WHEN DATEPART(HOUR, DateValue) >= 1 AND DATEPART(HOUR, DateValue) <= 8 THEN
DATEADD(Hour, 12, DateValue)
ELSE
DateValue
END As FixedDate
FROM CTEDates
)
-- finally, select order by the FixedDate column
SELECT WorkRequestId
FROM CTEFixedDates
ORDER BY FixedDate
Results:
WorkRequestId
07052018080504623
07062018095201231
07062018102203805
07062018103718059
07062018110304836
07062018115356135
07062018120624983
07062018124035480
07062018012756663
07062018020148130
07062018070504623
07062018080504623
You can try the following:
select
*
from #t
order by left (WorkRequestId, 8) + (case when SUBSTRING(WorkRequestId, 9,2) between '01' and '07' then CAST(SUBSTRING(WorkRequestId, 9,2) + 12 AS CHAR(2)) else SUBSTRING(WorkRequestId, 9,2) end) + SUBSTRING(WorkRequestId, 11,7)
Related
I need to return count on results per day between a time range that will overlap days.
So from 8 PM to 8 AM for every day that starts on M-F return a count for that time period. And do that for the entire year.
This is what I have, and I could do a simple and between if I only wanted on day but I'm not sure how to iterate through days especially when the start is on one day and the end on the next but skip the ones that Start on a Saturday or Sunday.
SELECT TOP (50)
ClientVisit.visittype,
ClientVisit.location_id,
ClientVisit.visittype_id,
Location.location_desc,
Location.location_code,
ClientVisit.timein,
ClientVisit.timeout,
ClientVisit.visit_dateday
FROM
ClientVisit
INNER JOIN
Location ON ClientVisit.location_id = Location.location_id
WHERE
(ClientVisit.visittype Like '%Open Chart%'
OR ClientVisit.visittype LIKE '%Diag%')
AND (Location.location_code = 'Access-505'
OR Location.location_code = 'Access-hosp')
AND (ClientVisit.timein BETWEEN #param1 AND #param2)
Filtering days of week and hours of the day is easy enough. Does this group by get at what you're trying to accomplish for the counts?
SELECT CAST(ClientVisit.timein AS DATE) AS DT, COUNT(*)
FROM ClientVisit INNER JOIN Location
ON ClientVisit.location_id = Location.location_id
WHERE
(ClientVisit.visittype Like '%Open Chart%' OR ClientVisit.visittype LIKE '%Diag%')
AND (Location.location_code = 'Access-505' OR Location.location_code = 'Access-hosp')
-- Use date params rather than datetime
AND CAST(ClientVisit.timein AS DATE) BETWEEN #param1 AND #param2
-- M-F assuming ##DATEFIRST is Sunday (7)
AND DATEPART(weekday, ClientVisit.timein) BETWEEN 2 AND 6
-- time of day. won't include the instant of 8:00:00am
AND ( DATEPART(hour, ClientVisit.timein) BETWEEN 8 AND 23
OR DATEPART(hour, ClientVisit.timein) BETWEEN 0 AND 7)
GROUP BY CAST(ClientVisit.timein AS DATE);
If you need to treat the hours from 8PM to 8AM as a single shift then you can adjust the times prior to so that times after midnight are treated as part of the preceeding day:
WITH AdjustedVisit AS (
SELECT *, DATEADD(hour, -8, timein) AS adjustedin FROM ClientVisit)
-- Use date params rather than datetime
WHERE CAST(timein AS DATE) BETWEEN #param1 AND #param2
)
SELECT CAST(v.adjustedin AS DATE) AS DT, COUNT(*)
FROM AdjustedVisit AS v INNER JOIN Location AS l
ON v.location_id = l.location_id
WHERE
(v.visittype Like '%Open Chart%' OR v.visittype LIKE '%Diag%')
AND (l.location_code = 'Access-505' OR l.location_code = 'Access-hosp')
-- M-F assuming ##DATEFIRST is Sunday (7)
AND DATEPART(weekday, v.adjustedin) BETWEEN 2 AND 6
-- time of day. won't include the instant of 8:00:00am
AND DATEPART(hour, v.adjustedin) BETWEEN 12 AND 23
GROUP BY CAST(v.adjustedin AS DATE);
I have a number of hours which I need to display in the format of days and hours.
This number is derived from a DATEDIFF instruction.
For numbers less than 24, I wish to display only hours - ie, 21 hours.
For larger numbers, I wish to display days and hours - ie, 3 days, 14 hours
I do not need to display any smaller unit than hours, and values should be rounded down to the preceding hour, so 1 hour and 59 minutes will be 1 hour.
I cannot use a stored procedure - this must run as a single select statement.
I am aware that I can calculate the value by using modulo, so assuming 71 hours:
select concat((71 - (71 % 24)) / 24, ' days, ', 71 % 24, ' hours')
This however is somewhat messy, and as the statement must be a single select, I will have to calculate the DATEDIFF 3 times as below.
SELECT CONCAT (
(DATEDIFF(HOUR, StartDate, EndDate) -
(DATEDIFF(HOUR, StartDate, EndDate) % 24)) / 24,
' days, ',
DATEDIFF(HOUR, StartDate, EndDate) % 24,
' hours')
FROM RecordsTable
Is it possible to either format a number of hours as days and hours directly using an inbuilt SQL command, or failing that, select (datediff(hour, StartDate, EndDate) into a variable which I can reuse in the single select?
EDIT - As suggested, the solution was to use a CTE as follows:
WITH totalhours (htotal) AS
(
SELECT
DATEDIFF(HOUR, StartDate, EndDate) AS htotal
FROM
RecordsTable
)
SELECT
CONCAT ((htotal - (htotal % 24)) / 24,
' days, ',
htotal % 24,
' hours')
FROM
RecordsTable;
Use a CTE to generate your total once, and reference that total in your select against the CTE. Or use a subquery to generate the total once and then select from the subquery to get the desired results.
The fundamental issue is you need to materialize the total once to be able to reference it; forcing the engine to materialize a value is generally done via a CTE or subquery.
You can do a lot with datetime objects and format strings or datepart. For example,
declare #n int = 105;
select format(dateadd(day, -1, dateadd(hour, #n, '1753-1-1')), 'd h');
-- 4 9
Taking the minimum datetime value (1753-01-01), adding the requisite number of hours, subtracting one day (because on the first day you want days = 0), and then formatting.
You could improve the formatting like this:
select format(dateadd(day, -1, dateadd(hour, #n, '1753-1-1')), 'd \da\y(\s), h \hour(\s)');
-- 4 day(s), 9 hour(s)
Of course this will only work up to 31 days, because then you'll be out of the month of January in 1753 and into February. If that's the case, revert to datepart. This is uglier, but will work for larger values
select
datepart(day, (dateadd(day, -1, dateadd(hour, #n, '1753-1-1')))),
datepart(hour, (dateadd(day, -1, dateadd(hour, #n, '1753-1-1'))));
If a user selects a range such as:
Start: November 2016
End: September 2017,
I want to include all results that fall within the range of 2016-11-01 to 2017-09-30.
I tried concatenating together the year, month, and day, however the issue comes that not all months have the same last day. While I know all months start on day 01, a month's end day can be 28, 29, 30, or 31.
Is there a way to do this without constructing the date? SqlServer 2008 doesn't have the EOMONTH function, and I feel like anything more complex than that is not the right solution. I would like to avoid this:
WHERE
DateCol >= '2016' + '-' + '11' + '-01' AND
DateCol <= '2017' + '-' + '09' + '-30'
It really seems to me that the easiest and best answer is to go from the first of the beginning month to the first of the month after the ending month, and make the second comparison not inclusive.
In other words, instead of this:
WHERE
DateCol >= '2016' + '-' + '11' + '-01' AND
DateCol <= '2017' + '-' + '09' + '-30'
simply this:
WHERE
DateCol >= '2016' + '-' + '11' + '-01' AND
DateCol < '2017' + '-' + '10' + '-01'
There is a faster way to do so :
DECLARE #minDate DATE
DECLARE #maxDate DATE
SET #minDate = XXXXX
SET #maxDate = YYYYY
-- Get the first day of the month minDate.
SET #minDate = CONVERT(datetime,CONVERT(varchar(6),#minDate,112)+'01',112)
-- Get the last day of the month minDate.
SET #maxDate = CONVERT(datetime,CONVERT(varchar(6),#maxDate,112)+'01',112)
SET #maxDate = DATEADD(day, -1, DATEADD(month, 1, #maxDate))
SELECT * FROM myTABLE WHERE DateCol >= #minDate AND DateCol <= #maxDate
Or :
SELECT * FROM myTABLE
WHERE DateCol >= CONVERT(datetime,CONVERT(varchar(6),XXXXX,112)+'01',112)
AND DateCol <= DATEADD(day, -1, DATEADD(month, 1, CONVERT(datetime,CONVERT(varchar(6),YYYYY,112)+'01',112)))
Use syntax like CONVERT(datetime,'20170930',112) or CONVERT(datetime,'09-30-2017',110) for XXXXX and YYYYY rather than '2017-09-30' that use SQL Server implicit convertion from char to datetime (rely on the server configuration : can be hazardous!!!)).
Using this syntax is faster because #minDate and #maxDate do not need any evaluation. So that indexes can be used directly...
Otherwise a scalar function that will simulate the eomonth() behaviour could be usefull...
You could use following select statement to get last date of any month (and any year) by passing a field or date to it:
DECLARE #dtDate DATE
SET #dtDate = '09/25/2016'
SELECT CAST(DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,#dtDate)+1,0)) AS DATE) AS LastDay_AnyMonth
Please provide some example data and desired result and I will update my answer further.
Here you go:
DECLARE #YourTable TABLE (YourData DATE);
INSERT INTO #YourTable VALUES
('2016-11-01'),
('2016-09-05'),
('2017-03-03'),
('2017-11-11'),
('2017-12-14'),
('2017-09-30');
WITH CTE AS (
SELECT YourData
FROM #YourTable
WHERE YEAR(YourData) =2016 AND MONTH (YourData) >= 11
)
SELECT YourData
FROM #YourTable
WHERE YEAR(YourData) =2017 AND MONTH (YourData) <= 9
UNION ALL
SELECT YourData
FROM CTE;
There is no need to know the end of the month (28 or 30 or 31).
For 2008, you can simply convert the string
Example
Select Date1=convert(date,'November 2016')
,Date2=dateadd(DAY,-1,dateadd(MONTH,1,convert(date,'September 2017')))
Returns
Date1 Date2
2016-11-01 2017-09-30
So the WHERE would be somthing like this
...
Where DateCol between convert(date,'November 2016')
and dateadd(DAY,-1,dateadd(MONTH,1,convert(date,'September 2017')))
A useful construct for performing date-based operations is to make use of a Date Dimension table. You are creating a lookup table that is populated with a lot of information about dates over a large span of time. You can then query the table based on the information that you do have. The table is small enough so that it does not impose significant performance concerns.
In your particular case, you have the month and year. You would plug that into the date dimension table to get the first of the month from the beginning month and the last of the month from the ending month. You now have a time range to search over without any complex logic or calculations on the fly.
Aaron Bertrand explains it in depth here: https://www.mssqltips.com/sqlservertip/4054/creating-a-date-dimension-or-calendar-table-in-sql-server/
I'm trying to query our database to find all records that were created between 6am yesterday and 6am today. This will be run in a report at any point during the day so set times/dates are useless.
I have this so far:-
SELECT * FROM DaySummaryDetail DSD
WHERE DSD.FromDateTime BETWEEN DATEADD(DAY, -1, GetDate())
AND DATEADD(Day, 1, GetDate())
But obviously this only works for 24 hours ago from right now until right now. I can't figure out how to apply a time as well as date.
Every example I find online seems slightly different and uses set dates/times ie, >= 20/02/2015 06:00:00.
I normally use Oracle SQL which would simply work using this:-
ptt.mod_date_time >= TRUNC (SYSDATE - 1) - 2 / 24
AND ptt.mod_date_time <= TRUNC (SYSDATE - 1) + 22 / 24
This would return results from 10pm to 10pm but the format appears totally different in SQL Server.
You can get the datetime values you are after by doing the following:
SELECT DATEADD(HOUR,6,CONVERT(DATETIME, CONVERT(DATE ,GETDATE()))) Today6AM,
DATEADD(HOUR,-18,CONVERT(DATETIME, CONVERT(DATE ,GETDATE()))) Yesterday6AM
By doing this: CONVERT(DATE ,GETDATE()) you are stripping off the time portion of today's date. Converting it back to datetime gives you midnight for today.
The query adds 6 hours to midnight of the current day for 6am today and subtracts 18 hours from midnight of the current day to give you 6am on the previous day.
Output:
Today6AM Yesterday6AM
================================================
2015-02-20 06:00:00.000 2015-02-19 06:00:00.000
So adding that to your query:
SELECT *
FROM DaySummaryDetail DSD
WHERE DSD.FromDateTime
BETWEEN DATEADD(HOUR,-18,CONVERT(DATETIME, CONVERT(DATE ,GETDATE())))
AND DATEADD(HOUR,6,CONVERT(DATETIME, CONVERT(DATE ,GETDATE())))
DECLARE #StartTimestamp datetime
DECLARE #EndTimestamp datetime
DECLARE #HourPartOfSearchRange nvarchar(6)
SET #HourPartOfSearchRange = ' 06:30'
SET #StartTimestamp =
CAST((CONVERT(varchar(11), DATEADD(DAY,-1,#CurrentUTCDateTime), 106) + #HourPartOfSearchRange) AS datetime)
SET #EndTimestamp =
CAST((CONVERT(varchar(11), #CurrentUTCDateTime, 106) + #HourPartOfSearchRange) AS datetime)
SELECT * FROM dbo.Test Where Timestamp Between #StartTimestamp AND #EndTimestamp
today 6am is
dateadd(hour,6,cast(cast(getdate() as date) as datetime))
cast(getdate() as date) truncates the timepart, cast it back as datetime because dateadd won't add hours otherwise and add 6hours
One solution would be like so:
select *
from DaySummaryDetail DSD
where DSD.FromDateTime between cast(cast(cast(getdate()-1 as date) as varchar(30)) + ' 06:00:00.000' as datetime)
and cast(cast(cast(getdate() as date) as varchar(30)) + ' 06:00:00.000' as datetime)
This should help ...
SELECT DATEADD( hour, 6, CAST(CAST(GETDATE(), AS Date) AS DateTime) ) AS 'Today#6am'
SELECT DATEADD( hour, 6, CAST(CAST(GETDATE()-1, AS Date) AS DateTime) ) AS 'Yesterday#6am'
In SQL Server 2012 you can use SMALLDATETIMEFROMPARTS to construct a datetime value that is today at 6am like this:
SMALLDATETIMEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), DAY(GETDATE()), 6, 0)
Output: 2015-02-20 06:00:00
then you can use the above expression in place of GETDATE() in the WHERE clause:
DECLARE #TodayAt6AM DATETIME = SMALLDATETIMEFROMPARTS(YEAR(GETDATE()),
MONTH(GETDATE()),
DAY(GETDATE()),
6,
0)
SELECT *
FROM DaySummaryDetail DSD
WHERE DSD.FromDateTime BETWEEN DATEADD(DAY, -1, #TodayAt6AM) AND
DATEADD(Day, 1, #TodayAt6AM)
What is the best way to convert a UTC datetime into local datetime. It isn't as simple as a getutcdate() and getdate() difference because the difference changes depending on what the date is.
CLR integration isn't an option for me either.
The solution that I had come up with for this problem a few months back was to have a daylight savings time table that stored the beginning and ending daylight savings days for the next 100 or so years, this solution seemed inelegant but conversions were quick (simple table lookup)
Create two tables and then join to them to convert stored GMT dates to local time:
TimeZones e.g.
--------- ----
TimeZoneId 19
Name Eastern (GMT -5)
Offset -5
Create the daylight savings table and populate it with as much information as you can (local laws change all the time so there's no way to predict what the data will look like years in the future)
DaylightSavings
---------------
TimeZoneId 19
BeginDst 3/9/2008 2:00 AM
EndDst 11/2/2008 2:00 AM
Join them like this:
inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId
left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone
and x.TheDateToConvert between ds.BeginDst and ds.EndDst
Convert dates like this:
dateadd(hh, tz.Offset +
case when ds.LocalTimeZone is not null
then 1 else 0 end, TheDateToConvert)
If you're in the US and only interested in going from UTC/GMT to a fixed time zone (such as EDT) this code should suffice. I whipped it up today and believe it's correct but use at your own risk.
Adds a computed column to a table 'myTable' assuming your dates are on the 'date' column. Hope someone else finds this useful.
ALTER TABLE myTable ADD date_edt AS
dateadd(hh,
-- The schedule through 2006 in the United States was that DST began on the first Sunday in April
-- (April 2, 2006), and changed back to standard time on the last Sunday in October (October 29, 2006).
-- The time is adjusted at 02:00 local time.
CASE WHEN YEAR(date) <= 2006 THEN
CASE WHEN
date >= '4/' + CAST(abs(8-DATEPART(dw,'4/1/' + CAST(YEAR(date) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00'
AND
date < '10/' + CAST(32-DATEPART(dw,'10/31/' + CAST(YEAR(date) as varchar)) as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00'
THEN -4 ELSE -5 END
ELSE
-- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007.
-- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on
-- the first Sunday of November, one week later than in years past. This change resulted in a new DST period
-- that is four weeks (five in years when March has five Sundays) longer than in previous years.[35] In 2008
-- daylight saving time ended at 02:00 on Sunday, November 2, and in 2009 it began at 02:00 on Sunday, March 8.[36]
CASE WHEN
date >= '3/' + CAST(abs(8-DATEPART(dw,'3/1/' + CAST(YEAR(date) as varchar)))%7 + 8 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00'
AND
date <
'11/' + CAST(abs(8-DATEPART(dw,'11/1/' + CAST(YEAR(date) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00'
THEN -4 ELSE -5 END
END
,date)
FOR READ-ONLY Use this(inspired by Bob Albright's incorrect solution ):
SELECT
date1,
dateadd(hh,
-- The schedule through 2006 in the United States was that DST began on the first Sunday in April
-- (April 2, 2006), and changed back to standard time on the last Sunday in October (October 29, 2006).
-- The time is adjusted at 02:00 local time (which, for edt, is 07:00 UTC at the start, and 06:00 GMT at the end).
CASE WHEN YEAR(date1) <= 2006 THEN
CASE WHEN
date1 >= '4/' + CAST((8-DATEPART(dw,'4/1/' + CAST(YEAR(date1) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 7:00'
AND
date1 < '10/' + CAST(32-DATEPART(dw,'10/31/' + CAST(YEAR(date1) as varchar)) as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 6:00'
THEN -4 ELSE -5 END
ELSE
-- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007.
-- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on
-- the first Sunday of November, one week later than in years past. This change resulted in a new DST period
-- that is four weeks (five in years when March has five Sundays) longer than in previous years. In 2008
-- daylight saving time ended at 02:00 edt (06:00 UTC) on Sunday, November 2, and in 2009 it began at 02:00 edt (07:00 UTC) on Sunday, March 8
CASE WHEN
date1 >= '3/' + CAST((8-DATEPART(dw,'3/1/' + CAST(YEAR(date1) as varchar)))%7 + 8 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 7:00'
AND
date1 < '11/' + CAST((8-DATEPART(dw,'11/1/' + CAST(YEAR(date1) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 6:00'
THEN -4 ELSE -5 END
END
, date1) as date1Edt
from MyTbl
I posted this answer after I tried to edit Bob Albright's wrong answer. I corrected the times and removed superfluous abs(), but my edits were rejected multiple times. I tried explaining, but was dismissed as a noob. His is a GREAT approach to the problem! It got me started in the right direction. I hate to create this separate answer when his just needs a minor tweak, but I tried ¯\_(ツ)_/¯
A much simpler and generic solution that considers daylight savings. Given an UTC date in "YourDateHere":
--Use Minutes ("MI") here instead of hours because sometimes
-- the UTC offset may be half an hour (e.g. 9.5 hours).
SELECT DATEADD(MI,
DATEDIFF(MI, SYSUTCDATETIME(),SYSDATETIME()),
YourUtcDateHere)[LocalDateTime]
If either of these issues affects you, you should never store local times in the database:
With DST is that there is an "hour of uncertainty" around the falling back period where a local time cannot be unambiguously converted. If exact dates & times are required, then store in UTC.
If you want to show users the date & time in their own timezone, rather than the timezone in which the action took place, store in UTC.
In Eric Z Beard's answer, the following SQL
inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId
left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone
and x.TheDateToConvert between ds.BeginDst and ds.EndDst
might more accurately be:
inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId
left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone
and x.TheDateToConvert >= ds.BeginDst and x.TheDateToConvert < ds.EndDst
(above code not tested)
The reason for this is that the sql "between" statement is inclusive. On the back-end of DST, this would result in a 2AM time NOT being converted to 1AM. Of course the likelihood of the time being 2AM precisely is small, but it can happen, and it would result in an invalid conversion.
Maintain a TimeZone table, or shell out with an extended stored proc (xp_cmdshell or a COM component, or your own) and ask the OS to do it. If you go the xp route, you'd probably want to cache the offset for a day.
I like the answer #Eric Z Beard provided.
However, to avoid performing a join everytime, what about this?
TimeZoneOffsets
---------------
TimeZoneId 19
Begin 1/4/2008 2:00 AM
End 1/9/2008 2:00 AM
Offset -5
TimeZoneId 19
Begin 1/9/2008 2:00 AM
End 1/4/2009 2:00 AM
Offset -6
TimeZoneId 20 --Hong Kong for example - no DST
Begin 1/1/1900
End 31/12/9999
Offset +8
Then
Declare #offset INT = (Select IsNull(tz.Offset,0) from YourTable ds
join TimeZoneOffsets tz on tz.TimeZoneId=ds.LocalTimeZoneId
and x.TheDateToConvert >= ds.Begin and x.TheDateToConvert < ds.End)
finally becoming
dateadd(hh, #offset, TheDateToConvert)
I've read through a lot of StackOverflow posts in regards to this issue and found many methods. Some "sort of" ok. I also found this MS reference (https://msdn.microsoft.com/en-us/library/mt612795.aspx) which I tried to utilize in my script. I have managed to achieve the required result BUT I am not sure if this will run on 2005 version. Either way, I hope this helps.
Fnc to return PST from the system UTC default
CREATE FUNCTION dbo.GetPst()
RETURNS DATETIME
AS
BEGIN
RETURN SYSDATETIMEOFFSET() AT TIME ZONE 'Pacific Standard Time'
END
SELECT dbo.GetPst()
Fnc to return PST from the provided timestamp
CREATE FUNCTION dbo.ConvertUtcToPst(#utcTime DATETIME)
RETURNS DATETIME
AS
BEGIN
RETURN DATEADD(HOUR, 0 - DATEDIFF(HOUR, CAST(SYSDATETIMEOFFSET() AT TIME ZONE 'Pacific Standard Time' AS DATETIME), SYSDATETIME()), #utcTime)
END
SELECT dbo.ConvertUtcToPst('2016-04-25 22:50:01.900')
I am using this because all of my dates are from now forward.
DATEADD(HH,(DATEPART(HOUR, GETUTCDATE())-DATEPART(HOUR, GETDATE()))*-1, GETDATE())
For historical dates (or to handle future changes in DST, I'm guessing Bob Albright's solution would be the way to go.
The modification I make to my code is to use the target column:
DATEADD(HH,(DATEPART(HOUR, GETUTCDATE())-DATEPART(HOUR, GETDATE()))*-1, [MySourceColumn])
So far, this seems to work, but I'm happy to receive feedback.
Here is the code I use to make my timezone table. It's a bit naive, but is usually good enough.
Assumptions:
It assumes US only rules (DST is 2AM on some pre-defined Sunday,
etc).
It assumes you don't have dates prior to 1970
It assumes you know the local timezone offsets (i.e.: EST=-05:00, EDT=-04:00, etc.)
Here's the SQL:
-- make a table (#dst) of years 1970-2101. Note that DST could change in the future and
-- everything was all custom and jacked before 1970 in the US.
declare #first_year varchar(4) = '1970'
declare #last_year varchar(4) = '2101'
-- make a table of all the years desired
if object_id('tempdb..#years') is not null drop table #years
;with cte as (
select cast(#first_year as int) as int_year
,#first_year as str_year
,cast(#first_year + '-01-01' as datetime) as start_of_year
union all
select int_year + 1
,cast(int_year + 1 as varchar(4))
,dateadd(year, 1, start_of_year)
from cte
where int_year + 1 <= #last_year
)
select *
into #years
from cte
option (maxrecursion 500);
-- make a staging table of all the important DST dates each year
if object_id('tempdb..#dst_stage') is not null drop table #dst_stage
select dst_date
,time_period
,int_year
,row_number() over (order by dst_date) as ordinal
into #dst_stage
from (
-- start of year
select y.start_of_year as dst_date
,'start of year' as time_period
,int_year
from #years y
union all
select dateadd(year, 1, y.start_of_year)
,'start of year' as time_period
,int_year
from #years y
where y.str_year = #last_year
-- start of dst
union all
select
case
when y.int_year >= 2007 then
-- second sunday in march
dateadd(day, ((7 - datepart(weekday, y.str_year + '-03-08')) + 1) % 7, y.str_year + '-03-08')
when y.int_year between 1987 and 2006 then
-- first sunday in april
dateadd(day, ((7 - datepart(weekday, y.str_year + '-04-01')) + 1) % 7, y.str_year + '-04-01')
when y.int_year = 1974 then
-- special case
cast('1974-01-06' as datetime)
when y.int_year = 1975 then
-- special case
cast('1975-02-23' as datetime)
else
-- last sunday in april
dateadd(day, ((7 - datepart(weekday, y.str_year + '-04-24')) + 1) % 7, y.str_year + '-04-24')
end
,'start of dst' as time_period
,int_year
from #years y
-- end of dst
union all
select
case
when y.int_year >= 2007 then
-- first sunday in november
dateadd(day, ((7 - datepart(weekday, y.str_year + '-11-01')) + 1) % 7, y.str_year + '-11-01')
else
-- last sunday in october
dateadd(day, ((7 - datepart(weekday, y.str_year + '-10-25')) + 1) % 7, y.str_year + '-10-25')
end
,'end of dst' as time_period
,int_year
from #years y
) y
order by 1
-- assemble a final table
if object_id('tempdb..#dst') is not null drop table #dst
select a.dst_date +
case
when a.time_period = 'start of dst' then ' 03:00'
when a.time_period = 'end of dst' then ' 02:00'
else ' 00:00'
end as start_date
,b.dst_date +
case
when b.time_period = 'start of dst' then ' 02:00'
when b.time_period = 'end of dst' then ' 01:00'
else ' 00:00'
end as end_date
,cast(case when a.time_period = 'start of dst' then 1 else 0 end as bit) as is_dst
,cast(0 as bit) as is_ambiguous
,cast(0 as bit) as is_invalid
into #dst
from #dst_stage a
join #dst_stage b on a.ordinal + 1 = b.ordinal
union all
select a.dst_date + ' 02:00' as start_date
,a.dst_date + ' 03:00' as end_date
,cast(1 as bit) as is_dst
,cast(0 as bit) as is_ambiguous
,cast(1 as bit) as is_invalid
from #dst_stage a
where a.time_period = 'start of dst'
union all
select a.dst_date + ' 01:00' as start_date
,a.dst_date + ' 02:00' as end_date
,cast(0 as bit) as is_dst
,cast(1 as bit) as is_ambiguous
,cast(0 as bit) as is_invalid
from #dst_stage a
where a.time_period = 'end of dst'
order by 1
-------------------------------------------------------------------------------
-- Test Eastern
select
the_date as eastern_local
,todatetimeoffset(the_date, case when b.is_dst = 1 then '-04:00' else '-05:00' end) as eastern_local_tz
,switchoffset(todatetimeoffset(the_date, case when b.is_dst = 1 then '-04:00' else '-05:00' end), '+00:00') as utc_tz
--,b.*
from (
select cast('2015-03-08' as datetime) as the_date
union all select cast('2015-03-08 02:30' as datetime) as the_date
union all select cast('2015-03-08 13:00' as datetime) as the_date
union all select cast('2015-11-01 01:30' as datetime) as the_date
union all select cast('2015-11-01 03:00' as datetime) as the_date
) a left join
#dst b on b.start_date <= a.the_date and a.the_date < b.end_date
--Adapted Bob Albright and WillDeStijl suggestions for SQL server 2014
--
--In this instance I had no dates prior to 2006, therefore I simplified the case example
--I had to add the variables for the assignment to allow trimming the timestamp from my resultset
DECLARE #MARCH_DST as DATETIME
SET #MARCH_DST='3/' + CAST((8-DATEPART(dw,'3/1/' + CAST(YEAR(getdate()) as varchar)))%7 + 8 as varchar) + '/' + CAST(YEAR(getdate()) as varchar) + ' 7:00'
DECLARE #NOV_DST as DATETIME
SET #NOV_DST='11/' + CAST((8-DATEPART(dw,'11/1/' + CAST(YEAR(getdate()) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(getdate()) as varchar) + ' 6:00'
select cast(dateadd(HOUR,
-- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007.
-- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on
-- the first Sunday of November, one week later than in years past. This change resulted in a new DST period
-- that is four weeks (five in years when March has five Sundays) longer than in previous years. In 2008
-- daylight saving time ended at 02:00 edt (06:00 UTC) on Sunday, November 2, and in 2009 it began at 02:00 edt (07:00 UTC) on Sunday, March 8
CASE WHEN
date1 >=#MARCH_DST
AND
date1< #NOV_DST
THEN -4 ELSE -5 END
, date1) as DATE) as date1_edited
I found Simple Way to convert any date to any timezone.
Currently i have changed date to India Standard Time
DECLARE #SqlServerTimeZone VARCHAR(50)
DECLARE #LocalTimeZone VARCHAR(50)='India Standard Time'
EXEC MASTER.dbo.xp_regread 'HKEY_LOCAL_MACHINE',
'SYSTEM\CurrentControlSet\Control\TimeZoneInformation',
'TimeZoneKeyName',#SqlServerTimeZone OUT
DECLARE #DateToConvert datetime= GetDate()
SELECT LocalDate = #DateToConvert AT TIME ZONE #SqlServerTimeZone AT TIME ZONE #LocalTimeZone