現在、OracleからSQL Serverにデータを移行している最中ですが、移行後にデータを検証しようとして問題が発生しています。
環境の詳細:
SQL Server 2016
OracleでDBMS_CRYPTO.HASHを使用して行全体のチェックサムを生成してから、SQLにコピーし、HASHBYTESを使用して行全体のチェックサムを生成しています。これを比較して、データの一致を検証しています。
チェックサムは、マルチバイト文字を含むものを除き、すべての行で一致します。
たとえば、次の文字を含む行:◦データが正しく転送されていても、チェックサムが一致しません。 OracleでDUMPを使用するか、SQL ServerでVARBINARYに変換すると、この文字のバイトを除いて、データは正確に一致します。
SQL Serverでは、バイトは0xE625、Oracleでは0x25E6です。
なぜ順序が異なるのですか?また、マルチバイト文字を含む文字列に対して、もう一方の端のチェックサムが一致することを保証するために、一方を他方に変換する信頼できる方法はありますか?
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) のさまざまなエンコーディング形式と、CONVERT
をAL16UTF16LE
と一緒に使用すると 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/SQLUTL_I18N.STRING_TO_RAW
などの文字セット変換関数でのみ有効です。
追伸OracleではAL32UTF8
を使用しているため、SQL ServerではLatin1_General_100_CI_AS_SC
ではなくLatin1_General_CI_AS
照合順序を使用する必要があります。使用しているものは古く、補足文字を完全にはサポートしていません(存在する場合はデータの損失はありませんが、組み込み関数はそれらを単一のエンティティではなく2文字として扱います)。
表示されているのは、Little-Endian
エンコーディングがSQL Server
がUnicode
文字を格納するために使用しているものです(より正確には、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 Server
で0xE625
、つまりinverted
。