Schemas ändern sich. Plane das von Anfang an ein.
Die erste Version deines Schemas ist nie die letzte. Spalten kommen dazu, Tabellen werden zerlegt, Indizes neu durchdacht. Die Frage ist nicht, ob sich dein Schema ändern wird – sondern ob die Änderung auf jedem Laptop, jedem Server und jedem Endgerät, das bereits eine ältere Kopie der Datenbank hat, sauber ankommt.
Genau dafür gibt es SQLite Migrationen: eine Folge kleiner, geordneter Skripte, die eine Datenbank von Version N auf Version N+1 heben. Führst du sie der Reihe nach aus, ist jede Datenbank wieder auf dem aktuellen Stand. Lässt du diese Disziplin schleifen, handelst du dir „läuft bei mir"-Bugs ein, an denen du dann einen ganzen Nachmittag sitzt.
SQLite gibt dir dafür genau ein eingebautes Werkzeug an die Hand: PRAGMA user_version. Das ist ein 32-Bit-Integer, den die Datenbank für dich speichert und den SQLite selbst nie anfasst. Was er bedeutet, legst du fest.
Eine frische Datenbank startet bei 0. Setze den Wert auf die Nummer der Migration, die du gerade angewendet hast. Beim Start liest du ihn aus, um zu wissen, wo du stehst.
Eine minimale Migrationsschleife
Das Konzept dahinter: Jede Migration ist ein nummeriertes SQL-Skript. Deine Anwendung liest die aktuelle user_version, führt der Reihe nach alle Skripte mit höherer Nummer aus und aktualisiert die user_version nach jedem einzelnen Schritt.
Hier ist Migration 1 — sie legt das initiale Schema an:
Zwei Dinge sind hier wichtig. Das Ganze ist in BEGIN; ... COMMIT; eingepackt und läuft damit atomar – wenn das CREATE TABLE fehlschlägt, wird auch user_version nicht hochgezählt, und du kannst den Fehler beheben und es erneut versuchen. Außerdem steht PRAGMA user_version = 1 als letztes Statement vor dem Commit, sodass die Version nur dann hochgezählt wird, wenn alles davor sauber durchgelaufen ist.
Angenommen, du musst jetzt eine Spalte created_at ergänzen. Das wäre dann Migration 2:
Eine Datenbank auf Version 0 führt beide Skripte aus. Eine Datenbank auf Version 1 nur das zweite. Auf Version 2 läuft gar nichts mehr. Die Reihenfolge ist der Vertrag.
Was SQLite ALTER TABLE kann – und was nicht
ALTER TABLE ist in SQLite bewusst sehr eingeschränkt gehalten. Unterstützt werden nur:
ADD COLUMN– fügt eine neue Spalte hinzu, optional mit Default-Wert.DROP COLUMN– entfernt eine Spalte (ab Version 3.35).RENAME COLUMN– benennt eine Spalte um (ab Version 3.25).RENAME TO– benennt die Tabelle selbst um.
Mehr ist nicht drin. Den Datentyp einer Spalte ändern, NOT NULL anpassen, eine CHECK-Bedingung umbauen oder einer bestehenden Spalte einen FOREIGN KEY verpassen – all das geht nicht direkt.
-- Nicht unterstützt:
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);
ALTER TABLE users ADD CONSTRAINT users_email_check CHECK (email LIKE '%@%');
Wenn SQLite eine Änderung nicht direkt unterstützt, lautet das offizielle Rezept: „Tabelle neu aufbauen." Etwas umständlicher, dafür aber bombensicher.
Tabelle neu aufbauen für größere Schemaänderungen
Das Vorgehen ist immer gleich: eine neue Tabelle in der gewünschten Form anlegen, die Daten rüberkopieren, die alte Tabelle löschen und die neue umbenennen. Das Ganze läuft in einer Transaktion ab.
Die offizielle SQLite-Doku nennt das Ganze das 12-Step-Rezept und führt noch ein paar zusätzliche Stolperfallen rund um Trigger, Views und Fremdschlüssel-Referenzen auf — lies es ruhig einmal komplett durch, bevor du das auf einem Produktiv-Schema anwendest. Für die meisten Fälle reicht aber die oben gezeigte Vier-Schritt-Variante völlig aus.
Ein Hinweis vorweg: Wenn Fremdschlüssel auf die Tabelle zeigen, die du gerade umbaust, setze vor der Migration PRAGMA foreign_keys = OFF und danach wieder PRAGMA foreign_keys = ON. Sonst kann das DROP TABLE mitten im Vorgang die referenzielle Integrität zerschießen.
SQLite Migrationen aus der Anwendung heraus steuern
Die Buchführung dahinter ist so überschaubar, dass du sie problemlos selbst schreiben kannst. In Python mit der Standardbibliothek sieht das so aus:
Die wichtigsten Invarianten
- Migrationen werden fortlaufend ab 1 nummeriert. Keine Lücken, keine Umsortierung.
- Jede Migration läuft in einer Transaktion – zusammen mit dem
PRAGMA user_version = N-Bump. - Sobald eine Migration committet und ausgeliefert ist, fasst du sie nie wieder an. Neue Änderungen kommen in eine neue Migration.
Genau die letzte Regel wird in Teams am häufigsten gebrochen. Wenn du Migration 3 nachträglich bearbeitest, nachdem die Datenbank eines Kollegen sie bereits angewendet hat, ist seine Datenbank für immer still und heimlich nicht mehr synchron mit deiner.
Audit-Trail mitschreiben
user_version sagt dir, wo eine Datenbank gerade steht. Es sagt dir aber nicht, wann welcher Schritt gelaufen ist oder was er gemacht hat. Eine kleine Buchhaltungstabelle löst das:
Jetzt hast du eine Zeile pro Migration mit Name und Zeitstempel — sehr praktisch beim Debuggen der Frage „Warum hat diese Datenbank eine Spalte, die der Code gar nicht erwartet?"
Für die Schleife bleibt PRAGMA user_version die maßgebliche Quelle; die Tabelle ist für uns Menschen gedacht.
Rollback: Was dir Transaktionen abnehmen — und was nicht
DDL-Statements in SQLite laufen transaktional. Angenommen, Migration 5 legt eine Tabelle an, kopiert Daten und erhöht anschließend user_version — bricht der Kopiervorgang mittendrin ab, macht ROLLBACK alles rückgängig, auch das CREATE TABLE. Die Datenbank steht danach exakt so da wie vor dem BEGIN.
Damit sind fehlgeschlagene Migrationen abgedeckt. Was hier nicht behandelt wird, sind Migrationen, die sauber committet wurden – und die du jetzt bereust. Dafür schreibst du ein separates Down-Migration-Skript, das die Änderung wieder rückgängig macht. SQLite kennt keinen automatischen Rückwärtsgang. Hat Migration 7 eine Spalte hinzugefügt, lässt die Down-Version sie wieder fallen. Hat Migration 7 eine Spalte entfernt, kann die Down-Version die Daten nicht zurückholen – sie kann die Spalte höchstens leer neu anlegen.
In der Praxis verzichten viele kleinere Projekte komplett auf Down-Migrations und setzen für das „Rückgängig" auf Backups. Das ist eine völlig legitime Entscheidung – solange du die Backups auch wirklich machst.
Ein paar Gewohnheiten, die später Schmerzen ersparen
- Eine Migration pro logischer Änderung. Eine Migration, die drei zusammenhanglose Spalten hinzufügt, ist schwerer zu reviewen und schwerer zurückzudrehen als drei einzelne Migrationen.
- Migrationen gegen eine Kopie der Produktion testen. Schema-Änderungen können auf großen Tabellen lahm sein – und das erst in Produktion zu merken, ist kein Spaß.
- Eine ausgelieferte Migration niemals nachträglich ändern. Lieber eine neue dranhängen.
- Vorher Backup machen. Ein schnelles
.backupin der CLI oder eine Dateikopie bei geschlossener Datenbank ist günstige Versicherung vor jeder nicht-trivialen Migration. - Auf
PRAGMA foreign_keysachten. Beim Tabellen-Rebuild ausschalten, danach wieder an.
Für größere Projekte greifst du besser zu einem dedizierten Tool – Alembic mit SQLAlchemy, golang-migrate, Knex oder Flyway. Die kümmern sich um Reihenfolge, parallele Runner und Team-Konventionen, die du sonst selbst neu erfindest. Die Prinzipien sind dieselben wie in der Schleife oben; das Tool nimmt dir nur den Boilerplate ab.
Weiter geht's: WAL-Modus und Nebenläufigkeit
Migrationen laufen meist, während die Anwendung offline ist oder einen exklusiven Lock hält. Die restliche Zeit bedient deine Datenbank Reads und Writes von mehreren Verbindungen gleichzeitig – und SQLites Standard-Journal-Modus ist dafür nicht immer die beste Wahl. Die nächste Seite zeigt, was der WAL-Modus ändert und wann sich der Umstieg lohnt.
Häufig gestellte Fragen
Wie versioniere ich ein SQLite-Schema?
SQLite bringt pro Datenbank einen eingebauten 32-Bit-Integer-Slot namens user_version mit, den du über PRAGMA user_version ausliest und setzt. Beim Start der Anwendung liest du den Wert, vergleichst ihn mit der höchsten Migrationsnummer, die dein Code kennt, und führst die fehlenden Migrationen der Reihe nach aus. Eine zusätzliche Tabelle brauchst du dafür nicht — viele Projekte legen aber trotzdem eine an, um einen Audit-Trail zu haben.
Kann ich eine SQLite-Migration zurückrollen?
Pack jede Migration in BEGIN; ... COMMIT;. Geht innerhalb des Blocks etwas schief, macht ROLLBACK den kompletten Schritt rückgängig — Schemaänderungen genauso wie Datenänderungen, denn DDL ist in SQLite transaktional. Willst du dagegen eine bereits committete Migration rückgängig machen, brauchst du ein eigenes Down-Skript, das du selbst schreibst. Das nimmt SQLite dir nicht ab.
Warum ist ALTER TABLE in SQLite so eingeschränkt?
SQLite kennt zwar ALTER TABLE ADD COLUMN, RENAME TABLE, RENAME COLUMN und DROP COLUMN, aber keine beliebigen Änderungen wie das Anpassen von Spaltentypen oder Constraints. Der bewährte Workaround ist das 12-Schritte-Rezept aus der offiziellen Doku: neue Tabelle mit der gewünschten Struktur anlegen, mit INSERT INTO new_table SELECT ... FROM old_table umkopieren, alte Tabelle droppen und die neue umbenennen.
Migrationstool nutzen oder selbst bauen?
Bei kleinen Projekten reicht eine handgeschriebene Schleife über durchnummerierte .sql-Dateien, gesteuert über PRAGMA user_version — das sind vielleicht 30 Zeilen Code und läuft problemlos. Für größere Anwendungen lohnen sich Tools wie Alembic (Python), golang-migrate (Go) oder Knex (Node), weil sie Reihenfolge, Locking und Team-Workflows von Haus aus mitbringen, die du sonst alle selbst nachbauen müsstest.