口音敏感排序


19

为什么这两个SELECT语句导致排序顺序不同?

USE tempdb;
CREATE TABLE dbo.OddSort 
(
    id INT IDENTITY(1,1) PRIMARY KEY
    , col1 NVARCHAR(2)
    , col2 NVARCHAR(2)
);
GO
INSERT dbo.OddSort (col1, col2) 
VALUES (N'e', N'eA')
    , (N'é', N'éB')
    , (N'ë', N'ëC')
    , (N'è', N'èD')
    , (N'ê', N'êE')
    , (N'ē', N'ēF');
GO

SELECT * 
FROM dbo.OddSort 
ORDER BY col1 COLLATE Latin1_General_100_CS_AS;
╔════╦══════╦══════╗
║id║col1║col2║
╠════╬══════╬══════╣
║1║e║eA║
║2║║B B
║4║D D D D-应该是ID 3吗?
║5║║E E║
║3║C C C║
║6ē║FF║
╚════牛皮══════牛皮══════╝
SELECT * 
FROM dbo.OddSort 
ORDER BY col2 COLLATE Latin1_General_100_CS_AS;
╔════╦══════╦══════╗
║id║col1║col2║
╠════╬══════╬══════╣
║1║e║eA║
║2║║B B
║3║C C C║
║4║║D DD
║5║║E E║
║6ē║FF║
╚════牛皮══════牛皮══════╝

Answers:


13

这个问题与数据库不是很相关,而更多地与Unicode处理和规则有关。

基于https://docs.microsoft.com/zh-cn/sql/t-sql/statements/windows-collat​​ion-name-transact-sql Latin1_General_100_CS_AS的意思是:“排序规则使用Latin1 General字典排序规则并将其映射到代码页1252“,添加的CS =区分大小写,AS =重音敏感。

Windows代码页1252和Unicode(http://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT)之间的映射对我们正在处理的所有字符显示相同的值(带有宏的e除外) (在Microsoft映射中不存在),因此不知道在这种情况下如何处理),因此我们现在可以集中精力研究Unicode工具和术语。

首先,让我们确切地知道您要处理的所有字符串的处理方式:

0065  LATIN SMALL LETTER E
0041  LATIN CAPITAL LETTER A
00E9  LATIN SMALL LETTER E WITH ACUTE
0042  LATIN CAPITAL LETTER B
00EB  LATIN SMALL LETTER E WITH DIAERESIS
0043  LATIN CAPITAL LETTER C
00E8  LATIN SMALL LETTER E WITH GRAVE
0044  LATIN CAPITAL LETTER D
00EA  LATIN SMALL LETTER E WITH CIRCUMFLEX
0045  LATIN CAPITAL LETTER E
0113  LATIN SMALL LETTER E WITH MACRON
0046  LATIN CAPITAL LETTER F

Unicode归类算法在此处进行了描述:https : //www.unicode.org/reports/tr10/

请参阅第1.3节“上下文敏感度”,其中解释说排序不能仅取决于一个字符,因为某些规则是上下文相关的。

另请注意1.8中的这些要点:

排序规则不是字符串的属性。通常,在串联或子字符串操作下不保留排序规则。

默认情况下,该算法使用三个完全可自定义的级别。对于拉丁文字,这些级别大致对应于:

alphabetic ordering
diacritic ordering
case ordering.

但是算法本身有点密集。其要点是:简要地说,Unicode归类算法采用一个输入Unicode字符串和一个归类元素表,其中包含字符的映射数据。它产生一个排序键,它是一个无符号的16位整数的数组。然后可以对这样生成的两个或更多排序键进行二进制比较,以在为其生成字符串的字符串之间进行正确比较。

您可以在此处查看特定的拉丁语排序规则:http : //developer.mimer.com/collat​​ions/charts/latin.htm 或更直接(特别是针对MS SQL):http : //collat​​ion-charts.org/mssql/mssql。 0409.1252.Latin1_General_CS_AS.html

对于e字符,它显示:

e EéèÈêêëËË

这解释了订购时的结果,col1但代码页1252中不存在ē,因此我绝对不知道它的作用。

或者,如果我们手动执行Unicode算法,请使用http://www.unicode.org/Public/UCA/latest/allkeys.txt上的DUCET键值:

步骤1:标准化表格D,因此每种情况变为:

e => U+0065
é => U+0065 U+0301
ë => U+0065 U+0308
è => U+0065 U+0300
ê => U+0065 U+0302
ē => U+0065 U+0304

第2步,产生排序规则数组(在file中查找allkeys.txt

e => [.1D10.0020.0002]
é => [.1D10.0020.0002] [.0000.0024.0002]
ë => [.1D10.0020.0002] [.0000.002B.0002]
è => [.1D10.0020.0002] [.0000.0025.0002]
ê => [.1D10.0020.0002] [.0000.0027.0002]
ē => [.1D10.0020.0002] [.0000.0032.0002]

第3步,表单排序键(对于每个级别,将每个值放在每个排序规则数组中,然后将0000用作分隔符,并重新开始下一个级别)

e => 1D10 0000 0020 0000 0002
é => 1D10 0000 0020 0024 0000 0002 0002
ë => 1D10 0000 0020 002B 0000 0002 0002
è => 1D10 0000 0020 0025 0000 0002 0002
ê => 1D10 0000 0020 0027 0000 0002 0002
ē => 1D10 0000 0020 0032 0000 0002 0002

步骤4,比较排序键(每个值的简单二进制比较):第四个值足以对它们进行排序,因此最终顺序为:

e
é
è
ê
ë
ē

以相同的方式订购col2

步骤1:NFD

eA => U+0065 U+0041
éB => U+0065 U+0301 U+0042
ëC => U+0065 U+0308 U+0043
èD => U+0065 U+0300 U+0044
êE => U+0065 U+0302 U+0045
ēF => U+0065 U+0304 U+0046

步骤2:整理数组

eA => [.1D10.0020.0002] [.1CAD.0020.0008]
éB => [.1D10.0020.0002] [.0000.0024.0002] [.1CC6.0020.0008]
ëC => [.1D10.0020.0002] [.0000.002B.0002] [.1CE0.0020.0008]
èD => [.1D10.0020.0002] [.0000.0025.0002] [.1CF5.0020.0008]
êE => [.1D10.0020.0002] [.0000.0027.0002] [.1D10.0020.0008]
ēF => [.1D10.0020.0002] [.0000.0032.0002] [.1D4B.0020.0008]

步骤3:表单排序键

eA => 1D10 1CAD 0000 0020 0020 0000 0002 0008
éB => 1D10 1CC6 0000 0020 0024 0020 0000 0002 0002 0008
ëC => 1D10 1CE0 0000 0020 002B 0020 0000 0002 0002 0008
èD => 1D10 1CF5 0000 0020 0025 0020 0000 0002 0002 0008
êE => 1D10 1D10 0000 0020 0027 0020 0000 0002 0002 0008
ēF => 1D10 1D4B 0000 0020 0032 0020 0000 0002 0002 0008

第4步:比较排序键:第二个值足以对它们全部排序,并且实际上已经按升序排列,因此最终的顺序确实是:

eA
éB
ëC
èD
êE
ēF

更新:添加所罗门·鲁兹基的第三种情况,由于启用了新规则的空间,这种情况比较棘手(我选择了“不可忽略的情况”):

步骤1,NFD:

è 1 => U+0065 U+0300 U+0020 U+0031
ê 5 => U+0065 U+0302 U+0020 U+0035
e 2 => U+0065 U+0020 U+0032
é 4 => U+0065 U+0301 U+0020 U+0034
ē 3 => U+0065 U+0304 U+0020 U+0033
ë 6 => U+0065 U+0308 U+0020 U+0036

步骤2,产生排序规则阵列:

è 1 => [.1D10.0020.0002] [.0000.0025.0002] [*0209.0020.0002] [.1CA4.0020.0002]
ê 5 => [.1D10.0020.0002] [.0000.0027.0002] [*0209.0020.0002] [.1CA8.0020.0002]
e 2 => [.1D10.0020.0002] [*0209.0020.0002] [.1CA5.0020.0002]
é 4 => [.1D10.0020.0002] [.0000.0024.0002] [*0209.0020.0002] [.1CA7.0020.0002]
ē 3 => [.1D10.0020.0002] [.0000.0032.0002] [*0209.0020.0002] [.1CA6.0020.0002]
ë 6 => [.1D10.0020.0002] [.0000.002B.0002] [*0209.0020.0002] [.1CA9.0020.0002]

步骤3,表单排序键:

è 1 => 1D10 0209 1CA4 0000 0020 0025 0020 0020 0000 0002 0002 0002 0002
ê 5 => 1D10 0209 1CA8 0000 0020 0027 0020 0020 0000 0002 0002 0002 0002
e 2 => 1D10 0209 1CA5 0000 0020 0020 0020 0000 0002 0002 0002
é 4 => 1D10 0209 1CA7 0000 0020 0024 0020 0020 0000 0002 0002 0002 0002
ē 3 => 1D10 0209 1CA6 0000 0020 0032 0020 0020 0000 0002 0002 0002 0002
ë 6 => 1D10 0209 1CA9 0000 0020 002B 0020 0020 0000 0002 0002 0002 0002

步骤4,比较排序键:

基本上,第三个值确定顺序,并且实际上仅基于最后一位,因此顺序应为:

è 1
e 2
ē 3
é 4
ê 5
ë 6

基于Solomon Rutzky关于Unicode版本的注释的第二次更新

我此时使用的是allkeys.txt有关最新Unicode版本的数据,即版本10.0。

如果我们需要考虑Unicode 5.1的话,则为:http : //www.unicode.org/Public/UCA/5.1.0/allkeys.txt

我刚刚检查了上面所有字符的排序规则数组,而不是以下内容:

e => [.119D.0020.0002.0065]
é => [.119D.0020.0002.0065] [.0000.0032.0002.0301]
ë => [.119D.0020.0002.0065] [.0000.0047.0002.0308]
è => [.119D.0020.0002.0065] [.0000.0035.0002.0300]
ê => [.119D.0020.0002.0065] [.0000.003C.0002.0302]
ē => [.119D.0020.0002.0065] [.0000.005B.0002.0304]

和:

eA => [.119D.0020.0002.0065] [.1141.0020.0008.0041]
éB => [.119D.0020.0002.0065] [.0000.0032.0002.0301] [.1157.0020.0008.0042]
ëC => [.119D.0020.0002.0065] [.0000.0047.0002.0308] [.116F.0020.0008.0043]
èD => [.119D.0020.0002.0065] [.0000.0035.0002.0300] [.1182.0020.0008.0044]
êE => [.119D.0020.0002.0065] [.0000.003C.0002.0302] [.119D.0020.0008.0045]
ēF => [.119D.0020.0002.0065] [.0000.005B.0002.0304] [.11D5.0020.0008.0046]

和:

è 1 => [.119D.0020.0002.0065] [.0000.0035.0002.0300] [*0209.0020.0002.0020] [.1138.0020.0002.0031]
ê 5 => [.119D.0020.0002.0065] [.0000.003C.0002.0302] [*0209.0020.0002.0020] [.113C.0020.0002.0035]
e 2 => [.119D.0020.0002.0065] [*0209.0020.0002.0020] [.1139.0020.0002.0032]
é 4 => [.119D.0020.0002.0065] [.0000.0032.0002.0301] [*0209.0020.0002.0020] [.113B.0020.0002.0034]
ē 3 => [.119D.0020.0002.0065] [.0000.005B.0002.0304] [*0209.0020.0002.0020] [.113A.0020.0002.0033]
ë 6 => [.119D.0020.0002.0065] [.0000.0047.0002.0308] [*0209.0020.0002.0020] [.113D.0020.0002.0036]

然后计算出以下排序键:

e => 119D 0000 0020 0000 0002 0000 0065
é => 119D 0000 0020 0032 0000 0002 0002 0000 0065 0301
ë => 119D 0000 0020 0047 0000 0002 0002 0000 0065 0308
è => 119D 0000 0020 0035 0000 0002 0002 0000 0065 0300
ê => 119D 0000 0020 003C 0000 0002 0002 0000 0065 0302
ē => 119D 0000 0020 005B 0000 0002 0002 0000 0065 0304

和:

eA => 119D 1141 0000 0020 0020 0000 0002 0008 0000 0065 0041
éB => 119D 1157 0000 0020 0032 0020 0000 0002 0002 0008 0000 0065 0301 0042
ëC => 119D 116F 0000 0020 0047 0020 0000 0002 0002 0008 0000 0065 0308 0043
èD => 119D 1182 0000 0020 0035 0020 0000 0002 0002 0008 0000 0065 0300 0044
êE => 119D 119D 0000 0020 003C 0020 0000 0002 0002 0008 0000 0065 0302 0045
ēF => 119D 11D5 0000 0020 005B 0020 0000 0002 0002 0008 0000 0065 0304 0046

和:

è 1 => 119D 0209 1138 0000 0020 0035 0020 0020 0000 0002 0002 0002 0002 0000 0065 0300 0020 0031
ê 5 => 119D 0209 113C 0000 0020 003C 0020 0020 0000 0002 0002 0002 0002 0000 0065 0302 0020 0035
e 2 => 119D 0209 1139 0000 0020 0020 0020 0000 0002 0002 0002 0000 0065 0020 0032
é 4 => 119D 0209 113B 0000 0020 0032 0020 0020 0000 0002 0002 0002 0002 0000 0065 0301 0020 0034
ē 3 => 119D 0209 113A 0000 0020 005B 0020 0020 0000 0002 0002 0002 0002 0000 0065 0304 0020 0033
ë 6 => 119D 0209 113D 0000 0020 0047 0020 0020 0000 0002 0002 0002 0002 0000 0065 0308 0020 0036

再次给出这三个排序结果:

e
é
è
ê
ë
ē

eA
éB
ëC
èD
êE
ēF

è 1
e 2
ē 3
é 4
ê 5
ë 6

嗨,帕特里克。感谢您发布此详细信息。一些注意事项:1)您可以忽略代码页1252。这是针对VARCHAR(即非Unicode)数据的,此处不再使用。这就是ē角色正常工作的原因。2) “归类图表”信息有些过时。它用于此排序规则的先前版本,自2009年以来他们未发布任何内容。3)这里的Unicode版本绝对不是最新版本(版本10)。该_100_系列排序规则来与SQL 2008,因此这将是Unicode的5.0或5.1:unicode.org/standard/versions/#TUS_Earlier_Versions
所罗门Rutzky

我不认为allKeys.txt Unicode版本之间的变化,除了添加了新字符之外,因此上面的内容应该保持不变,但是当然可以用以前的旧数据重做,我只是缺乏再次投入几个小时的精力。至于CP1252,它只是来自MS-SQL给出的定义(我自己不使用此产品)。
Patrick Mevzek '18年

1)版本之间可能没有重大变化,但是我很确定至少有权重/分类的更正。但是,是的,我当然受到时间限制了;)2)关于CP1252,我之所以提到它是因为Unicode中不存在代码页的概念。Unicode是一种不需要代码页的方法。当然,MS doc尚不清楚,但是它确实提到了“ 用于存储非Unicode字符数据的代码页 ”。您正确地认为CP1252中没有一个字符,但是代码页在这里没有起作用。
所罗门·鲁兹基'18

是的,我从未说过与Unicode相关的代码页。只是MS SQL文档说此排序规则名称可用于“代码页1252”。可能没有任何意义(对我来说不是,但不是用户),但这就是该软件文档中的内容。至于重做工作,可以随意做,或者只是在Unicode 5和最新版本之间(如果有的话)提供与正在使用的字符相关的排序更改。我相信我的回答是准确的,并且会根据输入(而不是其他方式)建立结果。
Patrick Mevzek '18

3) re:第1句话:虽然主要涉及Unicode,但是由于MS实施了规范,因此这个问题是一个操作系统问题,可能还没有全部解决和/或可能犯了一些错误。我确实在.NET中发现了2个关于如何确定特定字符所在的“类别”或“块”的错误(它们会误识别一小部分字符)。此外,这,至少轻微,一个SQL Server的问题,不仅是因为SQL Server有有效的一每一对比的快照在时间(版本之间的一致性)的一个点,这样他们就可以拥有什么现在是SQL Server特定的行为。
所罗门·鲁兹基'18

16

在一般情况下,您在此处看到的行为是由于Unicode排序算法(UCA)允许进行复杂的多级排序这一事实所致。进一步来说:

  1. 排序不是比较:

    直接确定两个字符串是相同还是不同(给定特定的区域设置/语言和敏感度集)。但是确定2个或更多字符串的顺序可能会非常复杂。

  2. 排序是通过一系列步骤完成的,每个步骤都应用于整个字符串,而不是逐个字符:

    1. 标准:对基本字符排序(不考虑重音和大小写)
    2. 如果对口音敏感,则应用重音符号
    3. 如果区分大小写,则应用套管重量

当您按col1(单个字符)排序时,它首先确定所有字符都具有相同的权重,因为它们都是“ e ”。接下来,它应用重音符号/变音符号权重。没有大小写差异,因此第三步不会更改任何内容。因此,唯一的区别是在步骤2中,这就是为什么基于的行具有首选顺序的原因col1

当您按col2(两个字符)进行排序时,由于两个字符都用于确定排序权重(例如“ ea ”,“ eb ”等),因此它首先确定每一行的权重都不同。接下来,它应用重音符号/变音符号权重。没有大小写差异,因此第三步不会更改任何内容。因此,这次步骤1和步骤2存在差异。但是,由于在考虑了步骤2的权重之前,已经对每个字符串应用了步骤1的差异,所以步骤2的权重对排序没有任何影响;仅当步骤1中两行或更多行的权重相同时,它们才适用。

从问题中对示例代码进行的以下修改有望说明上述排序行为。我添加了一些附加的行和一个附加的列来帮助说明排序规则区分大小写的影响(因为原始样本数据全部为小写):

设定

USE [tempdb];

-- DROP TABLE dbo.OddSort;
CREATE TABLE dbo.OddSort
(
    id INT IDENTITY(1,1) PRIMARY KEY,
    col1 NVARCHAR(5) COLLATE Latin1_General_100_CS_AS,
    col2 NVARCHAR(5) COLLATE Latin1_General_100_CS_AS,
    col3 NVARCHAR(5) COLLATE Latin1_General_100_CS_AS
);
GO

INSERT dbo.OddSort (col1, col2, col3)
VALUES (N'e', N'eA', N'e A')
     , (N'ê', N'êE', N'ê E')
     , (N'é', N'éH', N'é H')
     , (N'ë', N'ëC', N'ë C')
     , (N'E', N'EG', N'E G')
     , (N'Ë', N'ëh', N'ë h')
     , (N'è', N'èD', N'è D')
     , (N'é', N'éB', N'é B')
     , (N'ë', N'ëH', N'ë H')
     , (N'ē', N'ēF', N'ē F');

测试1

SELECT [id], [col1], UNICODE([col1]) AS [CodePoint]
FROM dbo.OddSort 
ORDER BY col1;

返回值:

id    col1    CodePoint
1     e       101
5     E       69
8     é       233
3     é       233
7     è       232
2     ê       234
4     ë       235
9     ë       235
6     Ë       203
10    ē       275

我们可以在上面的结果中看到:

  1. 代码点未确定排序顺序
  2. 非重音字符在重音字符之前排序(在同一字母内:f仍在所有这些字符之后)。显然,重音重于案例重之前。
  3. 小写排序前相同的重音符(或非重音)字符内大写(即Ë然后Ëë然后Ë)。大多数Windows排序规则都使用这种剪裁,而大多数SQL Server排序规则首先将大写排序。

测试2

SELECT [id], [col2]
FROM dbo.OddSort 
ORDER BY col2;

返回值:

id    col2
1     eA
8     éB
4     ëC
7     èD
2     êE
10    ēF
5     EG
3     éH
6     ëh
9     ëH

我们可以在上面的结果中看到:

  1. 一级排序确实是基本字符。如果是重音符号/变音符号,则C(id = 4),ēF(id = 10)和EG(id = 5)行将不在它们的位置。如果是套管,则EG(id = 5)行将不在此处。
  2. 二级排序确实是重音符号/变音符号。这解释了为什么最后三行是éH- > ëh- > ëH而不是 ëh- > éH- > ëH(即ID 3-> 6-> 9而不是6-> 3-> 9)。
  3. 真正的三级分类就是机壳。这就是为什么最后两行是 «h -> «H的原因,因为小写字母第一位。

测试3

SELECT [id], [col3]
FROM dbo.OddSort 
ORDER BY col3;

返回值:

id    col3
1     e A
8     é B
4     ë C
7     è D
2     ê E
10    ē F
5     E G
3     é H
6     ë h
9     ë H

我们可以在上面的结果中看到:

  1. 排序顺序与测试2完全相同。这里测试值的唯一区别是,每个字符之间都有一个空格,从而消除了上下文规则的可能性。因此,我们知道col2问题中排序顺序不同的原因再次是由于“多级比较”,而不是 “上下文敏感度”。

补充说明:

  1. 关于获得确切的规则,这并不应该如此简单。对这些规则进行具体解释的问题在于,尽管已明确记录了Unicode排序规则,但仍是建议。实施这些建议取决于供应商(例如Microsoft)。Microsoft并未完全按照Unicode文档中的说明实施建议,因此存在一个脱节(类似于跨供应商完全或什至没有以相同方式实施HTML或CSS规范)。然后,有不同版本的Windows排序规则(您使用的100是SQL Server 2008附带的版本),并且绑定到的Unicode版本比当前Unicode版本或 ICU排序规则演示正在使用的Unicode版本都要旧得多。例如,SQL Server 2008“排序规则和Unicode支持”文档的SQL Server 2008排序规则部分中的关于“ _100_整理” 系列的“新内容”,有2个非常有趣的观点:

    1. Unicode 5.0案例表。

      Unicode 5.0于2006年7月发布(嗯,然后发布了字符数据库,并于2006年底发布了完整规范)。当前版本为10.0,于2017年6月发布。考虑到过去4年的发布模式,版本11.0可能会在2018年中期发布。

    2. 加权已添加到本来可以比较的以前未加权的字符中。

      这些权重很可能是在Unicode标准中定义的,只是在此实现中并未定义。

     
    尽管如此,上面链接的UCA文档还是一个不错的起点。

  2. Windows / .NET / SQL Server使用的排序键与Unicode标准(请参阅@Patrick的答案)中所示或在ICU中实现的不完全相同。要查看Windows / .NET / SQL Server使用的是什么,可以尝试CompareInfo.GetSortKey方法。我创建了一个SQLCLR UDF来传递这些值并获取排序键。请注意,我在Windows 10上使用SQL Server 2017,并且安装了.NET Framework 4.5-4.6.1,因此.NET 应该使用Unicode 6.0.0。同样,Level4也不用于这些字符串。

    CHAR    L1     L2     L3     L4
    e      0E21
    E      0E21           12
    ë      0E21    13
    Ë      0E21    13     12

    查看测试1的这些排序键,并认识到级别的排序方式类似于ORDER BY子句中的多个列(L3排序在L2的相同值内,L3排序在L1的相同值内),应说明行为的原因问题中提到的实际上是Unicode的多级排序功能。同样地:

    CHAR       L1         L2       L3       L4
    EG      0E210E25              1212
    éH      0E210E2C      0E      0212
    ëh      0E210E2C      13
    ëH      0E210E2C      13      0212

    查看Test 2的一些排序键,我们可以看到首先对基本字符进行了排序(L1),然后对重音符号进行了排序(L2),然后对大小写进行了排序(L3)。

  3. 由于数据类型为NVARCHAR,我们只关心Unicode代码点和排序算法,因此UNICODE()在TEST 1中使用该功能。尽管大多数归类指定代码页,但它们仅与VARCHAR数据有关。含义,虽然代码页1252是由Latin1_General*一系列排序规则指定的,但在此处可以忽略。

  4. 默认Unicode排序规则元素表(DUCET)(应映射到系列排序规则的版本5.0.0)中描述的权重_100_适用于美国英语,但不适用于其他语言环境/语言。其他语言需要 DUCET 开始,然后应用通用语言环境数据存储库(CLDR)项目定义的特定于语言环境的替代规则。据我所知,版本1.4 / 1.4.1于2006年发布。要获取这些替代,请通过http://unicode.org/Public/cldr/1.4.0/core.zip下载CLDR 1.4“核心”文件。,然后在该zip文件中,转到排序规则文件夹并找到与所使用的语言环境相对应的XML文件。这些文件仅包含替代,不是完整的归类规则集。

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.