02 de set. de 2025·7 min de leitura

Checklist de refatoração de banco de dados para mover um protótipo para produção

Use este checklist de refatoração de banco de dados para levar um schema de protótipo à produção: nomenclatura, constraints, índices, chaves estrangeiras, tipos de dados e passos de rollback.

Checklist de refatoração de banco de dados para mover um protótipo para produção

Por que bancos de protótipo falham em produção

Bancos de dados de protótipo são construídos para provar que um recurso funciona uma vez, não para se comportar do mesmo jeito todo dia sob carga real. Quando usuários reais chegam, o banco de dados é onde atalhos pequenos viram grandes quedas.

O que costuma quebrar primeiro é a previsibilidade. Colunas aceitam valores que você não esperava. O mesmo registro pode existir duas vezes. Um relacionamento “ausente” (como um pedido sem cliente) entra porque nada o impõe. Depois, a aplicação falha de formas confusas e o debug vira tentativa e erro.

Performance é a próxima surpresa. Um protótipo roda com tabelas pequenas, então queries lentas passam despercebidas. Em produção, uma tela popular pode disparar varreduras por milhares ou milhões de linhas, e os tempos de resposta disparam.

Builds de hackathon e apps gerados por AI (de ferramentas como Lovable, Bolt, v0, Cursor ou Replit) frequentemente esbarram nesses problemas cedo. Eles entregam rápido, mas a camada de banco de dados costuma ser “boa o suficiente” até que não seja mais.

O objetivo aqui é simples: comportamento previsível, mudanças seguras e debug mais fácil quando algo der errado.

Uma expectativa importante: faça isso em passos pequenos, não numa grande reescrita. Em vez de renomear todas as tabelas de uma vez, comece impedindo novos dados ruins (constraints), depois backfille as linhas antigas, então altere o código da aplicação e só então limpe colunas legadas. Cada passo deve ser fácil de testar e fácil de desfazer.

Antes de mudar qualquer coisa: faça um inventário e defina o escopo

Refatorações de banco de dados dão errado quando pessoas começam a “limpar” sem saber o que existe e do que a aplicação depende. Antes de mexer numa coluna, construa um inventário e combine o que significa “concluído”.

Liste todo objeto de banco que sua app pode depender, não apenas tabelas. Protótipos costumam esconder lógica em lugares estranhos.

Anote:

  • Tabelas e colunas-chave (especialmente IDs, timestamps e campos de status)
  • Views, triggers, funções/procedures armazenadas
  • Histórico de migrações (o que rodou, o que falhou, o que foi editado à mão)
  • Jobs e scripts que escrevem dados (imports, tarefas cron, workers em background)
  • Ambientes e cópias (dev, staging, snapshots de produção)

Em seguida, esclareça propriedade e promessas. Quem é responsável pelos dados (engenharia, ops, um cliente)? O que o produto promete aos usuários: podem deletar contas, exportar dados ou mudar emails? Essas promessas decidem o que você pode renomear, mesclar ou dropar com segurança.

Registre os pontos de dor atuais em linguagem simples e os vincule a evidências: “login está lento” (qual query?), “vemos duplicados” (quais tabelas/campos?), “relacionamentos estão implícitos” (onde chaves faltantes criam linhas órfãs?). Se estiver corrigindo um app gerado por AI, espere migrações pela metade e suposições escondidas.

Finalmente, defina escopo com uma linha de base chata: contagem de linhas, maiores tabelas e 3–5 fluxos críticos de usuário que devem continuar funcionando (cadastro, checkout, busca, relatórios admin). Decida o que consertar agora versus depois. Por exemplo: constraints e índices-chave agora, views de relatório e polimento de nomes depois.

Uma linha de base rápida vence uma refatoração esperta que quebra a produção.

Limpeza de nomes e estrutura que compensa rápido

Schemas de protótipo crescem por acidente. Uma tabela usa camelCase, outra usa snake_case, IDs têm nomes diferentes e ninguém tem certeza do que é seguro tocar. Limpar isso cedo facilita qualquer mudança posterior, de constraints a debug de incidentes.

Comece escolhendo um estilo de nomenclatura e usando-o em todo lugar.

Um conjunto simples de convenções:

  • Um padrão para tabelas (singular ou plural) e colunas (snake_case é comum)
  • Uma convenção de chave primária (por exemplo, toda tabela tem uma coluna id)
  • Uma convenção de chave estrangeira (por exemplo, user_id sempre aponta para users.id)
  • Um padrão de nomes para índices (para identificar duplicatas rapidamente)

Renomeie campos que convidam a bugs. createdAt e created_at em tabelas diferentes levam a joins errados e mapeamentos de ORM quebrados. O mesmo vale para id significando coisas diferentes em lugares distintos. Se uma coluna armazena referência a usuário, nomeie-a como referência a usuário.

Depois trate do peso morto. Schemas iterados rápido costumam manter colunas antigas que a app não lê mais. Se você não está pronto para dropar uma coluna, marque-a como deprecada em um comentário e pare de escrever nela.

Adicione comentários curtos onde o significado não for óbvio, especialmente para:

  • Valores especiais permitidos (mesmo que ainda não sejam aplicados)
  • Unidades ou formatos mistos (centavos vs dólares, UTC vs hora local)
  • Campos fáceis de usar errado (flags de soft delete, campos de status)

Constraints: torne mais difícil inserir dados ruins

Constraints são a forma mais rápida de transformar dados de protótipo em algo confiável.

Comece pelas chaves primárias. Toda tabela deve ter uma, e sua app deve realmente usá-la. Se uma tabela depende de “nome” ou “created_at” como identificador, você vai acabar com colisões, updates quebrados e joins confusos.

Adicione NOT NULL onde sua app espera um valor sempre. Se um usuário precisa ter email, exija-o. Se um pedido precisa de status, exija-o. Antes de aplicar NOT NULL, preencha (backfill) as linhas existentes para que a migração não falhe no meio.

Adicione constraints UNIQUE para parar duplicatas que você limparia para sempre: emails, IDs externos, códigos de convite, slugs e “um perfil por usuário”. Se duplicatas já existem, decida qual registro vence, mescle com cuidado e só então imponha UNIQUE.

Use CHECK para regras simples de sanidade que nunca deveriam ser violadas. Mantenha-as claras e previsíveis.

Lista rápida de constraints

Para cada tabela:

  • Existe chave primária
  • Campos obrigatórios estão NOT NULL (depois do backfill)
  • Identificadores naturais são UNIQUE (email, external_id, slug)
  • Regras básicas de sanidade são validadas com CHECK (amount >= 0, rating entre 1 e 5)
  • Defaults batem com a realidade (status default pending, timestamps auto-preenchidos)

Decida o que vive no banco vs na aplicação, depois seja consistente. Regras de negócio que mudam com frequência podem ficar no código, mas identidade, unicidade e validações básicas pertencem ao banco.

Exemplo: um protótipo pode permitir múltiplos usuários com o mesmo email e senhas nulas. Em produção, isso leva a logins quebrados e risco de takeover de conta. Um passo prático é limpar as linhas existentes, depois adicionar UNIQUE(email) e regras NOT NULL para que dados ruins não reapareçam.

Chaves estrangeiras e relacionamentos: transforme suposições em regras

Stop login bugs at the source
Find and fix broken authentication flows tied to duplicate or inconsistent user data.

Muitos protótipos confiam em “vamos cuidar no código”. Em produção, isso vira linhas órfãs, casos de borda e debug bagunçado.

Comece encontrando relacionamentos que existem na app, mas não no banco. Se seu código usa userId, team_id ou order_id, trate isso como um relacionamento real mesmo que nada o imponha ainda.

Uma forma prática de mapear o que é real:

  • Escaneie tabelas por colunas *_id e confirme para que elas apontam no código
  • Procure por jobs de limpeza ou correções que mencionem “parent missing” ou “not found”
  • Conte órfãos prováveis (linhas filhas sem pai correspondente) antes de adicionar regras
  • Adicione foreign keys apenas para relacionamentos dos quais você realmente depende
  • Adicione um índice na coluna de foreign key quando você fizer joins ou filtros nela

Ao adicionar uma foreign key, decida o que acontece em deletes com base nas regras do produto:

  • RESTRICT: bloqueia deletes quando filhos existem (bom para faturas, logs de auditoria)
  • CASCADE: remove filhos automaticamente (bom para dados temporários ou derivados)
  • SET NULL: mantém filhos mas os desvincula (bom para propriedade opcional)
  • Soft delete ao invés de hard delete quando o histórico importa

Relacionamentos circulares podem morder durante migrações. Se users referencia teams e teams referencia users (dono), adicione um lado como nullable primeiro, backfille em lotes e então aperte as constraints depois que os dados estiverem consistentes.

Foreign keys melhoram a corretude, mas não tornam consultas rápidas automaticamente. Se você faz join constante orders.customer_id com customers.id, indexar orders.customer_id geralmente é a diferença entre “ok em staging” e “doloroso em produção”.

Tipos de dados e defaults: reduza surpresas futuras

Schemas de protótipo costumam usar text para tudo porque “funciona”. Problemas de produção aparecem depois: ordenação quebra, relatórios mentem e pequenos erros de input viram trabalho de limpeza.

Substitua tipos vagos por aqueles que correspondem ao significado. Datas devem ser datas, flags devem ser booleanos e contadores devem ser inteiros.

Dinheiro e medições exigem cuidado especial. Evite tipos de ponto flutuante para moeda. Use números de precisão fixa (por exemplo, numeric(12,2)) para que somas batam sempre. Faça o mesmo para pesos, distâncias e percentuais: escolha uma precisão e mantenha-a.

Fusos horários são outra fonte comum de bugs. Um padrão prático é armazenar timestamps em UTC no banco e formatá-los para os usuários na app.

Defaults removem “valores mágicos” do código frontend. Em vez de confiar que o frontend vai preencher tudo, dê defaults claros como created_at = now() e um status com um pequeno conjunto de valores permitidos.

Antes de impor novos tipos, planeje limpeza e backfill. Um padrão seguro:

  • Adicione uma coluna nova com o tipo correto
  • Backfille a partir da coluna antiga (tratando valores ruins)
  • Valide com contagens e checagens manuais
  • Altere a app para escrever na nova coluna
  • Drop ou trave a coluna antiga

Exemplo: se users.is_active hoje é text com valores como "yes", "Y", "1" e em branco, defina um mapeamento, backfille para booleano e só então adicione NOT NULL e um default após validação.

Índices: acelere queries reais sem exagerar

Índices podem fazer um protótipo parecer pronto para produção, mas também podem retardar escritas se adicionados sem critério. Comece pelo que os usuários realmente fazem.

Trabalhe a partir das ações de usuário mais lentas para as queries por trás delas. Uma tela de login que travuca, um dashboard que carrega 5 segundos tarde ou uma página de “pedidos” que dá timeout normalmente mapeia para um pequeno conjunto de queries que valem a pena corrigir primeiro.

Crie índices que correspondam a filtros e joins reais. Se sua app frequentemente faz “encontrar invoices para conta X nos últimos 30 dias”, um índice só em created_at pode não ajudar. Um índice composto em (account_id, created_at) costuma ajudar porque corresponde à forma como a query reduz resultados.

Evite indexar tudo. Cada índice extra adiciona custo em inserts, updates e deletes. Esse tradeoff é aceitável para tabelas de leitura majoritária, mas doloroso para tabelas de alta escrita como logs de eventos.

Uma passagem curta por índices:

  • Capture as queries exatas por trás das top 5 telas/API lentas
  • Indexe colunas usadas em WHERE e chaves de JOIN (especialmente colunas de foreign key)
  • Adicione índices compostos quando queries filtram por duas colunas juntas
  • Evite índices sobrepostos (novos que tornam antigos inúteis)
  • Planeje uma verificação posterior para remover índices nunca usados

Mantenha uma nota de rollback para cada mudança de índice. Dropar um índice novo é fácil, mas recriar um índice droppado durante um incidente pode demorar em tabelas grandes.

Checagens de segurança e privacidade para um schema de produção

Get to production-ready faster
Get expert-verified fixes fast, with most projects completed within 48-72 hours.

Protótipos costumam confiar demais: assumem input limpo, acesso correto e logs inofensivos. Produção é o oposto. Decida o que deve ser protegido e faça o banco suportar isso.

Comece nomeando dados sensíveis: hashes de senha, tokens de reset, tokens de sessão, chaves de API, emails, telefones, endereços e qualquer coisa que identifique um usuário. Se não precisa armazenar, não armazene.

Armazenamento, logging e controle de acesso

Certifique-se de que segredos não estejam em colunas em texto puro, mensagens de erro ou tabelas de debug. Tokens devem ser hasheados quando possível e curtos.

Cheque o logging também. Muitos protótipos logam corpos de requisição, o que pode capturar senhas ou PII sem querer.

Defina roles de banco para que a conta da app só faça o que precisa. Como regra, a role da app não deveria conseguir dropar tabelas ou acessar dados admin-only.

Uma configuração simples de roles:

  • Role da app: leitura/escrita apenas nas tabelas necessárias
  • Role admin: migrações, backfills, correções pontuais
  • Role read-only: views de analytics ou suporte (se necessário)
  • Credenciais separadas por ambiente (dev, staging, prod)

Injeção e decisões de retenção

Mesmo com um schema “seguro”, queries inseguras podem quebrar tudo. Procure por SQL dinâmico montado a partir de strings (especialmente filtros, campos de ordenação e buscas).

Finalmente, decida regras de retenção e deleção. Exemplos: “Deletar contas inativas após 24 meses” ou “Excluir attachments de suporte após 30 dias”. Documente isso e desenhe para suportar (timestamps, flags de soft-delete, jobs de purge se necessário).

Passo-a-passo: uma ordem segura para mudanças de schema

Refatorações falham quando você muda demais de uma vez e não tem volta fácil. O objetivo não é design perfeito. É mudanças reversíveis.

Comece provando que você pode recuperar. Faça um backup fresco, restaure em um ambiente separado e rode um smoke test. Se você não consegue restaurar rapidamente, não tem um plano de rollback.

Uma ordem segura que a maioria dos times pode seguir:

  1. Adicione antes de remover. Crie tabelas, colunas e constraints de maneira não breaking. Mantenha as partes antigas por enquanto.
  2. Backfill em pequenos lotes. Mova dados em pedaços para acompanhar erros, tempo de lock e performance. Verifique contagens e registros após cada lote.
  3. Escreva em ambos os lugares (temporariamente). Atualize a app para que novas escritas vão ao novo schema (e opcionalmente espelhem no antigo) enquanto leituras podem cair de volta se necessário.
  4. Troque leituras para o novo schema. Altere queries, relatórios e jobs em background. Fique de olho em queries lentas e casos de borda ausentes.
  5. Remova partes antigas por último. Após um período estável, drope colunas antigas, tabelas antigas e caminhos temporários de código.

Exemplo: substituir users.phone (texto livre) por user_phones (uma linha por telefone, validado). Adicione a tabela nova, backfille, ajuste a app para ler user_phones e só depois remova users.phone.

Documente cada mudança com um gatilho de rollback: “se a taxa de erro subir X”, “se a latência de checkout aumentar Y” ou “se o backfill criar mais de N linhas inválidas”.

Armadilhas comuns em refatorações de banco

Prepare for safe releases
Get deployment-ready guidance so schema changes don’t lock tables in production.

A maioria das refatorações falha pelos mesmos motivos: muitas mudanças de uma vez, pouca prova de que os dados estão limpos e pouco respeito pelo que já está rodando.

Armadilhas que causam outages (ou rollbacks lentos)

Agrupar várias mudanças breaking numa única migração é um erro clássico. Mantenha mudanças pequenas e saiba sempre como voltar ao estado “normal” antes de deployar.

Problemas comuns:

  • Apertar regras antes de consertar linhas existentes. Novos NOT NULL, UNIQUE ou CHECK falharão se os dados antigos já os quebram.
  • Faltar índices em colunas de relacionamento e filtros comuns (como status, user_id, created_at).
  • Renomear colunas sem perseguir cada dependência: jobs em background, scripts, dashboards, ETLs e relatórios pontuais.
  • Migrações que travam tabelas por mais tempo que o esperado. Grandes backfills, constraints e builds de índice podem bloquear escritas.
  • Nenhum plano claro de rollback. Se você não consegue reverter rápido, acaba correndo para consertar para frente sob pressão.

Exemplo: um protótipo tem uma tabela orders onde algumas linhas têm user_id nulo, e status é texto livre. Se você adiciona uma foreign key e um CHECK de uma vez, a migração falha e o deploy empaca. Um caminho mais seguro é: backfille user_id onde possível, colocar o resto em quarentena, padronizar status e só então adicionar constraints.

Lista prática: do protótipo bagunçado ao pronto para produção

Uma história comum: um protótipo gerado por AI entrega rápido com tabelas como users, orders e payments. Funciona em demos, depois o tráfego de produção chega e bugs estranhos aparecem.

Um time nota que clientes às vezes entram na conta errada. A causa é simples: users.email não é único, então duplicatas existem (como [email protected] e [email protected]). Outro bug: um pedido aparece sem usuário porque orders.user_id é só um inteiro, não um relacionamento real. O suporte também vê pagamentos sem pedido porque payments.order_id está ausente ou aponta para uma linha deletada.

Uma lista de ações que corrige os maiores problemas sem redesenhar tudo:

  • Normalize e imponha emails únicos (lowercase), depois adicione uma constraint UNIQUE.
  • Adicione foreign keys: orders.user_id REFERENCES users.id e payments.order_id REFERENCES orders.id. Use regras de delete claras (frequentemente RESTRICT para pagamentos).
  • Corrija tipos que causam bugs silenciosos: use números de precisão fixa para dinheiro, timestamps adequados e evite IDs em texto livre.
  • Adicione alguns índices de alto valor: orders(user_id, created_at) para páginas de conta e payments(order_id) para queries de reconciliação.

Planeje um momento de rollback: você adiciona a regra de email único e ela falha por duplicatas. Faça em fases: adicione a coluna normalizada e backfille, dedupe (mantenha o usuário ativo mais novo, mescle pedidos relacionados), e só então adicione a constraint. Se precisar reverter, você pode dropar a constraint nova sem desfazer o trabalho de limpeza.

Próximos passos: rode uma auditoria curta e mude em fases. Se você herdou um protótipo gerado por AI, FixMyMess (fixmymess.ai) foca em diagnosticar problemas de banco e código como migrações frágeis, segredos expostos e autenticação quebrada, para que você possa consertar as partes de maior risco primeiro antes de mexer em qualquer polimento.

Perguntas Frequentes

Qual é a primeira mudança no banco de dados que devo fazer ao migrar de protótipo para produção?

Comece pela integridade dos dados. Adicione chaves primárias onde faltam, preencha campos obrigatórios (backfill) e aplique NOT NULL e UNIQUE em identificadores como email ou IDs externos. Isso impede que novos dados ruins entrem, o que torna todas as mudanças posteriores mais seguras.

Como refatoro um schema sem arriscar um grande incidente?

Faça em passos pequenos e reversíveis. Adicione colunas ou tabelas novas primeiro, faça o backfill em lotes, ajuste a aplicação para escrever/ler no novo caminho e remova colunas antigas só após um período estável. Cada etapa deve ter um gatilho de rollback claro e um caminho rápido para desfazer.

O que preciso inventariar antes de mexer em qualquer coluna?

Inventarie tudo que interage com o banco, não apenas tabelas. Verifique views, triggers, funções/procedures armazenadas, jobs em background, scripts cron, scripts administrativos pontuais e consultas de analytics. Renomear colunas é seguro só depois de atualizar todas as dependências.

Quais convenções de nomenclatura realmente importam para a estabilidade em produção?

Escolha uma convenção e aplique de forma consistente. Um padrão comum é snake_case, chave primária chamada id e chaves estrangeiras como user_id apontando para users.id. Nomes consistentes reduzem bugs em ORMs e facilitam o debug em incidentes.

Como adiciono `NOT NULL` ou `UNIQUE` se os dados já estão bagunçados?

Preencha os dados primeiro, depois imponha a restrição. Encontre as linhas que violam a regra, decida como corrigi-las ou colocá-las em quarentena e só então adicione a restrição para que a migração não falhe no meio do deploy. Em tabelas grandes, teste o tempo de backfill e da constraint em uma cópia de staging para evitar locks longos.

Quando devo adicionar chaves estrangeiras, e qual é o risco?

Adicione foreign keys para relacionamentos dos quais a aplicação realmente depende, como orders.user_id ou payments.order_id. Antes de impor, meça linhas órfãs e limpe-as; caso contrário a migração vai falhar. Também indexe a coluna foreign key quando você fizer joins ou filtros frequentemente, porque corretude sozinha não torna as queries rápidas.

Como escolho os índices certos sem exagerar?

Parta da dor real do usuário: as telas e APIs mais lentas. Capture a query exata e adicione índices que combinem com os filtros WHERE e as chaves de JOIN; índices compostos ajudam quando a consulta filtra por duas colunas juntas. Evite adicionar índices “só por precaução”, pois cada índice torna gravações mais lentas e aumenta manutenção.

Quais erros de tipo de dado causam mais bugs em produção?

Use tipos que reflitam o significado: booleanos para flags, inteiros para contadores, timestamps para tempo e números de precisão fixa para dinheiro. Armazene timestamps em UTC por padrão e formate para o usuário na aplicação. Para mudar tipo com segurança, adicione uma nova coluna tipada, faça o backfill, valide, altere as escritas e depois aposente a coluna antiga.

Que checagens de segurança devo rodar em um banco de protótipo antes do lançamento?

Presuma que protótipos vazam: segredos em colunas, permissões de banco amplas e logs que capturam PII. Use roles com menor privilégio para a conta da aplicação, mantenha privilégios de admin separados e evite armazenar tokens raw quando for possível hashear. Defina regras de retenção e deleção cedo para não acumular dados sensíveis indefinidamente.

O que é diferente ao refatorar um banco gerado por AI ou feito numa hackathon?

Você provavelmente tem migrações pela metade, suposições escondidas e regras inconsistentes nunca impostas. O caminho mais rápido é uma auditoria focada que identifique fluxos de autenticação quebrados, segredos expostos, constraints faltando e consultas que vão falhar sob carga. Times frequentemente contratam um serviço como FixMyMess quando precisam de diagnóstico rápido e correções verificadas sem reescrever tudo.