SELECT中的MSSQL强制转换([varcharColumn]为int)在WHERE子句筛选出错误值之前执行

时间:2012-09-06 18:01:14

标签: sql sql-server sql-server-2008

假设以下架构和查询:

请查看我们希望在varchar列中包含值的明显设计问题。

create table dbo.Parent (
    Id bigint NOT NULL,
    TypeId int NOT NULL
)

create table dbo.Child (
    Id bigint NOT NULL,
    ParentId bigint NOT NULL,
    TypeId int NOT NULL,
    varcharColumn varchar(300) NULL
)

select cast(c.varcharColumn as int)
from dbo.Parent p (nolock)
    inner join dbo.Child c (nolock)
        on p.Id = c.ParentId
            and c.TypeId = 2
where p.TypeId = 13

休息:

由于无法转换为int的值,我们得到一个转换中断。在这种情况下:“123-1”。奇怪的是,正在转换的值会从最终结果集中过滤掉。

例如,返回零结果

select c.varcharColumn
from dbo.Parent p (nolock)
    inner join dbo.Child c (nolock)
        on p.Id = c.ParentId
            and c.TypeId = 2
where p.TypeId = 13
    and c.varcharColumn = '123-1'

查询计划最终查看Child表并实际在where子句之前应用cast函数。

我们能够通过在子表上创建一个新索引来解决这个问题(它正在进行PK 扫描

create index [NCIDX_dbo_Child__TypeId] on dbo.Child (
    TypeId
)
include (
    ParentId,
    varcharColumn
)

现在首先过滤父表的where子句。

有没有办法在没有额外索引的情况下解决这个问题?同样,请不要提出任何与修复架构相关的建议。在这种情况下,这绝对是正确的解决方法。

我最感兴趣的是理解为什么在过滤结果集之前应用了强制转换。

由于

编辑 - 答案:

非常感谢Aaron和Gordon。如果我获得超过15个代表点,我会回复你的两个回复。

我们最终需要Gordon的答案,因为我们想在视图中使用此查询。办公室的一些人对使用案例陈述持谨慎态度,因为他们更愿意控制以确保我们首先获得较小的结果集(Aaron的答案),但这一切都归结为查看查询计划并检查您的阅读计数。

再次感谢所有回复!

4 个答案:

答案 0 :(得分:6)

您无法轻松控制SQL Server处理查询的方式。你可以通过深入研究执行计划来找出一些原因,但在我认为这个特定情况下,理解这是你最少的问题。你也许可以用连接提示做一点,但这对我来说很糟糕,而且行为仍然无法保证(特别是当你转向新版本等)时。您可以尝试两种解决方法:

;WITH c AS 
(
  SELECT varcharColumn, ParentID, TypeId
   FROM dbo.Child AS c
   WHERE c.TypeId = 2
   AND ISNUMERIC(varcharColumn) = 1 --*
)
SELECT CONVERT(INT, c.varcharColumn)
FROM dbo.Parent AS p
INNER JOIN c
ON c.ParentId = p.Id
WHERE p.TypeId = 13;

但是我听说过将这个分成CTE的情况可能导致导致转换首先发生的糟糕计划。所以你可能需要进一步分解它:

SELECT varcharColumn, ParentID, TypeId
INTO #c
   FROM dbo.Child AS c
   WHERE c.TypeId = 2
   AND ISNUMERIC(varcharColumn) = 1; --*

SELECT CONVERT(INT, c.varcharColumn)
  FROM dbo.Parent AS p
  INNER JOIN #c AS c
  ON c.ParentId = p.Id
  WHERE p.TypeId = 13;

(我还谈到this answer中的CASE表达式解决方案。)

如果您使用的是SQL Server 2012,则可以执行此操作 - 现在,如果在过滤器之前尝试转换并不重要,并且您不必依赖于不稳定的ISNUMERIC()函数。 *

SELECT TRY_CONVERT(INT, c.varcharColumn)
  FROM dbo.Parent AS p
  INNER JOIN dbo.Child AS c
  ON c.ParentId = p.Id
  WHERE c.TypeId = 2
  AND p.TypeId = 13;

*请注意,IsNumeric并不完美。几年前我写了this article来帮助解决这个问题。

答案 1 :(得分:6)

首先,这不是一个“明显的设计问题”。 SQL是输出的描述性语言,而不是指定如何完成prcoessing的过程语言。一般而言,不能保证处理顺序,这是一个优点。我可能会说存在设计问题,但它是围绕SQL语句中异常的一般处理。

根据SQL Server文档(http://msdn.microsoft.com/en-us/library/ms181765.aspx),您可以依赖于标量表达式的CASE语句的evauation顺序。因此,以下内容应该有效:

select (case when isnumeric(c.varcharColumn) = 1 then cast(c.varcharColumn as int) end)

或者,更接近“int”表达式:

select (case when isnumeric(c.varcharColumn) = 1 and c.varcharColumn not like '%.%' and c.varcharColumn not like '%e%'
             then cast(c.varcharColumn as int)
        end)

至少你的代码正在做一个明确的CAST。当演员表是隐含的(并且有数百列)时,这种情况会更加糟糕。

答案 2 :(得分:0)

根据我的技术观点

 create table dbo.Parent (
    Id bigint NOT NULL,
    TypeId int NOT NULL
)
    create table dbo.Child (
        Id bigint NOT NULL,
        ParentId bigint NOT NULL,
        TypeId int NOT NULL,
        varcharColumn varchar(300) NULL
    )

    select cast(c.varcharColumn as int)
    from dbo.Parent p (nolock)
        inner join dbo.Child c (nolock)
            on p.Id = c.ParentId
                and c.TypeId = 2
    where p.TypeId = 13

选择时,您正在进行内部联接,这导致表查找并过滤出数据 现在,根据我的说法,SQL Server正在应用“所谓的优化” 并进行强制转换操作。显然是在过滤之前应用了这种转换,从而导致转化问题。

又一个好奇的话题带给我,

SELECT    MAX( Cast(dealer_number AS INT) + 1)
                    FROM   dealer_number dn
                    INNER JOIN dealer d
                    ON dn.dealer_seq = d.dealer_seq 
                    INNER JOIN dealer_type(nolock) dt
                    ON dt.dealer_number_seq = dn.dealer_number_seq
                    INNER JOIN program_dealer_type(nolock) pdt
                     ON pdt.program_dealer_type_seq = dt.program_dealer_type_seq
                    WHERE 
                    Isnumeric(dealer_number) = 1 AND
                    pdt.dealer_type = 'Dealer'
                    AND d.program_seq = 57

遇到以下问题

varchar值'10054239051'的转换溢出了int列。

我添加了 Isnumeric (我是 Isnumeric )以证明我没有得到任何字母数字。

但是由于Isnumeric在表查找性能时会导致相应的错误

现在只需删除 Isnumeric ,它就可以正常工作

SELECT    MAX( Cast(dealer_number AS INT) + 1)
                    FROM   dealer_number dn
                    INNER JOIN dealer d
                    ON dn.dealer_seq = d.dealer_seq 
                    INNER JOIN dealer_type(nolock) dt
                    ON dt.dealer_number_seq = dn.dealer_number_seq
                    INNER JOIN program_dealer_type(nolock) pdt
                    ON pdt.program_dealer_type_seq = dt.program_dealer_type_seq
                    WHERE 
                    pdt.dealer_type = 'Dealer'
                    AND d.program_seq = 57

现在,我刚刚删除了该预处理过滤器,它只是解决了问题,但是100%证明我没有得到我必须在select中实现大小写的任何字母数字数字

SELECT    MAX(case WHEN 
                    ISNUMERIC(DEALER_NUMBER )=1 
                        THEN  Cast(dealer_number AS INT) 
                        ELSE 0 END 
                    + 1) 
                    FROM   dealer_number dn
                    INNER JOIN dealer d
                    ON dn.dealer_seq = d.dealer_seq 
                    INNER JOIN dealer_type(nolock) dt
                    ON dt.dealer_number_seq = dn.dealer_number_seq
                    INNER JOIN program_dealer_type(nolock) pdt
                    ON pdt.program_dealer_type_seq = dt.program_dealer_type_seq
                    WHERE 
                    pdt.dealer_type = 'Dealer'
                    AND d.program_seq = 57

以上将解决这两个问题。任何人对此表示欢迎。 :)

答案 3 :(得分:-1)

您可以将过滤器移动到子查询,该子查询将首先过滤掉不良值,然后转换所有内容。这并没有回答为什么,但它确实能让你得到你想要的东西,在我看来,这是最简单的方法。

select cast(varcharColumn as int)
FROM(
select c.varcharColumn

from dbo.Parent p (nolock)
    inner join dbo.Child c (nolock)
        on p.Id = c.ParentId
            and c.TypeId = 2
where p.TypeId = 13
) table1