
utf8 vs. utf8mb4
Heutiger Bug: Wir wollen einen UTF-8-String in einer MySQL „utf8“-kodierten Datenbank speichern, und PHP meldet sich mit einem bizarren Fehler:
Incorrect string value: ‘\xF0\x9F\x98\x83 <…’ for column ‘description’ at row 1
Auf einem UTF-8 Client, einem UTF-8 Server, mit einer UTF-8 Datenbank und einer UTF-8 Kollation. Der Text, “😃 <…”, ist valides UTF-8.
Und der Haken ist: MySQLs „utf8“ ist nicht UTF-8.
Die „utf8“-Kodierung unterstützt nur drei Bytes pro Zeichen. Die echte UTF-8-Codierung, die man sonst so verwendet verwendet, benötigt bis zu vier Bytes pro Zeichen.
Die MySQL-Entwickler haben diesen Fehler nie behoben. Sie haben 2010 einen Workaround veröffentlicht: einen neuen Zeichensatz namens „utf8mb4“.
Natürlich haben sie das nie groß bekannt gemacht (wahrscheinlich, weil der Bug so peinlich ist).Wenn man googelt findet man meist nur Tutorials mit der Verwendung von „utf8“. Naja. Klappt nicht.
Kurz gesagt:
MySQLs „utf8mb4“ bedeutet „UTF-8„.
MySQLs „utf8“ bedeutet „ich bin eine proprietäre Zeichenkodierung„. Und diese Kodierung kann nicht viele Unicode-Zeichen kodieren.
Ich möchte hier maL eine pauschale Aussage machen: Alle MySQL- und MariaDB-Anwender, die derzeit „utf8“ verwenden, sollten eigentlich „utf8mb4“ verwenden. Niemand sollte „utf8“ benutzen.
Was ist Encoding? Was ist UTF-8?
Joel on Software hier hierzu einen wunderschönen Artikel geschrieben.
Computer speichern Text als Einsen und Nullen. Der erste Buchstabe in diesem Absatz wurde als „010000011“ gespeichert und der Computer schreibt ein „C“. Hierzu gibt es zwei Steps:
- Der Computer liest „010000011“ und stellt fest, dass es sich um die Zahl 67 handelt. Das liegt daran, dass 67 als „010000011“ kodiert wurde.
- Der Computer hat die Zeichennummer 67 im Unicode-Zeichensatz nachgeschlagen und festgestellt, dass 67 „C“ bedeutet.
Dasselbe auch im Editor, als ich das „C“ eintippte:
- Mein Computer hat „C“ auf 67 im Unicode-Zeichensatz abgebildet.
- Mein Computer kodierte 67 und schickte „010000011“ an meinen Webserver.
Zeichensätze sind ein gelöstes Problem. Fast jedes Programm im Internet verwendet den Unicode-Zeichensatz, weil es keinen Anreiz gibt einen anderen zu verwenden.
Aber die Kodierung ist eher eine Ermessensentscheidung. Unicode hat Platz für über eine Million Zeichen. („C“ und „“💩“ sind zwei solcher Zeichen.) Die einfachste Kodierung, UTF-32, lässt jedes Zeichen 32 Bit annehmen. Das ist einfach, weil Computer Gruppen von 32 Bits seit langem als Zahlen behandeln, und sie sind wirklich gut darin. Aber es ist nicht nützlich: Es ist Platzverschwendung.
UTF-8 spart Platz. In UTF-8 nehmen gewöhnliche Zeichen wie „C“ 8 Bit, seltene Zeichen wie „“💩“ 32 Bit. Andere Zeichen nehmen 16 oder 24 Bit. Ein Blog-Post wie dieser nimmt in UTF-8 etwa viermal weniger Platz in Anspruch als in UTF-32. So lädt er viermal schneller.
Grob gesagt haben wir uns hinter den Kulissen auf UTF-8 geeinigt. Sonst würde man bei der Eingabe von „💩“ ein Durcheinander von Zufallsdaten sehen.
Der „utf8“-Zeichensatz von MySQL stimmt also nicht mit anderen Programmen überein. Wenn Sie „💩“ sagen, crasht es.
Ein wenig MySQL-Geschichte
Warum haben MySQL-Entwickler „utf8“ ungültig gemacht? Wir können es erraten, wenn wir uns die Commit-Logs ansehen.
MySQL unterstützt UTF-8 seit Version 4.1. Das war 2003 – vor dem heutigen UTF-8-Standard RFC 3629.
Der bisherige UTF-8-Standard RFC 2279 unterstützte bis zu sechs Bytes pro Zeichen. MySQL-Entwickler codierten RFC 2279 in der ersten Pre-Pre-Release Version von MySQL 4.1 am 28. März 2002.
Dann kam im September eine kryptische, ein Byte lange Anpassung an den MySQL-Quellcode: „UTF8 now works with up to 3 byte sequences only.“.
Warum?Keine Ahnung. Das Code-Repository von MySQL hat keine Namen in den alten Commits mehr, seitdem es ins Git übernommen wurde. (MySQL verwendete davor BitKeeper, wie der Linux-Kernel.) Es gibt auch nichts auf der Mailingliste vom September 2003, was die Änderung erklärt.
Schauen wir uns doch mal an, an was es liegen könnte:
Im Jahr 2002 gab MySQL den Benutzern einen Geschwindigkeitsschub, wenn sie garantieren konnten, dass jede Zeile in einer Tabelle die gleiche Anzahl von Bytes hat. Dazu würden Benutzer Textspalten als „CHAR“ deklarieren. Eine „CHAR“-Spalte hat immer die gleiche Anzahl von Zeichen. Wenn man zu wenige Zeichen eingibt, werden Leerzeichen am Ende hinzugefügt; wenn man zu viele Zeichen eingibt, werden die letzten abgeschnitten.
Als die MySQL-Entwickler das erste Mal UTF-8 ausprobierten, mit seinen sechs Bytes pro Zeichen, haben sie sich wahrscheinlich geweigert: eine CHAR(1)-Spalte würde sechs Bytes benötigen; eine CHAR(2)-Spalte würde 12 Bytes benötigen; und so weiter.
Ergo stellen wir eines klar: Das anfängliche Verhalten, das nie veröffentlicht wurde, war korrekt. Es war gut dokumentiert und weit verbreitet, und jeder, der UTF-8 verstand, würde zustimmen, dass es richtig war.
Aber offensichtlich war ein MySQL-Entwickler besorgt, dass ein oder zwei Benutzer zwei Dinge tun würden:
Man nimmt eine CHAR-Spalte. (Das CHAR-Format ist heutzutage ein Relikt. Damals war MySQL mit CHAR-Spalten schneller. Seit ~2005 ist das nicht mehr der Fall.
Man wählt die CHAR-Spalte als „utf8“ aus.
Meine Vermutung ist, dass MySQL-Entwickler ihre „utf8“-Kodierung gebrochen haben, um diesen Benutzern zu helfen: Benutzer, die beide
- versuchten, für Speicherplatz und Geschwindigkeit zu optimieren; und
- nicht in der Lage waren, für Geschwindigkeit und Speicherplatz zu optimieren.
Niemand hat gewonnen. Benutzer, die Geschwindigkeit und Platz wollten, hatten immer noch Unrecht, „utf8“ CHAR-Spalten zu verwenden, weil diese Spalten immer noch größer und langsamer waren, als sie sein mussten. Und Entwickler, die auf Korrektheit Wert legten, waren falsch, „utf8“ zu verwenden, da es „💩“ nicht speichern kann.
Als MySQL diesen ungültigen Zeichensatz veröffentlicht hatte, konnte es ihn niemand mehr reparieren: Das würde jeden Benutzer dazu zwingen, jede Datenbank neu zu erstellen. MySQL hat 2010 endlich UTF-8-Unterstützung veröffentlicht, mit einem anderen Namen: „utf8mb4“.
Warum es so frustrierend ist.
Offensichtlich war ich diese Woche frustriert. Mein Fehler war schwer zu finden, denn ich wurde durch den Namen „utf8“ getäuscht. Und ich bin nicht der Einzige – fast jeder Artikel, den ich im Internet gefunden habe, hat „utf8“ als auch UTF-8 angekündigt.
Der Name „utf8“ war immer ein Fehler. Es ist ein proprietärer Zeichensatz. Es schuf neue Probleme, und es löste nicht das Problem, das es zu lösen bedeutete.
6 Gedanken zu „utf8 vs. utf8mb4“
Sehr schön aufgearbeiteter Artikel. Erheiternd und erhellend! Vielen Dank!
Grandios!
Ich musste sehr lachen und bin sehr dankbar für diese nützliche Erhellung.
Danke für die ausführliche Recherche. Ein sehr schöner Kontrast zu den vielen schlampig rausgehauenen Artikeln die man normalerweise findet. Und tatsächlich die erste gute Erklärung für den ganzen Komplex.
Ich möchte dazu noch ergänzen, dass MySQL ohne Fehlermeldung den Input abschneidet, wenn er nicht zum Zeichensatz des Datenfeldes passt. Also hat man utf8 eingestellt und es soll ein 4 Byte Charakter abgespeichert werden, dann landet in der Datenbank alles vor diesem Charakter. Das führte in WordPress zu einer massiven Sicherheitslücke, weil Kommentare ab einem gewissen Zeichen abgeschnitten wurden und im nächsten Kommentar baute man dann wieder das Zeichen ein um HTML Code einzuschleusen, der eigentlich maskiert war. Siehe auch hier:
https://cedricvb.be/post/wordpress-stored-xss-vulnerability-4-1-2/
Das MySQL ohne Fehlermeldung die Daten modifiziert ist der eigentliche Skandal in meinen Augen. Das passiert übrigens auch, wenn man Latin Daten in ein utf8mb4 Feld schreibt. In reinem Latin hatte ich das Problem dagegen nie.
Toller Artikel! Die Bit-Folge für den Buchstaben „C“ hat übrigens eine 0 zu viel.
Danke für den Hinweis! Direkt korrigiert!