Things To Avoid, Episode 1: INSERT IGNORE
Legacy Code strikes back
Auf undokumentierte Sprachfunktionen zu vertrauen ist ziemlich riskant. Und wenn es mit INSERT IGNORE gemischt wird, wird es richtig schlimm.
Bei einem der Betriebssystem-Upgrades Anfang 2016 wurde Perl von 5.14 auf 5.18 aktualisiert. Leider änderte sich die Handhabung von Hashes, die Reihenfolge der Schlüssel wurde in einem ganz bestimmten Szenario nicht mehr eingehalten und der Code ging kaputt. Du hast recht, Perl hat die Reihenfolge nie versprochen, aber unser Code hat sich auf einige Implementierungsspezifika verlassen.
Ein Auszug aus dem Perl Changelog für v5.18:
Standardmäßig können zwei unterschiedliche Hash-Variablen mit identischen Schlüsseln und Werten ihre Inhalte nun in einer anderen Reihenfolge bereitstellen, wo sie zuvor identisch waren.
Warum war das ein Problem? Der Code bereitete eine Liste von Hashes vor, die in die Datenbank eingefügt werden sollten. Dann wurde das erste Element verwendet, um die Liste der Spaltennamen vorzubereiten, die an nachfolgende INSERT-Abfragen übergeben werden. Bei jeder INSERT-Abfrage wurden jedoch unterschiedliche Hashes verwendet, um die Liste der einzufügenden Werte vorzubereiten. Dies führte zu folgendem Ergebnis:
INSERT IGNORE INTO foo (id, date) VALUES (1, ‚2006-10-11`), (‚2007-12-09‘, 2), …
Grundsätzlich wurde die Reihenfolge der Spaltennamen und der jeweiligen Werte nicht eingehalten. Natürlich gibt MySQL normalerweise einen Fehler aus, wenn man versucht, ein Datum in eine Integer-Spalte einzufügen. Aber Sie erinnern sich doch an das Zitat aus der MySQL-Dokumentation, oder? Diese Fehler werden nicht ausgelöst, wenn INSERT IGNORE verwendet wird und eine stille Datenkonvertierung stattfindet:
Daten konsistent inkonsistent
Das Skript lief nach dem Perl-Upgrade einwandfrei (nun, zumindest war sein stderr stumm), aber nach ein paar Tagen erhielten wir einen Fehlerbericht, dass mit unseren internen Tools, die Perl-gestützte Tabellen verwenden, etwas nicht stimmt. Dem Bericht nach zu urteilen, war irgendetwas ziemlich verdächtig. Und unsere Befürchtungen wurden durch eine schnelle SELECT-Abfrage bestätigt:
So, was haben wir hier. Zeitstempel und Integer-Werte, die in eine Zeichenkette umgewandelt und als Benutzernamen eingefügt werden, Jahre, die in Integer-Spalten gespeichert werden (ist 2016 die Anzahl der Bearbeitungen oder das Jahr der letzten Bearbeitung). Dateninkonsistenz vom Feinsten.
Sie müssen nicht zustimmen, aber für mich sind keine Daten besser als „Daten“ wie die Zeilen oben.
Die betroffene Tabelle wiegt über 75 GiB und speichert 460 mm Zeilen. Wir können sie neu generieren, aber das ist ein langer Prozess. Das Skript läuft gerade, während Sie diesen Artikel lesen. Nun, es läuft bereits seit fünf Wochen… Alles dank IGNORE in einer einzigen Abfrage.
Lessons learned
- INSERT IGNORE ist eine stille Bestie, die nur darauf wartet, dass Sie einen Fehler machen, um die Daten in eine bedeutungslose Reihe von Werten zu verwandeln.
- Ignorieren Sie niemals Fehler. Machen Sie sie laut. Fail early.
- Sich auf undokumentierte Sprachfunktionen zu verlassen (Perl und die Reihenfolge der Hash-Schlüssel ist nur eines von zahlreichen Beispielen) ist ein riskantes Spiel.
Sie wurden gewarnt. Überprüfen Sie jetzt Ihren Code oder ngrep Ihren Datenverkehr auf INSERT IGNORE-Abfragen.