更多警告而不是问题:
今天早上我们解决了一个非常令人费解的错误。我们有各种报告,允许用户输入他们想要运行的日期范围。假设是,如果您要求从2010年8月1日到2010年8月10日的报告,您打算包含 8/10/2010,那么报告的结束日期不是8 / 10,之后就是这样。
这不可能是8/11/2010因为这些报告中的一些报告汇总了当天在午夜将它们分组的所有事情,因此每日汇总将包括额外的一天 - 而不是我们想要的。
为了避免错过任何非常接近当天结束的项目的可能性,我们将结束日期计算为比明天更少的“一个标记”:
public static DateTime EndOfDay(DateTime day)
{
return day.Date.AddDays(1).AddTicks(-1);
}
在内部,这最终会像8/10/2010 12:59:59.9999PM
好吧,当你将这个DateTime传递给SQL Server中的DATETIME参数时,它会将值UP舍入为8/11/2010 00:00:00!因为我们的查询使用
DateField BETWEEN @FromDate AND @ToDate
而不是
DateField >= @FromDate AND DateField < @ToDate
我们看到2010年8月1日至2010年10月10日的报告包括2010年11月8日的报道。
我们发现真正问题的唯一方法是通过字符串对日期进行往返。 DateTime.ToString()也是如此,所以我们最终会遇到SQL Server很满意的8/1/2010 12:59:59 PM。
所以现在我们的'结束日'方法看起来像这样:
public static DateTime EndOfDay(DateTime day)
{
// Cant' subtract anything smaller (like a tick) because SQL Server rounds UP! Nice, eh?
return day.Date.AddDays(1).AddSeconds(-1);
}
很抱歉不是问题 - 只是觉得有人觉得它很有用。
答案 0 :(得分:5)
这是因为DATETIME数据类型的准确性具有准确性(quote):
舍入为.000,.003的增量, 或.007秒
所以是的,在某些情况下你必须要小心(例如23:59:59.999将被调整到第二天的00:00,23:59:59.998将被舍入到23:59:59.997)
SELECT CAST('2010-08-27T23:59:59.997' AS DATETIME)
SELECT CAST('2010-08-27T23:59:59.998' AS DATETIME)
SELECT CAST('2010-08-27T23:59:59.999' AS DATETIME)
从SQL Server 2008开始,有一个新的DATETIME2数据类型可以提供高达100纳秒的更高精度。
当我在包含时间元素的DATETIME字段上进行查询时,我不会因此而使用BETWEEN。
e.g。我更喜欢
WHERE DateField >= '2010-08-27' AND DateField < '2010-08-28'
而不是:
WHERE DateField BETWEEN '2010-08-27' AND '2010-08-27T23:59:59.997'
答案 1 :(得分:2)
你发布的解决方案不幸添加了另一个新的微妙问题,最终会回来并咬你:现在你要跳过&gt; = 23:59:59.003和&lt; = 23:59的所有日期: 59.997。我强烈怀疑你可以减去小于1秒的东西,这是3个滴答,除非你做的事情剥夺了额外的时间。请注意,smalldatetime甚至不会存储秒数。
在处理边界边缘时,利用表示该值的系统的“已知分辨率”来模拟具有包含值的独占端点是绝对不错的。在具有不同分辨率的两个代表性系统之间进行转换时,您发现了这一点。直接进入边界但排除它的最佳实践方式是使用普通的比较运算符。
所以,正如其他海报所说,正确的答案是使用Dt >= @Dt1 AND Dt < @Dt2
。我意识到你有十亿个存储过程需要修复,所以我可以建议以下方案来纠正它:
将您的功能更改为第二天返回,不会减去任何秒数或滴答。
以编程方式为使用原始SP名称的所有存储过程构建“包装器”SP。将原始文件重命名为SPName_NDC(非日期兼容)。在将其传递给NDC版本之前,从每个“迄今”减去3毫秒。
注意:您可以从系统表中获取SP参数类型和名称。这可能会对您有所帮助:
SELECT
ObjectSchema = Schema_Name(SO.schema_id),
ObjectName = SO.name,
ObjectType = SO.Type_Desc,
Position = P.parameter_id,
ParameterName = P.name,
ParameterDataType = Type_name(P.user_type_id),
P.max_length,
P.[Precision],
P.Scale,
P.Is_Output,
P.Has_Default_Value,
P.Default_Value
FROM
sys.objects AS SO
INNER JOIN sys.parameters AS P ON SO.OBJECT_ID = P.OBJECT_ID
WHERE
SO.Type = 'P'
现在您可以慢慢修复每个不合规的SP。使用包装器不应该有明显的性能损失。
示例包装SP可能如下所示:
CREATE PROCEDURE dbo.ProfitReport
@FromDate datetime,
@ToDate datetime = NULL OUT
AS
SET @ToDate = DateAdd(ms, -3, @ToDate)
DECLARE @RC int
EXEC @RC = dbo.ProfitReport_NDC @FromDate, @ToDate OUT
RETURN @RC
您需要获取所有日期参数的列表,并确定哪些日期参数代表结束日期边界。如果您的SP具有任何XML或表值参数,请务必小心。
你可以再次回到理智的世界!
注意:如果您升级到SQL 2008,在使用datetime2数据类型之前,无论如何都需要修复所有内容。
答案 2 :(得分:1)
SQL Server datetime数据类型精确到333秒 - 即.003,.006,.009,依此类推。这就是为什么你的.999会向上舍入到0.欢迎来到开发人员(也就是“我们所有人”)的行列,这种实现会搞砸一次。
答案 3 :(得分:1)
可以存储在datetime
中的最大亚秒值是.997。
所以要使用between
,它需要(例如)
between '2010-08-27 00:00:00.000' and '2010-08-27 23:59:59.997'
理想情况下,您应使用<
而非between
,以便您的代码与datetime2
数据类型兼容,此假设不适用。