web-dev-qa-db-ja.com

SQL ServerとOracleのマルチバイト文字のバイト順

現在、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です。

なぜ順序が異なるのですか?また、マルチバイト文字を含む文字列に対して、もう一方の端のチェックサムが一致することを保証するために、一方を他方に変換する信頼できる方法はありますか?

4
HandyD

NVARCHAR/NCHAR/NTEXT列の照合順序は、その列にデータを格納するために使用されるエンコーディングとは関係ありません。 NVARCHARデータはalwaysUTF-16リトルエンディアン(LE)です。 NVARCHARデータの照合順序は、並べ替えと比較にのみ影響します。照合はVARCHARデータのエンコードに影響を与えます。これは、照合がその列/変数/リテラル​​にデータを格納するために使用されるコードページを決定するためですが、ここでは扱いません。

sepupicについて のように、データをバイナリ形式で表示すると、エンディアンの違いがわかります(SQL Serverがリトルエンディアンを使用しているのに対し、Oracleはビッグエンディアンを使用しています)。ただし、Oracleで文字列のバイナリ形式を表示したときに表示されるのは、データが実際に格納されている方法ではありません。 UTF-8であるAL32UTF8を使用しています。これは、その文字を2ではなく3バイトでエンコードします: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);

Oracleでは、CONVERT関数を使用して文字列をAL16UTF16LEエンコーディングに入れ、その値をハッシュする必要があります。 SQL Serverが持っているものと一致するはずです。たとえば、 White Bullet(U + 25E6) のさまざまなエンコーディング形式と、CONVERTAL16UTF16LEと一緒に使用すると 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;

それは返します:

UTF8:     Typ=1 Len=3 CharacterSet=AL32UTF8: e2,97,a6
UTF16BE:  Typ=1 Len=2 CharacterSet=AL16UTF16: 25,e6
UTF16LE:  Typ=1 Len=2 CharacterSet=AL16UTF16: e6,25


UTF8:     Typ=1 Len=4 CharacterSet=AL32UTF8: 61,e2,97,a6
UTF16BE:  Typ=1 Len=4 CharacterSet=AL16UTF16: 0,61,25,e6
UTF16LE:  Typ=1 Len=4 CharacterSet=AL16UTF16: 61,0,e6,25

3列目に示されているように、2バイトの順序に基づいて明らかにリトルエンディアンである場合、文字セットはビッグエンディアンであると誤って報告されます。また、両方の文字がUTF-16で2バイトであり、それらのbothの順序が、ビッグエンディアンとリトルエンディアンで異なるだけでなく、 UTF-8で1バイトを超える文字。

このすべてを考慮すると、データはUTF-8として格納されているにもかかわらず、DUMP関数を介してUTF-16ビッグエンディアンとして表示されているため、すでにUTF-16に変換しているようですが、 OracleのデフォルトのUTF-16がビッグエンディアンであることをおそらく認識していません。

Oracleドキュメントの用語集ページの「UTF-16」定義 を見ると、次のように記載されています(次の文を2つの部分に分けたので、BEとLEを区別しやすくなります)。

AL16UTF16は、UTF-16エンコード形式のビッグエンディアンエンコードスキームを実装します(各コードユニットの上位バイトがメモリ内で最初に来ます)。 AL16UTF16は有効な国別文字セットです。

そして:

AL16UTF16LEは、リトルエンディアンUTF-16エンコーディングスキームを実装しています。これは変換専用の文字セットであり、SQL CONVERTやPL/SQL UTL_I18N.STRING_TO_RAWなどの文字セット変換関数でのみ有効です。

追伸OracleではAL32UTF8を使用しているため、SQL ServerではLatin1_General_100_CI_AS_SCではなくLatin1_General_CI_AS照合順序を使用する必要があります。使用しているものは古く、補足文字を完全にはサポートしていません(存在する場合はデータの損失はありませんが、組み込み関数はそれらを単一のエンティティではなく2文字として扱います)。

4
Solomon Rutzky

表示されているのは、Little-EndianエンコーディングがSQL ServerUnicode文字を格納するために使用しているものです(より正確には、UCS-2 LEを使用しています)。

Little-Endianの詳細はこちら: ビッグエンディアンとリトルエンディアンのバイト順の違い

どうしてそれが可能だったのかわかりません

OracleでDUMPを使用するか、SQL ServerでVARBINARYに変換すると、データはこの文字のバイトを除いて完全に一致します

すべてSQL Serverに格納されているUnicode文字がbinaryに変換され、「反転」されます。つまり、実際のコードを表示するには、それらを2 bytesのグループに分割し、すべてのペア内で順序を逆にする必要があります。

例:

declare @str varchar(3) = 'abc';
declare @str_n nvarchar(3) = N'abc';

select cast(@str as varbinary(3));
select cast(@str_n as varbinary(6));

結果は

0x616263

0x610062006300

Unicode文字の場合に見られるように、バイトは反転されます。「a」は0x6100ではなく0x0061として表されます。

同じ話は、実際のUnicodeコードである0x25E6についてですが、binary表現ではSQL Server0xE625、つまりinverted

4
sepupic