【问题标题】:How does Microsoft SQL Server compare datetime to datetimeoffset values in a query statement?Microsoft SQL Server 如何在查询语句中比较 datetime 和 datetimeoffset 值?
【发布时间】:2021-09-28 13:35:42
【问题描述】:

我正在使用一个旧数据库(在 SQL Server 2019 上运行),它使用 datetime 存储时间戳信息的列,我试图更好地处理 SQL Server 在比较datetime 值转换为 datetimeoffset 值。

我们正在尝试将一些代码迁移到将使用 Microsoft SQL Server JDBC 驱动程序的新平台。为了在兼容级别 150(适用于 SQL Server 2019)下运行数据库,Microsoft JDBC 驱动程序会将所有时间戳转换为 datetimeoffset 参数。当datetime 值以37 结尾时,这会导致精度问题。

我一直在阅读其他 Stack Overflow 响应(例如 SQL Server behaviour change while using datetimeoffset 和 [x][2])并了解 SQL Server 如何将 datetime 值强制转换为 datetimeoffset,但这种强制似乎不会正是在 SQL 语句的 WHERE 子句中发生的事情。

以下面的 SQL 为例:

declare @tmp table (id int identity primary key, createDate datetime not null)
insert into
    @tmp
        (createDate)
    values
          ('2021-09-27 18:36:01.930')
        , ('2021-09-27 18:36:01.933')
        , ('2021-09-27 18:36:01.937')

declare @input datetimeoffset = '2021-09-27 18:36:01.937'

select
    *
from
    @tmp
where
    createDate = @input

运行此查询时,您没有得到任何匹配项。

我知道,如果你运行 select cast('2021-09-27 18:36:01.937' as datetimeoffset),你会得到 2021-09-27 18:36:01.9370000 +00:00,如果你运行 select cast(cast('2021-09-27 18:36:01.937' as datetime) as datetimeoffset),你会得到 2021-09-27 18:36:01.9366667 +00:00,所以 可能如果createDate 列被隐式转换为datetimeoffset,为什么这些值可能不匹配。但是,当我查看执行计划时,情况似乎并非如此,如果我将代码更改为:

declare @tmp table (id int identity primary key, createDate datetime not null)
insert into
    @tmp
        (createDate)
    values
          ('2021-09-27 18:36:01.930')
        , ('2021-09-27 18:36:01.933')
        , ('2021-09-27 18:36:01.937')

declare @input datetimeoffset = '2021-09-27 18:36:01.9366667 +00:00'

select
    *
from
    @tmp
where
    createDate = @input

我仍然没有得到匹配,这似乎意味着 createDate 没有被强制转换为 datetimeoffset 数据类型进行比较。现在,如果我明确地将 datetimeoffset 转换为 datetime,我会得到匹配:

declare @tmp table (id int identity primary key, createDate datetime not null)
insert into
    @tmp
        (createDate)
    values
          ('2021-09-27 18:36:01.930')
        , ('2021-09-27 18:36:01.933')
        , ('2021-09-27 18:36:01.937')

declare @input datetimeoffset = '2021-09-27 18:36:01.937'

select
    *
from
    @tmp
where
    createDate = cast(@input as datetime)

或者,如果我尝试查找匹配 2021-09-27 18:36:01.930 值,我不需要显式转换该值:

declare @tmp table (id int identity primary key, createDate datetime not null)
insert into
    @tmp
        (createDate)
    values
          ('2021-09-27 18:36:01.930')
        , ('2021-09-27 18:36:01.933')
        , ('2021-09-27 18:36:01.937')

declare @input datetimeoffset = '2021-09-27 18:36:01.930'

select
    *
from
    @tmp
where
    createDate = @input

这告诉我正在进行某种转换,但我无法弄清楚幕后发生了什么。

任何人都可以帮助我了解 SQL Server 在运行以下命令时在后台做什么?

declare @tmp table (id int identity primary key, createDate datetime not null)
insert into
    @tmp
        (createDate)
    values
          ('2021-09-27 18:36:01.930')
        , ('2021-09-27 18:36:01.933')
        , ('2021-09-27 18:36:01.937')

declare @input datetimeoffset = '2021-09-27 18:36:01.937'

select
    *
from
    @tmp
where
    createDate = @input

我正在尝试查看是否有某种方法可以格式化时间戳数据,以便所有现有代码都不会中断。我可以将毫秒精度降低到 1/100(而不是 3.33 毫秒精度),但我想保持尽可能高的精度。我最初开始将datetime 值转换为datetime2 的路径,但由于数据量、索引、统计数据等围绕数据转换所有内容都需要相当长的停机时间。更不用说代码使用了需要重构的各种 datetime 数学技巧。

我知道我可以浏览所有代码,还可以添加从datetimeoffsetdatetime 的显式转换,但是一些代码是由流利的 SQL 构建器生成的,所以这样做也不是很简单。

最后,我知道我可以将值转换为 varchar(23) 字段并允许 SQL Server 以这种方式进行隐式转换,但如果可能的话我想避免这种情况。

所以,如果有人能阐明 SQL Server 在内部做什么来比较 datetimedatetimeoffset 的值,我将不胜感激。

RANT — 我真的不知道为什么 Microsoft 更改 SQL Server 2016 以在转换为 datetimeoffsetdatetime2。尤其是自从铸造了datetimeoffset(3)datetime2(3)datetimeoffset 会导致 2021-09-27 18:36:01.9370000 +00:00。例如:

select 
    cast(cast('2021-09-27 18:36:01.937' as datetime) as datetimeoffset) as [datetime]
  , cast(cast('2021-09-27 18:36:01.937' as datetimeoffset(3)) as datetimeoffset) as [datetimeoffset(3)]
  ,   cast(cast('2021-09-27 18:36:01.937' as datetime2(3)) as datetimeoffset) as [datetime2(3)]

结果: |日期时间 |日期时间偏移(3) |日期时间2(3) | |--|--|--| | 2021-09-27 18:36:01.9366667 +00:00 | 2021-09-27 18:36:01.9370000 +00:00 | 2021-09-27 18:36:01.9370000 +00:00 |

如果他们都是一样的,看起来会更一致 方式。我知道这个改变据说是为了提高精度 准确性,但我的猜测是它导致的问题比它解决的问题要多。

【问题讨论】:

    标签: sql-server datetime jdbc


    【解决方案1】:

    好的,这里的问题是隐式转换。正如您所指出的,当您在 最近 版本的 SQL Server(SQL Server 2016+)上将 datetime 转换为 datetimeoffset 时,该值被正确转换为精确到 1/300一秒。例如,18:36:01.937 转换为datetimeoffset(7)(甚至是datetime2(7))的时间将为18:36:01.9366667(在旧版本的 SQL Server 上不是这种情况,它会错误地将值转换为 18:36:01.9370000。)

    您将参数定义为datetimeoffset无精度(坏主意),因此默认为datetimeoffset(7)。因此,2021-09-27T18:36:01.937 的值变为 2021-09-27T18:36:01.9370000+00:00

    这里的datetime 值隐式转换为datetimeoffset,因为后者具有更高的Data Type Precedence。所以你的 3 个值变成:

    2021-09-27T18:36:01.9300000+00:00
    2021-09-27T18:36:01.9333333+00:00
    2021-09-27T18:36:01.9366667+00:00
    

    如您所见,这些都与您的变量@input 的值不同,即2021-09-27T18:36:01.970000+00:00 的值。

    有人会认为因此将@input 的精度定义为3 可以解决这个问题,可惜它没有。似乎在幕后 SQL Server 在进行比较时仍将datetime 隐式转换为datetimeoffset(7)

    因此解决方案是因此使用显式强制转换,documentation 中也有说明:

    在数据库兼容级别 130 下,从 datetime 到 datetime2 数据类型的隐式转换通过考虑小数毫秒来提高准确性,从而产生不同的转换值,如上面的示例所示。只要存在 datetime 和 datetime2 数据类型之间的混合比较方案,就使用显式转换为 datetime2 数据类型。如需更多信息,请参阅此Microsoft Support Article

    由于您要查询的表有一个datetime,您希望将参数显式转换为datetime,而不是转换列,以保持可搜索性:所以您想要的正确查询是:

    DECLARE @tmp table (id int IDENTITY PRIMARY KEY,
                        createDate datetime NOT NULL);
    INSERT INTO @tmp (createDate)
    VALUES ('2021-09-27T18:36:01.930'),
           ('2021-09-27T18:36:01.933'),
           ('2021-09-27T18:36:01.937');
    
    DECLARE @input datetimeoffset(3) = '2021-09-27T18:36:01.937';
    
    SELECT createDate
    FROM @tmp
    WHERE createDate = CONVERT(datetime, @input);
    

    如果反过来,您的列是datetimeoffset,参数是datetime,那么您可以将所述参数转换为datetimeoffset(3)

    DECLARE @tmp table (id int IDENTITY PRIMARY KEY,
                        createDate datetimeoffset NOT NULL);
    INSERT INTO @tmp (createDate)
    VALUES ('2021-09-27T18:36:01.930'),
           ('2021-09-27T18:36:01.933'),
           ('2021-09-27T18:36:01.937');
    
    DECLARE @input datetime = '2021-09-27T18:36:01.937';
    
    SELECT createDate
    FROM @tmp
    WHERE createDate = CONVERT(datetimeoffset(3), @input);
    

    解决您的“咆哮”:
    因为“新”方法更准确(正如我之前所说的)。 datetime2021-09-27T18:36:01.937 不是2021-09-27T18:36:01.9370000~datetime 精确到 1/300 秒。这就是为什么datetime 值都以037 结尾的原因;后 2 个是 0.0033333~0.0066666~ 秒,四舍五入到小数点后 3 位。因此,例如,当您将 datetime 值转换为 datetime2(7) 时,转换后的值完全符合先前的精度,因此 datetime2021-09-27T18:36:01.937 被转换为 datetime2(7)2021-09-27T18:36:01.9366667(即 2021-09-27T18:36:01.9366666666~ 舍入到 7 的精度)。

    【讨论】:

    • 感谢您的意见。令人困惑的是执行计划没有显示任何隐式转换。此外,在上面的示例中,如果我将@datetimeoffset 明确声明为2021-09-27 18:36:01.9366667 +00:00,则查询仍然不会返回任何值。我的示例对datetimeoffset 没有精确度的原因是因为JDBC 驱动程序没有生成一个,这是我无法控制的。就进行显式转换而言,我知道这是可行的,但在我们的应用程序堆栈中存在问题。这就是我试图绕过它的原因。
    • 您是否知道在不使用显式转换的情况下获取 where 子句以匹配不提供 datetimeoffset 精度的方法?
    • 不,如前所述,文档显式告诉您使用显式转换,@dswitzer2。
    • 由于我们有一些第三方流畅的 SQL 构建器,显式转换成为一个问题。我希望 Microsoft JDBC 驱动程序有一个选项,例如 Progress DataDirect JDBC 驱动程序的DateTimeInputParameterType,它允许您指定使用datetime 而不是datetimeoffset。解决该问题的一种方法是根本不使用java.sql.timestamp,而是只使用一个字符串并将varchar 字段隐式转换为datetime,但这感觉很难看。 /cc: @larnu
    • 听起来像你想问的 real 问题,如果如何在 JDBC 中传递 datetime @dswitzer2 。然而,这需要作为一个新问题提出。
    猜你喜欢
    • 1970-01-01
    • 2013-08-15
    • 1970-01-01
    • 2014-01-07
    • 2012-07-30
    • 2018-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多