Unicode-Äquivalenz
Unicode ist ein kompliziertes Tier. Eine seiner zahlreichen Besonderheiten ist, dass verschiedene Folgen von Codepunkten gleich sein können. Dies ist bei Legacy-Codierungen nicht der Fall. In LATIN1 zum Beispiel ist das einzige, was gleich „a“ ist, „a“, und das einzige, was gleich „ä“ ist, ist „ä“. In Unicode können Zeichen mit diakritischen Zeichen jedoch häufig (abhängig vom jeweiligen Zeichen) auf unterschiedliche Weise codiert werden:entweder als zusammengesetztes Zeichen, wie es in älteren Codierungen wie LATIN1 der Fall war, oder zerlegt, bestehend aus dem Basiszeichen 'a ' gefolgt von dem diakritischen Zeichen ◌̈ hier. Dies wird als kanonische Äquivalenz bezeichnet . Der Vorteil dieser beiden Optionen ist, dass Sie einerseits Zeichen aus alten Kodierungen einfach konvertieren können und andererseits nicht jede Akzentkombination als separates Zeichen zu Unicode hinzufügen müssen. Aber dieses Schema erschwert die Sache für Software, die Unicode verwendet.
Solange Sie sich nur das resultierende Zeichen ansehen, z. B. in einem Browser, sollten Sie keinen Unterschied feststellen, und dies ist Ihnen egal. In einem Datenbanksystem, in dem das Suchen und Sortieren von Zeichenfolgen eine grundlegende und leistungskritische Funktionalität ist, können die Dinge jedoch kompliziert werden.
Erstens muss sich die verwendete Kollatierungsbibliothek dessen bewusst sein. Die meisten System-C-Bibliotheken, einschließlich glibc, sind dies jedoch nicht. Wenn Sie also in glibc nach „ä“ suchen, werden Sie „ä“ nicht finden. Siehst du was ich dort gemacht habe? Die zweite ist anders codiert, sieht aber beim Lesen wahrscheinlich gleich aus. (Zumindest habe ich es so eingegeben. Es könnte irgendwo auf dem Weg zu Ihrem Browser geändert worden sein.) Verwirrend. Wenn Sie ICU für Sortierungen verwenden, funktioniert dies und wird vollständig unterstützt.
Zweitens, wenn PostgreSQL Zeichenfolgen auf Gleichheit vergleicht, vergleicht es nur die Bytes und berücksichtigt nicht die Möglichkeit, dass dieselbe Zeichenfolge auf unterschiedliche Weise dargestellt werden kann. Dies ist bei der Verwendung von Unicode technisch falsch, aber eine notwendige Leistungsoptimierung. Um dies zu umgehen, können Sie nichtdeterministische Sortierungen verwenden , eine in PostgreSQL 12 eingeführte Funktion. Eine auf diese Weise deklarierte Sortierung wird nicht vergleiche einfach die Bytes
führt jedoch alle erforderlichen Vorverarbeitungen durch, um Zeichenfolgen vergleichen oder hashen zu können, die möglicherweise auf unterschiedliche Weise codiert sind. Beispiel:
CREATE COLLATION ndcoll (provider = icu, locale = 'und', deterministic = false);
Normalisierungsformen
Obwohl es verschiedene gültige Möglichkeiten gibt, bestimmte Unicode-Zeichen zu codieren, ist es manchmal nützlich, sie alle in eine konsistente Form zu konvertieren. Dies wird als Normalisierung bezeichnet . Es gibt zwei Normalisierungsformen :vollständig komponiert , was bedeutet, dass wir alle Codepunktsequenzen so weit wie möglich in vorkomponierte Zeichen umwandeln und vollständig zerlegen , was bedeutet, dass wir alle Codepunkte so weit wie möglich in ihre Bestandteile (Buchstabe plus Akzent) umwandeln. In der Unicode-Terminologie sind diese Formen als NFC bzw. NFD bekannt. Dazu gibt es noch einige weitere Details, wie z. B. das Einfügen aller kombinierenden Zeichen in eine kanonische Reihenfolge, aber das ist die allgemeine Idee. Der Punkt ist, wenn Sie eine Unicode-Zeichenfolge in eine der Normalisierungsformen konvertieren, können Sie sie byteweise vergleichen oder hashen, ohne sich um Codierungsvarianten kümmern zu müssen. Welche Sie verwenden, spielt keine Rolle, solange sich das gesamte System auf eine einigt.
In der Praxis verwendet der größte Teil der Welt NFC. Darüber hinaus sind viele Systeme insofern fehlerhaft, als sie Nicht-NFC-Unicode nicht korrekt verarbeiten, einschließlich der Kollatierungsfunktionen der meisten C-Bibliotheken und sogar PostgreSQL standardmäßig, wie oben erwähnt. Daher ist es eine gute Möglichkeit, eine bessere Interoperabilität zu gewährleisten, indem sichergestellt wird, dass Unicode vollständig in NFC konvertiert wird.
Normalisierung in PostgreSQL
PostgreSQL 13 enthält jetzt zwei neue Funktionen zur Handhabung der Unicode-Normalisierung:eine Funktion zum Testen der Normalisierung und eine zum Konvertieren in eine Normalisierungsform. Zum Beispiel:
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
(Die Syntax ist im SQL-Standard festgelegt.)
Eine Möglichkeit besteht darin, dies in einer Domäne zu verwenden, zum Beispiel:
CREATE DOMAIN norm_text AS text CHECK (VALUE IS NORMALIZED);
Beachten Sie, dass die Normalisierung von beliebigem Text nicht ganz billig ist. Wenden Sie dies also sinnvoll und nur dort an, wo es wirklich darauf ankommt.
Beachten Sie auch, dass die Normalisierung nicht unter der Verkettung geschlossen ist. Das bedeutet, dass das Anhängen zweier normalisierter Strings nicht immer zu einem normalisierten String führt. Also selbst wenn Sie diese Funktionen sorgfältig anwenden und auch sonst prüfen, ob Ihr System nur normalisierte Strings verwendet, können sie sich bei legitimen Operationen immer noch "einschleichen". Einfach anzunehmen, dass nicht normalisierte Zeichenfolgen nicht vorkommen können, schlägt fehl; Dieses Problem muss richtig behandelt werden.
Kompatibilitätszeichen
Es gibt einen weiteren Anwendungsfall für die Normalisierung. Unicode enthält einige alternative Formen von Buchstaben und anderen Zeichen für verschiedene Legacy- und Kompatibilitätszwecke. Sie können beispielsweise Fraktur schreiben:
SELECT '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢';
Stellen Sie sich nun vor, Ihre Anwendung weist Benutzernamen oder ähnliche Kennungen zu, und es gibt einen Benutzer namens 'somename'
und ein weiteres namens '𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢'
. Dies wäre zumindest verwirrend, aber möglicherweise ein Sicherheitsrisiko. Die Ausnutzung solcher Ähnlichkeiten wird häufig bei Phishing-Angriffen, gefälschten URLs und ähnlichen Bedenken verwendet. Unicode enthält also zwei zusätzliche Normalisierungsformen, die diese Ähnlichkeiten auflösen und solche alternativen Formen in einen kanonischen Basisbuchstaben umwandeln. Diese Formen werden NFKC und NFKD genannt. Sie sind ansonsten die gleichen wie NFC bzw. NFD. Zum Beispiel:
=> select normalize('𝔰𝔬𝔪𝔢𝔫𝔞𝔪𝔢', nfkc); normalize ----------- somename
Auch hier kann die Verwendung von Check Constraints vielleicht als Teil einer Domäne nützlich sein:
CREATE DOMAIN username AS text CHECK (VALUE IS NFKC NORMALIZED OR VALUE IS NFKD NORMALIZED);
(Die eigentliche Normalisierung sollte wahrscheinlich im Frontend der Benutzeroberfläche erfolgen.)
Siehe auch RFC 3454 für eine Behandlung von Zeichenfolgen, um solche Bedenken auszuräumen.
Zusammenfassung
Probleme mit der Unicode-Äquivalenz werden oft ohne Konsequenzen ignoriert. In vielen Zusammenhängen liegen die meisten Daten in NFC-Form vor, sodass keine Probleme auftreten. Das Ignorieren dieser Probleme kann jedoch zu seltsamem Verhalten, scheinbar fehlenden Daten und in einigen Situationen zu Sicherheitsrisiken führen. Daher ist es für Datenbankdesigner wichtig, sich dieser Probleme bewusst zu sein, und die in diesem Artikel beschriebenen Tools können verwendet werden, um damit umzugehen.