SQL Server 与 Oracle 中多字节字符的字节排序

Han*_*dyD 5 oracle sql-server encoding hashing unicode

我目前正在将数据从 Oracle 迁移到 SQL Server,但在尝试验证迁移后的数据时遇到了问题。

环境详情:

  • Oracle 12 - AL32UTF8 字符集
  • 客户端 - NLS_LANG - WE8MSWIN1252
  • VARCHAR2 字段

SQL Server 2016

  • Latin1_General_CI_AS 整理
  • NVARCHAR 字段

我在 Oracle 上使用 DBMS_CRYPTO.HASH 生成整行的校验和,然后复制到 SQL 并使用 HASHBYTES 生成整行的校验和,然后我将其进行比较以验证数据匹配。

除具有多字节字符的行外,所有行的校验和都匹配。

例如,具有以下字符的行: ? 校验和不匹配,即使数据传输正确。当我在 Oracle 中使用 DUMP 或在 SQL Server 中转换为 VARBINARY 时,除此字符的字节外,数据完全匹配。

在 SQL Server 中,字节为 0xE625,在 Oracle 中为 0x25E6。

为什么它们的顺序不同,是否有可靠的方法将一个转换为另一个以确保另一端的校验和与具有多字节字符的字符串匹配?

Sol*_*zky 5

一个的整理NVARCHAR/ NCHAR/NTEXT列具有对用于将数据存储在该列中的编码无关。NVARCHAR数据始终是UTF-16 Little Endian (LE)。NVARCHAR数据的整理只影响排序和比较。排序规则确实会影响VARCHAR数据的编码,因为排序规则决定了用于在该列/变量/文字中存储数据的代码页,但我们在这里不处理。

正如sepupic 所提到的,当您以二进制形式查看数据时,您看到的是字节序的差异(Oracle 使用的是 Big Endian,而 SQL Server 使用的是 Little Endian)。然而,当您在 Oracle 中查看字符串的二进制形式时,您看到的并不是数据的实际存储方式。您使用的AL32UTF8是 UTF-8,它将该字符编码为 3 个字节,而不是 2 个,如:E2, 97, A6

此外,对于只有“a”的行,散列不可能相同,但当它们包含“?”时不可能相同,除非 Oracle 中的散列是在没有转换的情况下完成的,因此使用 UTF-8 编码,并且SQL Server 中的散列不小心转换为VARCHAR第一个。否则,没有哈希算法会像您描述的那样运行,因为您可以通过在 SQL Server 中运行以下内容进行验证:

DECLARE @Algorithm NVARCHAR(50) = N'MD4';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
SET @Algorithm = N'MD5';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
SET @Algorithm = N'SHA1';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
SET @Algorithm = N'SHA2_256';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
SET @Algorithm = N'SHA2_512';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
Run Code Online (Sandbox Code Playgroud)

在 Oracle 中,您应该使用该CONVERT函数将字符串放入AL16UTF16LE编码中,然后散列该值。这应该与 SQL Server 所拥有的相匹配。例如,您可以看到White Bullet (U+25E6)的不同编码形式以及如何使用CONVERTwithAL16UTF16LE将在dbfiddle及以下版本中更正此问题:

SELECT DUMP(CHR(14849958), 1016) AS "UTF8",
       DUMP(CHR(9702 USING NCHAR_CS), 1016) AS "UTF16BE",
       DUMP(CONVERT(CHR(9702 USING NCHAR_CS), 'AL16UTF16LE' ), 1016) AS "UTF16LE"
FROM DUAL;

SELECT DUMP('a' || CHR(14849958), 1016) AS "UTF8",
       DUMP('a' || CHR(9702 USING NCHAR_CS), 1016) AS "UTF16BE",
       DUMP(CONVERT('a' || CHR(9702 USING NCHAR_CS), 'AL16UTF16LE' ), 1016) AS "UTF16LE"
FROM DUAL;
Run Code Online (Sandbox Code Playgroud)

那返回:

DECLARE @Algorithm NVARCHAR(50) = N'MD4';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
SET @Algorithm = N'MD5';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
SET @Algorithm = N'SHA1';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
SET @Algorithm = N'SHA2_256';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
SET @Algorithm = N'SHA2_512';
SELECT HASHBYTES(@Algorithm, 0x3100), HASHBYTES(@Algorithm, 0x0031);
Run Code Online (Sandbox Code Playgroud)

正如您在第 3 列中看到的那样,当根据两个字节的顺序显然是小端字符集时,字符集被误报为大端字符集。您还可以看到两个字符是UTF-16两个字节,顺序他们是大和little endian,不仅是人物>在UTF-8 1个字节之间的不同。

鉴于所有这些,由于数据以 UTF-8 存储,但您通过DUMP函数将其视为 UTF-16 Big Endian ,您似乎已经将其转换为 UTF-16,但可能没有意识到默认设置Oracle 中的 UTF-16 是 Big Endian。

查看Oracle 文档词汇表页面上“UTF-16”定义,它指出(我将以下句子分成两部分,以便更容易区分 BE 和 LE):

AL16UTF16 实现了 UTF-16 编码形式的 big-endian 编码方案(每个代码单元的更重要的字节在内存中最先出现)。AL16UTF16 是有效的国家字符集。

和:

AL16UTF16LE 实现了 little-endian UTF-16 编码方案。它是一个仅限转换的字符集,仅在 SQLCONVERT或 PL/SQL等字符集转换函数中有效UTL_I18N.STRING_TO_RAW

PS 由于您AL32UTF8在 Oracle中使用,您应该Latin1_General_100_CI_AS_SC在 SQL Server 中使用排序规则,而不是Latin1_General_CI_AS. 您使用的是较旧的并且不完全支持补充字符(如果存在则不会丢失数据,但内置函数将它们处理为 2 个字符而不是单个实体)。