System.DateTime和SQL Server DateTime2比较产生意外结果

时间:2020-07-15 16:35:42

标签: c# sql-server

我在下面提供了完整的复制品,但这是导致奇怪结果的基本操作。

  1. 在带有datetime2(7)列的SQL Server表中插入一行,并将该列设置为SYSUTCDATETIME()
  2. 将记录的日期时间读取为.net System.DateTime类型(使用System.Data.SqlClient
  3. 运行一个SELECT * FROM table WHERE DateColumn < @readDate
  4. 的查询
  5. 大约50%的时间,该查询将返回创建的记录,即使代码传递的是它从创建的列中读取的日期。

我的假设是这是一个精度问题(例如,.net datetime的精度高于SQL Server datetime2(7)的精度,反之亦然)。

那么我的问题是:

  • 为什么会出现此问题?
  • 如何编写代码,以便这些查询能够始终如一地正常工作

我还发现了以下内容:

  • 使用GETUTCDATE()而不是SYSUTCDATETIME()只会导致20%的时间失败
  • 使用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();
      }
    }
  }
}

使用的版本:

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

2 个答案:

答案 0 :(得分:2)

SQL Server 2016中引入了一项重大更改,更改了DATETIME值转换为DATETIME2值的方式,因此与DATETIME2列进行比较时始终使用DATETIME2参数至关重要。

在数据库兼容级别130下,来自的隐式转换 datetime到datetime2数据类型通过记帐显示出更高的准确性 小数毫秒,导致不同的转换 价值观。每当混合使用显式强制转换为datetime2数据类型 存在datetime和datetime2数据类型之间的比较方案。 有关更多信息,请参见此Microsoft Support Article

Breaking changes to Database Engine features in SQL Server 2016

另请参阅此blog

从本质上讲,这是另一个绝不使用AddWithValue的原因,因为它应始终基于SQL Server列类型进行设置,而基于.NET参数值类型来设置参数类型。

要解决此问题,只需使用DATETIME2参数即可。

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

答案 1 :(得分:1)

像这样使用DateTime很不常见,这表明真正的问题是另外一回事。从评论看来,实际问题是如何删除较旧的记录。

最有效的方法是使用table partitioning。它对应用程序是透明的,自SQL Server 2016起所有版本(从Express到Enterprise)都可用。

删除几乎是瞬时的-您可以使用partition switching between a full table and an empty one,有效地使空分区成为源表的一部分。这只是一个元数据操作,因此非常快。您还可以将分区从实时表移动到存档表,可能存储在速度较慢的介质中