Individualsoftware-Entwicklung
Individualsoftware mit Next.js und PostgreSQL
Was die Wahl von PostgreSQL in einem Next.js-Bau konkret ändert - die Garantien, die es erzwingt, die Suche, für die kein Dienst mehr nötig ist, und die Falle, in die jedes Team einmal läuft.
PostgreSQL für einen individuellen Bau zu wählen hat konkrete Folgen, und die gehören ausgesprochen statt vorausgesetzt. Es ist bei diesen Projekten der Standard, keine Regel, und der Grund ist, dass es Arbeit aus der Anwendung an eine Stelle verschiebt, an der sie nicht umgangen werden kann.
Garantien, die die Anwendung nicht vergessen kann
Das stärkste Argument für eine relationale Datenbank in Individualsoftware hat nichts mit Geschwindigkeit zu tun.
Eine Anwendung wird über Jahre von mehreren Leuten geschrieben, und nicht alle kennen die Regeln. Eine im Anwendungscode erzwungene Garantie gilt für die Pfade, an die jemand gedacht hat - und nicht für das Importskript, das Console-Command, den Cron-Job oder die Admin-Maske, die im nächsten Frühjahr dazukommt.
Eine in der Datenbank erzwungene Garantie gilt für alle. Ein Fremdschlüssel bedeutet, dass ein Auftrag keinen Kunden referenzieren kann, der nicht existiert. Ein Unique-Constraint bedeutet, dass zwei gleichzeitige Anfragen nicht beide denselben Datensatz anlegen können - was kein Prüfen-dann-Einfügen im Code erreicht. Ein Check-Constraint bedeutet, dass eine Menge nicht negativ sein kann, in der Zeile und nicht bloß im Formular.
PostgreSQL geht dabei weiter als die meisten, und zwar auf eine Weise, die für echte Anwendungen zählt. Ein Exclusion-Constraint kann eine Buchung abweisen, deren Zeitraum sich mit einer bestehenden für dieselbe Ressource überschneidet - eine Race Condition, die im Anwendungscode wirklich schwer und hier trivial zu schließen ist. Partielle Indexe indexieren nur die Zeilen, die zählen, sodass eine Abfrage über aktive Datensätze nicht für das Archiv mitbezahlt.
Was das aus dem Code entfernt
Jede dieser Garantien ist Validierungscode, den Sie nicht schreiben, nicht testen und nicht erinnern müssen, wenn eine siebte Art hinzukommt, einen Datensatz zu erzeugen.
Es ist außerdem der Unterschied zwischen einem unmöglichen und einem unwahrscheinlichen Fehler. In einem System, das Geld, Bestand oder Termine verwaltet, ist das der ganze Punkt.
JSONB, und wo die Linie liegt
Manche Daten widersetzen sich einem Schema wirklich. Die Nutzlast eines Webhooks von einem Dritten. Ein Formular, dessen Felder der Kunde konfiguriert. Einstellungen pro Mandant, die sich je Mandant unterscheiden.
PostgreSQL behandelt das ehrlich: JSONB wird effizient gespeichert, mit echten Operatoren abgefragt und mit GIN indexiert, also sind unstrukturierte Daten eine erstklassige Wahl und keine Notlösung.
Die Linie, die man halten sollte: alles, wonach Sie filtern, sortieren, berichten, joinen oder was Sie einschränken, bekommt eine Spalte. Das Fehlermuster ist bekannt und langsam - eine JSON-Spalte wird zum Ort, an den neue Felder wandern, weil eine Migration schwer wirkte, und zwei Jahre später kann niemand eine Frage zu den Daten beantworten, ohne Anwendungscode zu lesen.
Suche, ohne ein zweites System
Ein großer Teil der Anwendungen nimmt einen Suchdienst, eine Synchronisation und eine betriebliche Abhängigkeit in Kauf, um etwas zu tun, was die Datenbank schon kann.
Volltextsuche in PostgreSQL hat Stemming, Ranking und Konfigurationen für mehrere Sprachen, und sie indexiert gut. Bei einigen Hunderttausend Datensätzen ist sie schnell, und sie hat den erheblichen Vorteil, dass Suchergebnisse nicht veralten können, weil es nichts zu synchronisieren gibt.
Wo sie ausgeht, ist real und gehört benannt - Tippfehlertoleranz, großflächiges Relevanz-Tuning, facettierte Suche über Millionen Dokumente. Diesen Punkt zu erreichen ist eine Messung, keine Vorhersage, und später dort anzukommen kostet weit weniger, als ab Woche eins einen Suchcluster für einen Datenbestand zu betreiben, der ihn nie gebraucht hätte.
Die eine Falle, und wie sie behandelt wird
Jedes Team begegnet ihr einmal. Eine Next.js-Anwendung auf einer serverlosen Laufzeit öffnet pro Instanz eine Datenbankverbindung, und PostgreSQL akzeptiert eine feste Anzahl. Unter Verkehr wird die Anwendung nicht langsamer - sie beginnt Verbindungen abzulehnen, was wie ein Datenbankausfall liest und ein Konfigurationsfehler ist.
Die Antwort ist ein Connection Pooler zwischen Anwendung und Datenbank, und was dabei erwischt: Pooling im Transaktionsmodus ist mit Prepared Statements unvereinbar, solange der Client nicht dafür konfiguriert ist. Connection Pooling in einer serverlosen Laufzeit ist die vollständige Erklärung.
Bei diesen Bauten wird das in Woche eins entschieden, und das ist der Unterschied zwischen einer bekannten Rahmenbedingung und einem Vorfall.
Wie ein Projekt abläuft
In Phasen, jede vor Beginn schriftlich fixiert und jede endet mit etwas Ausgelieferten, das Sie benutzen können. Die erste Phase nimmt einen Ablauf von Anfang bis Ende statt des Umrisses mehrerer, denn das entscheidet das Schema an echten Daten.
Die Datenbankentscheidungen fallen in dieser ersten Phase und nicht danach. Welche Constraints wo erzwungen werden, was eine Spalte ist und was JSONB, wie Migrationen gegen eine Tabelle laufen, die später groß wird, und wie das Pooling aussieht. Keine davon ist früh teuer, und alle sind teuer zu ändern, wenn Produktionsdaten dahinterliegen.
Was Sie bekommen
Eine ausgelieferte Anwendung am Ende jeder Phase, das Repository mit intakter Historie, und das Schema als Migrationen statt als Ist-Zustand - damit die Datenbank eine Historie hat, die sich abspielen und reviewen lässt.
Dazu: eine schriftliche Notiz zum Datenmodell und warum es so geformt ist, die Indexe und wofür jeder einzelne da ist, die Pooling-Konfiguration, und die Constraints mit je einer Zeile dazu, was sie verhindern. Letzteres ist das Dokument, zu dem Leute wirklich zurückkehren, denn ein Constraint, den niemand versteht, ist ein Constraint, den irgendwann jemand entfernt.
Wann PostgreSQL nicht die Antwort ist
Wenn die Anwendung eine dünne Schicht über der API eines Dritten ist und fast keinen eigenen Zustand hält, betreiben Sie eine Komponente ohne Grund - und ein einfacherer Speicher ist die bessere Antwort.
Wenn die Last wirklich ein Cache oder eine Queue ist, sollte es ein Cache oder eine Queue sein. PostgreSQL kann beides passabel und ist für keines von beiden gedacht.
Und wenn die Daten hochvolumige Messwerte sind, die nur angehängt werden - Sensordaten, Telemetrie im großen Stil -, nimmt eine relationale Tabelle davon überraschend viel, aber es gibt einen Punkt, jenseits dessen ein spezialisierter Speicher gewinnt. Dieser Punkt sollte durch Messen festgestellt werden und nicht durch die Annahme, er sei schon erreicht.
