将 Nvarchar 转换为 Int 失败 SQL Server 2008

Cast Nvarchar to Int Failing SQL Server 2008

我试图将一些行转换为整数以获得序列中的最后一个数字。

这是我的原始查询。

SELECT
  MAX(CAST(REPLACE(ItemName, 'CA', '') AS INT)) + 1
FROM InventoryItem ii
JOIN InventoryItemDepartment iid
  ON ii.ItemCode = iid.ItemCode
WHERE iid.DepartmentCode = 'Filters'
AND ItemName LIKE 'CA4%____'
AND CAST(REPLACE(ItemName, 'CA', '') AS INT) < 41000

但是我收到一条错误消息: Error (1,1): Conversion failed when converting the nvarchar value '41020-S' to data type int.

显然,我知道这条消息的意思。但是我很困惑为什么它会抛出错误,因为我指定的 WHERE 子句的唯一目的是排除可能无法转换的记录。

如果我将查询修改为 select 原始值,而不进行任何替换或转换...

SELECT
  ItemName
FROM InventoryItem ii
JOIN InventoryItemDepartment iid
  ON ii.ItemCode = iid.ItemCode
WHERE iid.DepartmentCode = 'Filters'
AND ItemName LIKE 'CA4%____'

这returns一些数据是这样的:

CA40000
CA40001
CA40002
CA40003
CA40004
CA40005
.... etc

如我所料,麻烦的值“41020-S”(替换后最初 'CA41020-S')未包含在第二个调试结果集中。

谁能帮我解释一下这种奇怪的行为,我该如何克服它?

使用这个

MAX(CAST(REPLACE(REPLACE(name, 'CA', ''),'-S','') AS INT)) + 1

代替

MAX(CAST(REPLACE(ItemName, 'CA', '') AS INT)) + 1

而对于

CAST(REPLACE(REPLACE(name, 'CA', ''),'-S','') AS INT)

到位

CAST(REPLACE(ItemName, 'CA', '') AS INT)

试试这个查询,改变 where 子句中的条件,只检查 CA4

之后的四个字符
SELECT
  MAX(CAST(REPLACE(ItemName, 'CA', '') AS INT)) + 1
FROM InventoryItem ii
JOIN InventoryItemDepartment iid
  ON ii.ItemCode = iid.ItemCode
WHERE iid.DepartmentCode = 'Filters'
AND ItemName LIKE 'CA4____'
AND CAST(REPLACE(ItemName, 'CA', '') AS INT) < 41000

如果执行计划选择首先评估条件 CAST(REPLACE(ItemName, 'CA', '') AS INT) < 41000,上述查询可能会失败。为了安全起见,您可以使用以下查询。

SELECT
  MAX(CAST(REPLACE(ItemName, 'CA', '') AS INT)) + 1
FROM 
(   SELECT ItemName
    FROM InventoryItem ii
    JOIN InventoryItemDepartment iid
      ON ii.ItemCode = iid.ItemCode
    WHERE iid.DepartmentCode = 'Filters'
    AND ItemName LIKE 'CA4____'
) AS SubQ
WHERE CAST(REPLACE(ItemName, 'CA', '') AS INT) < 41000

如果您想删除任何具有意外值的行,例如其中包含未知字符,您可以使用 ISNUMERIC

SELECT
  MAX(CAST(REPLACE(ItemName, 'CA', '') AS INT)) + 1
FROM InventoryItem ii
JOIN InventoryItemDepartment iid
  ON ii.ItemCode = iid.ItemCode
WHERE iid.DepartmentCode = 'Filters'
AND ItemName LIKE 'CA4%____'
AND ISNUMERIC(CAST(REPLACE(ItemName, 'CA', '')) = 1 AND CAST(REPLACE(ItemName, 'CA', '') AS INT) < 41000.

注意:ISNUMERIC并不完美。它也会将某些字符视为数字。你可以阅读here.

WHERE子句*中,没有保证单个谓词的求值顺序。 (SQL 服务器也不保证不计算 SELECT 子句中应由 WHERE 子句过滤的值的表达式。

不幸的是,保证您的过滤器生效的最有效方法是将您的查询分成两个单独的查询 - 第一个查询执行所需的过滤并将其结果放入临时 table/table 变量和您的第二个查询构建并执行数据转换。1

几乎总是有效的稍微弱一点的方法,除了有时使用聚合它可能有点有趣2是使用CASE 表达式改为:

SELECT
  MAX(CASE WHEN iid.DepartmentCode = 'Filters'
AND ItemName LIKE 'CA4%____'
THEN CAST(REPLACE(ItemName, 'CA', '') AS INT)
ELSE 60000 END) + 1
FROM InventoryItem ii
JOIN InventoryItemDepartment iid
  ON ii.ItemCode = iid.ItemCode
WHERE CASE WHEN iid.DepartmentCode = 'Filters'
AND ItemName LIKE 'CA4%____'
THEN CAST(REPLACE(ItemName, 'CA', '') AS INT)
ELSE 60000 END < 41000

1这违背了构建单个大型查询并让优化器找到评估查询的最佳方式的通常建议.不幸的是,优化器经常出错,并且没有迹象表明 Microsoft 计划修复此问题,因为它已经 known issue for more than a decade+.

请注意,任何声称通过 重新排列 查询(例如将部分放入子查询)或 添加 额外保护来解决此问题的答案子句可能表面上看起来是通过意外地强制优化器选择不同的计划来解决问题。但是您无法保证优化器是否或何时会 return 使用会再次生成错误消息的计划。

2CASE: "In some situations, an expression is evaluated before a CASE statement receives the results of the expression as its input. Errors in evaluating these expressions are possible. Aggregate expressions that appear in WHEN arguments to a CASE statement are evaluated first, then provided to the CASE statement."

* 与其他一些编程语言不同,SQL 不提供诸如 left-to-right 评估之类的保证,也不提供任何方式来影响它是否展示任何short-circuiting 行为。

+这个问题最初是在 User Voice 上报告的。不幸的是,在迁移到 Azure 反馈论坛的过程中,许多细节被压缩到单个 Microsoft "response" 中,这使得阅读变得困难,并且 "oh dear" 也失去了之前在 User 上获得的大量选票声音。

这里有两个学习点:

  1. 尽可能避免在 WHERE 表达式中使用 CASTCONVERT - 它们会破坏查询并降低性能,因为对将被排除的行执行转换.

  2. MAX() 也适用于字符串值。

假设您要从中找到最大值的值在 CA40000 到 CA40999 范围内并且数据格式正确,除了 41020-S 等偶尔出现的后缀这打破了你的查询,你可以使用:

SELECT CAST(MAX(SUBSTRING(ItemName, 3, 5)) AS INT) + 1 FROM InventoryItem ii INNER JOIN InventoryItemDepartment iid ON ii.ItemCode = iid.ItemCode WHERE iid.DepartmentCode = 'Filters' AND ItemName LIKE 'CA40___%'

在一个非常大的table中,MAX(LEFT(ItemName, 7))可能会更快,因为它可以直接在ItemName上使用索引,但是这样会使查询稍微复杂一些。

如果数据在 CA40 之后可能有 non-numeric 个值,可以使用 LIKE 进行范围匹配以避免错误:LIKE 'CA40[0-9][0-9][0-9]%'