Jedes Produkt, das ich ausgeliefert habe, hat mindestens eine Framework-Entscheidung überlebt. Die Datenbank hat sie alle überlebt. Next.js-Versionen ändern sich, das ORM wird ausgetauscht, das Team wechselt – die Datensätze bleiben, ebenso die Annahmen, die im Schema festgeschrieben sind. Deshalb ist SQL für mich der eigentliche Vertrag eines Produkts und kein Implementierungsdetail hinter einer Repository-Klasse.
Prisma deckt einen Großteil meines Alltags ab: typisiertes CRUD, Migrationen im Repository und ein Schema, das neue Teammitglieder lesen können. Aber ich lese auch das generierte SQL. Fragt eine Listenseite jede Zeile einzeln ab, braucht ein Bericht eine Window Function oder ein Dashboard ein GROUP BY über drei Joins, schreibe ich die Abfrage mit $queryRaw selbst oder lege sie als View an. Bevor jemand Redis ergänzt, öffne ich EXPLAIN ANALYZE. Hinter den meisten Tickets „Wir brauchen einen Cache“ steckt ein fehlender Index.
Constraints sind die günstigste Qualitätssicherung überhaupt. NOT NULL, UNIQUE, FOREIGN KEY und CHECK greifen bei jedem Schreibvorgang, aus jedem Service – sogar bei einem Skript, das jemand freitags von Hand startet. Zod schützt die API, die Datenbank schützt die Wahrheit. Geld wird in ganzzahligen Minor Units oder als NUMERIC gespeichert, niemals als Float. Payment-Webhooks kommen doppelt an; deshalb erhält der Idempotency-Key einen eindeutigen Index und keinen Kommentar im Handler.
Nebenläufigkeit ist die Quelle vieler FinTech-Fehler. Zwei Requests lesen einen Kontostand, beide sehen genug Guthaben und schreiben beide – ein Lost Update, das keine Frontend-Validierung beheben kann. Ich bevorzuge ein atomares Statement – UPDATE ... SET balance = balance - $1 WHERE id = $2 AND balance >= $1 – und prüfe danach, ob genau eine Zeile geändert wurde. Alternativ nutze ich eine Transaktion mit SELECT ... FOR UPDATE sowie eine append-only Ledger-Tabelle, damit sich jede Bewegung später nachvollziehen lässt. Bei Systemen mit mehr als 1,5 Mio. Dollar verarbeitetem Volumen ist „später erklären können“ die eigentliche Aufgabe.
Indizes richten sich nach den Abfragen, die tatsächlich laufen, nicht nach scheinbar wichtigen Spalten. Bei zusammengesetzten Indizes kommen Gleichheitsbedingungen zuerst, Bereich oder Sortierung zuletzt; partielle Indizes eignen sich etwa für „aktive“ Datensätze, Keyset-Paginierung ist OFFSET 50000 vorzuziehen. Migrationen auf einer Live-Tabelle erfolgen schrittweise: Spalte zunächst nullable hinzufügen, in Batches nachfüllen, danach Constraint setzen; den Index mit CONCURRENTLY bauen. Eine Migration, die mittags die Orders-Tabelle sperrt, ist ein Ausfall mit angehängtem Pull Request.
Ich verzichte auf eine „flexible“ Entity-Attribute-Value-Tabelle statt eines Schemas, JSONB als Versteck für zentrale Felder, die niemand modellieren möchte, Soft Deletes überall ohne Rücksicht auf Unique-Constraints und einen Cache vor einer Abfrage, die niemand mit EXPLAIN untersucht hat. Wenn Sie mir eine langsame Seite oder ein unsicheres Schema schicken, senden Sie die Abfrage und die Zeilenzahlen mit. Ich antworte mit dem Plan, dem Index und dem Constraint, das ich zuerst ergänzen würde.
Geben Sie dem nächsten Produkt jemanden, der bleibt.
Bereit für den Start? Schreiben Sie mir – ich kann sofort beginnen und bleiben. Langfristige Mandate statt Übergabe nach zwei Wochen. Lassen Sie uns über Ihr Projekt, die Anforderungen und den besten Weg nach vorn sprechen.