将 nvarchar(10) 复制到 char(10) 时出现“字符串或二进制数据将被截断”错误

use*_*489 3 sql-server collation encoding sql-server-2014 unicode

我正在将一个 SQL 表/列中的值插入到另一个中。由于其他原因,这些列的数据类型不同,但我不明白为什么 nvarchar(10) 源和 char(10) 目标有时会在 SQL Server 2014 中导致错误:

字符串或二进制数据将被截断。

len(sourcecol) = 10 和 datalength(sourcecol) = 20。

可能是因为 nvarchar 类型的源列中存储了一些不可见的空格/字符?

Sol*_*zky 11

如果没有来自 OP 的数据示例和表 DDL,则很难确定导致OP出现此错误的确切原因是什么。以下原因:

某些代码页(由每个CHAR/VARCHAR字段的排序规则确定)允许使用双字节字符,以便映射超过 256 个可放入单个字节的字符。由于字符串字段的最大长度实际上是字节而不是字符的问题,最大长度为 10的CHAR/VARCHAR字段只能容纳 10 个字节,可以容纳 5 - 10 个字符。源字段为 时NVARCHAR(10),这 10 个字符可以映射到VARCHAR字段中需要超过 1 个字节的字符。获得此错误所需要的只是 9 个“常规”单字节字符和 1 个双字节字符,这将需要 11 个实际字节。

因此,请检查CHAR(10)目标表中字段的排序规则。一旦您知道该字段的排序规则的名称,它将指示正在使用的代码页,如果它是 932、936、949 或 950,那么这很可能是问题的根源。在这种情况下,只需将目标字段更改为最大长度为 20(为了安全,以防所有 10 个字符都是双字节)。我会推荐VARCHAR(20),但如果你真的非常喜欢空白填充,那就做CHAR(20).

一个稍微更详细的解释如下:


众所周知,VARCHAR(即8位扩展ASCII)数据是单字节的,NVARCHAR(即Unicode)数据是双字节的。这种对字符串数据(和编码)的理解在使用美国-英语字母表(以及相当多的其他语言,至少是大多数“活跃”语言)时总是正确的。虽然在使用大多数语言时它往往是正确的,但它肯定并不总是正确的。

对于 Unicode 数据,这种理解不正确的方式超出了本问题的范围,在此不再详述。

但是说到我们的老朋友先生VARCHAR,有一些情况会允许字符占用2个字节而不是一个字节。是的,你没看错。但是如何?嗯,在 Unicode 出现之前,一些字母表中字符超过 255 个的文化仍然希望使用他们的母语字母表。由于这在单个字节(范围为 256 个值)中是不可能的,因此他们提出了双字节字符集 (DBCS)。它们作为不同的代码页处理,Windows 和 SQL Server 支持其中的四个:

  • 932 = 日语(Microsoft 内的 Shift-JIS,Microsoft 外的 Windows-31J)
  • 936 = 简体中文 (GB2312)
  • 949 = 韩文
  • 950 = 中国繁体 (Big5)

不要让术语“双字节”混淆您的含义,即它们就像NVARCHAR仅适用于 16 位序列一样。这些代码页实际上是可变长度编码,类似于 UTF-8,并且将使用单个字节(8 位)来表示至少前 128 个值(0 - 127)和一些(128 - 255 范围)值。

这些如何适应截断错误?好吧,NVARCHARVARCHAR数据类型的最大长度实际上是用字节而不是字符来表示的。意思VARCHAR(10)是,最多 10 个字节,即使少于 10 个字符适合这 10 个字节。同样,NVARCHAR(10)最多 20 个字节,即使少于 10 个字符适合那 20 个字节。

考虑到这一点,我们知道从 Unicode 转换为代码页将尝试映射到相同的字符。在大多数代码页中,这些字符的大小都是 1 个字节。但是在 4 个 DBCS 代码页中,存在相当多的字符(因此可以映射到)并且是 2 个字节(否则它们将不存在)。

这里的问题是正在使用 DBCS 代码页(用于目标),并且至少一个被映射的字符在VARCHAR类型中占用了 2 个字节。

以下是此行为的工作示例。

测试设置

首先,运行此代码以设置测试。运行此测试的数据库的默认排序规则无关紧要。在这里,我们创建了一个临时表来保存主要 Unicode 字符集的前 65,536 个代码点中的每一个(超出初始 65,536 的字符需要两个代码点,因此每个是 4 个字节,但同样超出了此处的范围,因为它们不会更改与当前问题相关的行为;)

SET NOCOUNT ON;

IF (OBJECT_ID(N'tempdb..#Source') IS NOT NULL)
BEGIN
  DROP TABLE #Source;
END;

CREATE TABLE #Source ([Character] NVARCHAR(1));

;WITH nums (num) AS
(
  SELECT TOP (65536) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
  FROM   [master].[sys].[columns] sc1
  CROSS JOIN [master].[sys].[columns] sc2
)
INSERT INTO #Source ([Character])
  SELECT NCHAR(num - 1)
  FROM nums;

SELECT * FROM #Source;
Run Code Online (Sandbox Code Playgroud)

测试 1:Latin1 字符集(代码页 1252)

运行以下将不会产生任何错误。它只是有效。但它只转换前 256 个值,因为这是所有可以放入任何单字节字符集 (SBCS) 的值。

IF (OBJECT_ID(N'tempdb..#Destination_CP1252') IS NOT NULL)
BEGIN
  DROP TABLE #Destination_CP1252;
END;

CREATE TABLE #Destination_CP1252 ([Character] VARCHAR(1) COLLATE Latin1_General_100_CI_AS);

INSERT INTO #Destination_CP1252 ([Character])
  SELECT [Character]
  FROM   #Source;

SELECT [Character],
       LEN([Character]) AS [NumberOfCharacters],
       DATALENGTH([Character]) AS [NumberOfBytes],
       CONVERT(VARBINARY(2), [Character]) AS [BinaryValue]
FROM   #Destination_CP1252;
Run Code Online (Sandbox Code Playgroud)

测试 2:日语 (Shift-JIS) 字符集(代码页 932)

IF (OBJECT_ID(N'tempdb..#Destination_CP932') IS NOT NULL)
BEGIN
  DROP TABLE #Destination_CP932;
END;

CREATE TABLE #Destination_CP932 ([Character] VARCHAR(1) COLLATE Japanese_Unicode_CI_AS);

INSERT INTO #Destination_CP932 ([Character])
  SELECT [Character]
  FROM   #Source;
Run Code Online (Sandbox Code Playgroud)

运行上面显示的代码将产生以下错误:

消息 8152,级别 16,状态 2,第 1 行
字符串或二进制数据将被截断。
该语句已终止。

如果这是双字节字符集 (DBCS) 转换的问题,那么将字段大小增加 1 个字节应该可以解决它。首先,让我们确保表中没有实际插入任何内容(以便我们确定下INSERT一条语句是将数据放入的内容):

SELECT *
FROM   #Destination_CP932;
-- no rows
Run Code Online (Sandbox Code Playgroud)

伟大的。现在运行以下命令:

ALTER TABLE #Destination_CP932
  ALTER COLUMN [Character] VARCHAR(2) COLLATE Japanese_Unicode_CI_AS;

INSERT INTO #Destination_CP932 ([Character])
  SELECT [Character]
  FROM   #Source;
Run Code Online (Sandbox Code Playgroud)

没有错误。呜呼!让我们看看转换了哪些字符:

SELECT [Character],
       LEN([Character]) AS [NumberOfCharacters],
       DATALENGTH([Character]) AS [NumberOfBytes],
       CONVERT(VARBINARY(2), [Character]) AS [BinaryValue]
FROM   #Destination_CP932
WHERE  1 = 1
--AND    DATALENGTH([Character]) > 1
--AND    [Character] <> '?'
Run Code Online (Sandbox Code Playgroud)

如果您滚动浏览结果,您应该注意[NumberOfBytes][BinaryValue]字段。

要仅查看双字节值,请取消注释以下行并重新运行:

AND    DATALENGTH([Character]) > 1
Run Code Online (Sandbox Code Playgroud)

要查看代码页 932 中的全部值,请重新注释掉DATALENGTHwhere 条件并取消注释以下行并重新运行:

AND    [Character] <> '?'
Run Code Online (Sandbox Code Playgroud)

我的计数显示 9483 个字符。公平地说,加上 1 来说明?被排除的实际字符,我们在一个VARCHAR字段中得到总共 9484 个字符的授权。