@gbn已经解释了基本原因和解决方法,但是您所看到的行为的特定原因是这样的:
- 您使用的是
VARCHAR
文字(无N
前缀)而不是NVARCHAR
文字(带N
前缀的字符串),因此Unicode字符将转换为VARCHAR
。
VARCHAR
是8位编码,在大多数情况下,每个字符一个字节,但也可以每个字符两个字节。另一方面,NVARCHAR
是16位编码(UTF-16 Little Endian),每个字符两个字节或四个字节。
- 由于用于映射字符的可用字节数的差异,从本质上来说,8位编码在可映射的字符数上受到更多限制。
VARCHAR
单字节字符集的数据最多为256个字符(大多数),双字节字符集的数据最多为65536个字符(仅其中一些)。另一方面,NVARCHAR
数据可以映射超过110万个Unicode字符(尽管当前映射的字符数不到25万)。
- 由于可以使用8位/
VARCHAR
数据完成的映射数量有限,因此不同的字符分组(基于语言/文化)分布在多个“代码页”(即字符集)中
- 每个归类指定要用于
VARCHAR
数据的代码页(如果有)(NVARCHAR
)全部字符)
- 将字符串文字或变量从
NVARCHAR
(即Unicode / UTF-16 /所有字符)转换为VARCHAR
(基于大多数归类中指定的基于代码页的字符集)时,将使用数据库的默认归类
- 如果用于转换的归类的代码页不包含相同字符,但包含“最适合”映射,则将使用“最适合”映射。
- 如果用于转换的归类的代码页不包含相同字符或不包含“最适合”映射,则将使用默认的“替换”字符(最常见
?
)。
所以,你看到的是一个NVARCHAR
以VARCHAR
转换由于缺少N
对字符串文字前缀。并且,数据库默认排序规则的代码页不包含完全相同的字符,但是找到了“最佳匹配”映射,这就是为什么要使用2
而不是的原因?
。
通过执行以下简单测试,您可以看到这种效果:
SELECT '₂', N'₂';
返回值:
2 ₂
需要明确的是,如果数据库默认排序规则的代码页确实包含完全相同的字符,那么它将在该代码页中转换为相同的字符。然后,根据您的情况,由于要存储到NVARCHAR
列中,因此它将再次转换回原始Unicode字符。下面的最后一个示例显示了此行为。
重要说明:请注意,转换是在解释字符串文字时进行的,这是在将其存储到列中之前进行的。这意味着即使该列可以容纳该字符,也将由于数据库默认的归类而将其转换为其他字符,这都是由于N
在该字符串文字上省略了前缀。这正是您正在(或曾经)经历的。
例如,如果数据库的默认排序规则是韩文排序规则之一(四个双字节字符集之一),那么您将不会看到此问题,因为该字符中有“下标2”字符设置(代码页949)。尝试以下测试以查看(它使用列的排序规则而不是数据库的默认排序规则,因为这样更易于显示):
CREATE TABLE #TestChar
(
[8bit_Latin1_General-1252] VARCHAR(2) COLLATE Latin1_General_100_CI_AS_SC,
[8bit_Korean-949] VARCHAR(2) COLLATE Korean_100_CI_AS_SC,
[UTF16LE_Latin1_General-1252] NVARCHAR(2) COLLATE Latin1_General_100_CI_AS_SC
);
INSERT INTO #TestChar VALUES (N'₂', N'₂', N'₂');
SELECT * FROM #TestChar;
返回值:
8bit_Latin1_General-1252 8bit_Korean-949 UTF16LE_Latin1_General-1252
2 ₂ ₂
如您所见,Latin1_General归类使用的是代码页1252(Modern_Spanish
归类使用的相同代码页)作为VARCHAR
数据,没有完全匹配,但它们确实具有“最合适”的映射(这就是您所看到的) )。但是,使用代码页949进行VARCHAR
数据的韩文排序规则确实与“下标2”字符完全匹配。
为了进一步说明,我们可以使用朝鲜语排序规则之一的默认排序规则创建一个新的数据库,然后运行问题中的确切SQL:
CREATE DATABASE [TestKorean-949] COLLATE Korean_100_CI_AS_KS_WS_SC;
ALTER DATABASE [TestKorean-949] SET RECOVERY SIMPLE;
GO
USE [TestKorean-949];
CREATE TABLE test (
id INT NOT NULL,
description NVARCHAR(100) COLLATE Modern_Spanish_CI_AS NOT NULL
);
INSERT INTO test (id, description) VALUES (1, 'CO2');
SELECT * FROM test WHERE id = 1;
UPDATE test SET description = 'CO₂' WHERE id = 1;
SELECT * FROM test WHERE id = 1;
返回值:
id description
1 CO2
id description
1 CO₂
更新
对于有兴趣了解更多有关这里到底发生了什么的人(即所有血腥细节),请参阅我刚刚发布的两部分调查: