Еквивалентност на Unicode
Unicode е сложен звяр. Една от многобройните му особености е, че различните последователности от кодови точки могат да бъдат равни. Това не е така в наследените кодировки. В LATIN1, например, единственото нещо, което е равно на „a“, е „a“, а единственото нещо, което е равно на „ä“, е „ä“. В Unicode обаче символите с диакритични знаци често (в зависимост от конкретния знак) могат да бъдат кодирани по различни начини:или като предварително съставен знак, както беше направено в наследени кодировки като LATIN1, или разложени, състоящи се от основния знак 'a “ следван от диакритичен знак ◌̈ тук. Това се нарича канонична еквивалентност . Предимството на наличието и на двете опции е, че можете, от една страна, лесно да конвертирате знаци от наследени кодировки и, от друга страна, не е необходимо да добавяте всяка комбинация от акценти към Unicode като отделен знак. Но тази схема прави нещата по-трудни за софтуера, използващ Unicode.
Докато просто гледате получения знак, например в браузър, не трябва да забелязвате разлика и това няма значение за вас. Въпреки това, в система от база данни, където търсенето и сортирането на низове е основна и критична за производителността функционалност, нещата могат да се усложнят.
Първо, използваната библиотека за съпоставяне трябва да е наясно с това. Въпреки това повечето системни C библиотеки, включително glibc, не са. Така че в glibc, когато търсите „ä“, няма да намерите „ä“. Вижте какво направих там? Вторият е кодиран по различен начин, но вероятно изглежда същото за вас, че четете. (Поне аз го въведох така. Може да е променен някъде по пътя към вашия браузър.) Объркващо. Ако използвате ICU за съпоставяне, тогава това работи и се поддържа напълно.
Второ, когато PostgreSQL сравнява низовете за равенство, той просто сравнява байтовете, не взема предвид възможността един и същи низ да бъде представен по различни начини. Това е технически погрешно при използване на Unicode, но е необходима оптимизация на производителността. За да заобиколите това, можете да използвате недетерминистични съпоставяния , функция, въведена в PostgreSQL 12. Съпоставяне, декларирано по този начин, не просто сравнете байтовете
но ще извърши необходимата предварителна обработка, за да може да сравнява или хешира низове, които могат да бъдат кодирани по различни начини. Пример:
CREATE COLLATION ndcoll (provider = icu, locale = 'und', deterministic = false);
Формуляри за нормализиране
Така че, въпреки че има различни валидни начини за кодиране на определени символи в Unicode, понякога е полезно да ги преобразувате всички в последователна форма. Това се нарича нормализация . Има две формуляри за нормализиране :напълно съставен , което означава, че преобразуваме всички последователности от кодови точки в предварително съставени знаци, доколкото е възможно, и напълно разложени , което означава, че преобразуваме всички кодови точки в техните компоненти (буква плюс акцент) колкото е възможно повече. В терминологията на Unicode тези форми са известни като NFC и NFD, съответно. Има още някои подробности за това, като например поставянето на всички комбинирани знаци в каноничен ред, но това е общата идея. Въпросът е, че когато конвертирате Unicode низ в една от формите за нормализиране, тогава можете да ги сравнявате или хеширате по байтове, без да се притеснявате за вариантите на кодиране. Коя от тях използвате, няма значение, стига цялата система да е съгласна с едно.
На практика по-голямата част от света използва NFC. И освен това, много системи са дефектни, тъй като не обработват правилно Unicode, който не е NFC, включително средствата за съпоставяне на повечето C библиотеки и дори PostgreSQL по подразбиране, както беше споменато по-горе. Така че гарантирането, че целият Unicode е преобразуван в NFC, е добър начин да се гарантира по-добра оперативна съвместимост.
Нормализация в PostgreSQL
PostgreSQL 13 вече съдържа две нови средства за справяне с нормализирането на Unicode:функция за тестване за нормализиране и една за преобразуване във форма за нормализиране. Например:
SELECT 'foo' IS NFC NORMALIZED; SELECT 'foo' IS NFD NORMALIZED; SELECT 'foo' IS NORMALIZED; -- NFC is the default SELECT NORMALIZE('foo', NFC); SELECT NORMALIZE('foo', NFD); SELECT NORMALIZE('foo'); -- NFC is the default
(Синтаксисът е посочен в SQL стандарта.)
Една от опциите е да използвате това в домейн, например:
CREATE DOMAIN norm_text AS text CHECK (VALUE IS NORMALIZED);
Имайте предвид, че нормализирането на произволен текст не е съвсем евтино. Така че прилагайте това разумно и само там, където наистина има значение.
Имайте предвид също, че нормализирането не е затворено при конкатенация. Това означава, че добавянето на два нормализирани низа не винаги води до нормализиран низ. Така че, дори ако внимателно прилагате тези функции и също така по друг начин проверявате дали вашата система използва само нормализирани низове, те все още могат да се „вмъкнат“ по време на легитимни операции. Така че само ако приемем, че ненормализираните низове не могат да се случат, ще се провали; този проблем трябва да бъде решен правилно.
Символи за съвместимост
Има и друг случай на използване за нормализиране. Unicode съдържа някои алтернативни форми на букви и други знаци за различни наследени и съвместими цели. Например, можете да напишете Fraktur:
SELECT '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢';
Сега си представете, че приложението ви присвоява потребителски имена или други подобни идентификатори и има потребител с име 'somename'
и още един на име '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢'
. Това поне би било объркващо, но вероятно риск за сигурността. Използването на подобни прилики често се използва при фишинг атаки, фалшиви URL адреси и подобни проблеми. Така Unicode съдържа две допълнителни формуляри за нормализиране, които разрешават тези прилики и преобразуват такива алтернативни форми в канонична основна буква. Тези форми се наричат NFKC и NFKD. Иначе са същите като NFC и NFD, съответно. Например:
=> select normalize('𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢', nfkc); normalize ----------- somename
Отново, използването на ограничения за проверка може би като част от домейн може да бъде полезно:
CREATE DOMAIN username AS text CHECK (VALUE IS NFKC NORMALIZED OR VALUE IS NFKD NORMALIZED);
(Действителната нормализиране вероятно трябва да се извърши в интерфейса на потребителския интерфейс.)
Вижте също RFC 3454 за третиране на низове за справяне с подобни опасения.
Резюме
Проблемите с еквивалентността на Unicode често се игнорират без последствия. В много контексти повечето данни са под формата на NFC, така че не възникват проблеми. Игнорирането на тези проблеми обаче може да доведе до странно поведение, очевидно липсващи данни и в някои ситуации рискове за сигурността. Така че осъзнаването на тези проблеми е важно за дизайнерите на бази данни и инструментите, описани в тази статия, могат да се използват за справяне с тях.