无法在表格行中将“ CO2”更新为“CO²”


19

给定此表:

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

(1 affected rows)

(1 affected rows)

id          description
----------- -----------
1           CO2

(1 affected rows)

就像SQL Server确定那样,因为 ²显然只是很小的2,所以最终值不会改变,因此不值得更改。

有人可以对此有所启发,并提出解决方法(除了更新为中间值之外)吗?


1
阿尔瓦罗:如果您想了解更多有关此行为的信息,以便更好地了解为什么会发生这种情况,请查看我刚刚在答案底部添加的两个链接。
所罗门·鲁兹基

Answers:


29

下标2不是varchar字符集的一部分(在任何排序规则中,不仅是Modern_Spanish)。因此,使其为nvarchar常量:

UPDATE test SET description = N'CO₂' WHERE id = 1;

1
我不仅固定了价值,而且还首先了解了它是如何实现的。谢谢!
阿尔瓦罗·冈萨雷斯

2
@ÁlvaroGonzález和gbn:请注意,在相关数据库的默认归类指定的代码页中,“下标2”不可用,该默认归类是用于字符串文字和变量的归类,而不是列的归类(尽管两者可能使用相同的代码页)。但是,在代码页949中,通过韩文归类可以使用“下标2”。这在这里无济于事,只是仅供参考。我的回答中有详细信息和示例。
所罗门·鲁兹基

21

@gbn已经解释了基本原因和解决方法,但是您所看到的行为的特定原因是这样的:

  1. 您使用的是VARCHAR文字(无N前缀)而不是NVARCHAR文字(带N前缀的字符串),因此Unicode字符将转换为VARCHAR
  2. VARCHAR是8位编码,在大多数情况下,每个字符一个字节,但也可以每个字符两个字节。另一方面,NVARCHAR是16位编码(UTF-16 Little Endian),每个字符两个字节或四个字节。
  3. 由于用于映射字符的可用字节数的差异,从本质上来说,8位编码在可映射的字符数上受到更多限制。VARCHAR单字节字符集的数据最多为256个字符(大多数),双字节字符集的数据最多为65536个字符(仅其中一些)。另一方面,NVARCHAR数据可以映射超过110万个Unicode字符(尽管当前映射的字符数不到25万)。
  4. 由于可以使用8位/ VARCHAR数据完成的映射数量有限,因此不同的字符分组(基于语言/文化)分布在多个“代码页”(即字符集)中
  5. 每个归类指定要用于VARCHAR数据的代码页(如果有)(NVARCHAR)全部字符)
  6. 将字符串文字或变量从NVARCHAR(即Unicode / UTF-16 /所有字符)转换为VARCHAR(基于大多数归类中指定的基于代码页的字符集)时,将使用数据库的默认归类
  7. 如果用于转换的归类的代码页不包含相同字符,但包含“最适合”映射,则将使用“最适合”映射。
  8. 如果用于转换的归类的代码页不包含相同字符或不包含“最适合”映射,则将使用默认的“替换”字符(最常见?)。

所以,你看到的是一个NVARCHARVARCHAR转换由于缺少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₂

更新

对于有兴趣了解更多有关这里到底发生了什么的人(即所有血腥细节),请参阅我刚刚发布的两部分调查:

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.