【问题标题】:DateAdd side effects?日期添加副作用?
【发布时间】:2021-04-19 09:33:51
【问题描述】:

我在使用 Microsoft SQL Server 2016 (SP2-CU15) 时遇到了一个非常奇怪的行为:

select convert(datetime, max(TS) + 1.0/24) as A 
from table;

收益2021-01-16 11:59:00.000

同时

select convert(datetime, max(TS) + 1.0/24) as A
     , dateadd(hour, 1, max(TS)) as B
from table;

给我2021-01-16 11:58:59.943 用于A(和2021-01-16 11:59:00.000 用于B)。所以,在我看来,添加第二列会改变第一列的结果?!

我可以通过将 1.0 转换为 real 来强制两列版本工作,顺便说一句:convert(datetime, max(TS) + cast(1.0 as real)/24),但我可以通过写 convert(datetime, max(TS) + cast(1.0 as float)/24) 来强制一列版本失败。

有什么想法吗?

谢谢!

亨德里克。

更新:根据要求,这是一个最小示例:

CREATE TABLE TestTS (TS FLOAT);
INSERT INTO TestTS (TS) VALUES (44210.4993055556);

SELECT convert(datetime, max(TS) + 1.0/24) as A
    , dateadd(hour, 1, max(TS)) as B
from TestTS

如前所述,如果注释掉 B 列,A 的值会发生变化。

【问题讨论】:

  • DATEADD 没有副作用。发布一个可重现的示例:CREATE TABLE 命令,INSERT 与实际值,SELECT 与查询,期望和。实际的。价值观。数据错误或什至没有存储为日期的可能性要大得多。 convert(datetime, max(TS) + 1.0/24) 有什么意义?
  • 顺便说一句 1.0/24 导致一个无理数:0,04166666666....。将数字添加到日期是一种仅适用于 datetime 的技巧 - 只有 datetime 可以被视为数字。浮点运算会导致精度问题,因此您必须假设添加小数值会总是导致结果不准确。请改用DATEADD
  • DATEADD 没有错。该错误首先使用max(TS) + 1.0/24
  • 1.0/24 返回 0.041666 所以这并没有保留很多小数位,这并不奇怪你在查看毫秒时会遇到问题 - 0.041666 * 24 * 60 * 60 * 1000 意味着只有 3599942 毫秒一天内。而不是3600000
  • 至于这里的问题。我可以重现这个问题。当第二列存在与不存在时,它会将不同的值编译到计划中 i.stack.imgur.com/R9mEQ.png

标签: sql-server datetime casting dateadd


【解决方案1】:

DATEADD 没有任何问题。问题是剩下的问题。

首先,有一个严重的错误。日期存储为浮点数。应改为使用适当的类型,例如datetime2datetimedatetimeoffset。最好的选择是datetime2(0)datetimeoffset(0),假设不需要毫秒精度。

datetime 本质上是一个遗留类型,其内部存储格式是...... OADate 格式的浮点数。但这并不意味着浮点数应该代替正确的类型,但应该使用 varbinary 代替 intbigint

然后,尝试通过以该格式计算 1 小时的浮点值 1/24,将一小时添加到 OADate 值。这是一个不合理的数字(0.04166666666....),这意味着舍入错误总是会导致值不准确。

解决方案

真正的解决方案是使用正确的类型和DATEADD,例如:

CREATE TABLE TestTS (TS datetime2(0));
INSERT INTO TestTS (TS) VALUES ('2021-01-16 10:59:00.000');

SELECT dateadd(hour, 1, max(TS)) as B
from TestTS

如果您想要毫秒精度,请使用datetime2(3)

让 hack 发挥作用。

如果您使用datetime,则最终不需要转换为日期时间,但结果仍然不精确。这个:

declare @TestTS table (TS datetime);
INSERT INTO @TestTS (TS) VALUES ('2021-01-16 10:59:00.000');

SELECT max(ts)+ (1.0/24)
from @TestTS

产生2021-01-16 11:58:59.943。 hack 最初看起来有效的唯一原因可能是由于转换期间的舍入错误。

通过添加浮点数获得正确结果的唯一方法是将精度提高到 8 个小数位:

declare @TestTS table (TS datetime);
INSERT INTO @TestTS (TS) VALUES ('2021-01-16 10:59:00.000');

SELECT max(ts)+ (1.00000/24)--, dateadd(hour, 1, max(TS)) as B
from @TestTS

这会产生2021-01-16 11:59:00.000

1.0 是一个小数 (2,1)。 T-SQL 根据操作数的功能位数计算十进制除法的小数位数。如果操作数最多有 4 个小数位,则结果将有 6 个小数位,这还不够。任何大于 4 的小数位都添加 1 个数字。1.00000 产生 8 个小数位 0.04166666

不要这样做。

【讨论】:

  • 也非常感谢您提供这些见解和最佳实践(尽管我将 Alex 的答案标记为正确,因为他挖掘了基本解释)。
【解决方案2】:

原因

感谢@MartinSmith 提供线索。

原因是查询自动参数化和选择存储值的数据类型。

查询 1 是自动参数化的:

StatementText="SELECT CONVERT([datetime],MAX([TS])+@1/@2)
....
<ColumnReference Column="@2" ParameterCompiledValue="(24)" ParameterRuntimeValue="(24)" />
<ColumnReference Column="@1" ParameterCompiledValue="(1.0)" ParameterRuntimeValue="(1.0)" />

查询 2 自动参数化:

StatementText="SELECT convert(datetime, max(TS) + 1.0/24) as A...."

为什么会发生是第一个查询而不是第二个查询有点像黑魔法。

来自SQL Server data types页面:

当您使用 +、-、*、/ 或 % 算术运算符执行 int、smallint、tinyint 或 bigint 的隐式或显式转换 浮点数、实数、小数或数字数据类型的常量值, SQL Server 在计算数据类型时应用的规则和 表达式结果的精度取决于是否 查询是否自动参数化。

因此,查询中的类似表达式有时会产生 不同的结果。当查询未自动参数化时,常量 值首先转换为数字,其精度刚刚大 足以保存常量的值,然后再转换为 指定的数据类型。例如,常数值 1 转换为 numeric (1, 0),常量值250转换为numeric (3, 0)

当查询被自动参数化时,常量值总是 在转换为最终数据之前转换为numeric (10, 0) 类型。当涉及 / 运算符时,不仅可以将结果类型的 相似查询之间的精度不同,但结果值可以 也不同。例如,自动参数化的结果值 包含表达式 SELECT CAST (1.0 / 7 AS float) 的查询 将不同于不是相同查询的结果值 自动参数化,因为自动参​​数化查询的结果 将被截断以适应 numeric (10, 0) 数据类型。

效果

基于上述,使用了以下数据类型(有关如何计算结果类型的说明,请参阅:Precision, scale, and Length (Transact-SQL)):

查询 1 提供更高的精度:

NUMERIC( 2, 1 ) / NUMERIC( 10, 0 ) = NUMERIC( 13, 12 )

查询 2:

NUMERIC( 2, 1 ) / NUMERIC( 2, 0 ) = NUMERIC( 7, 6 )

解决方案

将您的文字和/或中间结果转换为所需的类型以避免意外。 在您的具体情况下,最好的解决方案是不要像 Panagiotis Kanavos 在他的 answer 中解释的那样使用数字算术来操纵日期。

或者,强制浮点数据类型(根据 Dan Guzman 评论)convert(datetime, max(TS) + 1e/24) 也可以解决问题。

This question 处理相同的问题。

【讨论】:

  • "当查询被自动参数化时,常量值总是在转换为最终数据类型之前转换为数字 (10, 0)。"解释它
  • 参数类型问题也可以用float字面量来解决:convert(datetime, max(TS) + 1e/24)
  • 非常感谢您的解释。对于我作为数据库系统的单纯用户来说,有时真的很难理解发生了什么。
猜你喜欢
  • 2017-04-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-11
  • 2013-01-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多