Unicode 字符串的等价性很奇怪 (2016)
摘要
Unicode 字符串等价性很复杂,尤其是涉及校对规则时,会导致意外的结果,例如删除控制字符和非确定性分组。作者讨论了在数据库系统中正确实现 Unicode 支持所面临的挑战。
<p><a href="https://lobste.rs/s/awaoc2/equivalence_unicode_strings_is_strange">评论</a></p>
查看缓存全文
缓存时间: 2026/05/29 09:54
# Unicode 字符串的相等性很古怪
来源: http://databasearchitects.blogspot.com/2016/08/equivalence-of-unicode-strings-is.html
最初,HyPer(http://wwww.hyper-db.com/)对字符串采用了一个非常简单的模型:我们确保所有字符串都是有效的 UTF-8(https://en.wikipedia.org/wiki/UTF-8),但除此之外并不真正关心 Unicode 的细节。实际上,这对大多数应用(http://utf8everywhere.org/)来说是一个相当合理的模型。通常我们并不关心字符串的具体结构,而在少数需要关心的地方(例如 strpos(https://www.postgresql.org/docs/9.4/static/functions-string.html)和 substr(https://www.postgresql.org/docs/9.4/static/functions-string.html)),我们会添加一些额外的逻辑来正确处理 UTF-8。
这一切都很好,直到用户开始抱怨排序顺序。当对 UTF-8 进行简单排序时,我们按码点值排序,这通常没问题,但并不总是用户想要的。于是我们增加了对排序规则(collations)的支持:(https://www.postgresql.org/docs/9.1/static/collation.html)
```
select * from foo order by x collate "de_DE";
```
然后事情就开始变得有趣了。排序规则(collate)语句可以在查询中的任何位置显式给出,如上所示,也可以在 *create table* 语句中添加,为列提供默认排序规则。因此,基本上在 SQL 语句中每个被排序或比较的值都可以关联一个排序规则。
最初我天真地以为这只会影响比较操作,比如用 strcoll(http://en.cppreference.com/w/cpp/string/byte/strcoll)代替 strcmp(http://en.cppreference.com/w/cpp/string/byte/strcmp),但遗憾的是实际情况要复杂得多。首先,Unicode 排序算法(http://www.unicode.org/reports/tr10/)相当复杂,它会在比较之前将输入序列转换为 3(或 4)级权重的序列;其次,一些疯狂的数据库系统默认使用不区分大小写的排序规则(https://msdn.microsoft.com/en-us/library/ms143726.aspx),而一些用户确实想要这种表现。
为什么说大小写不敏感是疯狂的呢?因为它导致了奇怪的语义。首先,整个 Unicode 权重机制就很奇怪。其中一些古怪之处可以通过使用 ICU(http://site.icu-project.org/)来隐藏,但你仍然会得到奇怪的结果。例如,考虑以下两个字符串(\u 是 Unicode 转义):
abc abc\u007F
显然它们是不同的,对吧?好吧,如果你去问 ICU 排序演示(http://demo.icu-project.org/icu-bin/collation.html),它们并不不同,被认为是相等的。原因是 DUCET 表(http://www.unicode.org/Public/UCA/latest/allkeys.txt)中对于 007F 的条目是:
```
007F ; [.0000.0000.0000] # DELETE (in ISO 6429)
```
所以这个字符在比较时必须被忽略。还有其他一些码点,它们与普通比较相关,但与不区分重音的排序规则无关,等等。这带来了很多乐趣,特别是当你想实现基于哈希的算法时。
但抛开技术问题不谈,这真是用户想要的吗?用户真的期望这两个字符串相等吗?就像他们显然期望 'abc' 和 'äbc' 在不区分重音的排序规则下比较相等一样?那查询呢?例如:
```
select x,count(*)
from (values('abc'),('ABC'),('äbc')) s(x)
group by x collate "de_DE_ci_ai";
```
这个查询以不区分大小写、不区分重音的方式进行 group by,这意味着三个字符串比较结果相等。那么结果是什么?abc 3?ABC 3?äbc 3?三个都是有效的答案,因为根据所选排序规则,它们是“相等的”。如果查询被并行化到多个线程并且第一个值胜出,结果甚至可能是不确定的。
用户真的想要这样吗?嗯,显然他们想要,至少我被告知如此,而且有些系统甚至默认采用不区分大小写的行为。但我认为这很奇怪,这些查询的语义可能相当令人困惑。
相似文章
文件名的Unicode组合
本文讨论了在Subversion版本控制系统中,不同操作系统之间Unicode文件名组合(NFC与NFD)面临的挑战,并提出了处理这些差异的解决方案。
升级资源字符串到Unicode时,别忘了指定L前缀
解释:当在Windows资源字符串中使用Unicode转义序列(如\x2019)时,必须在字符串前加上L前缀使其成为宽字符字符串;否则转义序列会被误解为8位序列。
我最喜欢的Bug:无效的代理对
一篇博客文章,回顾了一个Bug:在CRDT库中,插入相邻的多字节表情符号导致了一个拼接操作,分割了代理对,并静默地破坏了协作编辑器的同步。
超越困惑度:面向字节感知语言模型中的UTF-8有效性
本文研究了字节级语言模型中训练规模与UTF-8生成可靠性之间的关系,发现UTF-8有效性收敛的速度比困惑度大约慢一倍。作者引入了用于隔离结构有效性的评估协议,并表明可靠的UTF-8生成是一种需要单独评估的独特能力。
为什么 ASCII 中小写字母没有紧跟在大写字母后面?
本文解释了 ASCII 表的设计,特别是为什么大写和小写字母之间存在间隔,并说明了这种结构如何通过位运算实现高效的字母大小写转换。