02Database5 min

I miei primi schemi di dati non sopravvivono quasi mai al primo mese

Lo schema del giorno 1 è un'ipotesi su un dominio che ancora non capisci. Accettarlo cambia come lo scrivi e quanto fa male cambiarlo.

Di Salva Castells

Immagine di copertina: Sundaram Perumal, CC BY-SA 4.0.
In questo post

In un'app per generare i turni di lavoro, in due settimane ho toccato lo schema tre volte in modo incompatibile. Ho creato una tabella worker_verifications per salvare il risultato dei controlli che il modello faceva su ogni lavoratore. Cinque giorni dopo l'ho cancellata del tutto e ho spostato la sua unica colonna utile, checked_rules, dentro worker_week_rules. Due giorni più tardi ho eliminato extra_closed_days dalla tabella weeks, che avevo aggiunto nella riprogettazione precedente.

Nessuno di quei cambiamenti è arrivato da una decisione di prodotto nuova. Sono arrivati dall'aver capito meglio il dominio.

Lo schema del giorno 1 e quello del giorno 30

La tabella che ho cancellato aveva otto colonne: ai_result, worker_hash, context_hash, verified_at, checked_rules e le chiavi. L'idea era mettere in cache la verifica: se l'hash del lavoratore e quello del contesto non cambiavano, non richiamavi il modello. Sembra ragionevole.

Quello che è successo è che l'unità reale del dominio non era "una verifica", era "le regole di un lavoratore in una settimana". E quella riga esisteva già. Le verifiche erano un attributo di quella riga, non un'entità. La migrazione che l'ha sistemato è corta e si legge da sola:

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

Quattro istruzioni. Il costo non è stato l'SQL.

Quali cambiamenti sono il segno che ci hai preso e quali no

Ci sono due tipi di cambio di schema e non si somigliano per niente.

  • Aggiungere una colonna. checked_week_rules in weeks, calendar_rules in calendars. È un ALTER TABLE ADD COLUMN ... DEFAULT con un default, si rilascia senza fermare niente, il codice vecchio continua a funzionare. Questo è il sistema che cresce. È buon segno.
  • Spostare l'entità. Cancellare una tabella, rinominare una colonna, cambiare da cosa dipende un dato. Vuol dire che la prima volta hai modellato male il dominio, non che il dominio sia cambiato.

In quello stesso progetto ho portato preferred_day_off integer a preferred_days_off text, perché è saltato fuori che un lavoratore poteva preferire due giorni. Anche questo è dei buoni: è informazione nuova sul mondo, non un errore di modellazione. La migrazione sta in tre righe e conserva i dati con un CAST.

Quello del tipo cattivo si riconosce perché il diff non è nella cartella delle migrazioni.

Il costo vero sta nella UI, non nell'SQL

In uno strumento di documentazione con IA, ho iniziato chiamando documents quello che l'utente caricava. Un mese dopo il concetto corretto era contexts: pezzi di testo che l'agente poteva leggere, citare e riscrivere, e che potevano arrivare da una URL, non solo da un file. Il nome era sbagliato e si trascinava dietro l'intero modello.

Il commit che l'ha cambiato non tocca lo schema. Tocca la pagina di chat, l'input, e cancella context-url-dialog, context-view e context-viewer interi: 1.127 righe eliminate in un colpo. Un RENAME COLUMN da due secondi e un giorno intero a toccare componenti.

E un caso ancora più economico da eseguire e più caro da propagare: in quello stesso progetto ho rinominato starCount in favoriteCount. L'SQL sono cinque righe in due migrazioni. Il diff tocca lo schema, due file di azioni di database e la UI che lo disegnava.

È questa la regola che mi sono portato dietro: il costo di cambiare il nome di una colonna è proporzionale a quanti componenti lo scrivono letteralmente. Non a quanto costa la migrazione.

Come scrivo oggi il primo schema

Quattro cose che faccio diversamente da allora, e nessuna è "progettare meglio".

  • Do il nome per quello che è, non per da dove viene. documents era l'origine (un file). contexts è il ruolo che il dato gioca nel sistema. Il secondo nome regge il cambio di origine.
  • Non creo una tabella per un attributo. Se la nuova entità ha sempre esattamente una riga per ogni riga di un'altra tabella, non è un'entità. È un gruppo di colonne. worker_verifications era questo e ci ho messo cinque giorni a vederlo.
  • Un tipo per colonna fin dall'inizio. Nella riprogettazione grande ho convertito allo stesso tempo created_at da text a timestamptz, week_start da text a date e il JSON salvato come testo a jsonb. Tutto questo potevo metterlo giusto il primo giorno allo stesso prezzo. Sistemarlo dopo è un ALTER COLUMN ... USING in ogni tabella e rivedere tutto il codice che confrontava date come stringhe.
  • La UI non ripete il nome della colonna. Un tipo derivato dallo schema e un paio di funzioni di accesso. Così un RENAME COLUMN lo becca il compilatore invece di un grep.

Quando invece vale la pena fermarsi

In un editor di planimetrie d'ufficio sono passato da avere componenti DualDesk e QuadDesk separati a un unico MultiDesk con il numero di posti, e poi l'ho rifattorizzato di nuovo per aggiungere posti disabilitati. Due scrivanie da due e da quattro sembrano due casi; in realtà erano lo stesso con un parametro. Questo sì che si vede il primo giorno se ti fermi dieci minuti a chiederti se il 2 e il 4 sono costanti o sono dati.

La regola che uso: se la differenza tra due modelli è un numero, è un dato. Lì sì che vale la pena pensarci prima.

Per tutto il resto, scrivo lo schema sapendo che è sbagliato. Quello che ottimizzo non è indovinare, è che sistemarlo la settimana prossima costi una migrazione corta e non un giorno a toccare componenti.