Unicode-ekvivalens
Unicode är en komplicerad best. En av dess många säregna egenskaper är att olika sekvenser av kodpunkter kan vara lika. Detta är inte fallet i äldre kodningar. I LATIN1, till exempel, är det enda som är lika med 'a' 'a' och det enda som är lika med 'ä' är 'ä'. I Unicode kan tecken med diakritiska tecken ofta (beroende på det speciella tecknet) kodas på olika sätt:antingen som ett förkomponerat tecken, som gjordes i äldre kodningar som LATIN1, eller dekomponerade, bestående av bastecknet 'a ' följt av det diakritiska tecknet ◌̈ här. Detta kallas kanonisk ekvivalens . Fördelen med att ha båda dessa alternativ är att du å ena sidan enkelt kan konvertera tecken från äldre kodningar och å andra sidan inte behöver lägga till varje accentkombination till Unicode som ett separat tecken. Men detta schema gör det svårare för programvara som använder Unicode.
Så länge du bara tittar på den resulterande karaktären, till exempel i en webbläsare, bör du inte märka någon skillnad och detta spelar ingen roll för dig. Men i ett databassystem där sökning och sortering av strängar är grundläggande och prestandakritisk funktionalitet kan saker och ting bli komplicerade.
Först måste sorteringsbiblioteket som används vara medvetet om detta. Men de flesta system C-bibliotek inklusive glibc är det inte. Så i glibc, när du letar efter 'ä', hittar du inte 'ä'. Ser du vad jag gjorde där? Den andra är kodad annorlunda men ser förmodligen likadan ut för dig som läser. (Åtminstone var det så jag skrev in det. Det kan ha ändrats någonstans längs vägen till din webbläsare.) Förvirrande. Om du använder ICU för sammanställningar, fungerar detta och stöds fullt ut.
För det andra, när PostgreSQL jämför strängar för jämlikhet, jämför den bara byten, den tar inte hänsyn till möjligheten att samma sträng kan representeras på olika sätt. Detta är tekniskt fel när du använder Unicode, men det är en nödvändig prestandaoptimering. För att komma runt det kan du använda icke-deterministiska sammanställningar , en funktion som introduceras i PostgreSQL 12. En sammanställning som deklareras på det sättet kommer inte jämför bara byte
men kommer att göra all nödvändig förbearbetning för att kunna jämföra eller hasha strängar som kan vara kodade på olika sätt. Exempel:
CREATE COLLATION ndcoll (provider = icu, locale = 'und', deterministic = false);
Normaliseringsformulär
Så även om det finns olika giltiga sätt att koda vissa Unicode-tecken, är det ibland användbart att konvertera dem alla till en konsekvent form. Detta kallas normalisering . Det finns två normaliseringsformer :fullständigt sammansatt , vilket innebär att vi konverterar alla kodpunktssekvenser till förkomponerade tecken så mycket som möjligt och helt nedbrutna , vilket betyder att vi konverterar alla kodpunkter till deras beståndsdelar (bokstav plus accent) så mycket som möjligt. I Unicode-terminologi är dessa former kända som NFC respektive NFD. Det finns några fler detaljer i detta, som att sätta alla kombinerande karaktärer i en kanonisk ordning, men det är den allmänna idén. Poängen är att när du konverterar en Unicode-sträng till en av normaliseringsformerna kan du jämföra eller hasha dem bytevis utan att behöva oroa dig för kodningsvarianter. Vilken du använder spelar ingen roll, så länge hela systemet är överens om en.
I praktiken använder större delen av världen NFC. Och dessutom är många system felaktiga genom att de inte hanterar icke-NFC Unicode korrekt, inklusive de flesta C-biblioteks sorteringsfaciliteter, och till och med PostgreSQL som standard, som nämnts ovan. Så att se till att all Unicode konverteras till NFC är ett bra sätt att säkerställa bättre interoperabilitet.
Normalisering i PostgreSQL
PostgreSQL 13 innehåller nu två nya faciliteter för att hantera Unicode-normalisering:en funktion för att testa för normalisering och en för att konvertera till en normaliseringsform. Till exempel:
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
(Syntaxen anges i SQL-standarden.)
Ett alternativ är att använda detta i en domän, till exempel:
CREATE DOMAIN norm_text AS text CHECK (VALUE IS NORMALIZED);
Observera att normalisering av godtycklig text inte är helt billig. Så tillämpa detta förnuftigt och bara där det verkligen betyder något.
Observera också att normaliseringen inte stängs under sammanlänkning. Det betyder att två normaliserade strängar inte alltid resulterar i en normaliserad sträng. Så även om du noggrant tillämpar dessa funktioner och även i övrigt kontrollerar att ditt system bara använder normaliserade strängar, kan de fortfarande "krypa in" under legitima operationer. Så att bara anta att icke-normaliserade strängar inte kan hända kommer att misslyckas; det här problemet måste hanteras korrekt.
Kompatibilitetstecken
Det finns ett annat användningsfall för normalisering. Unicode innehåller några alternativa former av bokstäver och andra tecken, för olika äldre och kompatibilitetsändamål. Du kan till exempel skriva Fraktur:
SELECT '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢';
Föreställ dig nu att din applikation tilldelar användarnamn eller andra sådana identifierare, och det finns en användare som heter 'somename'
och en annan som heter '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢'
. Detta skulle åtminstone vara förvirrande, men möjligen en säkerhetsrisk. Att utnyttja sådana likheter används ofta i nätfiskeattacker, falska webbadresser och liknande problem. Så Unicode innehåller ytterligare två normaliseringsformer som löser dessa likheter och omvandlar sådana alternativa former till en kanonisk basbokstav. Dessa former kallas NFKC och NFKD. De är annars samma som NFC respektive NFD. Till exempel:
=> select normalize('𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢', nfkc); normalize ----------- somename
Återigen kan det vara användbart att använda kontrollbegränsningar som en del av en domän:
CREATE DOMAIN username AS text CHECK (VALUE IS NFKC NORMALIZED OR VALUE IS NFKD NORMALIZED);
(Själva normaliseringen bör förmodligen göras i användargränssnittets frontend.)
Se även RFC 3454 för en behandling av strängar för att hantera sådana problem.
Sammanfattning
Unicode-ekvivalensproblem ignoreras ofta utan konsekvens. I många sammanhang är de flesta data i NFC-form, så inga problem uppstår. Men att ignorera dessa problem kan leda till konstigt beteende, uppenbarligen saknad data och i vissa situationer säkerhetsrisker. Så medvetenhet om dessa frågor är viktigt för databasdesigners, och verktygen som beskrivs i den här artikeln kan användas för att hantera dem.