Ogni prodotto che ho rilasciato è sopravvissuto almeno a una scelta di framework. Il database è sopravvissuto a tutte. Le versioni di Next.js cambiano, l’ORM viene sostituito, il team ruota; le righe restano, insieme a tutte le ipotesi incorporate nello schema. Per questo considero SQL il vero contratto di un prodotto, non un dettaglio di implementazione nascosto dietro una classe repository.
Prisma copre gran parte delle mie giornate: CRUD tipizzato, migrazioni nel repository e uno schema leggibile da chi entra nel team. Ma leggo anche l’SQL che genera. Se una pagina di elenco lancia una query per ogni riga, se un report richiede una window function o una dashboard ha bisogno di un GROUP BY su tre join, scrivo la query a mano con $queryRaw o la inserisco in una view. Prima che qualcuno aggiunga Redis, apro EXPLAIN ANALYZE. Dietro la maggior parte dei ticket «ci serve una cache» manca un indice.
I vincoli sono il controllo qualità più economico che esista. NOT NULL, UNIQUE, FOREIGN KEY e CHECK si applicano a ogni scrittura, da ogni servizio, compreso lo script che qualcuno esegue a mano di venerdì. Zod protegge l’API; il database protegge la verità. Il denaro va salvato in unità minori intere o in NUMERIC, mai in float. I webhook di pagamento arrivano due volte: perciò la chiave di idempotenza richiede un indice univoco, non un commento nell’handler.
È nella concorrenza che si annidano i bug FinTech. Due richieste leggono un saldo, entrambe decidono che è sufficiente, entrambe scrivono: è un aggiornamento perso e nessuna validazione frontend può risolverlo. Preferisco una singola istruzione atomica —UPDATE ... SET balance = balance - $1 WHERE id = $2 AND balance >= $1— e poi verifico che sia cambiata esattamente una riga. In alternativa uso una transazione con SELECT ... FOR UPDATE e una tabella ledger append-only, così ogni movimento potrà essere spiegato in seguito. Nei sistemi con oltre 1,5 milioni di dollari di volume elaborato, «poterlo spiegare dopo» è il lavoro vero.
Gli indici devono seguire le query che esegui davvero, non le colonne che sembrano importanti. Negli indici composti vengono prima le colonne con uguaglianza e per ultima quella di intervallo o ordinamento; gli indici parziali servono per le righe «attive» e la paginazione keyset è preferibile a OFFSET 50000. Le migrazioni su tabelle vive si fanno per fasi: aggiungere la colonna nullable, riempirla a batch e poi aggiungere il vincolo; creare l’indice con CONCURRENTLY. Una migrazione che blocca la tabella degli ordini a mezzogiorno è un’interruzione di servizio con una pull request allegata.
Rifiuto una tabella entity-attribute-value «flessibile» al posto di uno schema, JSONB usato per nascondere campi fondamentali che nessuno vuole modellare, soft delete ovunque senza considerare i vincoli univoci e una cache davanti a una query che nessuno ha analizzato con EXPLAIN. Se mi mandi una pagina lenta o uno schema incerto, allega la query e il numero di righe. Ti risponderò con il piano, l’indice e il primo vincolo che aggiungerei.
Dai al prossimo prodotto qualcuno che resti.
Pronto a iniziare? Scrivimi: posso cominciare subito e restare. Collaborazioni a lungo termine, non passaggi di consegne dopo due settimane. Parliamo del progetto, dei requisiti e del modo migliore per procedere.