转换为datetime仅在WHERE子句上失败?

时间:2011-08-31 21:14:11

标签: sql sql-server-2008 datetime where-clause

我遇到一些SQL服务器查询问题。事实证明我有一个带有“Attibute_Name”和“Attibute_Value”字段的表,它可以是任何类型,存储在varchar中。 (是的......我知道。)

特定属性的所有日期似乎都存储在“YYYY-MM-DD hh:mm:ss”格式中(不是100%肯定,这里有数百万条记录),所以我可以执行此代码没有问题:

select /*...*/ CONVERT(DATETIME, pa.Attribute_Value)
from 
    ProductAttributes pa
    inner join Attributes a on a.Attribute_ID = pa.Attribute_ID
where 
    a.Attribute_Name = 'SomeDate'

但是,如果我执行以下代码:

select /*...*/ CONVERT(DATETIME, pa.Attribute_Value)
from 
    ProductAttributes pa
    inner join Attributes a on a.Attribute_ID = pa.Attribute_ID
where 
    a.Attribute_Name = 'SomeDate'
    and CONVERT(DATETIME, pa.Attribute_Value) < GETDATE()

我会收到以下错误: 从字符串转换日期和/或时间时转换失败。

怎么会在where子句上失败而不是在select子句中失败?

另一条线索:

如果不使用Attribute_Name进行过滤,而是使用存储在数据库(PK)中的实际Attribute_ID,它可以正常工作。

select /*...*/ CONVERT(DATETIME, pa.Attribute_Value)
from 
    ProductAttributes pa
    inner join Attributes a on a.Attribute_ID = pa.Attribute_ID
where 
    a.Attribute_ID = 15
    and CONVERT(DATETIME, pa.Attribute_Value) < GETDATE()

更新的 谢谢大家的答案。我发现很难真正选择正确的答案,因为每个人都指出了一些对理解问题有用的东西。这绝对与执行的顺序有关。 事实证明我的第一个查询正常工作,因为首先执行WHERE子句,然后执行SELECT。 由于相同的原因,我的第二个查询失败(因为未过滤属性,执行相同的WHERE子句时转换失败)。 我的第三个查询有效,因为ID是索引(PK)的一部分,所以它优先,并且首先在该条件下钻取结果。

谢谢!

6 个答案:

答案 0 :(得分:9)

您似乎正在假设某种短路评估或保证WHERE子句中谓词的排序。这不保证。当列中有混合数据类型时,处理它们的唯一安全方法是使用CASE表达式。

使用(例如)

CONVERT(DATETIME, 
      CASE WHEN ISDATE(pa.Attribute_Value) = 1 THEN pa.Attribute_Value END)

CONVERT(DATETIME, pa.Attribute_Value)

答案 1 :(得分:2)

如果转换在WHERE子句中,则可以评估它比投影列表中显示的记录(值)多得多。我之前在不同的背景下讨论了这个问题,请参阅T-SQL functions do no imply a certain order of executionOn SQL Server boolean operator short-circuit。你的情况更简单,但是相似,最终的根本原因是相同的:在处理像SQL这样的声明性语言时,不要假设一个命令式的执行顺序。

您的最佳解决方案是远程和大范围清理数据并将列类型更改为DATETIME或DATETIME2类型。 所有其他解决方法都会有一个缺点或其他缺点,所以你可能最好做正确的事情。

<强>更新

仔细观察后(对不起,我是@VLDB并且只在会话之间偷看SO)我意识到你有一个具有固有无类型语义的EAV商店(attribute_value可以是一个字符串,一个日期,一个int等)。我的观点是,您最好的选择是在存储中使用sql_variant并一直使用客户端(即项目sql_variant)。您可以在客户端中共享类型,所有客户端API都有从sql_variant中提取内部类型的方法,请参阅Using sql_variant Data(以及几乎所有客户端API ... Using the sql_variant datatype in CLR)。使用sql_variant,您可以存储多种类型而不会遇到字符串表示的问题,您可以使用SQL_VARIANT_PROPERTY检查存储值中BaseType之类的内容,甚至可以我认为像检查约束一样强制执行数据类型的正确性。

答案 2 :(得分:1)

这与处理SELECT查询的顺序有关。 WHERE子句在SELECT之前很久就被处理了。它必须确定要包含/排除的行。使用该名称的子句必须使用调查所有行的扫描,其中一些行不包含有效的日期/时间数据,而该键可能导致搜索,并且该点不包括任何无效行。 SELECT列表中的转换是最后执行的,显然此时它不会尝试转换无效行。由于您将日期/时间数据与其他数据混合,因此您可以考虑将日期或数字数据存储在具有正确数据类型的专用列中。在此期间,您可以通过以下方式推迟检查:

SELECT /* ... */
FROM
(
  SELECT /* ... */
    FROM ProductAttributes AS pa
    INNER JOIN dbo.Attributes AS a
    ON a.Attribute_ID = pa.Attribute_ID
    WHERE a.Attribute_Name = 'SomeDate'
    AND ISDATE (pa.Attribute_Value) = 1
) AS z
WHERE CONVERT(CHAR(8), AttributeValue, 112) < CONVERT(CHAR(8), GETDATE(), 112);

但更好的答案可能是使用Attribute_ID密钥而不是名称(如果可能)。

答案 3 :(得分:0)

对我来说似乎是一个数据问题。当您使用两种不同的方法选择数据时,请查看数据,尝试查找不同的长度,然后选择不同组中的项目并观察它们。还检查空值? (我不确定如果你尝试将null转换为日期时间会发生什么)

答案 4 :(得分:0)

我认为问题是你的数据库中有一个糟糕的日期(显然)。

在您的第一个示例中,您没有在WHERE子句中检查日期,a.attribute.Name ='SomeDate'的所有日期都是有效的,因此它永远不会尝试转换错误日期。

在第二个示例中,WHERE子句的添加导致查询计划实际转换所有这些日期并找到坏日期,然后查看属性名称。

在第三个示例中,更改为使用Attribute_Id可能会更改查询计划,以便它只查找id = 15的那些首先,然后检查这些记录是否具有有效日期,他们这样做。 (可能Attribute_Id已编入索引,而Attribute_name未编入索引

所以,你在某个地方有一个糟糕的约会,但它不在Arttribute_id = 15的任何记录上。

答案 5 :(得分:0)

您可以查看执行计划。可能是在第一个查询中,第二个条件(CONVERT(DATETIME, pa.Attribute_Value) < GETDATE())首先在所有行上进行评估,包括具有无效数据(不是日期)的行,而在第二个条件的情况下 - a.Attribute_ID = 15首先进行评估。因此排除具有非日期值的行。

顺便说一下,第二个也可能更快,如果你在选择列表中没有来自Attributes的任何内容,你就可以摆脱inner join Attributes a on a.Attribute_ID = pa.Attribute_ID

就此而言,建议摆脱EAV虽然不会太晚:)