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

Chr*_*rry 6 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
Run Code Online (Sandbox Code Playgroud)

休息时间:

由于无法转换为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'
Run Code Online (Sandbox Code Playgroud)

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

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

create index [NCIDX_dbo_Child__TypeId] on dbo.Child (
    TypeId
)
include (
    ParentId,
    varcharColumn
)
Run Code Online (Sandbox Code Playgroud)

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

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

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

谢谢

编辑 - 答案:

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

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

再次感谢所有的回复!

Aar*_*and 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;
Run Code Online (Sandbox Code Playgroud)

但是我听说过将这个分成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;
Run Code Online (Sandbox Code Playgroud)

(我也在这个答案中谈论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;
Run Code Online (Sandbox Code Playgroud)

*请注意,IsNumeric并不完美.几年前我写这篇文章是为了帮助解决这个问题:http://classicasp.aspfaq.com/general/what-is-wrong-with-isnumeric.html


Gor*_*off 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)
Run Code Online (Sandbox Code Playgroud)

或者,更接近"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)
Run Code Online (Sandbox Code Playgroud)

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

  • 嗨,戈登,我指的是_our_耀眼的设计问题.不是MSSQL的问题.就我们提出的而言,正确的解决方案是拥有一个单独的ChildInt表.我很好奇,哪种解决方案(你的或Aaron的)最终会多次调用IsNumeric(即性能更差).听起来您使用case语句的响应将确保它在where子句之后应用IsNumeric检查(以及演员).因此运行次数更少,更清洁.我理解正确吗? (3认同)
  • @bizzarry...是的,我同意存在设计错误.我只担心在极少数情况下调用isnumeric() - 比如在高容量事务系统中使用这些查询.Intead,我会更担心(缺乏)索引的使用和其他考虑因素. (2认同)
  • @Gordon...对我来说只是一个学习点。当您说在 isnumeric 调用周围放置 case 语句会强制它最后处理 select 语句时,我是否理解正确? (2认同)