02Datenbanken5 min

Meine ersten Datenschemata überleben fast nie den ersten Monat

Das Schema von Tag 1 ist eine Hypothese über eine Domäne, die du noch nicht verstehst. Das anzunehmen verändert, wie du es schreibst und wie weh es tut, es zu ändern.

Von Salva Castells

Titelbild: Sundaram Perumal, CC BY-SA 4.0.
In diesem Beitrag

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_rules in weeks, calendar_rules in calendars. Das ist ein ALTER TABLE ADD COLUMN ... DEFAULT mit 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. documents war die Herkunft (eine Datei). contexts ist 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_verifications war 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_at von text zu timestamptz konvertiert, week_start von text zu date und das als Text gespeicherte JSON zu jsonb. All das hätte ich am ersten Tag zum selben Preis richtig machen können. Es hinterher zu reparieren ist ein ALTER COLUMN ... USING in 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 COLUMN der Compiler ab und nicht ein grep.

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.