将 Nvarchar 转换为 Int 失败的 SQL Server 2008

Posted

技术标签:

【中文标题】将 Nvarchar 转换为 Int 失败的 SQL Server 2008【英文标题】:Cast Nvarchar to Int Failing SQL Server 2008 【发布时间】:2018-04-06 06:40:07 【问题描述】:

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

这是我的原始查询。

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
  ItemName
FROM InventoryItem ii
JOIN InventoryItemDepartment iid
  ON ii.ItemCode = iid.ItemCode
WHERE iid.DepartmentCode = 'Filters'
AND ItemName LIKE 'CA4%____'

这会返回一些数据,如下所示:

CA40000
CA40001
CA40002
CA40003
CA40004
CA40005
.... etc

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

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

【问题讨论】:

实际上它在 WHERE 子句中失败,因为它试图将值 41020-S 转换为 Int。 【参考方案1】:

WHERE 子句* 中的单个谓词的评估顺序没有保证。 (SQL Server 也不保证不对 SELECT 子句中的表达式求值,这些值应该由 WHERE 子句过滤)。

不幸的是,确保过滤器生效的最有力方法是将查询拆分为两个单独的查询 - 第一个查询执行所需的过滤并将其结果放入临时表/表变量中,第二个查询从并执行数据转换。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+

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

2CASE: “在某些情况下,在 CASE 语句接收表达式结果作为其输入之前计算表达式。计算这些表达式时可能会出错。出现的聚合表达式在 WHEN 中,首先评估 CASE 语句的参数,然后将其提供给 CASE 语句。”

*与其他一些编程语言不同,SQL 不提供诸如从左到右评估之类的保证,也不提供任何影响它是否表现出任何短路行为的方法。

+这个问题最初是在用户语音上报告的。不幸的是,在迁移到 Azure 反馈论坛的过程中,很多细节都被压缩到了单一的微软“回复”中,这使得它难以阅读,而且“哦,亲爱的”也失去了之前在 User Voise 上获得的大量选票。

【讨论】:

【参考方案2】:

这里有两个学习点:

    尽可能避免在WHERE 表达式中使用CASTCONVERT - 由于对将被排除的行执行转换,它们会使查询变得脆弱并降低性能。

    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___%'

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

如果数据在 CA40 之后可能有非数字值,可以使用 LIKE 的范围匹配来避免错误:LIKE 'CA40[0-9][0-9][0-9]%'

【讨论】:

谢谢格雷厄姆,我结合了你的答案和@Jatin 的答案。 WHERE ItemName LIKE 'CA4____' 只查找具有所需字符数的匹配项,因为任何带后缀的值都不属于序列。【参考方案3】:

试试这个查询,将 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) &lt; 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

【讨论】:

【参考方案4】:

使用这个

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)

【讨论】:

【参考方案5】:

如果您想删除任何具有意外值的行,例如其中包含未知字符,您可以使用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。

【讨论】:

以上是关于将 Nvarchar 转换为 Int 失败的 SQL Server 2008的主要内容,如果未能解决你的问题,请参考以下文章

SQL 异常:将 nvarchar 值“[anyvalue]”转换为数据类型 int 时转换失败

System.Data.SqlClient.SqlException:“将 nvarchar 值 'STORES' 转换为数据类型 int 时转换失败。”

将 nvarchar 值转换为数据类型 int 和其他 SQL Server 错误时失败

org.hibernate.exception.SQLGrammarException:将 nvarchar 值“ViewWebApp”转换为数据类型 int 时转换失败

在 SQL Server 2008 中将 NVARCHAR 转换为 INT 数据类型

转换 nvarchar 值“7575932”时转换失败。数据类型 int