从 varchar 到 datetime 的转换导致值超出范围

Conversion from varchar to datetime resulted in out-of-range value

注意:这不是一个简单的问题;视图中的转换很复杂。

我在查询几层视图的 select 语句中收到以下错误。

The conversion of a varchar data type to a datetime data type resulted in an out-of-range value.

通常这将是一个非常简单的问题,只需找到错误的数据并修复它,或者在 select 中添加一些额外的逻辑即可解决。那没有用。

通过一些调查,我确定这是导致错误的代码:

  ,case when (   (hm.Status >= 100 and hm.Status < 200) 
              or (hm.Status >= 300 and hm.Status < 400 and hm.StatusEff > dateadd(day, -30, GETDATE()))
              )  
         and coalesce(mb.isMortBilled, 0) = 0
         and hm.recurring = 1
        then '1' else '0' 
   end as isRecurable

所以我怀疑 StatusEff 中有垃圾,所以我添加了一个检查,以确保它是一个日期,当它不包含日期时,可能会保留 运行ning 的后续转换:

  ,case when (   (hm.Status >= 100 and hm.Status < 200) 
              or (hm.Status >= 300 and hm.Status < 400 and ISDATE(hm.Statuseff) = 1 and convert(datetime, hm.StatusEff) > dateadd(day, -30, GETDATE()))
              )  
         and coalesce(mb.isMortBilled, 0) = 0
         and hm.recurring = 1
        then '1' else '0' 
   end as isRecurable

我仍然得到错误。

如果我 运行 视图本身 运行 没问题。

我怀疑当优化器必须处理几层代码时,它搞砸了并尝试在 hm.StatusEff 上进行转换,而不强制执行评估顺序和检查 ISDATE() 的保护。

这是我们正在努力更新的 sql 服务器的旧版本;但是现在更新不了

我想可能有一些方法可以使用 CASE 语句来更明确地强制计算顺序。

另外一个想法是:SQL有求值顺序的概念吗?

[编辑] 澄清:hm.StatusEff varchar 始终包含一个日期,当状态在指定范围内时,该日期可以转换为日期时间。根据设计,当状态超出该范围时,它将包含除日期以外的日期。我认为问题在于,在没有首先检查 hm.Status 的保护的情况下,正在尝试对该 varchar 进行转换。这似乎违反了运算符的优先顺序。

[编辑] 使用@Used_by_Already 的建议我这样做了并且有效:

  ,case when hm.Status >= 100
         and hm.Status < 200 
         and coalesce(mb.isMortBilled, 0) = 0
         and hm.recurring = 1
        then '1' 
        when ISDATE(hm.Statuseff) = 0 -- force this to happen before convert(datetime) below.
        then '0'
        when hm.Status >= 300 and hm.Status < 400 and convert(datetime, hm.StatusEff) > dateadd(day, -30, GETDATE())
         and coalesce(mb.isMortBilled, 0) = 0
         and hm.recurring = 1
        then '1' 
        else '0' 
   end as isRecurable

您可以通过使用前面的 when 例如

来指定如果该列不是日期会发生什么
  ,case when ISDATE(hm.StatusEff) = 0 then '0'
        when (   (hm.Status >= 100 and hm.Status < 200) 
              or (hm.Status >= 300 and hm.Status < 400 and convert(datetime, hm.StatusEff) > dateadd(day, -30, GETDATE()))
              )  
         and coalesce(mb.isMortBilled, 0) = 0
         and hm.recurring = 1
        then '1' else '0' 
   end as isRecurable

这应该可以减少转换失败的发生率,但是如果该列是尝试存储日期字符串的 varchar,那么总会有潜在的问题。将该列作为真正的时间数据类型会好得多。

另请注意,这是一个 "implicit conversion",因为字符串被强制放入日期时间以与 dateadd() 函数进行比较。我认为应该避免隐式转换。

WHERE 中,引擎可以并且将会交换 ANDed 谓词,因为它 "wants"(如果它在优化方面看起来更有希望)。但我不知道 CASE 是否也适用。

但执行顺序不一定是造成这种情况的原因。

来自手册ISDATE (Transact-SQL):

Note that the range for datetime data is 1753-01-01 through 9999-12-31, while the range for date data is 0001-01-01 through 9999-12-31.

例如,如果您在其中输入“0001-01-01”,isdate() 会接受它,这是一个有效日期。但是转换为 datetime 会失败。所以你还应该检查 hm.statuseff 是在 1753-01-01 之后还是在 1753-01-01.

isdate(hm.statuseff) = 1
AND convert(date, hm.statuseff) >= convert(date, '1753-01-01')
AND convert(datetime, hm.statuseff) > dateadd(day, -30, getdate())

当然最好是更正该列的数据和正确的数据类型。