DateAdd 副作用?

DateAdd side effects?

我在使用 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 转换为真实来强制两列版本工作,顺便说一句: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 没问题。问题是剩下的问题。

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

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

然后,尝试通过以 1/24 格式计算 1 小时的浮点值,将 1 小时添加到 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)

让技巧发挥作用。

如果您使用 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。黑客看起来首先起作用的唯一原因可能是由于转换过程中的舍入错误。

通过添加浮点数获得正确结果的唯一方法是将精度提高到 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

不过不要这样做。

原因

感谢@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 页:

When you use the +, -, *, /, or % arithmetic operators to perform implicit or explicit conversion of int, smallint, tinyint, or bigint constant values to the float, real, decimal or numeric data types, the rules that SQL Server applies when it calculates the data type and precision of the expression results differ depending on whether the query is autoparameterized or not.

Therefore, similar expressions in queries can sometimes produce different results. When a query is not autoparameterized, the constant value is first converted to numeric, whose precision is just large enough to hold the value of the constant, before converting to the specified data type. For example, the constant value 1 is converted to numeric (1, 0), and the constant value 250 is converted to numeric (3, 0).

When a query is autoparameterized, the constant value is always converted to numeric (10, 0) before converting to the final data type. When the / operator is involved, not only can the result type's precision differ among similar queries, but the result value can differ also. For example, the result value of an autoparameterized query that includes the expression SELECT CAST (1.0 / 7 AS float) will differ from the result value of the same query that is not autoparameterized, because the results of the autoparameterized query will be truncated to fit into the numeric (10, 0) data type.

效果

基于以上,使用了以下数据类型(结果类型如何计算的解释参见: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 在他的 .

中解释的那样使用数字算术来操纵日期

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

This 问题涉及相同的问题。