SQL Server 大容量插入正确解释某些 Unicode 字符而不是其他字符?

Dav*_*ims 5 sql-server collation encoding unicode bulk-insert

出于某种原因,MS SQL Server 2016 批量插入会误解/翻译 Unicode 字符:

  • C9 (É) 变成 2B (+)
  • A1 (¡) 到 ED (í)
  • A0 ( ) 到 E1 (á)
  • AE (®) 到 AB («)
  • CB (Ë) 到 2D (-)
  • D1 (Ñ) 到 2D (-)
  • 92 (') 到 C6 (Æ)
  • 96 (–) 到 FB (û)

即 Notepad++ 和 xxd 显示平面文件有 0xC9,但在批量插入后,表显示“+”,并在 SQL Server 中转换为 varbinary 显示为 0x2B。备份也有 0xC9。

我正在向 MS SQL Server 2016 中批量插入 25 个平面文件。它是 15Gb 数据,我正在使用管道 ( | ) 字段分隔符和CRLF行分隔符。

我批量插入到我提供的备份的截断结构中。当我与备份进行比较时,存在差异。注意:我必须等待 25 小时才能从数据源备份,但可以在 15 分钟内获取平面文件。

有些差异是可以接受的(查找和替换我正在应用到平面文件),但许多差异是由于 Unicode 字符被误解了。

一个示例表的结构是:

CREATE TABLE [dbo].[obfuscated_name](
    [ob_1] [int] NOT NULL,
    [ob_2] [int] NOT NULL,
    [ob_3] [int] NOT NULL,
    [ob_4] [nvarchar](300) NULL
) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

数据库排序规则为默认值:SQL_Latin1_General_CP1_CI_AS。没有任何列具有不同的排序规则。此排序规则应使用代码页 1252,它应正确解释我遇到问题的字符。

我的流程正在针对不断变化的生产数据运行,所以我担心可能会弹出其他更改,我想知道问题的根源,而不是尝试隔离问题并手动更新误解。

Sol*_*zky 7

这不是 SQL Server(甚至 Windows)中的错误,也不是需要将文件转换为另一种编码(即转换为“Unicode”,在 Windows 世界中的意思是“UTF-16 Little端”)。这只是一个简单的误传。

通信故障的来源(它总是相同的,对;-)只是在源数据的性质上没有达成一致。将字符数据从一处移动到另一处时,指定两侧的编码很重要。是的,SQL_Latin1_General_CP1_*排序规则使用代码页 1252。但是,如果您不告诉BULK INSERTBCP.exe源文件的代码页是什么,那么它们将假定代码页是系统默认值。

BULK INSERT的文档甚至指出(对于CODEPAGE =参数):

'OEM'(默认)= charvarchartext数据类型的列从系统 OEM 代码页转换为 SQL Server 代码页。

BCP.exe的文档说明(对于-C开关):

OEM = 客户端使用的默认代码页。如果未指定 -C,则这是使用的默认代码页。

Windows 的默认代码页是(至少对于美国英语系统):437。如果您在命令提示符中执行以下命令,您可以看到这一点:

C:\> CHCP
Run Code Online (Sandbox Code Playgroud)

它将返回:

Active code page: 437
Run Code Online (Sandbox Code Playgroud)

但是您的源文件不是使用代码页 437 编码的。它是使用代码页 1252 编码的。

所以这就是正在发生的事情:

  1. 字节是字节。表示字符数据的字节只能通过编码来解释。任何读取文件的操作都不会从文件中读取字符,它会读取文件的字节并根据指定的编码显示字符。
  2. BULK INSERT/BCP 读取字节0xC90xC9显示为É使用代码页 1252 时。
  3. BULK INSERT / BCP 没有提供源代码页,因此它检查进程的当前代码页并被告知:437
  4. BULK INSERT / BCP 现在有一个字节0xC9用于代码页437(显示为?,但 BULK INSERT / BCP 不显示它,所以你不会看到这个)
  5. BULK INSERT/BCP 使用指定代码页1252的排序规则将此数据插入到列中。
  6. SQL Server 发现传入数据使用的代码页与目标使用的代码页不同,因此必须转换传入数据以使字符看起来相同(尽可能多),即使基础值发生变化。
  7. 代码页 437 到代码页 1252 的映射表明字节0xC9映射到字节0x2B。同样,代码页 437(显示为)上的字节0xAE®在代码页 1252 上«)映射到代码页 1252 上的字节0xAB(因为它也显示为«)。

以下示例显示了问题中提到的所有字符的这种转换:

DECLARE @CodePageConversion TABLE
(
   [ActualSource_CP1252] AS CONVERT(VARCHAR(10), CONVERT(BINARY(1),
                    [PerceivedSource_CP437])) COLLATE SQL_Latin1_General_CP1_CI_AS,

   [PerceivedSource_CP437] VARCHAR(10) COLLATE SQL_Latin1_General_CP437_CI_AS,

   [Source_Value] AS (CONVERT(BINARY(1), [PerceivedSource_CP437])),

   [Destination_CP1252] AS (CONVERT(VARCHAR(10), [PerceivedSource_CP437]
                  COLLATE SQL_Latin1_General_CP1_CI_AS)),

   [CP1252_Value] AS (CONVERT(BINARY(1), CONVERT(VARCHAR(10),
                  [PerceivedSource_CP437] COLLATE SQL_Latin1_General_CP1_CI_AS)))
);

INSERT INTO @CodePageConversion
VALUES      (0xC9), (0xA1), (0xA0), (0xAE), (0xCB), (0xD1), (0x92), (0x96);

SELECT * FROM @CodePageConversion;
Run Code Online (Sandbox Code Playgroud)

返回:

ActualSource_CP1252  PerceivedSource_CP437  Source_Value  Destination_CP1252  CP1252_Value
É                    ?                      0xC9          +                   0x2B
¡                    í                      0xA1          í                   0xED
                     á                      0xA0          á                   0xE1
®                    «                      0xAE          «                   0xAB
Ë                    ?                      0xCB          -                   0x2D
Ñ                    ?                      0xD1          -                   0x2D
’                    Æ                      0x92          Æ                   0xC6
–                    û                      0x96          û                   0xFB
Run Code Online (Sandbox Code Playgroud)

代码页 1252 中不存在 0xC9、0XCB 和 0xD1 的字符,因此使用了“最佳拟合”映射,这就是转换后最终得到+-字符的原因。

此外,即使目标列正在使用NVARCHAR,所有这些映射都是相同的,因此您会看到完全相同的行为。

所以,你的选择是:

  1. 如果使用 T-SQLBULK INSERT命令,请使用WITH CODEPAGE =以下值之一指定选项:

    1. 'ACP'(这与 相同'1252'
    2. 'RAW'(如果插入到VARCHAR,则使用列的排序规则的代码页,或者'OEM'在插入到时与/ 代码页 437 相同NVARCHAR
    3. '1252'(这与 相同'ACP'
  2. 或者,如果使用BCP.exe,则指示传入文件通过-C命令行开关使用代码页 1252以及以下值之一(请参阅选项 #1 中的注释):

    1. ACP
    2. RAW
    3. 1252

请注意:

  1. 我用测试BULK INSERT,插入到VARCHAR柱,使用该组中的问题指出的字符,和ACP(我认为代表NSI Ç ODE P年龄),RAW1252值的所有产生正确的结果。
  2. 不指定WITH CODEPAGE =产生的结果与 OP 在问题中报告的结果相同。这与指定WITH CODEPAGE = 'OEM'.
  3. 当插入到一个NVARCHAR列中,两个ACP1252如所期望的工作,但RAW所产生的相同的结果OEM(即,使用代码页437,而不是代码页1252由所述列的排序规则指定)。
  4. 我测试使用BCP.EXE,而不是指定-C开关并没有使用过程中的代码页。意思是,使用CHCP更改命令提示符的代码页无效:代码页 437 仍用作源代码页。

PS由于这里的数据都是8位编码的,所以没有“Unicode字符”,因为没有使用Unicode。