views:

157

answers:

3

Using SQL Server 2008. This is a really junior question and I could really use some elaborate information, but the information on Google seems to dance around the topic quite a bit and it would be nice if there was some detailed elaboration on how this works...

Let's say I have a datetime column and in ADO.NET I set it to DateTime.UtcNow.

1) Does SQL Server store DateTime.UtcNow accordingly, or does it offset it again based on the timezone of where the server is installed, and then return it offset-reversed when queried? I think I know that the answer is "of course it stores it without offsetting it again" but want to be certain.

So then I query for it and cast it from object to a DateTime after getting it from, say, an IDataReader column. As far as I know, System.DateTime has metadata that internally tracks whether it is a UTC DateTime or it is an offsetted DateTime, which may or may not cause .ToLocalTime() and .ToUniversalTime() to have different behavior depending on this state. So,

2) Does this casted System.DateTime object already know that it is a UTC DateTime instance, or does it assume that it has been offset?


Now let's say I don't use UtcNow, I use DateTime.Now, when performing an ADO.NET INSERT or UPDATE.

3) Does ADO.NET pass the offset to SQL Server and does SQL Server store DateTime.Now with the offset metadata?

So then I query for it and cast it from, say, an IDataReader column to a DateTime.

4) Does this casted System.DateTime object already know that it is an offset time, or does it assume that it is UTC?

+1  A: 

SQL Server does not store any timezone information with date/time info put into the database, nor does it do any adjustments to those values (other than using the GetUTCDate() function).

Thus, the interpretation of that data and its timezones is entirely up to your application. If you need to track timezone information you must do so outside of the DateTime columns in SQL Server.

One of our best practices here is to always store ONLY UTC dates in SQL Server and make any localtime adjustments where necessary.

Joe
Thanks. We already agreed to always just use UtcNow everywhere. I just wanted clarification on each step of the lifecycle between ADO.NET and SQL Server, i.e. what does ADO.NET do when it sends an offset date and when it parses it, as per my four specific questions. So SQL Server doesn't convert offset dates to UTC, like it assumes UTC (or rather "zoneless time"). OK. Thanks. However, this doesn't explain what ADO.NET does, though. I'm trying to get a better picture of what the metadata is on a System.DateTime as it transitions from ADO.NET to SQL Server and back.
stimpy77
... particularly in the context of an offset System.DateTime.Now, i.e. in scenarios where UtcNow wasn't used when it should have been.
stimpy77
I'm 96% sure that ADO.NET ignores the Kind attributes of the data when passing it along to SQL server; there is no adjustment made to the data.
Joe
What then of reading it back to System.DateTime? It assumes ..?
stimpy77
+1  A: 

DateTime doesn't keep any offset information - it only stores the datetime value. DateTimeOffset in .Net will keep the timezone offset. If you pass a DateTimeOffset to SQL Server, the offset will be preserved in the database and retrieved back for you when you query the table.

In the DateTimeOffset type, you can check the Offset property, as well as use ToLocalTime, ToUniversalTime and ToOffset methods.

Paul Kearney - pk
So SQL Server doesn't convert offset dates to UTC, it assumes UTC, OK. Thanks. However, this doesn't explain what ADO.NET does, though. I'm trying to get a better picture of what the metadata is on a System.DateTime as it transitions from ADO.NET to SQL Server and back, per my four questions.
stimpy77
The offset is part of the DateTimeOffset value. In the case of System.DateTime, there is no offset. If you put UTC into System.DateTime, you'll get the same value back out. If you put local time into System.DateTime, you get that out. But you have no way of knowing in System.DateTime what the offset is.In the case of a System.DateTime being sent to SQL Server, the value is stored as 2001-10-18 23:14:43.293. If a System.DateTimeOffset was sent to SQL Server, the value stored would be 2010-02-05 04:18:12.5700000 -08:00. Note the DateTimeOffset stores the offset with the value.
Paul Kearney - pk
"If you put local time into System.DateTime, you get that out. But you have no way of knowing in System.DateTime what the offset is." System.DateTime has a Kind property, which tracks whether it is UTC or local. So it certainly does know, going out to the DB, that it has an offset if it started out as DateTime.Now. I guess I'll have to test and find out manually whether it converts to UTC or not.
stimpy77
Interesting... I had not come across the Kind property. Does the Kind property automatically get set if you set the value to UtcNow/Now, or do you need to manually set it via SpecifyKind()?
Paul Kearney - pk
It's automatically set in the return values of DateTime.Now and of DateTime.UtcNow.
stimpy77
When using DateTime param while INSERTing, the offset on datetimeoffset column is +00 when the INSERT is -07:00, while the time part itself stays the same. This turns out to be a breaking incompatibility because 12:00 PM -07:00 during INSERT with a DateTime param comes back out as 12:00 PM +00:00 which is wrong by 7 hours. I assume, then, that to set a datetimeoffset properly you should use the DateTimeOffset struct in C# rather than System.DateTime, because even though System.DateTime is offset-aware, ADO.NET doesn't contemplate it, perhaps because ADO.NET is older than SQL's datetimeoffset?
stimpy77
+2  A: 

Performed some unit tests to answer my own question in all four parts.

1: Does SQL Server store DateTime.UtcNow accordingly, or does it offset it again based on the timezone of where the server is installed, and then return it offset-reversed when queried?

Executed this):

cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
cmd.Parameters.Add(new SqlParameter("@newval", DateTime.UtcNow));
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT CAST(val as varchar) value FROM testtbl";
Console.WriteLine(cmd.ExecuteScalar());

The result of this at 1:30 PM local time (-7h, or 8:30 PM UTC) was:

Jun  3 2010 8:30PM

Then I tried this:

cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
cmd.Parameters.Add(new SqlParameter("@newval", DateTime.UtcNow));
cmd.ExecuteNonQuery();
Console.WriteLine("change time zone to utc");
Console.ReadLine();
cmd.CommandText = "SELECT CAST(val as varchar) value FROM testtbl";
Console.WriteLine(cmd.ExecuteScalar());
Console.WriteLine("change time zone back to local");

Executed at 9:25 PM UTC, it returned

Jun  3 2010 9:25PM

Compare this to DateTime.Now:

cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
cmd.Parameters.Add(new SqlParameter("@newval", DateTime.Now));
cmd.ExecuteNonQuery();
Console.WriteLine("change time zone to utc");
Console.ReadLine();
cmd.CommandText = "SELECT CAST(val as varchar) value FROM testtbl";
Console.WriteLine(cmd.ExecuteScalar());
Console.WriteLine("change time zone back to local");

Executed at 3:55 PM (local; -7h), returned:

Jun  3 2010  3:55PM

2: So then I query for it and cast it from object to a DateTime after getting it from, say, an IDataReader column. Does this casted System.DateTime object already know that it is a UTC DateTime instance, or does it assume that it has been offset?

Neither.

cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
cmd.Parameters.Add(new SqlParameter("@newval", DateTime.UtcNow));
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT val value FROM testtbl";
var retval = (DateTime)cmd.ExecuteScalar();
Console.WriteLine("Kind: " + retval.Kind);
Console.WriteLine("UTC: " + retval.ToUniversalTime().ToString());
Console.WriteLine("Local: " + retval.ToLocalTime().ToString());

The result of this (executed at 1:58 PM local time) was:

Kind: Unspecified
UTC: 6/4/2010 3:58:42 AM
Local: 6/3/2010 1:58:42 PM

That is, .ToUniversalTime() ended up offsetting from local time to UTC time not once but twice (??), and .ToLocalTime() ended up not offsetting at all.

3: Does ADO.NET pass the offset to SQL Server and does SQL Server store DateTime.Now with the offset metadata?

Without performing any unit tests, the answer is already known to be "only with DateTimeOffset" SQL type. SQL's datetime does not do offsets.

4: Does this casted System.DateTime object already know that it is an offset time, or does it assume that it is UTC?

Neither. SQL's DateTimeOffset type is returned as a .NET DateTimeOffset struct.

The following executed at 3:31 PM local time where column offval is a datetimeoffset SQL type,

cmd.CommandText = "INSERT INTO testtbl (offval) VALUES (@newval)";
cmd.Parameters.Add(new SqlParameter("@newval", DateTime.Now));
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT offval value FROM testtbl";
object retvalobj = cmd.ExecuteScalar();
Console.WriteLine("Type: " + retvalobj.GetType().Name);
var retval = (DateTimeOffset)retvalobj;
Console.WriteLine("ToString(): " + retval.ToString());
Console.WriteLine("UTC: " + retval.ToUniversalTime().ToString());
Console.WriteLine("Local: " + retval.ToLocalTime().ToString());

This resulted in:

Type: DateTimeOffset
ToString(): 6/3/2010 3:31:47 PM +00:00
UTC: 6/3/2010 3:31:47 PM +00:00
Local: 6/3/2010 8:31:47 AM -07:00

A surprising disparity.


Going back and executing the test for question #1 above using DateTime.Now instead of DateTime.UtcNow, I validated that ADO.NET does NOT convert to universal time before storing to the database.

That is, this executed at 3:27 PM local time (-7h):

 cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
 cmd.Parameters.Add(new SqlParameter("@newval", DateTime.Now));
 cmd.ExecuteNonQuery();
 Console.WriteLine("change time zone to utc");
 Console.ReadLine();
 cmd.CommandText = "SELECT CAST(val as varchar) value FROM testtbl";
 Console.WriteLine(cmd.ExecuteScalar());
 Console.WriteLine("change time zone back to local");

.. returned ..

Jun  3 2010  3:27PM

Executing this at 3:17 PM local time:

cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
cmd.Parameters.Add(new SqlParameter("@newval", DateTime.UtcNow));
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT val FROM testtbl";
var result = (DateTime)cmd.ExecuteScalar();
Console.WriteLine("Kind: " + result.Kind);
Console.WriteLine("ToString(): " + result.ToString());
Console.WriteLine("Add 1 minute, is greater than UtcNow? "
 + (result.AddMinutes(1) > DateTime.UtcNow).ToString());
Console.WriteLine("Add 1 minute, is greater than Now? "
 + (result.AddMinutes(1) > DateTime.Now).ToString());
Console.WriteLine("Add 1 minute, is less than UtcNow? "
 + (result.AddMinutes(1) < DateTime.UtcNow).ToString());
Console.WriteLine("Add 1 minute, is less than Now? "
 + (result.AddMinutes(1) < DateTime.Now).ToString());
Console.WriteLine("Subtract 1 minute, is greater than UtcNow? "
 + (result.AddMinutes(-1) > DateTime.UtcNow).ToString());
Console.WriteLine("Subtract 1 minute, is greater than Now? "
 + (result.AddMinutes(-1) > DateTime.Now).ToString());
Console.WriteLine("Subtract 1 minute, is less than UtcNow? "
 + (result.AddMinutes(-1) < DateTime.UtcNow).ToString());
Console.WriteLine("Subtract 1 minute, is less than Now? "
 + (result.AddMinutes(-1) < DateTime.Now).ToString());

Resulted in:

Kind: Unspecified
ToString(): 6/3/2010 10:17:05 PM
Add 1 minute, is greater than UtcNow? True
Add 1 minute, is greater than Now? True
Add 1 minute, is less than UtcNow? False
Add 1 minute, is less than Now? False
Subtract 1 minute, is greater than UtcNow? False
Subtract 1 minute, is greater than Now? True
Subtract 1 minute, is less than UtcNow? True
Subtract 1 minute, is less than Now? False

Compare this to DateTime.Now:

cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
cmd.Parameters.Add(new SqlParameter("@newval", DateTime.Now));
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT val FROM testtbl";
var result = (DateTime)cmd.ExecuteScalar();
Console.WriteLine("Kind: " + result.Kind);
Console.WriteLine("ToString(): " + result.ToString());
Console.WriteLine("Add 1 minute, is greater than UtcNow? "
 + (result.AddMinutes(1) > DateTime.UtcNow).ToString());
Console.WriteLine("Add 1 minute, is greater than Now? "
 + (result.AddMinutes(1) > DateTime.Now).ToString());
Console.WriteLine("Add 1 minute, is less than UtcNow? "
 + (result.AddMinutes(1) < DateTime.UtcNow).ToString());
Console.WriteLine("Add 1 minute, is less than Now? "
 + (result.AddMinutes(1) < DateTime.Now).ToString());
Console.WriteLine("Subtract 1 minute, is greater than UtcNow? "
 + (result.AddMinutes(-1) > DateTime.UtcNow).ToString());
Console.WriteLine("Subtract 1 minute, is greater than Now? "
 + (result.AddMinutes(-1) > DateTime.Now).ToString());
Console.WriteLine("Subtract 1 minute, is less than UtcNow? "
 + (result.AddMinutes(-1) < DateTime.UtcNow).ToString());
Console.WriteLine("Subtract 1 minute, is less than Now? "
 + (result.AddMinutes(-1) < DateTime.Now).ToString());

Executed at 3:58 PM (local, -7h):

Kind: Unspecified
ToString(): 6/3/2010 3:59:26 PM
Add 1 minute, is greater than UtcNow? False
Add 1 minute, is greater than Now? True
Add 1 minute, is less than UtcNow? True
Add 1 minute, is less than Now? False
Subtract 1 minute, is greater than UtcNow? False
Subtract 1 minute, is greater than Now? False
Subtract 1 minute, is less than UtcNow? True
Subtract 1 minute, is less than Now? True
stimpy77