Uma migração de esquema pode falhar mesmo que a alteração de código tenha passado nos testes. Em produção, uma aplicação geralmente não muda de uma só vez: podem coexistir processos web, workers de fila, tarefas agendadas e réplicas que executam versões diferentes. Se uma versão nova remove uma coluna que um worker antigo ainda lê, ou se uma coluna passa a ser obrigatória antes que todos os escritores a preencham, o deploy deixa de ser compatível.
As migrações de banco de dados sem interrupções em PHP tratam o esquema e os dados como componentes de um contrato operacional. O objetivo não é apenas executar uma instrução DDL correta, mas manter leituras e escritas disponíveis enquanto as versões antiga e nova coexistem, e preservar um caminho de recuperação realista.
Por que o esquema pode quebrar código já testado

Os testes locais geralmente partem de um banco de dados criado do zero ou atualizado de forma instantânea. Esse cenário omite a transição: dados históricos incompletos, milhões de linhas, bloqueios, conexões persistentes e consumidores assíncronos. Uma modificação aparentemente menor pode causar erros ou degradação.
- Renomear ou remover uma coluna quebra consultas, mapeadores ORM, relatórios e processos que ainda usam o nome anterior.
- Adicionar uma restrição
NOT NULLfalha se houver linhas antigas sem valor ou se um escritor ainda não conhecer o novo campo. - Alterar um tipo pode truncar valores, modificar comparações, invalidar índices ou provocar conversões custosas.
- Criar um índice ou reescrever uma tabela grande pode reter bloqueios e aumentar a latência das operações normais.
- Uma atualização em massa em uma única transação pode esgotar o log transacional, competir por recursos ou dificultar a replicação.
A pergunta relevante é: quais versões do código podem ler e escrever cada representação de um dado durante toda a janela de deploy? A resposta deve incluir os executáveis que não são reiniciados automaticamente, não apenas as requisições HTTP.
Compatibilidade temporária entre código, dados e processos
Durante um deploy gradual, há pelo menos três estados que devem ser compatíveis: código antigo, código novo e dados com formatos antigo, novo ou parcialmente transformado. A compatibilidade não consiste necessariamente em que todo consumidor entenda todos os formatos para sempre; consiste em definir uma janela delimitada em que as combinações previsíveis funcionem.
Por exemplo, para substituir full_name por first_name e last_name, não convém excluir o campo original no início. A versão nova pode escrever ambos os formatos e ler primeiro os campos novos quando estiverem completos, com uma alternativa explícita para o valor antigo. A versão anterior continua operando com full_name. Quando o histórico tiver sido transformado e os consumidores antigos removidos, a leitura poderá depender apenas da nova estrutura.
Evite que a compatibilidade temporária fique dispersa em controladores. Centralize a leitura, a escrita e a normalização em um serviço de domínio ou repositório. Assim, é possível auditar qual versão do formato é produzida, qual valor tem prioridade e quando remover a lógica transitória. Um template de migração não substitui este modelo de compatibilidade: o template executa alterações; o modelo define como a aplicação se comporta durante a transição.
O padrão expandir, migrar e remover
1. Expandir sem invalidar os consumidores atuais
A primeira fase adiciona capacidades sem remover as existentes: uma coluna nullable, uma nova tabela, um índice adicional ou uma estrutura paralela. Deve evitar alterações destrutivas e, quando o mecanismo exigir, planejar o método de criação para reduzir bloqueios. Adicionar uma coluna não implica que seja seguro impor imediatamente um valor padrão, recalcular todas as linhas ou declará-la obrigatória.
Antes de executar a operação, revise o tamanho da tabela, as consultas mais frequentes, chaves estrangeiras, espaço disponível, carga de replicação e comportamento específico do mecanismo de banco de dados. Faça testes em uma cópia representativa ou em um ambiente com volume e concorrência comparáveis. Defina também limites observáveis: duração, latência aceitável, taxa de erros e condição de cancelamento.
2. Publicar escritores e leitores compatíveis
Em seguida, é feito o deploy de código que entende as duas representações. Os novos escritores podem realizar escrita dupla se o custo e a consistência permitirem. Os leitores devem estabelecer uma precedência inequívoca: ler o novo valor se estiver validado; caso contrário, usar o antigo. Não use uma exceção como mecanismo de fallback, pois ela oculta defeitos nos dados e adiciona trabalho desnecessário ao caminho crítico.
A escrita dupla exige decisões explícitas. Se uma atualização afetar ambas as estruturas, determine se ela deve ser realizada na mesma transação. Se isso não for possível, projete uma reconciliação idempotente e métricas para detectar divergências. Eventos, caches, APIs e exportações também são consumidores: modificar apenas o repositório PHP não garante compatibilidade de ponta a ponta.
3. Migrar o histórico de forma retomável
Após habilitar o código compatível, transforme os registros existentes em lotes pequenos. Cada lote deve poder ser repetido sem duplicar efeitos nem corromper dados. Use uma chave estável ou um cursor persistente, limites de tamanho, registro do progresso e tentativas controladas. Evite paginar com offsets sobre conjuntos que mudam, pois isso pode pular ou reprocessar linhas.
$lastId = 0; // Para uma chave primária positiva e crescente.
while (true) {
$rows = $repository->findPendingAfterId($lastId, 500);
if ($rows === []) {
break;
}
foreach ($rows as $row) {
$repository->migrateIfNeeded($row);
$lastId = $row->id;
}
}Este padrão requer que findPendingAfterId() retorne linhas ordenadas de forma ascendente pela mesma chave usada como cursor. O cursor começa em um valor anterior ao primeiro identificador válido e avança somente após processar cada linha; o término depende de que a consulta não retorne nenhum lote. Em uma execução retomada, o valor confirmado de $lastId deve ser persistido. migrateIfNeeded() deve verificar o estado atual e produzir o mesmo resultado se for executado novamente.
Meça linhas pendentes, linhas transformadas, erros de validação e diferenças entre formatos. Não declare a fase concluída por ter percorrido a tabela: verifique também integridade referencial, unicidade, totais de negócio e amostras de registros críticos.
4. Alterar leituras, observar e remover
Quando o histórico estiver completo e os processos antigos tiverem deixado de ser executados, altere as leituras para usar exclusivamente a nova estrutura. Essa ativação pode ser gradual mediante uma configuração controlada, mas não deve ser confundida com o deploy: fazer o deploy disponibiliza o código; ativar modifica qual caminho o tráfego usa.
Observe erros de consulta, campos nulos inesperados, discrepâncias funcionais, tempos de resposta e saúde dos workers. Somente após uma janela de observação definida remova a escrita dupla, as dependências transitórias e, por fim, a coluna, o índice ou a tabela antiga. Manter estruturas obsoletas indefinidamente aumenta a ambiguidade e o custo; removê-las cedo demais elimina a recuperação simples.
Nulos, tipos, restrições e índices sem interromper a operação
Uma nova coluna geralmente começa como nullable porque os registros históricos ainda não a têm. A aplicação deve tratar a ausência como um estado previsto, não como um caso impossível. Após concluir e validar o backfill, pode-se impor uma restrição, desde que todos os escritores ativos forneçam um valor válido.
Para alterações de tipo, crie uma nova coluna e converta os valores de forma explícita. Isso permite detectar valores não conversíveis, aplicar regras de arredondamento ou normalização e comparar ambos os resultados antes de substituir a coluna anterior. Alterar diretamente o tipo pode ser adequado em casos limitados, mas deve ser justificado pelo comportamento do mecanismo, pelo volume e pela compatibilidade das consultas.
Os índices exigem uma análise equivalente. Um novo índice pode melhorar leituras, mas sua construção consome recursos e uma estratégia de criação inadequada pode bloquear escritas. Valide o plano de execução da consulta que precisa dele; não adicione índices por intuição. Se o mecanismo oferecer modalidades de criação com menos bloqueio, compreenda seus requisitos e limitações antes de incorporá-las ao plano.
Rollback: reversão de código nem sempre implica reversão de dados
Uma reversão operacional deve ser separada em decisões. Enquanto a estrutura antiga e a escrita dupla existirem, geralmente é possível voltar ao código anterior. Mas, se o novo formato tiver aceitado informações que o modelo antigo não consegue representar, desfazer o esquema não recupera semanticamente esses dados.
- Reversível: desativar uma nova leitura e voltar ao fallback, mantendo ambas as estruturas.
- Compensável: corrigir ou reconstruir dados a partir de uma fonte definida, com um processo auditado.
- Irreversível: excluir uma estrutura ou aceitar transformações que perdem precisão sem preservar o original.
Documente o ponto sem retorno, o responsável por autorizá-lo, as cópias ou exportações necessárias e o procedimento para pausar workers. Um método down() em uma ferramenta de migrações não é, por si só, um plano de rollback: ele pode reverter DDL, mas não garante a validade dos dados gravados durante a transição.
Testes, evidências e lista de verificação

Teste uma matriz de compatibilidade: código antigo com esquema expandido, código novo com dados ainda não migrados, código novo com dados transformados e processos assíncronos em versões mistas. Inclua migrações interrompidas e retomadas, registros inválidos, concorrência de escrita e restauração de uma versão anterior quando aplicável.
- Inventariar tabelas, consultas, workers, integrações e relatórios afetados.
- Definir o contrato temporário de leitura e escrita, incluindo valores nulos e prioridades.
- Separar expansão, deploy compatível, backfill, ativação e remoção em etapas independentes.
- Estimar o impacto de DDL, índices e lotes com dados representativos.
- Tornar o processo de dados idempotente, retomável e mensurável.
- Estabelecer validações de integridade e limites de observação posteriores.
- Documentar rollback, compensações e o ponto sem retorno.
- Remover a compatibilidade e a estrutura antigas somente com evidências de que não restam consumidores.
Aplicado com disciplina, este padrão transforma uma alteração de banco de dados de alto risco em uma sequência verificável. A chave é projetar a coexistência como parte do produto e da operação, não como um detalhe oculto dentro de uma migração.



