Der Fehler und was er dir wirklich sagt
Du öffnest eine Datenbank, und der Client bleibt schlagartig stehen:
Error: database disk image is malformed
Dieselbe Situation zeigt sich als SQLITE_CORRUPT in der C-API, als SqliteException: SQLite Error 11 in Wrappern und als schlichtes „malformed“ in der sqlite3-Shell. Wie auch immer die Formulierung lautet, die Engine ist einem Zeiger innerhalb der Datei gefolgt und auf etwas gelandet, das keine gültige Seite sein kann, also hält sie an, statt dir Bytes zu geben, für die sie nicht bürgen kann.
Diese Strenge ist der Grund, warum die Datei oft schlimmer aussieht, als sie ist: Eine einzige beschädigte Seite kann eine ganze Tabelle unlesbar machen, während jede andere Seite unversehrt daliegt. Die Aufgabe der Wiederherstellung ist es, die Seiten direkt zu lesen und zu retten, was überlebt hat — und das beginnt damit, zu wissen, was eine SQLite-Datei ist.
Was „malformed“ für eine B-Tree-Datenbank bedeutet
Eine SQLite-Datenbank ist eine Datei, unterteilt in Seiten fester Größe (üblicherweise je 4096 Bytes). Die erste Seite enthält den Header und das Schema. Jede Tabelle und jeder Index ist ein B-Tree: eine verzweigte Struktur aus Seiten, die auf andere Seiten zeigen und in Blattseiten enden, welche die eigentlichen Zeilen enthalten. Eine Zeile lebt innerhalb einer Seite als Zelleund trägt ihre eigene Länge, ihre Spaltentypen und ihre Werte.
Eine einzige fehlerhafte Seite in einem B-Tree genügt, um eine Tabelle über Abfragen unlesbar zu machen, während die umliegenden Seiten weiterhin einwandfrei dekodierbar bleiben.
„Malformed“ sagt SQLite, wenn die Zeiger aufhören, mit den Daten übereinzustimmen: Eine Zweigseite verweist auf ein Kind, das keine gültige B-Tree-Seite ist, eine Zelle beansprucht eine Payload, die länger ist, als die Seite fassen kann, die Freelist dreht sich in einer Schleife, oder das Typ-Byte einer Seite bedeutet nichts. SQLite behandelt jeden einzelnen dieser Fälle als Grund, die ganze Datei für beschädigt zu erklären, obwohl der Schaden meist lokal begrenzt ist: Die fehlerhafte Seite und alles, was ihr in genau diesem B-Tree nachgelagert ist, sind zweifelhaft, während der Rest der Datei ganz gewöhnliche, dekodierbare Daten sind.
Wie SQLite-Dateien tatsächlich beschädigt werden
SQLite ist robust, und die meiste Korruption in der Praxis geht auf eine Handvoll Mechanismen zurück, nicht auf eine unzuverlässige Engine. Welcher davon dich trifft, bestimmt, was du zurückerwarten kannst.
Stromausfall oder Absturz mitten im Schreiben
Ein Schreibvorgang, der noch lief, als die Maschine den Strom verlor, der Prozess beendet oder der Container abgebaut wurde, kann eine Seite halb aktualisiert zurücklassen. Der Rollback-Journal- und der WAL-Modus sind darauf ausgelegt, das zu überstehen, aber nur, wenn auch das Journal beziehungsweise das Log und seine fsync-Garantien überlebt haben. Auf Speicher, der über das Flushen lügt, ist dieses Sicherheitsnetz weg, und du bekommst eine zerrissene Seite.
Eine fehlende oder nicht passende -wal-Datei
Im WAL-Modus können die neuesten committeten Daten in der -wal-Begleitdatei liegen und noch nicht in die Hauptdatenbank zurückgeführt sein. Kopierst du die .sqlite allein, lässt du diese Commits stranden; kombinierst du sie mit einer -wal aus einem anderen Zeitpunkt, wird es schlimmer. So oder so liest es sich als Korruption, weil die beiden Hälften nicht mehr dieselbe Datenbank beschreiben.
Eine laufende Datenbank ohne ihr WAL kopieren
Eine Datenbank einfach zu kopieren, während ein anderer Prozess aktiv in sie schreibt, erfasst einen inkonsistenten Schnappschuss: Die Kopie erwischt manche Seiten vor einer Transaktion und manche danach. Beim Kopieren sah sie gut aus und scheitert in dem Moment, in dem sie anderswo geöffnet wird. Nutze für laufende Datenbanken die Backup-API von SQLite oderVACUUM INTO statt einer rohen Kopie.
Zwei Schreiber, die es nicht geben dürfte
SQLite koordiniert den Zugriff über Datei-Locks. Leg die Datei auf eine Netzwerkfreigabe, auf der das Locking unzuverlässig ist, oder lass zwei Prozesse mit unterschiedlichen Locking-Annahmen gleichzeitig schreiben, und die Aktualisierungen verschränken sich zu einem Zustand, den kein einzelner Schreiber erzeugen würde. Vernetzte Dateisysteme sind der wiederkehrende Übeltäter, weshalb die Dokumentation von SQLite vor ihnen warnt.
Wie eine Rettung im Stil von .recover tatsächlich funktioniert
Der Reflex ist, zu PRAGMA integrity_check zu greifen. Das ist es wert, ausgeführt zu werden, aber sei dir klar darüber, was es tut: Esmeldet den Schaden, ohne etwas zu reparieren oder deine Zeilen herauszuholen. Dafür willst du den Ansatz, den der Befehl .recoverder sqlite3-Shell verfolgt, und er funktioniert ganz anders als eine normale Abfrage.
Statt den B-Tree-Zeigern zu vertrauen, durchläuft die Rettung die DateiSeite für Seite. Für jede Seite, die wie ein Tabellenblatt aussieht, liest sie jede Zelle direkt und dekodiert die Zeile: rowid, die seriellen Spaltentypen und die Werte. Jede dekodierbare Zeile wird in eine brandneue, leere Datenbank neu eingefügt, aufgebaut aus dem Schema, das sich rekonstruieren lässt. Beschädigte Seiten werden übersprungen; lesbare Seiten geben ihre Zeilen preis, ganz gleich, ob der Baum, der sie indexierte, überlebt hat oder nicht.
Zeilen, deren Ursprungstabelle sich nicht bestimmen lässt, werden nicht weggeworfen: Sie landen in einer Tabelle, die üblicherweise lost_and_foundheißt, geschlüsselt nach der Seite und der Zelle, aus der sie stammen, sodass du sie von Hand prüfen und neu einsortieren kannst. Das Ergebnis ist eine frische Datei, die sich sauber öffnet und jede Zeile enthält, die im Original dekodierbar war.
Das ist die ehrliche Obergrenze. Die Rettung stellt wieder her, was auf dem Datenträger überlebt hat; Zeilen auf der fehlerhaften Seite oder auf nachgelagerten Seiten, die nie geparst wurden, werden nicht rekonstruiert. Eine SQLite-Datei besitzt keine Redundanz, aus der sie sich regenerieren ließen, also ist eine Zeile, die nirgends dekodierbar ist, schlicht weg. Die Wiederherstellung baut eine saubere Datenbank rund um die Daten, die überlebt haben; sie erfindet nie Daten, die es nicht gab.
Soweit wir wissen, hat kein clientseitiges Browser-Tool dies zuvor angeboten. Eine Rettung dieser Art bedeutete bisher eine Kommandozeile und einen lokalen SQLite-Build, was sie für alle ausschloss, die die Datei nicht zuerst auf einen Server legen können — oder dürfen. Den Seitendurchlauf im Browser auszuführen schließt diese Lücke: dieselbe Technik, kein Upload.
Das wiederhergestellte Ergebnis ist immer fragwürdig
Eine gerettete Datenbank öffnet und lässt sich sauber abfragen, was dich verleitet, sie als maßgeblich zu behandeln. Widerstehe dem: Der Seitendurchlauf dekodiert rohe Bytes ohne die Constraints, die eine gesunde Datenbank erzwingt, deshalb bringt das Ergebnis Eigenheiten mit, die eine Prüfung wert sind:
- Auferstandene Löschungen. Eine von dir gelöschte Zeile lebt auf ihrer Seite weiter, bis dieser Platz wiederverwendet wird, und die Rettung kann eine lebende Zeile nicht von einer gelöschten, aber nicht überschriebenen unterscheiden, deshalb können gelöschte Datensätze wieder auftauchen.
- Typverschiebungen. Jeder Wert wird mit einem seriellen Typ gespeichert. Unter einem unerwarteten Typ dekodiert, kann ein Integer als Blob zurückkommen oder die Präzision einer Zahl sich verschieben. Prüfe stichprobenartig Spalten, deren Typen wichtig sind.
- Waisen in lost_and_found. Zeilen in
lost_and_foundkommen ohne ihre ursprüngliche Tabelle und ohne verlässliche Reihenfolge an; du brauchst das Schema und dein eigenes Wissen über die Daten, um sie an ihren Platz zu bringen. - Verletzte Invarianten. Fremdschlüssel, Unique-Constraints und Trigger wurden beim Neu-Einfügen umgangen, deshalb kann eine wiederhergestellte Datenbank Kombinationen enthalten, die das ursprüngliche Schema zurückgewiesen hätte.
Behandle eine wiederhergestellte Datenbank als hochwertigen Anhaltspunkt, nicht als Urteil: Prüfe sie gegen bekannt korrekte Datensätze, bevor irgendetwas Nachgelagertes von ihr abhängt.
Umgang mit einer begleitenden -wal-Datei
Bevor du schließt, dass die Datenbank selbst beschädigt ist, halte nach einer -wal-Datei daneben Ausschau (und nach ihrem Begleiter-shm). Im WAL-Modus können die jüngsten committeten Transaktionen noch in diesem Log liegen und noch nicht per Checkpoint in die Hauptdatei überführt sein. Stell ohne sie wieder her, und dir entgehen genau die neuesten Daten, die dir am wichtigsten sind.
Halte das Trio zusammen: die .sqlite, die -walund die -shm, aus demselben Moment kopiert. Von einem gesunden SQLite zusammen geöffnet, wird das Log angewendet, und die Datenbank erweist sich vielleicht doch als konsistent, ganz ohne Rettung. Ist sie wirklich beschädigt, kann eine Rettung, die auch das WAL liest, die dort gehaltenen committeten Seiten dekodieren. Das umgekehrte Risiko: Eine -walaus einer anderen Datenbank oder eine veraltete aus einer alten Sitzung erzeugt Korruption, sobald sie mit der falschen Datei kombiniert wird. Ist die Herkunft einer Begleitdatei ungewiss, rette die Datenbank für sich allein und vergleiche.
Häufige Fragen
Was bedeutet „database disk image is malformed“?
Es bedeutet, dass SQLite einem Zeiger innerhalb der Datei gefolgt ist und auf etwas gestoßen ist, das keine gültige Datenbankseite sein kann: eine B-Tree-Zelle, die über das Ende einer Seite hinaus zeigt, eine Seite, deren Typ-Byte Unsinn ist, eine Freelist, die sich in einer Schleife dreht, oder eine Zeile, deren Payload-Länge nicht zu ihrem Header passt. SQLite löst SQLITE_CORRUPT in dem Moment aus, in dem es der Struktur nicht mehr trauen kann. Es benennt ein Symptom, keine Ursache, und verrät dir nicht, wie viele Zeilen betroffen sind.
Lässt sich eine beschädigte SQLite-Datenbank wiederherstellen?
Meist ein großer Teil davon, ja. Jede Tabelle und jeder Index ist ein B-Tree, verteilt über Seiten fester Größe, und eine einzelne fehlerhafte Seite zerstört die anderen selten. Eine Rettung im Stil von .recover durchläuft die rohen Seiten, dekodiert jede Zelle, die sie parsen kann, und fügt diese Zeilen in eine frische Datenbank neu ein, dazu eine lost_and_found-Tabelle für Zeilen, deren Ursprungstabelle sich nicht bestimmen ließ.
Sollte ich die Wiederherstellung an der Originaldatei ausführen?
Niemals an deiner einzigen Kopie. Erstelle zuerst eine Byte-für-Byte-Kopie und rette aus dieser, damit ein fehlgeschlagener Versuch dich nichts kostet. Liegt eine -wal- oder -shm-Datei neben der Datenbank, kopiere auch diese, denn sie können die jüngsten committeten Änderungen enthalten. Die Rettung schreibt eine brandneue Ausgabedatei und lässt die Eingabe unberührt, aber eine Reservekopie ist trotzdem eine billige Absicherung.
Warum sehen wiederhergestellte Daten manchmal falsch aus?
Weil die Rettung genau die Bytes dekodiert, die physisch in jeder Zelle liegen, ohne die Prüfungen, die eine gesunde Datenbank erzwingt. Gelöschte Zeilen können wieder auftauchen, Werte können im falschen Typ zurückkommen, und verwaiste Zeilen landen ohne verlässliche Reihenfolge in lost_and_found. Das wiederhergestellte Ergebnis ist immer fragwürdig: Prüfe es gegen bekannt korrekte Datensätze, statt ihm blind zu vertrauen.
Weiterführende Lektüre: Ist es sicher, deine Dateien in ein Online-Reparatur-Tool hochzuladen?