In einer App zur Erstellung von Schichtplänen habe ich das Schema innerhalb von zwei Wochen drei Mal inkompatibel geändert. Ich legte eine Tabelle worker_verifications an, um das Ergebnis der Prüfungen zu speichern, die das Modell zu jedem Mitarbeiter durchführte. Fünf Tage später habe ich sie komplett gelöscht und ihre einzige nützliche Spalte, checked_rules, in worker_week_rules gesteckt. Zwei Tage danach habe ich extra_closed_days aus der Tabelle weeks entfernt, die ich beim vorherigen Redesign hinzugefügt hatte.
Keine dieser Änderungen kam durch eine neue Produktentscheidung zustande. Sie kamen daher, dass ich die Domäne besser verstanden habe.
Das Schema von Tag 1 und das von Tag 30
Die gelöschte Tabelle hatte acht Spalten: ai_result, worker_hash, context_hash, verified_at, checked_rules und die Schlüssel. Die Idee war, die Verifikation zu cachen: Wenn sich der Hash des Mitarbeiters und der des Kontexts nicht änderten, riefst du das Modell nicht erneut auf. Klingt vernünftig.
Was passierte: Die eigentliche Einheit der Domäne war nicht "eine Verifikation", sondern "die Regeln eines Mitarbeiters in einer Woche". Und diese Zeile existierte bereits. Die Verifikationen waren ein Attribut dieser Zeile, keine Entität. Die Migration, die das gerade rückte, ist kurz und liest sich von selbst:
ALTER TABLE "worker_week_rules" ADD COLUMN "checked_rules" text DEFAULT '{}' NOT NULL;
UPDATE "worker_week_rules" wwr
SET "checked_rules" = wv."checked_rules"
FROM "worker_verifications" wv
WHERE wwr."week_id" = wv."week_id"
AND wwr."worker_id" = wv."worker_id"
AND wv."checked_rules" != '{}';
DROP TABLE "worker_verifications";
ALTER TABLE "worker_week_rules" DROP COLUMN "created_at";
Vier Statements. Die Kosten lagen nicht im SQL.
Welche Änderungen ein Zeichen dafür sind, dass du richtig lagst, und welche nicht
Es gibt zwei Arten von Schemaänderungen, und sie ähneln sich überhaupt nicht.
- Eine Spalte hinzufügen.
checked_week_rulesinweeks,calendar_rulesincalendars. Das ist einALTER TABLE ADD COLUMN ... DEFAULTmit einem Default, wird ohne Ausfall ausgerollt, der alte Code funktioniert weiter. Das ist das System, das wächst. Ein gutes Zeichen. - Die Entität woanders hinschieben. Eine Tabelle löschen, eine Spalte umbenennen, ändern, woran ein Datum hängt. Das heißt, dass du die Domäne beim ersten Mal falsch modelliert hast, nicht dass sich die Domäne geändert hat.
Im selben Projekt habe ich preferred_day_off integer zu preferred_days_off text gemacht, weil sich herausstellte, dass ein Mitarbeiter zwei Tage bevorzugen konnte. Das gehört auch zu den guten: Es sind neue Informationen über die Welt, kein Modellierungsfehler. Die Migration passt in drei Zeilen und erhält die Daten mit einem CAST.
Die schlechte Sorte erkennst du daran, dass der Diff nicht im Migrationsordner liegt.
Die wahren Kosten stecken in der UI, nicht im SQL
In einem Dokumentationswerkzeug mit KI nannte ich anfangs documents, was der Nutzer hochlud. Einen Monat später war das richtige Konzept contexts: Textstücke, die der Agent lesen, zitieren und umschreiben konnte und die aus einer URL kommen konnten, nicht nur aus einer Datei. Der Name war falsch und zog das ganze Modell mit sich.
Der Commit, der das änderte, fasst das Schema nicht an. Er fasst die Chat-Seite an, das Input-Feld, und löscht context-url-dialog, context-view und context-viewer komplett: 1.127 Zeilen auf einen Schlag entfernt. Ein RENAME COLUMN von zwei Sekunden und ein ganzer Tag Komponentenarbeit.
Und ein Fall, der noch billiger auszuführen und noch teurer weiterzureichen ist: Im selben Projekt habe ich starCount zu favoriteCount umbenannt. Das SQL sind fünf Zeilen in zwei Migrationen. Der Diff fasst das Schema an, zwei Dateien mit Datenbank-Actions und die UI, die es darstellte.
Das ist die Regel, die ich mitgenommen habe: Die Kosten, einen Spaltennamen zu ändern, sind proportional dazu, wie viele Komponenten ihn wörtlich hinschreiben. Nicht dazu, was die Migration kostet.
Wie ich heute das erste Schema schreibe
Vier Dinge, die ich seitdem anders mache, und keines davon ist "besser designen".
- Ich benenne nach dem, was es ist, nicht nach dem, woher es kommt.
documentswar die Herkunft (eine Datei).contextsist die Rolle, die das Datum im System spielt. Der zweite Name hält den Wechsel der Herkunft aus. - Ich lege keine Tabelle für ein Attribut an. Wenn die neue Entität immer genau eine Zeile pro Zeile einer anderen Tabelle hat, ist sie keine Entität. Sie ist eine Gruppe von Spalten.
worker_verificationswar genau das, und ich habe fünf Tage gebraucht, um es zu sehen. - Von Anfang an ein Typ pro Spalte. Beim großen Redesign habe ich gleichzeitig
created_atvontextzutimestamptzkonvertiert,week_startvontextzudateund das als Text gespeicherte JSON zujsonb. All das hätte ich am ersten Tag zum selben Preis richtig machen können. Es hinterher zu reparieren ist einALTER COLUMN ... USINGin jeder Tabelle plus die Durchsicht des gesamten Codes, der Daten als Strings verglich. - Die UI wiederholt den Spaltennamen nicht. Ein aus dem Schema abgeleiteter Typ und ein paar Zugriffsfunktionen. Dann fängt ein
RENAME COLUMNder Compiler ab und nicht eingrep.
Wann es sich doch lohnt innezuhalten
In einem Editor für Bürogrundrisse bin ich von getrennten Komponenten DualDesk und QuadDesk zu einem einzigen MultiDesk mit einer Platzanzahl übergegangen und habe es danach noch einmal refaktoriert, um deaktivierte Plätze zu ergänzen. Zwei Tische mit zwei und mit vier Plätzen sehen nach zwei Fällen aus; in Wirklichkeit waren sie derselbe mit einem Parameter. Das sieht man sehr wohl am ersten Tag, wenn du dir zehn Minuten nimmst und fragst, ob die 2 und die 4 Konstanten oder Daten sind.
Die Regel, die ich benutze: Wenn der Unterschied zwischen zwei Modellen eine Zahl ist, ist es ein Datum. Da lohnt es sich, vorher nachzudenken.
Für den Rest schreibe ich das Schema im Wissen, dass es falsch ist. Was ich optimiere, ist nicht, richtig zu liegen, sondern dass die Reparatur nächste Woche eine kurze Migration kostet und nicht einen Tag an Komponenten.
