4 Min. Lesezeit
SQLite reicht
Eine Datei, kein Server, schnell genug für fast alles
Teil vier des Boring Stack. Die App ist in Go geschrieben und rendert HTML auf dem Server. Jetzt muss sie ihre Daten irgendwo ablegen.
Die Standardantwort ist Postgres. Eine ausgezeichnete Datenbank, die ich jahrelang benutzt habe. Für die Apps, die ich heute baue, nehme ich SQLite.
Eine Datenbank, die eine Datei ist
SQLite ist kein Server. Es ist eine Bibliothek, die eine einzige Datei liest und schreibt. Die App öffnet ./data/app.db, und das ist die ganze Datenbank.
Das spart überraschend viel Arbeit:
- Kein Datenbankserver, den man installieren, patchen, absichern oder überwachen muss. Nichts lauscht auf einem Port. Es gibt kein Passwort zu rotieren und kein Netzwerk zwischen App und Datenbank zu konfigurieren.
- Backups sind eine Dateikopie. Genauer: ein konsistenter Schnappschuss einer Datei, den SQLite erstellen kann, während die App läuft. Wiederherstellen heisst, die Datei zurücklegen.
- Entwicklung und Produktion sind gleich. Dieselbe Engine läuft in den Tests, auf meinem Laptop und auf dem Server. Meinen lokalen Stand setze ich zurück, indem ich einen Ordner lösche.
- Die App zu zügeln heisst, zwei Dinge zu verschieben: das Binary und den Datenordner.
Ist das schnell genug?
Für fast alles, was ein kleines Team baut, ja, und zwar mit grossem Abstand. SQLite liest im selben Prozess, ohne Umweg übers Netzwerk, was einfache Abfragen extrem schnell macht. Mit eingeschaltetem Write-Ahead-Logging blockieren Leser den Schreiber nicht und der Schreiber die Leser nicht.
Die echte Grenze ist, dass immer nur einer schreibt. Schreibzugriffe stellen sich an. Bei einem internen Tool, einem Buchungssystem oder einem kleinen SaaS ist diese Warteschlange fast immer leer. Wenn deine App Tausende Schreibzugriffe pro Sekunde von vielen Servern gleichzeitig verarbeitet, bist du darüber hinausgewachsen, und du wirst es merken.
Eine Datei pro Mandant, wenn es sein muss
Die meisten meiner Apps haben eine Datenbank. Wenn eine App mehrere Kunden bedient, deren Daten getrennt bleiben müssen, bietet SQLite eine Möglichkeit, die zuerst seltsam klingt und sich als die einfachste herausstellt: eine Datenbankdatei pro Mandant.
data/tenants/acme.db
data/tenants/globex.db
data/tenants/initech.db
Die App wählt die Datei anhand des Mandanten der Anfrage und öffnet sie. Beim ersten Öffnen laufen die Migrationen gegen diese Datei wie gegen jede andere.
Vergleich das mit den üblichen Wegen, eine Postgres-Datenbank aufzuteilen:
- Eine Spalte
tenant_idin jeder Tabelle, vielleicht mit Row-Level Security darüber. Jede Abfrage muss nach Mandant filtern. Ein vergessenesWHEREoder eine Tabelle ohne Policy, und ein Kunde sieht die Daten eines anderen. - Ein Schema pro Mandant. Sauberer, aber jede Migration muss über Hunderte Schemas laufen, jede Verbindung muss das richtige wählen, und einen einzelnen Kunden zu sichern oder wiederherzustellen heisst, ein einzelnes Schema zu exportieren und zu laden.
- Eine Datenbank pro Mandant. Die sauberste Trennung, aber jede braucht eigene Verbindungen, und ein Postgres-Server mag keine Tausende davon.
Mit einer Datei pro Mandant verschwinden die meisten dieser Probleme:
- Trennung durch Bauweise. Eine Abfrage sieht nur die Datei, gegen die sie läuft. Es gibt kein
WHERE, das man vergessen kann. - Arbeiten pro Mandant sind Dateioperationen. Einen Kunden sichern heisst, eine Datei zu kopieren. Ihn wiederherzustellen berührt niemand anderen. Einen Kunden zu löschen, zum Beispiel weil er es nach Datenschutzgesetz verlangt, heisst, eine Datei zu löschen, und man kann sicher sein, dass nichts übrig bleibt.
- Keine lauten Nachbarn beim Schreiben. SQLite erlaubt einen Schreiber gleichzeitig pro Datei. Ein viel beschäftigter Mandant lässt die anderen nicht warten.
- Mandanten können umziehen. Ein grosser Kunde bekommt seinen eigenen Server, indem man eine Datei verschiebt.
Gratis ist das nicht. Auswertungen über alle Mandanten heissen, jede Datei zu öffnen oder mehrere gleichzeitig anzuhängen. Daten, die zu keinem Mandanten gehören, etwa Benutzerkonten über mehrere Kunden hinweg, brauchen eine zentrale Datenbank neben den Mandantendateien. Und eine Migration, die bei einer Datei scheitert, lässt diesen Mandanten zurück, bis sie behoben ist. Die App muss also mit Dateien umgehen können, die noch nicht auf dem neuesten Stand sind.
Wenn Mandanten fast alles teilen, bleibt eine Spalte tenant_id in einer Datei die einfachere Wahl. Wenn sie getrennt bleiben müssen, ist eine Datei pro Mandant einfacher als alles, was ich in Postgres bauen würde.
Was es kostet
- Ein Server. SQLite liegt neben der App. Man kann die App nicht auf fünf Servern betreiben, die sich eine SQLite-Datei teilen. Für mich passt das: Ich betreibe sowieso einen Server.
- Weniger Funktionen. Keine Stored Procedures, weniger Datentypen, ein eingeschränktes
ALTER TABLE. Migrationen müssen manchmal eine Tabelle neu aufbauen, statt eine Spalte zu ändern. - Weniger Werkzeuge. Es gibt kein verwaltetes SQLite mit Dashboard bei deinem Cloud-Anbieter. Man öffnet die Datei mit der
sqlite3-Kommandozeile oder einem Desktop-Viewer.
Wann ich anders entscheiden würde
Wenn mehrere App-Server in dieselben Daten schreiben müssen, wenn die Schreiblast wirklich hoch ist oder wenn ein Team Postgres schon gut betreibt, würde ich Postgres nehmen. Auch wenn ich seine besonderen Funktionen bräuchte, etwa Volltextsuche in vielen Sprachen oder PostGIS.
Aber ich würde warten, bis dieser Bedarf auftaucht. Die meisten Apps kommen nie dorthin, und die, die es tun, können umziehen. Die Daten liegen so oder so in SQL-Tabellen.
Als Nächstes: wie die App mit der Datenbank spricht.