Search code examples
c#.netsql-serverdatetime.net-core

Why is C# DateTime.Now/DateTime.UtcNow ahead of SQL Server's SYSUTCDATETIME()/SYSDATETIME() even though C# code executes before the SQL Query


I want to know the reason why my C# date is larger than the SQL date even though the C# code is running first and after that the SQL query,

Logically the SQL date should be greater than C# date.

For your reference the .NET application and SQL Server are on my local machine.

C# Code:


using System.Data;
using System.Data.SqlClient;

for (int i = 1; i <= 20; i++)
{
    AddRecord();
}
Console.WriteLine("20 records added in database....");

void AddRecord()
{
    try
    {
        string ConnectionString = @"data source=OM5\SQL2019; database=TestDb; integrated security=SSPI";
        using (SqlConnection connection = new SqlConnection(ConnectionString))
        {
            SqlCommand cmd = new SqlCommand()
            {
                CommandText = "SP_AddRecord",
                Connection = connection,
                CommandType = CommandType.StoredProcedure
            };

            SqlParameter param1 = new SqlParameter
            {
                ParameterName = "@CSharp_DateNow",
                SqlDbType = SqlDbType.DateTime2,
                Value = DateTime.Now,
                Direction = ParameterDirection.Input
            };
            cmd.Parameters.Add(param1);

            SqlParameter param2 = new SqlParameter
            {
                ParameterName = "@CSharp_DateUTCNow",
                SqlDbType = SqlDbType.DateTime2,
                Value = DateTime.UtcNow,
                Direction = ParameterDirection.Input
            };
            cmd.Parameters.Add(param2);

            connection.Open();
            cmd.ExecuteNonQuery();
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Exception Occurred: {ex.Message}");
    }
}

SQL:

CREATE TABLE [dbo].[Records](
    [Id] [int] PRIMARY KEY IDENTITY(1,1) NOT NULL,
    [SQL_SysDateTime] [datetime2](7) NOT NULL,
    [CSharp_DateNow] [datetime2](7) NOT NULL,
    [SQL_SysUTCDateTime] [datetime2](7) NOT NULL,
    [CSharp_DateUTCNow] [datetime2](7) NOT NULL
)

CREATE OR ALTER   PROCEDURE [dbo].[SP_AddRecord]
@CSharp_DateNow datetime2,
@CSharp_DateUTCNow datetime2
AS
BEGIN
    SET NOCOUNT ON;

     insert into Records(SQL_SysDateTime, CSharp_DateNow, SQL_SysUTCDateTime, CSharp_DateUTCNow) values
     (SYSDATETIME(),@CSharp_DateNow,SYSUTCDATETIME(),@CSharp_DateUTCNow)
END

Result in table

SQL_SysDateTime CSharp_DateNow Diff. (MS) SQL_SysUTCDateTime CSharp_DateUTCNow Diff. (MS)
2024-07-26 13:26:35.2898391 2024-07-26 13:26:34.9701658 319 2024-07-26 07:56:35.2898391 2024-07-26 07:56:34.9726788 317
2024-07-26 13:26:35.3054610 2024-07-26 13:26:35.3174393 -12 2024-07-26 07:56:35.3054610 2024-07-26 07:56:35.3174492 -12
2024-07-26 13:26:35.3210815 2024-07-26 13:26:35.3217354 0 2024-07-26 07:56:35.3210815 2024-07-26 07:56:35.3217461 0
2024-07-26 13:26:35.3210815 2024-07-26 13:26:35.3261818 -5 2024-07-26 07:56:35.3210815 2024-07-26 07:56:35.3261915 -5
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3310309 5 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3310384 5
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3411312 -5 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3411394 -5
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3418632 -5 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3418676 -5
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3430069 -7 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3430104 -7
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3437519 -7 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3437554 -7
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3446140 -8 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3446172 -8
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3452865 -9 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3452894 -9
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3459309 -9 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3459336 -9
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3466520 -10 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3466552 -10
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3475280 -11 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3475305 -11
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3486445 -12 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3486474 -12
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3492964 -13 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3492991 -13
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3501936 -14 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3501961 -14
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3506370 -14 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3506392 -14
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3511339 -15 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3511362 -15
2024-07-26 13:26:35.3367030 2024-07-26 13:26:35.3517053 -15 2024-07-26 07:56:35.3367030 2024-07-26 07:56:35.3517087 -15

I want an actual reason or an authentic source which can explain this.


Solution

  • These values are precise but not accurate.

    If you take the distinct values from SQL_SysDateTime and compare them...

    SELECT MS_Diff = DATEDIFF(NANOSECOND, '2024-07-26 13:26:35.2898391', '2024-07-26 13:26:35.3054610')/1E6, 
           MS_Diff = DATEDIFF(NANOSECOND, '2024-07-26 13:26:35.3054610', '2024-07-26 13:26:35.3210815')/1E6, 
           MS_Diff = DATEDIFF(NANOSECOND, '2024-07-26 13:26:35.3210815', '2024-07-26 13:26:35.3367030')/1E6
    

    This returns 15.6219, 15.6205, 15.6215 as differences between them (in ms).

    As documented here SQL Server uses GetSystemTimeAsFileTime() for SYSDATETIME()/SYSUTCDATETIME().

    Raymond Chen indicates here that by default GetSystemTimeAsFileTime() is not especially accurate though mentions default refresh periods for the value returned by it of 55ms or 10ms rather than 15.62 so presumably this has changed since then.

    Various sites indicate that the default timer resolution in Windows 10 is 15.6 ms (or more specifically 15625000ns) so the above gaps are in line with that.

    For C# the documentation for DateTime.UtcNow doesn't look any more promising

    The resolution of this property depends on the system timer, which depends on the underlying operating system. It tends to be between 0.5 and 15 milliseconds.

    So there is still the question as to how that is achieving the greater accuracy.

    You have tagged .NET core. Per this pull request it now calls GetSystemTimePreciseAsFileTime when available (one of the later ones mentioned in the Raymond Chen post above).

    On my local machine (Win 11) I do mostly see diffs of around 1ms when running the following test. (But running powercfg -energy does tell me that various processes I have running (including chrome.exe and MongoDB) have requested a low time interval for the Platform Timer Resolution)

    SET NOCOUNT ON;
    
    DECLARE @Times TABLE(insert_time datetime2)
    
    DECLARE @Counter INT = 0
    
    WHILE @Counter < 10000
    BEGIN
    INSERT @Times VALUES (SYSUTCDATETIME())
    SET @Counter+=1;
    END
    
    
    SELECT  [rowcount] = COUNT(*), 
            insert_time, 
            prev_insert_time = LAG(insert_time) OVER (ORDER BY insert_time),
            diff_ms = DATEDIFF(NANOSECOND,LAG(insert_time) OVER (ORDER BY insert_time), insert_time)/1e6
    FROM @Times
    GROUP BY insert_time
    

    SQL Server doesn't currently have any native way of calling GetSystemTimePreciseAsFileTime and returning datetime2(7) so if this is important to you you will need to do it outside of the database (you could also use CLR integration for this but then the assembly would need to be marked as unsafe to invoke the WinAPI function).

    Running the above on Azure SQL database I got the following results so doesn't look like it is refreshed any more frequently there (and you only get ~64 unique values per second).

    interestingly replacing SYSUTCDATETIME() with GETUTCDATE() I do get diffs of 3.3333/3.3334 ms so this does appear less precise but more accurate. Presumably this as a result of the "correction" mentioned here.

    This situation appears to me to be less than ideal. There is a feedback request Have SYSDATETIME() return value from GetSystemTimePreciseAsFileTime() but it only has 3 votes and is tagged "Archived" so not sure if that means that it will never be considered.

    rowcount insert_time prev_insert_time diff_ms
    563 2024-07-29 07:21:30.6607494 NULL NULL
    646 2024-07-29 07:21:30.6763740 2024-07-29 07:21:30.6607494 15.6246
    659 2024-07-29 07:21:30.6919988 2024-07-29 07:21:30.6763740 15.6248
    673 2024-07-29 07:21:30.7076257 2024-07-29 07:21:30.6919988 15.6269
    666 2024-07-29 07:21:30.7232517 2024-07-29 07:21:30.7076257 15.626
    659 2024-07-29 07:21:30.7390662 2024-07-29 07:21:30.7232517 15.8145
    667 2024-07-29 07:21:30.7545026 2024-07-29 07:21:30.7390662 15.4364
    660 2024-07-29 07:21:30.7701262 2024-07-29 07:21:30.7545026 15.6236
    667 2024-07-29 07:21:30.7857507 2024-07-29 07:21:30.7701262 15.6245
    668 2024-07-29 07:21:30.8013755 2024-07-29 07:21:30.7857507 15.6248
    664 2024-07-29 07:21:30.8169996 2024-07-29 07:21:30.8013755 15.6241
    631 2024-07-29 07:21:30.8326241 2024-07-29 07:21:30.8169996 15.6245
    660 2024-07-29 07:21:30.8482510 2024-07-29 07:21:30.8326241 15.6269
    662 2024-07-29 07:21:30.8638744 2024-07-29 07:21:30.8482510 15.6234
    670 2024-07-29 07:21:30.8795009 2024-07-29 07:21:30.8638744 15.6265
    185 2024-07-29 07:21:30.8951255 2024-07-29 07:21:30.8795009 15.6246