简体   繁体   English

System.DateTime 和 SQL 服务器 DateTime2 比较产生意外结果

[英]System.DateTime and SQL Server DateTime2 Comparison Yielding Unexpected Result

I have included a complete reproduction below but here is the basic operation that is causing strange results.我在下面包含了一个完整的复制品,但这是导致奇怪结果的基本操作。

  1. INSERT a row into a SQL Server table with a datetime2(7) column and set the column to SYSUTCDATETIME()在具有datetime2(7)列的 SQL 服务器表中插入一行并将该列设置为SYSUTCDATETIME()
  2. Read the datetime of that record into a .net System.DateTime type (using System.Data.SqlClient )将该记录的日期时间读入 .net System.DateTime类型(使用System.Data.SqlClient
  3. Run a query that is SELECT * FROM table WHERE DateColumn < @readDate运行查询SELECT * FROM table WHERE DateColumn < @readDate
  4. About 50% of the time, that query will return the created record, even though the code is passing in the date it read from the created column大约 50% 的时间,该查询将返回创建的记录,即使代码传递的是它从创建的列中读取的日期

My assumption is this is a precision issue (eg, .net datetime higher precision than SQL Server datetime2(7) or vice versa).我的假设是这是一个精度问题(例如,.net datetime比 SQL Server datetime2(7)精度更高,反之亦然)。

My questions, then, are:那么,我的问题是:

  • Why does this issue occur?为什么会出现这个问题?
  • How can the code be written so these queries would consistently work correctly如何编写代码以使这些查询始终正常工作

I have also discovered the following:我还发现了以下内容:

  • Using GETUTCDATE() instead of SYSUTCDATETIME() only causes failures 20% of the time使用GETUTCDATE()而不是SYSUTCDATETIME()只会导致 20% 的时间失败
  • The issue does not occur using the datetime or datetime2([1-2]) date types, it is only when using precision of 3 or higher with datetime2 the issue occurs使用datetimedatetime2([1-2])日期类型不会出现此问题,只有在使用 datetime2 的精度为 3 或更高时才会出现此问题
using System;
using System.Data.SqlClient;

namespace DateTimeTesting
{
  public class Program
  {
    public static void Main()
    {
      const string connectionString = "Server=(localdb)\\mssqllocaldb;Trusted_Connection=True;ConnectRetryCount=0";
      const int numRuns = 1000;
      const int printLimiter = 100;

      var connection = new SqlConnection(connectionString);
      connection.Open();

      using (var dropDbCommand = connection.CreateCommand())
      {
        dropDbCommand.CommandText = @"IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = N'DateTimeTesting') BEGIN CREATE DATABASE [DateTimeTesting] END;";
        dropDbCommand.ExecuteNonQuery();
      }

      connection.ChangeDatabase("DateTimeTesting");

      using (var createTableCommand = connection.CreateCommand())
      {
        createTableCommand.CommandText = "IF OBJECT_ID('JustChecking') IS NULL CREATE TABLE JustChecking (id INT IDENTITY(1,1), CreateDateUTC datetime2(7))";
        createTableCommand.ExecuteNonQuery();
      }

      try
      {
        int weirdCounter = 0;
        for (int i = 0; i < numRuns; ++i)
        {
          if ((i + 1) % printLimiter == 0)
          {
            Console.WriteLine($"Run #{i + 1}");
          }

          int id = -1;
          DateTime found = DateTime.MinValue;

          using (var insertCommand = connection.CreateCommand())
          {
            insertCommand.CommandText = "INSERT INTO JustChecking (CreateDateUTC) VALUES (SYSUTCDATETIME()); SELECT @@IDENTITY as id;";
            using var insertReader = insertCommand.ExecuteReader();
            if (insertReader.Read())
            {
              id = (int)insertReader.GetDecimal(0);
            }
          }

          using (var selectCommand = connection.CreateCommand())
          {
            selectCommand.CommandText = "SELECT CreateDateUTC FROM JustChecking WHERE id = @id";
            selectCommand.Parameters.AddWithValue("@id", id);

            using var selectReader = selectCommand.ExecuteReader();

            if (selectReader.Read())
            {
              found = selectReader.GetDateTime(0);
            }
          }

          using (var weirdCommand = connection.CreateCommand())
          {
            weirdCommand.CommandText = "SELECT id, CreateDateUTC FROM JustChecking WHERE CreateDateUtc < @inputDate AND id = @inputId";
            weirdCommand.Parameters.AddWithValue("@inputDate", found);
            weirdCommand.Parameters.AddWithValue("@inputId", id);

            using var weirdReader = weirdCommand.ExecuteReader();
            while (weirdReader.Read())
            {
              weirdCounter++;

              if (weirdCounter % printLimiter == 0)
              {
                Console.WriteLine($"Weird #{weirdCounter} = id: {weirdReader.GetInt32(0)}, createDateUtc: {weirdReader.GetDateTime(1):O}, inputDate: {found:O}");
              }
            }
          }
        }

        Console.WriteLine($"Out of {numRuns} runs found {weirdCounter} weird results which accounted for {(double)weirdCounter / (double)numRuns} percent of runs");

        connection.ChangeDatabase("master");
        using (var dropDbCommand = connection.CreateCommand())
        {
          dropDbCommand.CommandText = "DROP DATABASE DateTimeTesting";
          dropDbCommand.ExecuteNonQuery();
        }
      }
      finally
      {
        connection.Close();
        connection.Dispose();
      }
    }
  }
}

Versions used:使用的版本:

  • .NET Core 3.1 (SDK v3.1.301) .NET Core 3.1(SDK v3.1.301)
  • System.Data.SqlClient v4.8.1 System.Data.SqlClient v4.8.1
  • LocalDB version = 13.1.4001.0 LocalDB 版本 = 13.1.4001.0

There was a breaking change introduced in SQL Server 2016 that changed how DATETIME values are converted to DATETIME2 values, and because of this it's critical to always use DATETIME2 parameters when comparing to DATETIME2 columns. SQL Server 2016 中引入了一项重大更改,该更改更改了 DATETIME 值转换为 DATETIME2 值的方式,因此在与 DATETIME2 列进行比较时始终使用 DATETIME2 参数至关重要。

Under database compatibility level 130, implicit conversions from datetime to datetime2 data types show improved accuracy by accounting for the fractional milliseconds, resulting in different converted values.在数据库兼容级别 130 下,从 datetime 到 datetime2 数据类型的隐式转换通过考虑小数毫秒来提高准确性,从而导致不同的转换值。 Use explicit casting to datetime2 datatype whenever a mixed comparison scenario between datetime and datetime2 datatypes exists.只要存在 datetime 和 datetime2 数据类型之间的混合比较方案,就使用显式转换为 datetime2 数据类型。 For more information, see this Microsoft Support Article .有关详细信息,请参阅此Microsoft 支持文章

Breaking changes to Database Engine features in SQL Server 2016 SQL Server 2016 中数据库引擎功能的重大更改

see also this blog另请参阅此博客

Essentially this is yet another reason to never use AddWithValue, as that sets the parameter type based on the .NET parameter value type, when it should always be set based on the SQL Server column type instead.本质上,这是从不使用 AddWithValue 的另一个原因,因为它基于 .NET 参数值类型设置参数类型,而应该始终基于 SQL 服务器列类型设置参数类型。

To fix just use a DATETIME2 parameter.要修复只需使用 DATETIME2 参数。

weirdCommand.Parameters.Add("@inputDate",System.Data.SqlDbType.DateTime2, 7).Value = found;

Using a DateTime like this is rather unusual and suggest the real problem is something else.像这样使用 DateTime 是相当不寻常的,并且表明真正的问题是其他问题。 From the comments it appears the actual problem is how to delete older records.从评论看来,实际问题是如何删除旧记录。

The most effective way to do that is to use table partitioning .最有效的方法是使用表分区 It's transparent to applications, available in all editions (Express to Enterprise) since SQL Server 2016.它对应用程序透明,自 SQL Server 2016 起在所有版本(Express to Enterprise)中都可用。

Deletion can be almost instantaneous - you can use partition switching between a full table and an empty one , effectively making the empty partition part of the source table.删除几乎可以瞬间完成 - 您可以在完整表和空表之间使用分区切换,从而有效地使空分区成为源表的一部分。 This is just a metadata operation, making it very fast.这只是一个元数据操作,速度非常快。 You could also move partitions from the live table to an archive table, possibly stored in slower media您还可以将分区从活动表移动到存档表,可能存储在较慢的介质中

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM