Der Planungsmodus für Postgres‑Schema hilft dir, Entitäten, Constraints, Indizes und Migrationen vor der Codegenerierung festzulegen und spätere Neuimplementierungen zu reduzieren.

Wenn du Endpunkte und Modelle baust, bevor die Form der Datenbank klar ist, endest du meist damit, dieselben Features zweimal umzuschreiben. Die App funktioniert für eine Demo, dann kommen echte Daten und echte Edge‑Cases und plötzlich wirkt alles brüchig.
Die meisten Rewrites entstehen durch drei vorhersehbare Probleme:
Jedes dieser Probleme erzwingt Änderungen, die sich durch Code, Tests und Client‑Apps ziehen.
Schema‑Planung bedeutet, zuerst den Datenvertrag zu entscheiden und dann Code zu generieren, der dazu passt. In der Praxis heißt das: Entitäten, Beziehungen und die wenigen wichtigen Abfragen aufschreiben, dann Constraints, Indizes und eine Migrationsstrategie wählen, bevor ein Tool Tabellen und CRUD‑Scaffolds erzeugt.
Das ist besonders wichtig, wenn du eine schnelle Code‑Generierungsplattform wie Koder.ai nutzt. Schnelles Generieren ist großartig, aber deutlich zuverlässiger, wenn das Schema steht. Deine generierten Modelle und Endpunkte brauchen später weniger Nachbearbeitung.
Das geht typischerweise schief, wenn du die Planung überspringst:
Ein guter Schema‑Plan ist einfach: eine knapp formulierte Beschreibung deiner Entitäten, ein Entwurf von Tabellen und Spalten, die wichtigsten Constraints und Indizes und eine Migrationsstrategie, die sichere Änderungen erlaubt, während das Produkt wächst.
Schema‑Planung funktioniert am besten, wenn du mit dem beginnst, was die App sich merken muss und was Menschen mit diesen Daten tun müssen. Schreibe das Ziel in 2–3 klaren Sätzen. Wenn du es nicht einfach erklären kannst, wirst du vermutlich zusätzliche Tabellen anlegen, die du nicht brauchst.
Konzentriere dich als Nächstes auf die Aktionen, die Daten erzeugen oder ändern. Diese Aktionen sind die eigentliche Quelle deiner Zeilen und zeigen, was validiert werden muss. Denk in Verben, nicht in Nomen.
Beispielsweise muss eine Buchungs‑App vielleicht eine Buchung erstellen, verschieben, stornieren, erstatten und den Kunden kontaktieren. Diese Verben deuten schnell an, was gespeichert werden muss (Zeitfenster, Statusänderungen, Geldbeträge), bevor du überhaupt einen Tabellennamen vergibst.
Dokumentiere auch die Lesewege, denn Lesefälle bestimmen später Struktur und Indexierung. Liste die Bildschirme oder Reports, die Nutzer wirklich verwenden, und wie sie die Daten filtern: „Meine Buchungen“ sortiert nach Datum und gefiltert nach Status, Admin‑Suche nach Kundenname oder Buchungsreferenz, täglicher Umsatz nach Standort und eine Audit‑Ansicht wer wann was geändert hat.
Beachte außerdem nicht‑funktionale Anforderungen, die Schema‑Entscheidungen beeinflussen, wie Audit‑History, Soft Deletes, Multi‑Tenant‑Trennung oder Datenschutzregeln (z. B. Beschränkung, wer Kontaktdaten sehen darf).
Wenn du planst, danach Code zu generieren, werden diese Notizen zu starken Prompts. Sie legen fest, was erforderlich ist, was sich ändern kann und was durchsuchbar sein muss. Wenn du Koder.ai verwendest, macht das Aufschreiben vor der Generierung den Planning Mode deutlich effektiver, weil die Plattform mit echten Anforderungen statt mit Vermutungen arbeitet.
Bevor du Tabellen anfasst, schreibe eine klare Beschreibung dessen, was deine App speichert. Beginne mit den Nomen, die immer wieder auftauchen: user, project, message, invoice, subscription, file, comment. Jedes Nomen ist eine Kandidaten‑Entität.
Füge dann pro Entität einen Satz hinzu, der beantwortet: Was ist das und warum existiert es? Zum Beispiel: „Ein Project ist ein Workspace, den ein Nutzer erstellt, um Arbeit zu gruppieren und andere einzuladen.“ Das verhindert vage Tabellen wie data, items oder misc.
Eigentum ist die nächste große Entscheidung und beeinflusst fast jede Abfrage, die du schreibst. Entscheide für jede Entität:
Bestimme anschließend, wie du Datensätze identifizierst. UUIDs sind sinnvoll, wenn Datensätze von vielen Orten erstellt werden (Web, Mobile, Background Jobs) oder wenn IDs nicht vorhersagbar sein sollen. Bigint‑IDs sind kleiner und schneller. Wenn du eine menschenfreundliche Kennung brauchst, halte sie getrennt (z. B. ein kurzes project_code, das innerhalb eines Accounts eindeutig ist) statt sie als Primärschlüssel zu erzwingen.
Schreibe Beziehungen in Worten, bevor du etwas zeichnest: ein Nutzer hat viele Projekte, ein Projekt hat viele Nachrichten, und Nutzer können zu vielen Projekten gehören. Markiere jeden Link als erforderlich oder optional, z. B. „eine Nachricht muss zu einem Projekt gehören“ vs. „eine Rechnung kann zu einem Projekt gehören“. Diese Sätze werden später deine einzige Quelle der Wahrheit für die Codegenerierung.
Sobald die Entitäten sprachlich klar sind, mache aus jeder eine Tabelle mit Spalten, die den Tatsachen entsprechen, die du speichern musst.
Beginne mit Namen und Typen, an denen du festhalten kannst. Wähle konsistente Muster: snake_case für Spaltennamen, denselben Typ für dieselbe Idee und vorhersehbare Primärschlüssel. Für Zeitstempel bevorzuge timestamptz, damit Zeitzonen dich später nicht überraschen. Für Geldbeträge nutze numeric(12,2) (oder speichere Cents als Integer) statt Floats.
Für Statusfelder nutze entweder ein Postgres‑Enum oder eine text‑Spalte mit einer CHECK‑Constraint, damit erlaubte Werte kontrolliert sind.
Entscheide, was erforderlich vs. optional ist, indem du Regeln in NOT NULL übersetzt. Wenn ein Wert für die Sinnhaftigkeit der Zeile existieren muss, mache ihn erforderlich. Wenn er wirklich unbekannt oder nicht anwendbar ist, erlaube Nullwerte.
Ein praktisches Standardset von Spalten, das du planen solltest:
id (uuid oder bigint, wähle eine Strategie und bleibe dabei)created_at und updated_atdeleted_at nur, wenn du wirklich Soft Deletes und Restore brauchstcreated_by wenn du eine klare Audit‑Spur brauchst, wer was getan hatMany‑to‑many‑Beziehungen sollten fast immer Join‑Tabellen werden. Wenn z. B. mehrere Nutzer an einer App zusammenarbeiten können, erstelle app_members mit app_id und user_id und erzwinge Eindeutigkeit auf dem Paar, damit Duplikate nicht vorkommen.
Denke früh an History. Wenn du Versionierung brauchst, plane eine unveränderliche Tabelle wie app_snapshots, in der jede Zeile eine gespeicherte Version ist, mit app_id verknüpft und mit created_at versehen.
Constraints sind die Leitplanken deines Schemas. Entscheide, welche Regeln immer gelten müssen, egal welcher Service, welches Skript oder welches Admin‑Tool die Daten schreibt.
Beginne mit Identität und Beziehungen. Jede Tabelle braucht einen Primärschlüssel, und jedes „belongs to“ Feld sollte ein echter Foreign Key sein, nicht nur eine Integer, von der du hoffst, dass sie passt.
Füge dann Eindeutigkeit dort ein, wo Duplikate echten Schaden anrichten würden, wie zwei Accounts mit derselben E‑Mail oder zwei Zeilen mit denselben (order_id, product_id).
Wertvolle Constraints, die du früh planen solltest:
Primary keys: wähle einen konsistenten Stil (UUIDs oder bigints), damit Joins vorhersehbar bleiben.Foreign keys: mache die Beziehung explizit und verhindere verwaiste Zeilen.Unique constraints: nutze sie für Business‑Identity (E‑Mail, Username) und „nur eines von diesen“ Regeln.Check constraints: günstige Regeln wie amount >= 0, status IN ('draft','paid','canceled') oder rating BETWEEN 1 AND 5.Not null: erzwinge Felder, die im echten Leben obligatorisch sind, nicht nur „meistens ausgefüllt".Das Verhalten bei Cascade hilft, spätere Probleme zu vermeiden. Frage dich, was Nutzer tatsächlich erwarten. Wenn ein Kunde gelöscht wird, sollten seine Bestellungen normalerweise nicht verschwinden. Das deutet auf RESTRICT/NO ACTION hin und darauf, History zu behalten. Für abhängige Daten wie Order‑Line‑Items kann CASCADE sinnvoll sein, weil die Items ohne Parent keinen Sinn ergeben.
Wenn du später Modelle und Endpunkte generierst, werden diese Constraints zu klaren Anforderungen: welche Fehler zu handeln sind, welche Felder benötigt werden und welche Edge‑Cases per Design unmöglich sind.
Indizes sollten eine Frage beantworten: Was muss für echte Nutzer schnell sein?
Beginne mit den Bildschirmen und API‑Aufrufen, die du zuerst ausliefern willst. Eine Listen‑Seite, die nach Status filtert und nach Neuheit sortiert, hat andere Anforderungen als eine Detail‑Seite, die verwandte Datensätze lädt.
Schreibe 5–10 Abfrage‑Muster in klarem Text, bevor du einen Index wählst. Zum Beispiel: „Zeige meine Rechnungen der letzten 30 Tage, filter nach bezahlt/unbezahlt, sortiere nach created_at“ oder „Öffne ein Projekt und liste dessen Tasks nach due_date“. Das hält Index‑Entscheidungen an der realen Nutzung fest.
Ein guter erster Satz von Indizes umfasst oft Foreign‑Key‑Spalten, die für Joins verwendet werden, gängige Filterspalten (wie status, user_id, created_at) und ein oder zwei zusammengesetzte Indizes für stabile Multi‑Filter‑Abfragen, z. B. (account_id, created_at), wenn du immer nach account_id filterst und dann nach Zeit sortierst.
Die Reihenfolge in zusammengesetzten Indizes ist wichtig. Stelle die Spalte zuerst, nach der du am häufigsten filterst (und die am selektivsten ist). Wenn du bei jeder Anfrage nach tenant_id filterst, gehört sie oft an den Anfang vieler Indizes.
Vermeide es, aus Vorsicht alles zu indexieren. Jeder Index erhöht die Arbeit bei INSERT und UPDATE und kann mehr schaden als eine etwas langsamere seltene Abfrage.
Plane Textsuche separat. Wenn du nur einfache „contains“‑Treffer brauchst, reicht anfangs ILIKE möglicherweise. Ist Suche zentral, plane früh für Full‑Text‑Search (tsvector), damit du später nicht neu designen musst.
Ein Schema ist nicht „fertig“, sobald du die ersten Tabellen erstellst. Es ändert sich bei jeder neuen Funktion, jedem Fix und jedem Learning über deine Daten. Wenn du die Migrationsstrategie im Vorfeld festlegst, vermeidest du schmerzhafte Rewrites nach Codegenerierung.
Behalte eine einfache Regel: Ändere die Datenbank in kleinen Schritten, eine Funktion pro Migration. Jede Migration sollte leicht zu reviewen und sicher in jeder Umgebung ausführbar sein.
Die meisten Brüche entstehen durch Umbenennungen oder Entfernen von Spalten oder Typänderungen. Statt alles auf einmal zu tun, plane einen sicheren Weg:
Das sind mehr Schritte, aber in der Praxis schneller, weil so Ausfälle und Not‑Patches reduziert werden.
Seed‑Daten gehören ebenfalls zu Migrationen. Entscheide, welche Referenztabellen „immer da“ sind (Rollen, Status, Länder, Plan‑Typen) und mache sie vorhersehbar. Lege Inserts/Updates für diese Tabellen in eigene Migrationen, damit jeder Entwickler und jedes Deployment dieselben Ergebnisse erhält.
Setze früh Erwartungen:
Rollbacks sind nicht immer perfekte „Down‑Migrations“. Manchmal ist die beste Rückkehr eine Backup‑Wiederherstellung. Wenn du Koder.ai einsetzt, lohnt es sich zudem, festzulegen, wann du Snapshots und Rollbacks nutzt, besonders vor riskanten Änderungen.
Stell dir eine kleine SaaS‑App vor, in der sich Leute Teams anschließen, Projekte erstellen und Aufgaben nachverfolgen.
Fange an, indem du die Entitäten und nur die Felder auflistest, die du am ersten Tag brauchst:
Die Beziehungen sind einfach: ein Team hat viele Projekte, ein Projekt hat viele Tasks und Nutzer treten Teams über team_members bei. Tasks gehören zu einem Projekt und können einem Nutzer zugewiesen sein.
Füge dann ein paar Constraints hinzu, die Bugs verhindern, die du sonst zu spät findest:
citext nutzt).Indizes sollten zu realen Bildschirmen passen. Wenn die Task‑Liste z. B. nach project und state filtert und nach Neuheit sortiert, plane einen Index wie tasks (project_id, state, created_at DESC). Wenn „Meine Aufgaben“ eine wichtige Ansicht ist, kann ein Index wie tasks (assignee_user_id, state, due_date) helfen.
Für Migrationen: Halte den ersten Satz sicher und langweilig: Tabellen erstellen, Primärschlüssel, Foreign Keys und die Kern‑Unique‑Constraints. Eine sinnvolle Folgeänderung führst du ein, nachdem die Nutzung es beweist, z. B. Soft Delete (deleted_at) auf Tasks einführen und Indexe für „aktive Tasks“ anpassen, um gelöschte Zeilen zu ignorieren.
Die meisten Rewrites passieren, weil das erste Schema Regeln und echte Nutzungsdetails vermissen lässt. Eine gute Planungsrunde geht nicht um perfekte Diagramme. Sie zielt darauf ab, Fallen früh zu erkennen.
Ein häufiger Fehler ist, wichtige Regeln nur in der Applikationslogik zu halten. Wenn ein Wert einzigartig, vorhanden oder innerhalb eines Bereichs sein muss, sollte die Datenbank es durchsetzen. Andernfalls kann ein Background‑Job, ein neuer Endpunkt oder ein manueller Import deine Logik umgehen.
Ein weiterer Fehler ist, Indizes als späteres Problem zu behandeln. Indizes nach dem Launch hinzuzufügen wird oft zur Ratesache, und du würdest am Ende vielleicht das falsche indexieren, während die echte langsame Abfrage ein Join oder ein Filter auf einem Statusfeld ist.
Many‑to‑many‑Tabellen sind ebenfalls eine Quelle stiller Bugs. Wenn deine Join‑Tabelle Duplikate nicht verhindert, speicherst du dieselbe Beziehung zweimal und verbringst Stunden damit, „warum hat dieser Nutzer zwei Rollen?“ zu debuggen.
Es ist auch leicht, zuerst Tabellen zu erstellen und dann festzustellen, dass du Audit‑Logs, Soft Deletes oder Event‑History brauchst. Solche Ergänzungen ziehen sich bis in Endpunkte und Reports hinein.
Schließlich sind JSON‑Spalten verlockend für „flexible“ Daten, aber sie entfernen Prüfungen und erschweren Indexierung. JSON ist gut für wirklich variable Nutzlasten, nicht für Kern‑Business‑Felder.
Bevor du Code generierst, lauf diese schnelle Checkliste durch:
Halte hier kurz an und stelle sicher, dass der Plan ausreichend komplett ist, um Code zu generieren, ohne ständig Fragen zu klären. Das Ziel ist nicht Perfektion. Es geht darum, Lücken zu schließen, die später Rewrites verursachen: fehlende Beziehungen, unklare Regeln und Indizes, die nicht zur tatsächlichen Nutzung passen.
Nutze das als schnellen Pre‑Flight Check:
amount >= 0 oder erlaubte Status).Ein schneller Sanity‑Test: Stell dir vor, ein Kollege fängt morgen an. Könnte er die ersten Endpunkte bauen, ohne jede Stunde zu fragen „darf das NULL sein?“ oder „was passiert bei Delete?“?
Sobald der Plan klar lesbar ist und die Hauptflüsse auf dem Papier Sinn ergeben, mache ihn ausführbar: echtes Schema + Migrationen.
Starte mit einer initialen Migration, die Tabellen, Typen (falls du Enums nutzt) und die unverzichtbaren Constraints anlegt. Halte die erste Version klein, aber korrekt. Lade etwas Seed‑Daten und führe die Abfragen aus, die deine App tatsächlich braucht. Fühlt sich ein Flow unbequem an, korrigiere das Schema, solange die Migrationshistorie noch kurz ist.
Generiere Modelle und Endpunkte erst, nachdem du ein paar End‑to‑End‑Abläufe mit dem Schema getestet hast (create, update, list, delete plus eine reale Business‑Aktion). Codegenerierung ist dann am schnellsten, wenn Tabellen, Keys und Namenskonventionen stabil genug sind, dass du nicht am nächsten Tag alles umbenennst.
Ein praktischer Loop, der Rewrites niedrig hält:
Entscheide früh, was du in der Datenbank vs. im API‑Layer validierst. Permanente Regeln gehören in die Datenbank (Foreign Keys, Unique, Check). Weiche Regeln gehören in die API (Feature Flags, temporäre Limits, komplexe Cross‑Table‑Logik, die sich oft ändert).
Wenn du Koder.ai nutzt, ist ein sinnvolles Vorgehen: Einigt euch zuerst auf Entitäten und Migrationen im Planning Mode, dann generiert ihr euer Go‑plus‑Postgres‑Backend. Wenn eine Änderung schiefgeht, helfen Snapshots und Rollbacks, schnell zu einer bekannten guten Version zurückzukehren, während ihr den Schema‑Plan anpasst.
Plane das Schema zuerst. Es legt einen stabilen Datenvertrag fest (Tabellen, Keys, Constraints), sodass generierte Modelle und Endpunkte später nicht ständig umbenannt oder neu geschrieben werden müssen.
In der Praxis: Schreibe deine Entitäten, Beziehungen und Top‑Abfragen auf und fixiere dann Constraints, Indizes und Migrationen, bevor du Code generierst.
Schreibe 2–3 Sätze, die beschreiben, was die App speichern muss und was Nutzer damit tun können.
Dann liste auf:
Das gibt genug Klarheit, um Tabellen zu entwerfen, ohne zu überbauen.
Fange an, die Nomen aufzuschreiben, die immer wieder auftauchen (user, project, invoice, task). Für jedes Nomen füge einen Satz hinzu: was es ist und warum es existiert.
Wenn du es nicht klar beschreiben kannst, entsteht wahrscheinlich eine vage Tabelle wie items oder misc und du wirst das später bereuen.
Lege eine konsistente ID‑Strategie im Schema fest.
UUIDs: ideal, wenn Datensätze von vielen Orten kommen (Web, Mobile, Background‑Jobs) oder IDs nicht vorhersagbar sein sollenbigint: kleiner, etwas schneller und ausreichend, wenn alles serverseitig erstellt wirdWenn du eine menschenlesbare Kennung brauchst, lege eine separate eindeutige Spalte an (z. B. project_code) statt sie als Primärschlüssel zu verwenden.
Entscheide für jede Beziehung, was die Nutzer erwarten und was erhalten bleiben muss.
Gängige Defaults:
RESTRICT/NO ACTION, wenn das Löschen des Elternteils wichtige Daten entfernen würde (z. B. customer → orders)Triff diese Entscheidung früh, weil sie API‑Verhalten und Edge‑Cases beeinflusst.
Setze permanente Regeln in die Datenbank, damit jeder Schreiber (API, Skripte, Importe, Admin‑Tools) sich daran hält.
Priorisiere:
Beginne bei echten Abfrage‑Mustern, nicht bei Vermutungen.
Schreibe 5–10 Abfragestellte in einfachem Englisch (Filter + Sortierung) und indexiere dafür:
status, user_id, created_atLege eine Join‑Tabelle mit zwei Foreign Keys an und sorge für eine zusammengesetzte Unique‑Constraint.
Beispielmuster:
team_members(team_id, user_id, role, joined_at)UNIQUE (team_id, user_id) hinzu, um Duplikate zu verhindernDas vermeidet subtile Fehler wie „warum taucht dieser Nutzer doppelt auf?“ und hält Abfragen sauber.
Gute Defaults:
timestamptz für Zeitstempel (weniger Zeitzonenüberraschungen)numeric(12,2) oder ganze Cents als Integer für Geldbeträge (keine Floats)CHECK‑ConstraintsHalte Typen über Tabellen hinweg konsistent (derselbe Typ für dasselbe Konzept), damit Joins und Validierungen vorhersehbar bleiben.
Nutze kleine, reviewbare Migrationen und vermeide große, brechende Änderungen auf einmal.
Sichere Vorgehensweise:
Entscheide außerdem im Voraus, wie Seed/Referenzdaten gehandhabt werden sollen, damit jede Umgebung übereinstimmt.
PRIMARY KEY auf jeder TabelleFOREIGN KEY für jedes „belongs to“ FeldUNIQUE wo Duplikate echten Schaden anrichten (E‑Mail, (team_id, user_id) auf Join‑Tabellen)CHECK für einfache Regeln (nicht‑negative Beträge, erlaubte Statuswerte)NOT NULL für Felder, die zwingend nötig sind, damit die Zeile Sinn ergibt(account_id, created_at))Vermeide, alles zu indexieren; jeder Index verlangsamt INSERTs und UPDATEs.