02Bancos de dados5 min

Meus primeiros esquemas de dados quase nunca sobrevivem ao primeiro mês

O esquema do dia 1 é uma hipótese sobre um domínio que você ainda não entende. Assumir isso muda como você o escreve e quanto dói mudá-lo.

Por Salva Castells

Imagem de capa: Sundaram Perumal, CC BY-SA 4.0.
Neste post

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_rules em weeks, calendar_rules em calendars. É um ALTER TABLE ADD COLUMN ... DEFAULT com 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. documents era 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_verifications era isso e levei cinco dias para ver.
  • Um tipo por coluna desde o começo. No redesenho grande converti de uma vez created_at de text para timestamptz, week_start de text para date e o JSON guardado como texto para jsonb. Tudo isso eu poderia ter feito certo no primeiro dia pelo mesmo preço. Consertar depois é um ALTER COLUMN ... USING em 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 um grep.

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.