Migrações mais seguras com Ruby on Rails e PostgreSQL
Quando trabalhamos com grandes aplicações em produção, alterações no banco de dados exigem muito cuidado. Uma migration aparentemente simples pode causar locks, indisponibilidade, degradação de performance e impactos significativos no sistema. O problema é que muitas operações funcionam perfeitamente em ambiente local, mas se tornam custosas em produção, principalmente em…
Quando trabalhamos com grandes aplicações em produção, alterações no banco de dados exigem muito cuidado. Uma migration aparentemente simples pode causar locks, indisponibilidade, degradação de performance e impactos significativos no sistema.
O problema é que muitas operações funcionam perfeitamente em ambiente local, mas se tornam custosas em produção, principalmente em tabelas grandes e com alto volume de acesso concorrente.
Neste artigo, vamos explorar práticas para implementar migrations mais seguras utilizando Ruby on Rails com PostgreSQL, abordando locks, índices, transações, alterações estruturais e estratégias utilizadas em ambientes de produção.
Por que migrations podem ser perigosas?
Migrations podem ser perigosas em produção porque alteram a estrutura ou os dados do banco enquanto a aplicação continua em uso. Os principais riscos estão nos locks e no consumo de recursos da infraestrutura. Algumas operações podem bloquear leituras ou escritas, enquanto outras podem exigir muito processamento, principalmente em tabelas grandes.
Alguns exemplos comuns:
• Atualizar milhões de registros em uma única transação
• Alterar tipo de coluna em tabelas grandes
• Remover colunas utilizadas pela aplicação
• Criar índices sem utilizar CONCURRENTLY
• Adicionar uma coluna com valor default em versões antigas do PostgreSQL
Como locks podem impactar suas migrations
O PostgreSQL utiliza diferentes tipos de lock para garantir a consistência dos dados durante leituras, escritas e alterações estruturais. Em migrations, o ponto de atenção é que algumas operações podem adquirir locks mais restritivos, bloqueando leituras ou escritas enquanto são executadas.
Alguns exemplos de locks comuns:
• ACCESS SHARE: lock mais leve, utilizado em operações de leitura, como SELECT. Ele não bloqueia outras leituras ou escritas comuns, mas entra em conflito com locks mais fortes, como ACCESS EXCLUSIVE.
• ROW EXCLUSIVE: utilizado em INSERT, UPDATE e DELETE, indicando que linhas da tabela estão sendo modificadas. Normalmente não bloqueia leituras comuns.
• SHARE UPDATE EXCLUSIVE: utilizado em operações de manutenção, como VACUUM, ANALYZE e CREATE INDEX CONCURRENTLY. Normalmente não bloqueia leituras e escritas comuns, mas pode bloquear ou ser bloqueado por alterações estruturais mais fortes.
• ACCESS EXCLUSIVE: lock mais restritivo, utilizado em operações como DROP COLUMN, ALTER COLUMN TYPE, TRUNCATE, criação de índices sem CONCURRENTLY e outras alterações estruturais em tabelas. Enquanto ativo, pode bloquear SELECT, INSERT, UPDATE e DELETE.
Estratégias para migrations mais seguras
Migrations transacionais e não transacionais
Por padrão, o Rails executa migrations dentro de uma transaction. Isso significa que, caso ocorra algum erro, todas as alterações podem ser revertidas automaticamente. Esse comportamento aumenta a segurança das migrations, mas também possui limitações, pois algumas operações do PostgreSQL não podem ser executadas dentro de transações, como CREATE INDEX CONCURRENTLY.
Nesses casos, é necessário utilizar:
disable_ddl_transaction!
Isso faz com que a migration seja executada fora de uma transaction.
Migrations reversíveis
No Rails, muitas migrations podem ser revertidas automaticamente utilizando rollback. Quando utilizamos o método change, o Rails consegue identificar como desfazer determinadas operações.
Exemplo:
class AddStatusToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :status, :string
end
end
Nesse caso, o Rails sabe que o rollback deve remover a coluna criada. Porém, algumas operações precisam de informações suficientes para serem revertidas corretamente. Um exemplo é remover uma coluna: se o tipo da coluna for informado, o Rails consegue recriá-la no rollback.
Exemplo:
class RemoveFullNameFromUsers < ActiveRecord::Migration[7.0]
def change
remove_column :users, :full_name, :string
end
end
Quando a reversão exige mais controle ou envolve uma lógica que o Rails não consegue inferir automaticamente, é mais seguro utilizar os métodos up e down explicitamente.
class ChangeUsersAgeType < ActiveRecord::Migration[7.0]
def up
change_column :users, :age, :bigint
end
def down
change_column :users, :age, :integer
end
end
Essa abordagem torna o comportamento da migration mais previsível durante rollbacks.
Migrations idempotentes
Uma migration idempotente é aquela que pode ser executada mais de uma vez sem gerar efeitos colaterais inesperados. Isso é importante principalmente em ambientes distribuídos e pipelines de deploy.
Exemplo:
add_column :users, :status, :string unless column_exists?(:users, :status)
Adicionando colunas de forma mais segura
Adicionar colunas em tabelas grandes exige atenção, principalmente quando envolvem DEFAULT e NOT NULL. Uma abordagem mais segura normalmente divide a alteração em etapas:
- Adicionar a coluna permitindo
NULL - Popular os registros existentes aos poucos
- Definir valor default
- Adicionar
NOT NULL
Exemplo:
add_column :users, :status, :string
Depois:
User.in_batches(of: 1000) do |relation|
relation.update_all(status: "active")
end
E somente após garantir que todos os registros possuem valor:
change_column_default :users, :status, "active"
change_column_null :users, :status, false
Essa estratégia reduz locks prolongados e evita operações pesadas em tabelas grandes.
Criando índices de forma segura
Criar índices em tabelas grandes pode causar impacto significativo dependendo da estratégia utilizada. Por padrão, o PostgreSQL bloqueia operações de escrita enquanto o índice é criado. Para reduzir esse impacto, o PostgreSQL oferece o CREATE INDEX CONCURRENTLY, permitindo que leituras e escritas continuem acontecendo durante a criação do índice.
No Rails, isso normalmente é feito assim:
class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
add_index :users, :email, algorithm: :concurrently
end
end
O CREATE INDEX CONCURRENTLY precisa rodar fora de uma transação porque o PostgreSQL cria o índice em etapas, permitindo que a tabela continue recebendo leituras e escritas durante o processo. Por isso, no Rails, é necessário usar disable_ddl_transaction! junto com algorithm: :concurrently.
Também é comum utilizar:
disable_statement_timeout!
disable_lock_timeout
O disable_statement_timeout! evita que migrations longas sejam interrompidas por timeout.Já o disable_lock_timeout! evita falhas caso a migration demore para conseguir adquirir lock na tabela.
Preenchimento de dados em lotes
Atualizar muitos registros pode até ser feito dentro de uma migration, mas essa abordagem aumenta o tempo de deploy e pode manter a migration executando por muito tempo. Em tabelas grandes, o mais seguro costuma ser separar essa atualização em um processo assíncrono, como uma rake task, um job ou um script controlado, executando os registros em lotes.
User.in_batches(of: 1000) do |relation|
relation.update_all(active: true)
end
Monitorando queries bloqueadas
Durante uma migration, é comum que queries fiquem aguardando liberação de lock no PostgreSQL. Quando isso acontece, podemos utilizar a view pg_stat_activity para identificar queries bloqueadas:
SELECT pid, query, state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE wait_event_type IS NOT NULL;

Também é possível utilizar pg_locks para analisar locks ativos no banco.
SELECT
pid,
locktype,
relation::regclass AS relation,
mode,
granted
FROM pg_locks
ORDER BY granted, pid DESC;

Conclusão
Implementar migrations mais seguras é uma prática essencial em aplicações Ruby on Rails que utilizam PostgreSQL em produção.
Conforme o volume de dados cresce, pequenas alterações estruturais passam a exigir maior atenção, principalmente em tabelas críticas e com alto volume de acesso concorrente.
Entender como o PostgreSQL trabalha com locks, transações e alterações estruturais ajuda a reduzir riscos durante deploys e evitar indisponibilidade da aplicação.
Criar índices concorrentemente, dividir alterações em etapas, monitorar locks e evitar operações pesadas em uma única transaction são práticas importantes para manter estabilidade e previsibilidade em ambientes de produção.
Referências
- ATKINSON, Andrew. High Performance PostgreSQL for Rails. Pragmatic Bookshelf, 2024.
- Documentação oficial do PostgreSQL
- Documentação oficial do Ruby on Rails