trilha

Tecnologias/02 - Bancos de Dados/01 - Relacionais/02 - MySQL6 min

DDL Online e Operação

Conceito

Mudar o schema de uma tabela de 300 milhões de linhas sem parar o serviço é o problema operacional mais recorrente do MySQL. Ele existe por duas razões estruturais:

  1. DDL não é transacional (1. PostgreSQL vs MySQL vs Oracle): cada ALTER faz commit implícito, e uma migração que falha no meio deixa estado intermediário.
  2. Historicamente, todo ALTER TABLE reconstruía a tabela inteira com lock de escrita.

O MySQL 8.0 melhorou muito isso com três algoritmos:

ALGORITHM O que faz Bloqueia escrita? Reconstrói?
INSTANT altera só os metadados não não — é instantâneo
INPLACE altera a tabela sem copiá-la (na maioria dos casos) não (só um lock breve no início/fim) às vezes
COPY cria tabela nova, copia tudo, troca sim sim
-- Declare a intenção e falhe se o MySQL não puder cumprir:
ALTER TABLE pedidos ADD COLUMN cupom VARCHAR(20), ALGORITHM=INSTANT;
-- ERROR se não for possível instantâneo — melhor descobrir agora que em produção

O que é INSTANT (8.0+)

  • ADD COLUMN (no fim da tabela; a partir do 8.0.29, em qualquer posição)
  • DROP COLUMN
  • Renomear coluna, alterar DEFAULT
  • SET/DROP de NOT NULL em alguns casos, adicionar valores ao fim de um ENUM

Limite importante: existe um teto de alterações instantâneas por tabela antes de exigir uma reconstrução (total_row_versions), então INSTANT não é infinito.

O que não é online e exige ferramenta

  • Mudar o tipo de uma coluna (INT → BIGINT) — reconstrói
  • Mudar a chave primária
  • Alterar charset/collation da tabela
  • Adicionar FULLTEXT (primeiro índice) ou SPATIAL
  • Qualquer coisa em versões antigas (5.6/5.7)

Para esses, o ecossistema resolveu com cópia com trigger ou binlog:

Ferramenta Como
pt-online-schema-change (Percona) cria tabela nova + triggers que espelham escritas, copia em lotes, troca por RENAME
gh-ost (GitHub) cria tabela nova e lê o binlog para espelhar (sem triggers) → menos impacto no caminho de escrita
gh-ost --host=primario --database=loja --table=pedidos \
  --alter="MODIFY id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" \
  --max-load=Threads_running=25 \
  --critical-load=Threads_running=100 \
  --chunk-size=1000 \
  --max-lag-millis=1500 \
  --allow-on-master --initially-drop-ghost-table --execute

O que faz o gh-ost superior em produção: ele pausa sozinho quando a carga ou o lag de réplica sobem (--max-load, --max-lag-millis), e a troca final é um RENAME atômico. Sem triggers, o caminho de escrita da aplicação não fica mais lento durante a migração.

Réplica é a vítima esquecida do DDL

Mesmo um ALTER "online" no primário chega na réplica como um evento que ela aplica serialmente. Um ALTER de 40 minutos no primário é 40 minutos de lag na réplica — e se sua aplicação lê da réplica, o incidente é lá, não no primário.

sql_mode: o interruptor de rigor

O MySQL histórico aceitava dado inválido com aviso. O sql_mode decide quanto disso sobrevive:

sql_mode = 'STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY,NO_ZERO_DATE,
            NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'
Modo Sem ele
STRICT_TRANS_TABLES INSERT de string em INT grava 0 com aviso
ONLY_FULL_GROUP_BY GROUP BY devolve coluna arbitrária de linha arbitrária
NO_ZERO_DATE '0000-00-00' é uma data válida (e explode em todo driver)

Verifique o sql_mode de produção hoje. Bancos antigos migrados de 5.x frequentemente rodam permissivos, e apertar depois quebra queries existentes — o que é bom saber antes do incidente.

performance_schema e sys

O MySQL moderno tem observabilidade boa, escondida atrás de nomes ruins. O schema sys são views prontas sobre o performance_schema:

SELECT * FROM sys.statement_analysis LIMIT 20;      -- queries por custo total
SELECT * FROM sys.schema_unused_indexes;            -- índices que ninguém usa
SELECT * FROM sys.schema_redundant_indexes;         -- índices duplicados
SELECT * FROM sys.schema_table_lock_waits;          -- quem espera lock de tabela
SELECT * FROM sys.io_global_by_file_by_bytes LIMIT 10;
SELECT * FROM sys.host_summary;                     -- carga por host de origem

Trade-offs

DDL online reduz o bloqueio e não elimina o custo. INPLACE ainda lê e escreve a tabela (I/O, redo, lag de réplica) e precisa de espaço temporário. "Online" quer dizer não bloqueia, não é grátis.

pt-osc usa triggers: correto e caro. Cada INSERT/UPDATE/DELETE durante a migração dispara uma trigger que escreve na tabela sombra — o que aumenta a latência de escrita da aplicação justamente durante uma operação longa. gh-ost evita isso lendo binlog, ao custo de precisar de acesso a binlog em formato ROW (5. Replicação e Alta Disponibilidade).

Migração precisa ser retomável, porque DDL não é transacional. Toda migração de MySQL deveria ser escrita como uma sequência de passos idempotentes (IF NOT EXISTS, checagem em information_schema), com registro de progresso próprio. Isso não é preciosismo: é a consequência direta do commit implícito.

O trade-off central: o MySQL escolheu simplicidade de motor e empurrou a complexidade de evolução de schema para ferramentas externas. É por isso que o ecossistema MySQL tem gh-ost e pt-osc, e o do Postgres não precisa de equivalente — lá o problema é o lock, e a solução é lock_timeout + CONCURRENTLY (5. Índices e o Planejador).

Exemplo prático

Migração em produção: o procedimento

-- 1) descubra o que o MySQL consegue fazer, antes de rodar
ALTER TABLE pedidos ADD COLUMN canal VARCHAR(20) NULL, ALGORITHM=INSTANT;
-- deu erro? tente INPLACE. Erro de novo? é caso de gh-ost.

-- 2) declare limites: nunca deixe um DDL esperar indefinidamente
SET SESSION lock_wait_timeout = 5;

-- 3) evolução compatível em duas fases (expand/contract) — sempre
--    Fase 1 (deploy A): adiciona coluna NULL, app escreve nos dois lugares
ALTER TABLE pedidos ADD COLUMN canal VARCHAR(20) NULL, ALGORITHM=INSTANT;
--    Fase 2 (deploy B): app lê só a nova, backfill em lotes
UPDATE pedidos SET canal = 'web' WHERE canal IS NULL LIMIT 5000;  -- em loop, com pausa
--    Fase 3 (deploy C): remove o código antigo e, por último, a coluna antiga
Backfill em lotes, com pausa

UPDATE de 50 milhões de linhas numa transação: undo gigante, lag de réplica enorme, rollback impossível de esperar. Faça em lotes de 1–5 mil com LIMIT, uma pausa entre eles, e um índice que ache o próximo lote rápido. Ver 3. Redo, Undo e Locks.

Checklist de operação MySQL

  • sql_mode estrito (verifique o valor real em produção)
  • utf8mb4 em servidor, banco, tabela e conexão do driver
  • gtid_mode = ON, binlog_format = ROW, sync_binlog = 1
  • super_read_only nas réplicas
  • innodb_buffer_pool_size em 60–75% da RAM (2. InnoDB por Dentro)
  • Pool do app limitado, com ConnMaxLifetime (5. Go em Produção)
  • gh-ost/pt-osc disponível e testado antes de precisar
  • performance_schema ligado; sys.statement_analysis no runbook
  • Slow query log com long_query_time = 1
  • Backup lógico (mysqldump/mydumper) e físico (xtrabackup/clone plugin), com restore testado

Como isso falha em produção

Sintoma Causa
ALTER travou toda a aplicação algoritmo COPY; ou lock de metadados esperando transação aberta
Migração falhou "no meio" DDL não transacional; a migração não era retomável
Réplica com 40 min de lag após deploy o ALTER é aplicado serialmente na réplica
Escrita lenta durante migração triggers do pt-osc no caminho de escrita
Waiting for table metadata lock uma transação longa impede o DDL — e o DDL enfileira todo o resto atrás
Emoji virou ? depois de migrar charset da conexão ainda latin1/utf8mb3
GROUP BY mudou de resultado ao apertar sql_mode a query sempre foi ambígua; o modo permissivo escondia

Aprofundamento aplicado

Migração também depende de quem já está usando a tabela

Uma operação online pode precisar de metadata locks no início ou no fim. Uma transação antiga pode impedir esse passo; novas consultas podem se acumular atrás da mudança. Online não significa ausência de espera, CPU, disco temporário ou impacto na replicação. Referência: limitações de DDL online.

Evolução em etapas

Para substituir uma coluna usada pela aplicação, uma estratégia é adicionar o novo campo, fazer as versões da aplicação conviverem, preencher o histórico em lotes e só remover o antigo após eliminar leitores e escritores dependentes.

O backfill deve ser retomável: grave progresso por chave, limite cada lote e monitore latência e atraso das réplicas. Uma única transação enorme torna rollback, retenção e recuperação mais caros.

Exercício: mantenha uma transação aberta e inicie uma alteração compatível com online em outra sessão de estudo. Resultado esperado: observe a fase que espera pelo lock; registre timeout e como a migração será retomada sem deixar o schema ambíguo.

Relacionado


Parte de MOC MySQL · Online DDL (docs) · gh-ost

Buscar

Busca por título, seção e texto das notas