Better approach for nested loops in SQL Server - sql

I have a nested loop that checks all the names and dates from descript column from table tmp13 and stores them as individual rows in other table (tmp14). The problem is that the while loop is executing for a long time. I don't know how to make it run faster. I have tried some suggests already from previous post, but I haven't been very successful. Can anyone give some suggestion to approach this horrible issue.
Here is my code checking the descript columns for names and date. Descript is a text column and can have multiple names and dates. I want to store those names and dates in separate rows.
DECLARE #Id INT
DECLARE #count INT
DECLARE #product_num INT
DECLARE #REQUESTED VARCHAR(50)
DECLARE #FirstDate VARCHAR(255)
DECLARE #RequestedBy VARCHAR(255)
DECLARE #name NVARCHAR(256)
DECLARE #date NVARCHAR(256)
DECLARE #desc NVARCHAR(256)
DECLARE #dateposition INT
DECLARE #nameposition INT
DECLARE #nameend INT
SELECT #count = MAX(id)
FROM #TMP13
SET #id = 1;
WHILE (#id <= #count)
BEGIN
SELECT #desc = descript FROM #TMP13 WHERE Id = #Id
SELECT #product_num = p_Num FROM #TMP13 WHERE Id = #Id
SELECT #REQUESTED = REQUESTED FROM #TMP13 WHERE Id = #Id
SELECT #FirstDate = DATE1 FROM #TMP13 WHERE Id = #Id
SELECT #RequestedBy = BY1 FROM #TMP13 WHERE Id = #Id
while (patindex('%[0-9][0-9]/[0-9][0-9]/[0-9][0-9][0-9][0-9]%',#desc) > 0)
begin
set #dateposition = patindex('%[0-9][0-9]/[0-9][0-9]/[0-9][0-9][0-9]%',#desc)
set #date = SUBSTRING(#desc,#dateposition,10)
set #nameposition = CHARINDEX('-', #desc)+2
set #nameend = CHARINDEX(' ', #desc, #nameposition)+1
set #name = SUBSTRING(#desc,#nameposition,#nameend-#nameposition)
insert into #TMP14
values (#Id,#product_num,#REQUESTED, #FirstDate ,#RequestedBY, #date, #name)
set #desc = SUBSTRING(#desc,#nameend,1024)
end
set #id = #id + 1;
end
select * from #tmp14;
---sample table
CREATE TABLE #Tmp13(
p_Num INTEGER NOT NULL PRIMARY KEY
REQUESTED varchar(50),
DATE1 VARCHAR(50),
BY1 VARCHAR(50),
DESCRIPT TEXT
);
INSERT INTO #tmp13 (p_Num, REQUESTED, DATE1, BY1, DESCRIPT)
VALUES (100, 'John', '5/30/2017', 'James', '05/30/2017 12:25am Eastern Standard Time - Mjames reported changes in the pages 05/30/2017 10:35AM JRachael agreed to work on the report and report to James 05/30/2017 10:00 AM James reports errors in page.',NULL);
INSERT INTO #tmp13(WO_NUM, Opendate, ClosedDate, Note)
VALUES (200, 'John', '6/1/2017', 'Rachael', '06/1/2017 3:20PM Eastern Standard Time - Rsubramaniam reported phone is not functional 06/1/2017 4:00PM Service took the phone and replaced it with new one');
OUTPUT:
Id product_num REQUESTED FirstDate RequestedBY date name date Name
1 100 John 5/30/2017 james 5/30/2017 mjames 5/30/2017 jRachael

Here's an option. This is only looking at parsing multiple dates and names from your column if it's following a consistent format. You'd need to adjust to fit your solution. And...
This will only work with the following:
Dates and names are stored in the field DESCRIPT in a consistent and repeatable format as: "dd/mm/yyyy time zone - name text dd/mm/yyyy
time zone - name text dd/mm/yyyy time zone - name text"
I'm not sure what sort of performance this will give you and if the format of how date and names are stored in that field change, it won't work. That's the importance of knowing if the format is consistent and repeatable.
In the example we're basically splitting the phase out into individual words and then filtering to get what you're after. Depending on version of SQL server I have included 2 difference options for how you can do that split.
SQL Server Version 2016+ since it'll use SPLIT_STRING
Another which should work back to 2012. It uses XML
There wasn't much sample data provide but based on your comments and replies, I'm making some assumptions and you may need to adjust for your specific needs.
We're after all occurrences of dates in the field
and also the name associated with a date and it comes right after the '-' after said date.
Here's an example:
DECLARE #tmp13 TABLE
(
[p_Num] INTEGER NOT NULL
, [DESCRIPT] NVARCHAR(MAX)
PRIMARY KEY([p_Num])
);
DECLARE #tmp13Parse TABLE
(
[Id] INT
, [Position] BIGINT
, [Value] NVARCHAR(500)
unique clustered ([Id], [Position])
);
--insert test data
INSERT INTO #tmp13 (
[p_Num]
, [DESCRIPT]
)
VALUES ( 100
, '05/30/2017 12:25am Eastern Standard Time - Mjames reported changes in the pages 05/30/2017 10:35AM Eastern Standard Time - JRachael agreed to work on the report and report to James 05/30/2017 10:00AM Eastern Standard Time - James reports errors in page.' )
, ( 200
, '05/29/2017 12:25am Central Stanard Time - TSmith reported changes in the pages 05/29/2017 10:35AM Central Stanard Time - JRachael agreed to work on the report and report to James 05/29/2017 10:00AM Central Stanard Time - GregNoName reports errors in page.' )
, ( 300
, '05/28/2017 12:25am Eastern Standard Time - Mjames reported changes in the pages 05/28/2017 10:35AM Eastern Standard Time - JName agreed to work on the report and report to James 05/28/2017 10:00AM Eastern Standard Time - James reports errors in page.' )
, ( 400
, '05/27/2017 12:25am Central Stanard Time - Mjames reported changes in the pages 05/27/2017 10:35AM Central Stanard Time - JRachael agreed to work on the report and report to James 05/27/2017 10:00AM Eastern Standard Time - AnotherName reports errors in page.' )
, ( 500
, '05/26/2017 12:25am Eastern Standard Time - MJohnson reported changes in the pages 05/26/2017 10:35AM Eastern Standard Time - FTestname agreed to work on the report and report to James 05/26/2017 10:00AM Eastern Standard Time - James reports errors in page.' )
, ( 600
, '05/25/2017 12:25am Eastern Standard Time - Mjames reported changes in the pages 05/25/2017 10:35AM Eastern Standard Time - JRachael agreed to work on the report and report to James 05/25/2017 10:00AM Eastern Standard Time - James reports errors in page.' )
, ( 700
, '05/24/2017 12:25am Eastern Standard Time - TTaylor reported changes in the pages 05/24/2017 10:35AM Eastern Standard Time - JRachael agreed to work on the report and report to James 05/24/2017 10:00AM Eastern Standard Time - TMoreTestNames reports errors in page.' );
--Basically what we are doing is loading a table with each individual word making sure we keep which Id it was associated with.
--along with the Position of where it was in the phrase.
--Two options below depending on SQL Version.
--SQL Version 2016+, we'll use SPLIT_STRING, code is a little more easier
INSERT INTO #tmp13Parse (
[Id]
, [Position]
, [Value]
)
SELECT [a].[p_Num]
, [b].[Position]
, [b].[Value]
FROM #tmp13 [a]
CROSS APPLY (
SELECT [Value]
, ROW_NUMBER() OVER ( ORDER BY (
SELECT 1
)
) AS [Position]
FROM STRING_SPLIT([a].[DESCRIPT], ' ') --this will handle returning a table based on how you split it, in this case a space.
) AS [b];
--Prior to SQL Version 2016 back to 2012, use this option which is using a XML to split the data.
INSERT INTO #tmp13Parse (
[Id]
, [Position]
, [Value]
)
SELECT [a].[p_Num]
, [ss].[Position]
, [ss].[Value]
FROM #tmp13 [a]
CROSS APPLY (
SELECT ROW_NUMBER() OVER ( ORDER BY (SELECT 1)) AS [Position]
, [y].[i].[value]('(./text())[1]', 'nvarchar(max)') AS [Value]
FROM (
SELECT [x] = CONVERT(XML, '<i>'+ REPLACE([a].[DESCRIPT], ' ', '</i><i>')+ '</i>').[query]('.')
) AS [a]
CROSS APPLY [x].[nodes]('i') AS [y]([i])
) AS [ss];
--After we have split the data we'll now go after the specific values
SELECT [a].[Id]
, [a].[Value] AS [Date]
, [ccc].[Value] AS [Name]
FROM #tmp13Parse [a]
--First cross apply - what is the position of '-' after my date field. add 1 since the next value should be the name I'm after.
CROSS APPLY (
SELECT MIN([aa].[Position]) + 1 AS [nameAnchorPosition]
FROM #tmp13Parse [aa]
WHERE [aa].[Id] = [a].[Id]
AND [aa].[Value] = '-'
AND [aa].[Position] > [a].[Position]
) AS [bb]
--Second cross apply - Now, based on where I identified '-' to be, plus 1, give me that value.
CROSS APPLY (
SELECT [cc].[Value]
FROM #tmp13Parse [cc]
WHERE [cc].[Id] = [a].[Id]
AND [cc].[Position] = [bb].[nameAnchorPosition]
) AS [ccc]
WHERE TRY_CONVERT(DATE, [a].[Value]) > '1900-01-01'; --will return all those values that are a date as starting point, long with their position.
I did a quick test on one of my servers with record set of 54000 using both options for splitting and parsing and both gave me results in 4-10 seconds. Your mileage may vary.

Related

BigInt won't convert to proper date format

So I have a query I'm trying to write where there are two columns that will have variable results. One is date and one is time. My query will look like
Select Schedule ID , Job_Name , next_run_date , next_run_time
The values will vary depending on what database I'm running against. For example, [next_run_date] might = 20181014 and [next_run_time] might read 1000 which would be 1am. But if I run it on a different server, it could have a completely different set of values, but just the same format.
I've unsuccessfully tried to convert the columns to date/time format by using
CONVERT(varchar(10),CONVERT(date,[next_run_date],110),110) AS 'Next Run'
And just get 'Explicit conversion from data type int to date is not allowed'
What I'd like it to display is [next_run_date] might = 10-14-2018 and [next_run_time] = 01:00. Just unsure how to convert this correctly. I do not have to write privs to the database. If I read correctly, at least for the date column, I would have to convert from Bigin to Varchar to ToDate, but unclear how to fully write that.
For the time field you can stuff a : in it.
And a FORMAT for the times below 10 AM.
And the date part can be done with 2 casts and a CONVERT or a FORMAT.
The date from an INT to a VARCHAR in the 'mm-dd-yyyy' format:
CONVERT(VARCHAR(10), CAST(CAST([next_run_date] AS VARCHAR(8)) AS DATE), 110)
The time from an INT to a VARCHAR in the 'hh:mi' format:
STUFF(CAST(FORMAT([next_run_time],'000000') AS VARCHAR(4)),3,0,':')
Example snippet:
DECLARE #Table TABLE (next_run_date INT, next_run_time INT);
INSERT INTO #Table (next_run_date, next_run_time) VALUES
(20180901, 13500)
,(20181015, 134200)
;
SELECT
CONVERT(VARCHAR(10), CAST(CAST([next_run_date] AS VARCHAR(8)) AS DATE), 110) AS [Next Run Date],
STUFF(CAST(FORMAT([next_run_time],'000000') AS VARCHAR(4)),3,0,':') AS [Next Run Time]
FROM #Table
Returns:
Next Run Date Next Run Time
------------- -------------
09-01-2018 01:35
10-15-2018 13:42
You need to convert the bigint to a varchar first, then to a date:
CONVERT(date,CONVERT(varchar(10),[next_run_date]),110) AS 'Next Run'
You could also break up the number into parts and craft a date and time.
DECLARE #Date INT=20181014
DECLARE #Time INT=123456
SELECT CONVERT(DATE,
SUBSTRING(CONVERT(VARCHAR(20),#Date),1,4)+'/'+
SUBSTRING(CONVERT(VARCHAR(20),#Date),5,2)+'/'+
SUBSTRING(CONVERT(VARCHAR(20),#Date),7,2)
) AS [Date]
SELECT CONVERT(TIME,
SUBSTRING(CONVERT(VARCHAR(20),#Time),1,LEN(CONVERT(VARCHAR(20),#Time))-4)+':'+
SUBSTRING(CONVERT(VARCHAR(20),#Time),LEN(CONVERT(VARCHAR(20),#Time))-3,2)+':'+
SUBSTRING(CONVERT(VARCHAR(20),#Time),LEN(CONVERT(VARCHAR(20),#Time))-1,2)
) AS [Time]
select convert(datetime, cast(20181014 as varchar), 102)
Note :
CAST is part of the ANSI-SQL specification; whereas, CONVERT is not.
In fact, CONVERT is SQL implementation specific. CONVERT differences
lie in that it accepts an optional style parameter which is used for
formatting.

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.

Values show up as rounded in SQL Server Management Studio but not when extracted

I have set up the query underneath in Microsoft SQL Server Mgmt studio and am receiving interesting results in regards to the Rate and Variable rate columns.
When I run my query in Management Studio I receive the following result set:
The columns are in the following order: TradeID,Dealer,IssuanceDate,MaturityDate,Face,Rate,Proceeds,TxnCCY,VariableRate
Trade ID Dealer IssuanceDate MaturityDate Face Rate Proceeds TXN CCY Variable Rate
PERNOD & RICARD BAR 20121212 20121221 10000000 0.24 9999400 USD NULL
PUT_30 04821QAP6 1ML POOL BAS 20121022 20130418 100000000 0.28 100000000 USD 0.2075
When I run my query outside of Management Studio in a batch file I receive the following result set:
Trade ID Dealer IssuanceDate MaturityDate Face Rate Proceeds TXN CCY Variable Rate
PERNOD & RICARD; BAR;20121212; 20121221; 10000000;0.23999999999999999;9999400;USD;
PUT_30 04821QAP6 1ML POOL;BAS;20121022; 20130418; 100000000;0.28000000000000003;100000000;USD;
0.20749999999999999
How come SQL Server Management Studio is rounding the value but my batch file is not? I need to see the rounded value in my batch extract. I have tried changing the data types of the columns to decimal and real only to receive errors.
Does anyone have any suggestions as to how I can make this work and show the rounded values in my extract?
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
ALTER PROCEDURE [dbo].[CALYON_TRADES_LIABILITIES_TEST]
-- Add the parameters for the stored procedure here
#BusDate datetime = NULL
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
IF #BusDate is null
BEGIN
DECLARE #yesterday datetime
set #yesterday = DATEADD(D, -1, GETDATE())
set #BusDate = CONVERT(datetime,
convert(varchar(2), datepart(month, #yesterday)) + '/' + convert(varchar(2),datepart
(day, #yesterday)) + '/' + convert(varchar(4), datepart(year, #yesterday))
)
END
Drop table dbo.VariableRateLiabilities
Create table dbo.VariableRateLiabilities
(TradeID VarChar(35) null,
Dealer VarChar(15) null,
IssuanceDate varchar (8) null,
MaturityDate varchar (8) null,
Face numeric (9) null,
Rate float null,
Proceeds numeric null,
TxnCCY VarChar (3) null,
VariableRate float null,
VariableRateDate varchar (8) null)
INSERT INTO dbo.VariableRateLiabilities SELECT DISTINCT
RPT.TradeName as TradeID,
RPT.DealerShortName as Dealer,
CONVERT(varchar(8), RPT.TxnValueDate, 112) as IssuanceDate,
CONVERT(varchar(8), RPT.MaturityDate, 112) as MaturityDate,
RPT.Face,
RPT.Rate,
RPT.Proceeds,
RPT.TxnCCY,
IRI.InterestIdxRate as VariableRate,
CONVERT (varchar (8), IRI.InterestIdxDate, 112) as VariableRateDate
From RPT_TradesIssuance RPT
INNER JOIN LiabilityTrades LT
ON RPT.Price = LT.Price
LEFT OUTER JOIN InterestRateIndexes IRI
ON LT.InterestRateCode = IRI.InterestRateCode
WHERE RPT.SPVId=12
AND RPT.MaturityDate > #BusDate
AND RPT.TxnValueDate <= #BusDate
select TradeId,Dealer,IssuanceDate,MaturityDate,Face,Rate,Proceeds,TxnCCY,VariableRate
from dbo.VariableRateLiabilities
where TradeId NOT LIKE 'PUT%'
UNION
select a.TradeId,a.Dealer,a.IssuanceDate,a.MaturityDate,a.Face,a.Rate,a.Proceeds,a.TxnCCY,a.VariableRate
from dbo.VariableRateLiabilities a
where a.VariableRateDate in (select MAX(b.VariableRateDate) from dbo.VariableRateLiabilities b where a.TradeID = b.TradeID)
ORDER BY Dealer,1
END
Your table definition would look like this:
CREATE TABLE dbo.VariableRateLiabilities (
TradeID VARCHAR(35) NULL
,Dealer VARCHAR(15) NULL
,IssuanceDate VARCHAR(8) NULL
,MaturityDate VARCHAR(8) NULL
,Face NUMERIC(9) NULL
,Rate NUMERIC(18, 2) NULL
,Proceeds NUMERIC NULL
,TxnCCY VARCHAR(3) NULL
,VariableRate NUMERIC(18, 4) NULL
,VariableRateDate VARCHAR(8) NULL
);
And your data insert would look like this (assuming that the original values are floats or something similar, so you'd need to CAST or CONVERT):
INSERT INTO dbo.VariableRateLiabilities
SELECT DISTINCT
RPT.TradeName AS TradeID
,RPT.DealerShortName AS Dealer
,CONVERT(VARCHAR(8), RPT.TxnValueDate, 112) AS IssuanceDate
,CONVERT(VARCHAR(8), RPT.MaturityDate, 112) AS MaturityDate
,RPT.Face
,CAST(RPT.Rate AS NUMERIC(18, 2)) AS Rate
,RPT.Proceeds
,RPT.TxnCCY
,CAST(IRI.InterestIdxRate AS NUMERIC(18,4)) AS VariableRate
,CONVERT (VARCHAR(8), IRI.InterestIdxDate, 112) AS VariableRateDate
FROM RPT_TradesIssuance RPT
INNER JOIN LiabilityTrades LT ON RPT.Price = LT.Price
LEFT OUTER JOIN InterestRateIndexes IRI ON LT.InterestRateCode = IRI.InterestRateCode
WHERE RPT.SPVId = 12
AND RPT.MaturityDate > #BusDate
AND RPT.TxnValueDate <= #BusDate;
I am not sure what will happen when you try to CAST the Rate and VariableRate columns like this, because that depends on the data types they have in the RPT_TradesIssuance and InterestRateIndexes tables. Are they FLOAT as well?
(Please note, I chose NUMERIC(18, 2) and NUMERIC(18, 4) as data types, but you should adapt those according to the data that will be stored in those columns.)
If you can avoid floating-point data types in dealing with money, you probably should. Wherever you see a float data type, try replacing it with numeric.
Contrary to common opinion, you can't round an arbitrary floating-point value to two places. And any calculation that involves a floating-point value is probably going to require all values be converted to floating-point, and it will probably return a float. (I looked for SQL Server docs on this, but haven't found them yet.)
Using the correct data type will fix the root cause of the problem.
You might be able to work around the real problem by explicit casting and rounding at the right time, but everybody will have to get that exactly right every time. Fixing the root cause makes everyone's life easier. (cast(float_column_name as numeric(n,m), where 'n' and 'm' are application-dependent. You might choose convert() instead of cast.)
You should convert the FLOAT variables to DECIMAL before rounding them.

NVARCHAR TO DATETIME conversion

I have a parameter that has a value of 14-Sep-2012 15:47:27 that I would like to update a table column with which is in 2012-08-10 05:00:00.000 format.
What would be the query needed '
#UpdateTime = '14-Sep-2012 15:47:27'
Update tbl
Set Datetimecol = CONVERT(datetime,#UpdateTime,110) ??
I am using SQL Server 2008. Thank you !
For the edited question, you only need to drop the 110 specification. There really isn't a specification for the format you have shown, but English installations of SQL Server will convert it.
e.g.
declare #UpdateTime datetime = '14-Sep-2012 15:47:27'
select CONVERT(datetime,#UpdateTime)
-- result
September, 14 2012 15:47:27
Assuming your month portion is at least 3 characters long, e.g. Mar, Marc, March, Sept, you can convert that very bad text datetime to a normal 3-char month format using the following
declare #updatetime nvarchar(20) = '18-Sept-2012'
declare #fixedtime nvarchar(20)
set #fixedtime = stuff(#updatetime,1,charindex('-',#updatetime),'')
set #fixedtime = Left(#updatetime,charindex('-',#updatetime))
+ stuff(#fixedtime,4,len(#fixedtime)-8,'')
-- #fixedtime contains `18-Sep-2012`
Update tbl
Set Datetimecol = #fixedtime
Yes, I deliberately left out the CAST/CONVERT in the update statement.
As long as your language settings are always English and your regional settings don't change, here is another approach (along with sample data of various potential formats):
DECLARE #x TABLE(y NVARCHAR(15));
INSERT #x VALUES('18-Sept-2012'),('9-May-2012'),('19-Oct-2012'),('04-March-2012');
SELECT z, CONVERT(DATETIME,z) FROM
(
SELECT SUBSTRING(y,s+1,3) + ' ' + LEFT(y,s-1) + ', ' + RIGHT(y,4) FROM
(
SELECT y, CHARINDEX('-',y) FROM #x
) AS y(y,s)
) AS z(z);
Results:
Sep 18, 2012 2012-09-18 00:00:00.000
May 9, 2012 2012-05-09 00:00:00.000
Oct 19, 2012 2012-10-19 00:00:00.000
Mar 04, 2012 2012-03-04 00:00:00.000
You can use the same calculation for a variable:
DECLARE
#y NVARCHAR(15) = N'18-Sept-2012',
#z DATETIME;
SELECT #z = CONVERT(DATETIME,z) FROM
(
SELECT SUBSTRING(y,s+1,3) + ' ' + LEFT(y,s-1) + ', ' + RIGHT(y,4) FROM
(
SELECT #y, CHARINDEX('-',#y)
) AS y(y,s)
) AS z(z);
SELECT #z;
-- UPDATE ...
(Now try with SET LANGUAGE FRENCH; and watch it all go to, well, somewhere.)
For this reason I highly recommend you stop storing / passing dates using the nvarchar type. We have strongly typed date/time types for a reason. When you need to pass a string literal, you should be using unambiguous, standard formats that aren't prone to differences in language, regional and dateformat settings. In this case the right format should be YYYYMMDD:
20120918
For more info, see:
Bad habits to kick : mis-handling date / range queries

sql timezone calculation

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