02Bazy danych5 min

Moje pierwsze schematy danych prawie nigdy nie przeżywają pierwszego miesiąca

Schemat z dnia 1 to hipoteza na temat domeny, której jeszcze nie rozumiesz. Przyjęcie tego zmienia sposób, w jaki go piszesz, i to, jak bardzo boli jego zmiana.

Autor: Salva Castells

Zdjęcie okładkowe: Sundaram Perumal, CC BY-SA 4.0.
W tym wpisie

W aplikacji do układania grafików zmian w dwa tygodnie ruszyłem schemat trzy razy, za każdym razem niekompatybilnie. Stworzyłem tabelę worker_verifications, żeby trzymać wynik sprawdzeń, które model robił dla każdego pracownika. Pięć dni później skasowałem ją w całości i wsadziłem jej jedyną użyteczną kolumnę, checked_rules, do worker_week_rules. Dwa dni później usunąłem extra_closed_days z tabeli weeks, dodaną w poprzednim przeprojektowaniu.

Żadna z tych zmian nie wynikała z nowej decyzji produktowej. Wynikały z lepszego zrozumienia domeny.

Schemat z dnia 1 i schemat z dnia 30

Tabela, którą skasowałem, miała osiem kolumn: ai_result, worker_hash, context_hash, verified_at, checked_rules i klucze. Pomysł polegał na cache'owaniu weryfikacji: jeśli hash pracownika i hash kontekstu się nie zmieniły, nie wołasz modelu ponownie. Brzmi rozsądnie.

Stało się to, że prawdziwą jednostką domeny nie była "weryfikacja", tylko "reguły pracownika w danym tygodniu". A ten wiersz już istniał. Weryfikacje były atrybutem tego wiersza, nie encją. Migracja, która to naprawiła, jest krótka i czyta się sama:

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";

Cztery instrukcje. Koszt nie leżał w SQL-u.

Które zmiany są znakiem, że trafiłeś, a które że nie

Są dwa rodzaje zmian schematu i nie przypominają się w niczym.

  • Dodanie kolumny. checked_week_rules w weeks, calendar_rules w calendars. To ALTER TABLE ADD COLUMN ... DEFAULT z wartością domyślną, wdraża się bez zatrzymywania czegokolwiek, stary kod nadal działa. To system rosnący. Dobry znak.
  • Przeniesienie encji gdzie indziej. Skasowanie tabeli, zmiana nazwy kolumny, zmiana tego, pod co dana wisi. To znaczy, że za pierwszym razem źle zamodelowałeś domenę, a nie że domena się zmieniła.

W tym samym projekcie zmieniłem preferred_day_off integer na preferred_days_off text, bo okazało się, że pracownik może preferować dwa dni. To też jest z tych dobrych: to nowa informacja o świecie, nie błąd modelowania. Migracja mieści się w trzech liniach i zachowuje dane przez CAST.

Ten zły rodzaj poznasz po tym, że diff nie jest w katalogu migracji.

Prawdziwy koszt siedzi w UI, nie w SQL-u

W narzędziu do dokumentacji z AI zacząłem od nazwania documents tego, co wrzucał użytkownik. Miesiąc później właściwym pojęciem było contexts: kawałki tekstu, które agent mógł czytać, cytować i przepisywać, i które mogły pochodzić z URL-a, nie tylko z pliku. Nazwa była zła i ciągnęła za sobą cały model.

Commit, który to zmienił, nie rusza schematu. Rusza stronę czatu, input, i kasuje context-url-dialog, context-view oraz context-viewer w całości: 1127 usuniętych linii za jednym zamachem. Dwusekundowy RENAME COLUMN i cały dzień grzebania w komponentach.

I przypadek jeszcze tańszy w wykonaniu, a droższy w propagacji: w tym samym projekcie zmieniłem nazwę starCount na favoriteCount. SQL to pięć linii w dwóch migracjach. Diff rusza schemat, dwa pliki akcji bazodanowych i UI, które to wyświetlało.

Taką regułę stąd wyniosłem: koszt zmiany nazwy kolumny jest proporcjonalny do tego, ile komponentów pisze ją dosłownie. Nie do tego, ile kosztuje migracja.

Jak dziś piszę pierwszy schemat

Cztery rzeczy, które od tamtej pory robię inaczej, i żadna z nich to nie "projektować lepiej".

  • Nazywam po tym, czym coś jest, nie skąd pochodzi. documents to było źródło (plik). contexts to rola, jaką dana odgrywa w systemie. Druga nazwa wytrzymuje zmianę źródła.
  • Nie tworzę tabeli dla jednego atrybutu. Jeśli nowa encja zawsze ma dokładnie jeden wiersz na każdy wiersz innej tabeli, to nie jest encja. To grupa kolumn. worker_verifications było właśnie tym i zajęło mi pięć dni, żeby to zobaczyć.
  • Właściwy typ kolumny od początku. W dużym przeprojektowaniu przerobiłem naraz created_at z text na timestamptz, week_start z text na date, a JSON trzymany jako tekst na jsonb. To wszystko mogłem ustawić dobrze pierwszego dnia za tę samą cenę. Naprawianie tego później to ALTER COLUMN ... USING w każdej tabeli i przegląd całego kodu, który porównywał daty jako łańcuchy znaków.
  • UI nie powtarza nazwy kolumny. Typ wyprowadzony ze schematu i kilka funkcji dostępowych. Wtedy RENAME COLUMN łapie kompilator, a nie grep.

Kiedy naprawdę warto się zatrzymać

W edytorze planów biura przeszedłem z osobnych komponentów DualDesk i QuadDesk na jeden MultiDesk z liczbą miejsc, a potem zrefaktorowałem to jeszcze raz, żeby dodać miejsca wyłączone. Biurko na dwa i biurko na cztery wyglądają jak dwa przypadki; w rzeczywistości były tym samym z parametrem. To akurat widać pierwszego dnia, jeśli zatrzymasz się na dziesięć minut i zapytasz, czy 2 i 4 to stałe, czy dane.

Reguła, której używam: jeśli różnica między dwoma modelami to liczba, to są dane. Tu warto pomyśleć wcześniej.

Co do reszty — piszę schemat wiedząc, że jest zły. Nie optymalizuję trafienia, tylko to, żeby naprawienie go w przyszłym tygodniu kosztowało krótką migrację, a nie dzień grzebania w komponentach.