Num app de geração de escalas de turnos, em duas semanas mexi no esquema três vezes de forma incompatível. Criei uma tabela worker_verifications para guardar o resultado das checagens que o modelo fazia sobre cada trabalhador. Cinco dias depois apaguei ela inteira e coloquei sua única coluna útil, checked_rules, dentro de worker_week_rules. Dois dias mais tarde eliminei extra_closed_days da tabela weeks, que eu tinha adicionado no redesenho anterior.
Nenhuma dessas mudanças veio de uma decisão nova de produto. Vieram de entender melhor o domínio.
O esquema do dia 1 e o do dia 30
A tabela que apaguei tinha oito colunas: ai_result, worker_hash, context_hash, verified_at, checked_rules e as chaves. A ideia era cachear a verificação: se o hash do trabalhador e o do contexto não mudassem, você não chamava o modelo de novo. Soa razoável.
O que aconteceu é que a unidade real do domínio não era "uma verificação", era "as regras de um trabalhador numa semana". E essa linha já existia. As verificações eram um atributo dessa linha, não uma entidade. A migração que consertou isso é curta e se lê sozinha:
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";
Quatro comandos. O custo não foi o SQL.
Que mudanças são sinal de que você acertou e quais de que não
Há dois tipos de mudança de esquema e eles não se parecem em nada.
- Adicionar uma coluna.
checked_week_rulesemweeks,calendar_rulesemcalendars. É umALTER TABLE ADD COLUMN ... DEFAULTcom um default, sobe sem parar nada, o código antigo continua funcionando. Isso é o sistema crescendo. É bom sinal. - Mudar a entidade de lugar. Apagar uma tabela, renomear uma coluna, mudar de quem um dado depende. Isso significa que na primeira vez você modelou mal o domínio, não que o domínio tenha mudado.
Nesse mesmo projeto passei preferred_day_off integer para preferred_days_off text, porque acabou que um trabalhador podia preferir dois dias. Essa também é das boas: é informação nova sobre o mundo, não um erro de modelagem. A migração cabe em três linhas e preserva os dados com um CAST.
A do tipo ruim se percebe porque o diff não está na pasta de migrações.
O custo real está na UI, não no SQL
Numa ferramenta de documentação com IA, comecei chamando de documents o que o usuário subia. Um mês depois o conceito correto era contexts: pedaços de texto que o agente podia ler, citar e reescrever, e que podiam vir de uma URL, não só de um arquivo. O nome estava errado e arrastava o modelo inteiro.
O commit que mudou isso não toca no esquema. Toca a página de chat, o input, e apaga context-url-dialog, context-view e context-viewer inteiros: 1.127 linhas removidas de uma vez. Um RENAME COLUMN de dois segundos e um dia inteiro mexendo em componentes.
E um caso ainda mais barato de executar e mais caro de propagar: nesse mesmo projeto renomeei starCount para favoriteCount. O SQL são cinco linhas em duas migrações. O diff toca o esquema, dois arquivos de ações de banco de dados, e a UI que pintava aquilo.
Essa é a regra que levei comigo: o custo de mudar o nome de uma coluna é proporcional a quantos componentes o escrevem literalmente. Não ao que custa a migração.
Como escrevo hoje o primeiro esquema
Quatro coisas que faço diferente desde então, e nenhuma é "projetar melhor".
- Nomeio pelo que a coisa é, não por de onde ela vem.
documentsera a origem (um arquivo).contextsé o papel que o dado cumpre no sistema. O segundo nome aguenta a mudança de origem. - Não crio uma tabela para um atributo. Se a nova entidade sempre tem exatamente uma linha para cada linha de outra tabela, ela não é uma entidade. É um grupo de colunas.
worker_verificationsera isso e levei cinco dias para ver. - Um tipo por coluna desde o começo. No redesenho grande converti de uma vez
created_atdetextparatimestamptz,week_startdetextparadatee o JSON guardado como texto parajsonb. Tudo isso eu poderia ter feito certo no primeiro dia pelo mesmo preço. Consertar depois é umALTER COLUMN ... USINGem cada tabela e revisar todo o código que comparava datas como strings. - A UI não repete o nome da coluna. Um tipo derivado do esquema e um par de funções de acesso. Aí um
RENAME COLUMNé pego pelo compilador em vez de por umgrep.
Quando vale a pena parar
Num editor de plantas de escritório passei de ter componentes DualDesk e QuadDesk separados para um único MultiDesk com número de lugares, e depois refatorei outra vez para adicionar lugares desabilitados. Duas mesas, de dois e de quatro, parecem dois casos; na verdade eram o mesmo com um parâmetro. Isso dá para ver no primeiro dia se você parar dez minutos para perguntar se o 2 e o 4 são constantes ou são dados.
A regra que uso: se a diferença entre dois modelos é um número, é um dado. Aí sim vale a pena pensar antes.
Para o resto, escrevo o esquema sabendo que está errado. O que otimizo não é acertar, é que consertar na semana que vem custe uma migração curta e não um dia mexendo em componentes.
