Der Standardmodus und seine Grenzen
Standardmäßig nutzt SQLite ein Rollback Journal. Bei jedem Schreibvorgang kopiert SQLite die ursprünglichen Pages in eine -journal-Datei, ändert dann die eigentliche Datenbank und löscht das Journal beim Commit. Stürzt der Prozess mitten im Schreiben ab, wird das Journal rückwärts abgespielt, um die unvollständige Änderung rückgängig zu machen.
Das Verfahren ist simpel und sicher – hat aber einen unschönen Haken: Schreiber und Leser konkurrieren um dieselbe Datei. Solange ein Schreiber den Datenbank-Lock hält, kann kein Leser eine neue Transaktion starten. Sind umgekehrt Leser aktiv, muss der Schreiber warten. In einer ausgelasteten Anwendung – etwa einem Webserver mit ein paar parallelen Requests – tauchen SQLITE_BUSY-Fehler schneller auf, als einem lieb ist.
Genau hier kommt der WAL-Modus ins Spiel.
Was der SQLite WAL-Modus tatsächlich macht
Write-Ahead Logging dreht das Prinzip um. Statt die Hauptdatenbankdatei direkt zu verändern, hängt der Schreiber committete Pages an eine separate Datei mit der Endung -wal an. Leser greifen weiterhin auf die Hauptdatei zu, schauen aber zusätzlich ins WAL, um neuere Versionen der benötigten Pages mitzunehmen.
Das Ergebnis: Ein Schreiber und beliebig viele Leser können gleichzeitig arbeiten. Jeder Leser sieht einen konsistenten Snapshot vom Beginn seiner Transaktion, während der Schreiber ungestört ans WAL anhängt, ohne den aktiven Lesern in die Quere zu kommen.
Mit diesem einen Pragma stellst du die Datenbank um. Die Einstellung ist persistent – sie wird im File-Header der Datenbank gespeichert, sodass jede neue Verbindung automatisch im WAL-Modus läuft. Du musst das also nicht bei jeder Verbindung neu ausführen, sondern nur einmal beim Anlegen der Datenbank (oder im Migrations-Skript).
Das Pragma gibt den neuen Modus zurück. Kommt wal zurück, ist alles in Butter. Kommt etwas anderes raus, unterstützt das Dateisystem vermutlich keinen Shared Memory (dazu gleich mehr).
SQLite WAL aktivieren und überprüfen
Den aktuellen Modus kannst du jederzeit abfragen:
Der erste Aufruf aktiviert den WAL-Modus und liefert den neuen Modus zurück. Der zweite Aufruf (ohne =) fragt ihn lediglich ab. Sobald Aktivität herrscht, liegen im Verzeichnis von messages.db drei Dateien: messages.db, messages.db-wal und messages.db-shm. Die beiden letzten tauchen auf und verschwinden wieder, je nachdem, ob gerade Verbindungen offen sind.
Die Dateien -wal und -shm
Mit dem WAL-Modus kommen zwei zusätzliche Dateien ins Spiel, und du solltest wissen, wofür sie da sind:
-walenthält committete Transaktionen, die noch nicht in die eigentliche Datenbank zurückgeschrieben wurden. Die Datei wächst mit jedem Schreibvorgang und schrumpft (oder wird zurückgesetzt) beim Checkpoint.-shmist eine Shared-Memory-Datei. Sie dient als Index in das WAL, damit sich alle Verbindungen darauf einigen können, wo welche Pages liegen, ohne bei jeder Query das WAL komplett zu scannen.
Die praktische Konsequenz daraus: Eine SQLite-Datenbank im WAL-Modus darfst du niemals kopieren, indem du nur die .db-Datei mitnimmst. Die aktuellsten Daten stecken im -wal, und ohne diese Datei ist deine Kopie veraltet oder kaputt. Kopiere entweder alle drei Dateien, während keine Verbindung schreibt, oder — deutlich besser — nutze die SQLite Backup-API (Thema des nächsten Kapitels).
Nebenläufigkeit: ein Writer, viele Reader
WAL bringt dir keine parallelen Schreibzugriffe. SQLite serialisiert Writes weiterhin: Zu jedem Zeitpunkt hält genau eine Transaktion den Write-Lock. Geändert hat sich aber Folgendes — Schreibvorgänge blockieren keine Leser mehr, und Leser blockieren keine Schreibvorgänge.
Eine typische Web-App im WAL-Modus verhält sich also so:
- Leselastige Endpoints laufen parallel, ohne sich gegenseitig auszubremsen.
- Schreibende Endpoints reihen sich kurz hintereinander ein, blockieren aber keine Reads.
- Langlaufende Reader (Analytics-Queries, Exporte) zwingen Writer nicht zum Warten.
Versuchen zwei Verbindungen gleichzeitig zu schreiben, bekommt die zweite ein SQLITE_BUSY. Die Lösung ist meistens ein vernünftiger Busy-Timeout — du sagst SQLite einfach, dass es kurz warten soll, bevor es aufgibt:
busy_timeout=5000 heißt sinngemäß: „Falls gerade eine Sperre aktiv ist, warte bis zu 5 Sekunden, bevor ein Fehler ausgelöst wird." Zusammen mit dem WAL-Modus deckt das die Konkurrenzsituationen ab, mit denen reale Anwendungen tatsächlich zu tun haben. Die Variante BEGIN IMMEDIATE holt sich die Schreibsperre bereits beim Start der Transaktion und nicht erst beim ersten Schreibzugriff – das verhindert eine ganze Klasse von Upgrade-Deadlocks, wenn mehrere Verbindungen gleichzeitig schreiben wollen.
Checkpoints: Das WAL zurückführen
Die WAL-Datei darf nicht unbegrenzt wachsen. Beim Checkpointing werden die committeten Seiten aus der WAL in die eigentliche Datenbankdatei übertragen und die WAL anschließend zurückgesetzt.
SQLite führt automatisch einen Checkpoint aus, sobald die WAL etwa 1000 Seiten überschreitet (Standardwert von wal_autocheckpoint). In den meisten Anwendungen kannst du das so lassen. Wenn du den Wert anpassen oder einen Checkpoint manuell auslösen möchtest:
Das wal_checkpoint-Pragma erwartet einen Modus:
PASSIVE— führt den Checkpoint so weit wie möglich durch, ohne Leser oder Schreiber zu stören. Das ist der Standardwert.FULL— wartet, bis aktive Schreibvorgänge abgeschlossen sind, und schreibt dann alles Committete in die Datenbank.RESTART— wie FULL, blockiert zusätzlich neue Leser daran, das alte WAL noch zu verwenden.TRUNCATE— wie RESTART, kürzt anschließend die WAL-Datei wieder auf null Bytes.
Auf Servern muss man das in der Regel nie von Hand aufrufen. Wenn du allerdings eine Desktop-Anwendung ausspielst und beim Beenden saubere Dateigrößen haben willst, ist ein TRUNCATE-Checkpoint vor dem Schließen der letzten Verbindung eine vernünftige Angewohnheit.
Pragmas, die gut mit dem WAL-Modus zusammenspielen
WAL allein ist schon eine gute Sache. WAL kombiniert mit ein paar weiteren Einstellungen ist allerdings das, was Produktiv-Apps üblicherweise einsetzen:
Eine kurze Übersicht:
synchronous=NORMAList die empfohlene Kombination mit dem WAL-Modus. Sie ist sicher gegen Anwendungs- und Betriebssystemabstürze; nur ein Stromausfall im allerschlechtesten Moment kann die letzten Transaktionen verlieren — selbst dann bleibt die Datenbank aber konsistent. Die VoreinstellungFULList sicherer, dafür spürbar langsamer.busy_timeouthaben wir oben bereits besprochen.foreign_keys=ONhat nichts mit WAL zu tun, lohnt sich aber auf jeder Verbindung — SQLite lässt die Foreign-Key-Prüfung aus Gründen der Abwärtskompatibilität standardmäßig deaktiviert.
Diese Einstellungen gelten pro Verbindung (außer journal_mode, das bleibt persistent). Setze sie in deinem Anwendungscode direkt nach dem Öffnen der Verbindung.
Wann der WAL-Modus nicht die richtige Wahl ist
WAL ist die Standardempfehlung, aber in ein paar Situationen sprechen gute Gründe dagegen:
- Netzwerk-Dateisysteme. Write-Ahead Logging in SQLite setzt auf Shared Memory (
mmap) zwischen den Prozessen, die auf die Datenbank zugreifen. NFS, SMB und Co. unterstützen das nicht zuverlässig. Liegt deine Datenbank auf einer Netzwerkfreigabe, bleib beim Rollback-Journal — oder, noch besser: leg SQLite gar nicht erst auf eine Netzwerkfreigabe. - Schreibgeschützte Medien. WAL muss die Dateien
-walund-shmschreiben können. Eine Datenbank auf einer CD-ROM oder einem ähnlichen Medium braucht einen Journal-Modus ohne Schreibzugriff (oder muss read-only mitmode=rogeöffnet werden). - Batch-Jobs mit nur einem Schreiber und ohne parallele Leser. WAL schadet nicht, bringt dir hier aber auch keinen Vorteil. Das voreingestellte Rollback-Journal reicht völlig.
Für 95 % aller Anwendungen — Web-Backends, Desktop-Apps, Mobile-Apps, Embedded-Geräte mit lokalem Speicher — ist WAL die richtige Entscheidung.
Ein realistisches Setup
So sehen die meisten produktiven SQLite-Setups aus, zusammengefasst als ausführbare Pragmas:
temp_store=MEMORY hält temporäre Tabellen und Indizes im Arbeitsspeicher statt auf der Festplatte – ein kleiner Bonus, der dich nichts kostet, wenn du genug RAM übrig hast.
Setz das einmal beim Verbindungsaufbau im Datenbank-Setup deiner Anwendung, und schon hast du den Großteil dessen abgedeckt, was eine SQLite-gestützte App braucht, um unter parallelen Schreibzugriffen sauber zu laufen.
Als Nächstes: Backup und Restore
Da deine Datenbank jetzt von -wal- und -shm-Dateien begleitet wird, reicht ein simples Kopieren der Datei nicht mehr als sichere Backup-Strategie aus. Im nächsten Kapitel sehen wir uns an, wie man eine laufende SQLite-Datenbank korrekt sichert – mit dem Befehl .backup, der Online-Backup-API und den passenden Mitteln, um einen konsistenten Snapshot zu ziehen, ohne die App vom Netz nehmen zu müssen.
Häufig gestellte Fragen
Was ist der WAL-Modus in SQLite?
WAL steht für Write-Ahead Logging. Statt Änderungen direkt in die Hauptdatenbankdatei zu schreiben und ein Rollback-Journal für den Fehlerfall zu führen, hängt SQLite die Änderungen an eine separate -wal-Datei an und führt sie regelmäßig wieder zusammen. Der große Gewinn: echte Nebenläufigkeit. Leser und ein Schreiber können gleichzeitig arbeiten, ohne sich zu blockieren.
Wie aktiviere ich den WAL-Modus in SQLite?
Einmal PRAGMA journal_mode=WAL; ausführen – das war's. Die Einstellung ist persistent, sie wird im Header der Datenbankdatei gespeichert, sodass auch alle künftigen Verbindungen automatisch im WAL-Modus laufen. Du musst das also nicht bei jeder Connection neu setzen. Bei Erfolg liefert das PRAGMA den neuen Modus (wal) zurück.
Erlaubt der WAL-Modus parallele Schreibvorgänge?
Nein – Schreibvorgänge werden in SQLite weiterhin serialisiert. Es kann immer nur ein Writer gleichzeitig den Schreib-Lock halten. Was WAL aber sehr wohl ändert: Leser blockieren den Schreiber nicht mehr, und der Schreiber blockiert die Leser nicht mehr. Genau das war für die meisten Anwendungen der eigentliche Flaschenhals.
Was haben es mit den -wal- und -shm-Dateien auf sich?
Die -wal-Datei enthält bereits committete Änderungen, die noch nicht in die Hauptdatenbank zurückgeschrieben wurden. Die -shm-Datei ist ein kleiner Shared-Memory-Index, mit dem Verbindungen schnell die passenden Pages innerhalb des WAL finden. Beide Dateien werden automatisch wieder erzeugt – aber: wenn du eine Datenbank kopierst, musst du sie zusammen mitkopieren oder gleich die Backup-API verwenden.