Migracao de banco — Arduino e IoT — semana 6 do 3o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 6 · Migracao de banco — Material de Apoio Arduino

Semana 6 de 15· 3o trimestre · 05/09 a 10/12

Migracao de banco

O banco que roda na aula da ultima semana precisa virar o banco de amanha.

Aula 1 — Migracao: mudar o schema sem perder dado

Objetivos

  • Explicar por que mudar o schema sem controle é a mesma coisa que editar a tabela com o produção em pé, e nomear o risco.
  • Escrever uma migração versionada, com número da migração e ordem de aplicação, e rodar as duas do projeto.
  • Fazer um backfill: adicionar coluna, preencher coluna nova e remover coluna velha, sem perder uma linha.
  • Escrever o rollback de uma migração e rodar para desfazer, medindo o que o rollback custa.
  • Ler o estado da tabela de migrações e dizer o que a equipe do dia 6 precisa escrever antes de rodar em produção.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 LED da placa no GPIO2, com o resistor de 330 ohm da bancada
  • 1 computador com o projeto do 2o trimestre aberto, com um banco vazio de testes
  • Folha de papel por dupla, com a lista das migrações em ordem
  • 1 computador com o monitor serial aberto a 115200
  • Projetor, para o terminal com a lista de migrações aplicadas e pendentes

Conceitos

Migração é o schema como código, e o schema é código

Uma migração — migration, em inglês — é uma mudança de versionamento de schema escrita como código, com número, nome e ordem. A definição curta esconde a consequência importante: quando o schema é código, ele entra no git, ele é revisado antes de rodar, e ele tem o mesmo histórico que o resto do sistema.

Sem migração, o schema é conhecimento oral. A pessoa que sabe quais colunas existem é a pessoa que estava no dia em que criou a tabela, e ela não está mais. A coluna nova que o professor acabou de adicionar "na mão" vai desaparecer na próxima vez que alguém rodar o script de criação em um banco novo, e o sintoma aparece semanas depois, em uma máquina diferente, com ninguém presente.

O número da migração é a parte que dá ordem, e ele tem duas propriedades que o professor exige:

  1. É crescente e único. Duas migrações com o mesmo número não podem coexistir, e é por isso que o número tem data e sequência.
  2. É imutável. Depois que a migração rodou em algum lugar, o número e o conteúdo não mudam mais. Corrigir uma migração já aplicada é o que cria banco com duas histórias diferentes.

O versionamento de schema tem uma propriedade que parece paradoxicala e é central: o schema do banco é a parte do sistema que nunca deveria ser editada depois de aplicada. Todo o resto do código evolui com o tempo; o schema evolui por acréscimo, e cada acréscimo é uma linha no histórico. É o mesmo princípio do log de append que o professor vai ver na fila do dia 7: nada é reescrito, tudo é acrescentado.

A tabela de migrações é o instrumento que torna isso verificável, e é ela que responde à pergunta de rodar migração: o que está pendente. Ela tem quatro colunas na forma mínima: número, nome, instante e quem aplicou. Consultá-la responde a três perguntas que a equipe faz toda semana: qual versão está no ar, o que falta aplicar, e o que foi aplicado que o código novo não conhece. A linha aplicada é escrita dentro da mesma transação que a mudança — e é por isso que o item 3 da atividade importa: migração aplicada sem registro é migração que o sistema não sabe que existe.

Adicionar, remover e renomear coluna, uma por uma

As três operações do dia têm custos e riscos muito diferentes, e a tabela abaixo é o resumo que a turma copia.

OperaçãoRisco com dado existenteCusto
adicionar colunabaixo, se com valor padrãoinstantâneo em tabela pequena
renomear colunamédio, se o código antigo ainda usa o nomedois comandos, na ordem certa
remover colunaalto, se alguém ainda lêinstantâneo, mas o dado foi
índice novomédio: trava a tabela durante a criaçãoproporcional ao tamanho

O detalhe que separa adicionar coluna de adicionar coluna com segurança é o valor padrão. Uma coluna nova sem valor padrão em tabela com linhas existentes viola a restrição na hora da criação, e a migração falha. Uma coluna nova com valor padrão é criada com o valor em todas as linhas, e é por isso que o professor insiste: coluna nova sempre nasce com DEFAULT.

O dado existente é a segunda metade do problema, e não perder dado é o requisito, e a metade que o DEFAULT não resolve. A coluna existe e está preenchida com o valor padrão; o dado bom está na coluna antiga. Aí entra o backfill — o preencher coluna nova — que é a etapa que leva tempo e é a que a aula 2 vai preparar para não derrubar a produção.

A ordem do adicionar coluna e do preencher coluna nova é o que a turma erra com mais frequência, e a razão é a intuição de "já que é a mesma informação, faço os dois de uma vez". Não dá: preencher a coluna nova exige que ela exista primeiro. Uma migração com os dois passos na ordem certa é a segunda da lista da atividade, e a ordem errada é a que a equipe descobre em produção, com a migração reprovada pela metade.

Renomear coluna é a operação que parece inofensiva e não é. O nome novo funciona para o código novo, e o código antigo continua usando o nome antigo até alguém recompilar. O resultado é um sistema em que metade do código funciona e a outra metade dá erro de coluna inexistente — e o erro só aparece na rota que o teste de ponta a ponta do dia 5 esqueceu de cobrir.

Índice novo entra na lista porque criar um índice é uma das operações que não é instantânea em tabela grande: criar um índice percorre a tabela inteira, e enquanto ele é criado a tabela fica travada para escrita. É a operação que a aula 2 vai medir e que separa uma migração de cinco segundos de uma migração de vinte minutos.

Backfill: a etapa que o tempo de downtime depende

O backfill é o preenchimento da coluna nova a partir da coluna antiga, linha por linha. Ele é a etapa que transforma "adicionei uma coluna" em "os dados estão na coluna certa", e é a etapa cujo tempo decide se a migração pode rodar com o sistema no ar.

A conta é simples e o professor pede que a turma faça com números do projeto: mil linhas preenchem em milissegundos; dez mil em dois segundos; um milhão em três minutos; cinco milhões em quinze. O número de linhas por segundo varia com o tamanho da linha, com o número de índices na tabela e com o que o banco está fazendo ao mesmo tempo — e o professor avisa que preencher enquanto o sistema grava em cima é a pior combinação, porque a leitura do UPDATE e a escrita da aplicação disputam a mesma linha.

Daí vem a pergunta que a aula 2 responde: em vez de um UPDATE que varre a tabela inteira, o backfill em lotes de mil linhas, com pausa entre os lotes. O custo é o mesmo no total, e o ganho é que a tabela nunca fica travada por muito tempo: cada lote dura milissegundos, e a pausa devolve a escrita ao sistema. O número que a turma anota é o tamanho do lote, e ele é decisão de projeto.

O que o backfill não faz, e é a armadilha da aula 2: ele não resolve o problema do código antigo que ainda escreve na coluna velha. Enquanto houver código gravando na antiga, o backfill pode ser repetido indefinidamente e nunca ficar completo. A solução é a ordem em duas etapas — coluna nova primeiro, código depois, coluna velha por último — e a aula 2 é sobre isso.

Rollback, e o que ele custa de verdade

O rollback é a operação que desfaz a migração. E é aqui que a turma descobre a parte desconfortável: nem toda migração é reversível, e a irreversibilidade não é defeito da migração, é consequência da operação.

OperaçãoRollback possível?O que o rollback custa
adicionar colunasim, remover a colunaperda do que foi escrito nela depois
renomear colunasim, renomear de voltanada, se ninguém gravou
remover colunanãoo dado foi
backfillsim, esvaziarnão recupera o valor original
criar índicesim, remover o índicenada, o custo é recriar depois

A linha da remoção de coluna é a que o professor sublinha duas vezes: não há rollback para dado removido. Não há porque o banco não guarda histórico de coluna. Por isso a ordem correta é remover por último, e por isso a migração de remoção é a que precisa de backup antes de migrar, que é o item da aula 2.

A migração reversível é a que a equipe deve tentar escrever sempre que puder, e o critério é objetivo antes de rodar: se eu desfizer isso, o que o sistema perde? Se a resposta é "nada", a migração é reversível. Se a resposta é uma lista, a migração precisa de backup, e o backup precisa estar escrito no procedimento com o nome do arquivo e onde ele está.

O que o rollback não é, e o professor alerta: ele não é plano de Contingência para o caso de o banco inteiro cair. Ele desfaz uma mudança de schema. Restaurar o banco é outro procedimento, com outro nome, e ele depende do backup. Misturar os dois é como llamar de reversão o botão de desligar do servidor.

A escrita da migração reversível segue um formato que o professor mostra, e que é o mesmo do código versionado: número, nome, o que faz, o que desfaz, e se é reversível. A última linha é a que ninguém escreve e que a equipe inteira acaba agradecendo seis meses depois.

Atividade

Montagem:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • Cabo USB conectado, monitor serial em 115200.
  • Banco de testes vazio, com a tabela de leitura do 2o trimestre já criada e com mil linhas.
  • Folha de papel com a lista das migrações em ordem de aplicação.
  1. Escreva no papel a tabela de migrações do seu projeto: número, nome, o que faz e se é reversível. Comece pela migração que criou a tabela de leitura. Quantas linhas a sua lista tem hoje?
  2. Escreva a migração 002: adicionar coluna umidade_fonte com DEFAULT 'dht11'. Rode e consulte a tabela de migrações. A linha da migração foi gravada? Ela está na mesma transação da alteração?
  3. Escreva a migração 003: o backfill, preenchendo umidade_fonte com o valor de origem para as mil linhas. Meça o tempo e calcule quantas linhas por segundo. Rode a consulta de contagem antes e depois: o número de linhas preenchidas bate com o total?
  4. Escreva a migração 004: remover coluna antiga. Antes de rodar, escreva a resposta da pergunta do rollback: se eu desfizer isso, o que o sistema perde? Rode e meça de novo o tempo. Essa migração é reversível?
  5. Escreva a migração 005 e rode duas vezes. O que a segunda execução respondeu? A tabela de migrações recusou? Se não recusou, o que aconteceu com o DEFAULT da coluna?
  6. Escreva o rollback da migração 002 e rode. A coluna foi removida? O que aconteceu com o dado que foi escrito nela depois? Escreva a resposta em uma frase e diga se a 002 era reversível de verdade.
  7. Meça o tempo da migração 003 com mil linhas e depois com a tabela cheia de dez mil. Calcule o tempo estimado para um milhão de linhas, e escreva o número ao lado da migração no papel.
  8. No seu projeto, escreva a frase de uma migração que não é reversível e diga por quê. Essa frase precisa aparecer no arquivo da migração, no começo. Escreva qual é.

Nota: 12 pontos. Critério de fim: a tabela de migrações com as cinco migrações escritas e a consulta da tabela de migrações mostrada, e os dois tempos do backfill — mil e dez mil linhas — com a conta por segundo.

Resolucao

O sketch da aula está em codigo/t3/dia06/aula1.ino.

#include <Arduino.h>

// ===========================================================================
// dia 6, aula 1: migracao. A placa nao tem banco, e por isso ela reproduz o
// QUE a migracao faz: uma lista ordenada de passos, cada um com um numero,
// um nome, e a informacao de se ele desfaz.
//
// A tabela de migracoes aqui e um arranjo de memoria, e a mesma forma que a do
// servidor. O que a placa mostra e o conceito: a ordem, o numero, e a linha
// de registro que fica gravada JUNTO com a mudanca.
// ===========================================================================

const int PIN_LED = 2;
const unsigned long INTERVALO_MS = 5000;

// ---------------------------------------------------------------------------
// A TABELA DE MIGRACOES. Quatro campos por linha, que e o minimo que responde
// as tres perguntas da semana: qual versao esta no ar, o que falta, o que foi
// aplicado. A coluna "reversivel" e a que ninguem escreve e todo mundo precisa.
// ---------------------------------------------------------------------------
struct Migracao {
  int numero;
  const char* nome;
  const char* faz;         // o que ela muda
  const char* desfaz;      // o que o rollback faria
  bool reversivel;
  bool aplicada;
};

Migracao MIGRACOES[] = {
  {1, "criar_tb_leitura", "cria a tabela de leitura",
   "remove a tabela", false, true},
  {2, "add_umidade_fonte", "adiciona coluna com DEFAULT",
   "remove a coluna", true, true},
  {3, "backfill_umidade_fonte", "preenche a coluna nova a partir da antiga",
   "esvazia a coluna", true, false},
  {4, "drop_origem", "remove a coluna antiga",
   "NAO EXISTE: o dado foi", false, false},
  {5, "add_indice_periodo", "cria indice sobre o periodo",
   "remove o indice", true, false}
};
const int TOTAL_MIGRACOES = 5;

// O que a placa do dia 5 e o dia 8 mediram: mil linhas por segundo no
// preenchimento. E o numero que multiplica pela contagem da tabela.
const int LINHAS_POR_SEGUNDO = 1000;

// ---------------------------------------------------------------------------
// A CONSULTA DA TABELA DE MIGRACOES. E a funcao que responde "qual versao do
// schema esta no ar" — a pergunta que a equipe faz toda semana e que, sem esta
// tabela, so quem trabalhou no dia da criacao do banco responde.
// ---------------------------------------------------------------------------
int versaoAplicada() {
  int maior = 0;
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    if (MIGRACOES[i].aplicada && MIGRACOES[i].numero > maior) {
      maior = MIGRACOES[i].numero;
    }
  }
  return maior;
}

int contarPendentes() {
  int n = 0;
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    if (!MIGRACOES[i].aplicada) n++;
  }
  return n;
}

// ---------------------------------------------------------------------------
// A OPERACAO DE ROLLBACK. Ela tem uma condicao que o professor pede para ler:
// migracao que remove dado nao tem rollback. O `if` e a regra, e nao um detalhe.
// ---------------------------------------------------------------------------
bool podeDesfazer(const Migracao& m) {
  if (m.reversivel) return true;
  Serial.printf("  migração %d (%s) NÃO é reversível: %s\n",
                m.numero, m.nome, m.desfaz);
  return false;
}

// O desenho da ordem que a atividade mede: adicionar, preencher, remover.
void mostrarOrdenacao() {
  Serial.println("  ordem que a sequência de migrações tem de ter:");
  Serial.println("    1. adicionar a coluna nova, COM DEFAULT");
  Serial.println("    2. preencher a coluna nova a partir da antiga (backfill)");
  Serial.println("    3. só então remover a coluna antiga");
  Serial.println("  remover antes de preencher perde o dado, e não há como");
  Serial.println("  recuperá-lo: o banco não guarda histórico de coluna.");
  Serial.println();
}

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("=== dia 6, aula 1: migração e versionamento de schema ===");
  Serial.println("O schema é código. Código muda com número e histórico.");
  Serial.println();
  Serial.println("tabela de migrações:");
  Serial.println("  numero  nome                  aplicada  reversivel");
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    Serial.printf("  %6d  %-20s  %-8s  %s\n",
                  MIGRACOES[i].numero, MIGRACOES[i].nome,
                  MIGRACOES[i].aplicada ? "sim" : "PENDENTE",
                  MIGRACOES[i].reversivel ? "sim" : "nao");
  }
  Serial.println();
  Serial.printf("  versão aplicada agora: %d\n", versaoAplicada());
  Serial.printf("  migrações pendentes:    %d\n", contarPendentes());
  Serial.println();

  Serial.println("o que cada uma faz e o que ela desfaz:");
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    Serial.printf("  %d %s\n    faz:    %s\n    desfaz: %s\n",
                  MIGRACOES[i].numero, MIGRACOES[i].nome,
                  MIGRACOES[i].faz, MIGRACOES[i].desfaz);
  }
  Serial.println();
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  Serial.println("--- o que acontece se rodar a 003 e a 004 fora de ordem ---");
  Serial.println("  se a 004 rodar antes da 003, a coluna que o backfill");
  Serial.println("  deveria preencher não existe mais. O UPDATE falha e a");
  Serial.println("  migração para no meio, com a 003 e a 004 aplicadas.");
  Serial.println("  O banco fica num estado que nenhuma versão do código usa.");
  Serial.println();
  mostrarOrdenacionamento();
  Serial.println("--- o rollback, linha a linha ---");
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    Serial.printf("  %d %-20s ", MIGRACOES[i].numero, MIGRACOES[i].nome);
    if (podeDesfazer(MIGRACOES[i])) {
      Serial.print("pode desfazer: ");
      Serial.println(MIGRACOES[i].desfaz);
    }
  }
  Serial.println();
  Serial.println("--- o custo do backfill, medido ---");
  int mil = 1000, dezMil = 10000, umMilhao = 1000000;
  Serial.print("     1.000 linhas: ");
  Serial.print(mil / LINHAS_POR_SEGUNDO);
  Serial.println(" s");
  Serial.print("    10.000 linhas: ");
  Serial.print(dezMil / LINHAS_POR_SEGUNDO);
  Serial.println(" s");
  Serial.print("  1.000.000 linhas: ");
  Serial.print(umMilhao / LINHAS_POR_SEGUNDO);
  Serial.println(" s");
  Serial.println();
  Serial.println("  é esse número que decide se a migração roda com o ar");
  Serial.println("  no ar ou precisa de duas etapas, que é a aula 2.");
  Serial.println();

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_MS);
}

Por que assim e não de outro jeito. A struct Migracao tem cinco campos, e o quinto — reversivel — é o que a equipe esquece. Os quatro primeiros respondem "o que aconteceu"; o quinto responde "o que eu faço se der errado", e ele é uma decisão que precisa ser escrita antes de a migração rodar, não depois do incidente. A mesma exigência vale para o campo desfaz: ele é a resposta escrita de rollback, e uma migração sem desfaz preenchido é uma migração em que ninguém pensou em como voltar atrás.

A versaoAplicada percorre a lista e devolve o maior número aplicado, e não a contagem. A diferença importa e o professor a explica: se o número 4 foi aplicado e o 3 não, a contagem de aplicadas dá dois e o maior número dá quatro. Quem usa a contagem como versão está mentindo sobre o estado do banco — e a próxima migração, que procura "maior número aplicado", vai rodar depois da 4 e não depois da 3. A versão é o número, não a quantidade.

O podeDesfazer é a função mais curta do arquivo e a que o professor lê com mais calma. Ela imprime a migração que não pode ser desfeita com a explicação do motivo, e não apenas false. A diferença é o que a equipe vai ler no incidente: "não é reversível: Não EXISTE: o dado foi" diz o que aconteceu, e um false seco obriga a pessoa a abrir o banco para descobrir.

O mostrarOrdenacao imprime a ordem das três etapas, e ela está no sketch como texto por um motivo que o professor admite: a ordem é conhecimento de requisito, e conhecimento de requisito no arquivo de migração é conhecimento que sobrevive à mudança de equipe. A sequência 002 adicionar, 003 preencher, 004 remover aparece na lista de migrações acima, e o nome de cada uma já diz a ordem — drop_origem é o número 4 porque é o quarto passo, não porque é o quarto que alguém pensou.

Os três números do custo do backfill usam a constante LINHAS_POR_SEGUNDO e não números escritos à mão, e isso é o que permite ao aluno mexer na constante e ver os três números mudarem. É a diferença entre uma tabela que demonstra e uma tabela que mede: com a constante, o item 7 da atividade — trocar por mil e depois por dez mil — vira uma edição de uma linha.

O LED acende e apaga a cada volta como sinal de vida. O intervalo de cinco segundos é escolha de aula: o professor precisa ler três blocos de texto na tela sem pausar, e cada bloco tem seis ou sete linhas.

Criterios de correcao

CritérioPontos
A tabela de migrações escrita, com número, nome, o que faz e reversibilidade2 pontos
A migração 002 rodada, com a consulta da tabela de migrações colada2 pontos
A migração 003 de backfill rodada, com a contagem antes e depois2 pontos
A migração 004 rodada, com a resposta escrita sobre a perda no rollback2 pontos
A migração 005 rodada duas vezes, com o que a segunda respondeu2 pontos
O rollback da 002 rodado, com o efeito no dado descrito1 ponto
Os dois tempos de backfill, com a conta de linhas por segundo1 ponto

Erros comuns

ErroComo apareceCorreção
Editar a tabela direto no bancoALTER TABLE executado no terminal e nada no git"O schema é código. A mudança que não está no git volta a desaparecer no próximo banco novo, e ninguém sabe que existiu."
Coluna nova sem DEFAULT em tabela com linhasA migração falha com violação de restrição"Coluna nova nasce com valor padrão. Sem ele, a criação quebra em qualquer tabela que já tenha linha."
Remover a coluna antiga antes do backfillO UPDATE do backfill falha com coluna inexistente"A ordem é adicionar, preencher, remover. Remover antes perde o dado e o banco não guarda histórico para recuperar."
Migrar corrigindo a versão antigaO arquivo da migração 002 foi editado depois de aplicada"Migração aplicada é imutável. Você cria a 006. Editar a 002 produz bancos com histórias diferentes e o rollback perde o sentido."
Registro da migração fora da transaçãoA alteração foi aplicada e a linha não foi gravada"A linha na tabela de migrações vai na mesma transação da mudança. Se ela falhar, a migração inteira falhou."
Versão calculada por contagemSELECT COUNT(*) para saber a versão"A versão é o maior número aplicado, não a quantidade. Com a 4 aplicada e a 3 pendente, a contagem dá dois e engana quem roda a próxima."
Dizer que toda migração tem rollbackA equipe promete reversão para a que remove coluna"Remover coluna não tem rollback: o dado foi. A pergunta antes de rodar é 'se eu desfizer, o que se perde?', e a resposta tem que estar no arquivo."
Backfill sem medir o tempoA migração entra no deploy sem saber quanto vai levar"Meça com mil linhas e extrapole. O tempo do backfill é o que decide entre rodar com o ar no ar e fazer em duas etapas."

Desafio extra

Escreva a migração do seu projeto que adiciona a coluna e faz o backfill, e meça o tempo do UPDATE com a tabela cheia do seu banco de testes. Depois escreva a versão em lotes — mil linhas por lote, com pausa de dois segundos entre um lote e o seguinte — e meça o tempo total e o tempo que cada lote segura a tabela. Escreva na frente os dois números e a frase que justifica a escolha. Em seguida, escreva o rollback de cada uma das duas versões e diga o que cada um perde. A pergunta que o professor espera de resposta: qual das duas versões vocêrodaria com o sistema no ar, e qual delas você rodaria com o sistema parado, e o que decide essa escolha além do tempo medido?

>

A resolucao, compilada

// Aula 1 do dia 6 do 3o trimestre de ESP32 e IoT.
// Gerado a partir da secao ## Resolucao de conteudo/t3/dia06/aula1.md.
// Se mudar o codigo, mude no .md e regere: o .ino e copia do .md.

#include <Arduino.h>

// ===========================================================================
// dia 6, aula 1: migracao. A placa nao tem banco, e por isso ela reproduz o
// QUE a migracao faz: uma lista ordenada de passos, cada um com um numero,
// um nome, e a informacao de se ele desfaz.
//
// A tabela de migracoes aqui e um arranjo de memoria, e a mesma forma que a do
// servidor. O que a placa mostra e o conceito: a ordem, o numero, e a linha
// de registro que fica gravada JUNTO com a mudanca.
// ===========================================================================

const int PIN_LED = 2;
const unsigned long INTERVALO_MS = 5000;

// ---------------------------------------------------------------------------
// A TABELA DE MIGRACOES. Quatro campos por linha, que e o minimo que responde
// as tres perguntas da semana: qual versao esta no ar, o que falta, o que foi
// aplicado. A coluna "reversivel" e a que ninguem escreve e todo mundo precisa.
// ---------------------------------------------------------------------------
struct Migracao {
  int numero;
  const char* nome;
  const char* faz;         // o que ela muda
  const char* desfaz;      // o que o rollback faria
  bool reversivel;
  bool aplicada;
};

Migracao MIGRACOES[] = {
  {1, "criar_tb_leitura", "cria a tabela de leitura",
   "remove a tabela", false, true},
  {2, "add_umidade_fonte", "adiciona coluna com DEFAULT",
   "remove a coluna", true, true},
  {3, "backfill_umidade_fonte", "preenche a coluna nova a partir da antiga",
   "esvazia a coluna", true, false},
  {4, "drop_origem", "remove a coluna antiga",
   "NAO EXISTE: o dado foi", false, false},
  {5, "add_indice_periodo", "cria indice sobre o periodo",
   "remove o indice", true, false}
};
const int TOTAL_MIGRACOES = 5;

// O que a placa do dia 5 e o dia 8 mediram: mil linhas por segundo no
// preenchimento. E o numero que multiplica pela contagem da tabela.
const int LINHAS_POR_SEGUNDO = 1000;

// ---------------------------------------------------------------------------
// A CONSULTA DA TABELA DE MIGRACOES. E a funcao que responde "qual versao do
// schema esta no ar" — a pergunta que a equipe faz toda semana e que, sem esta
// tabela, so quem trabalhou no dia da criacao do banco responde.
// ---------------------------------------------------------------------------
int versaoAplicada() {
  int maior = 0;
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    if (MIGRACOES[i].aplicada && MIGRACOES[i].numero > maior) {
      maior = MIGRACOES[i].numero;
    }
  }
  return maior;
}

int contarPendentes() {
  int n = 0;
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    if (!MIGRACOES[i].aplicada) n++;
  }
  return n;
}

// ---------------------------------------------------------------------------
// A OPERACAO DE ROLLBACK. Ela tem uma condicao que o professor pede para ler:
// migracao que remove dado nao tem rollback. O `if` e a regra, e nao um detalhe.
// ---------------------------------------------------------------------------
bool podeDesfazer(const Migracao& m) {
  if (m.reversivel) return true;
  Serial.printf("  migração %d (%s) NÃO é reversível: %s\n",
                m.numero, m.nome, m.desfaz);
  return false;
}

// O desenho da ordem que a atividade mede: adicionar, preencher, remover.
void mostrarOrdenacao() {
  Serial.println("  ordem que a sequência de migrações tem de ter:");
  Serial.println("    1. adicionar a coluna nova, COM DEFAULT");
  Serial.println("    2. preencher a coluna nova a partir da antiga (backfill)");
  Serial.println("    3. só então remover a coluna antiga");
  Serial.println("  remover antes de preencher perde o dado, e não há como");
  Serial.println("  recuperá-lo: o banco não guarda histórico de coluna.");
  Serial.println();
}

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("=== dia 6, aula 1: migração e versionamento de schema ===");
  Serial.println("O schema é código. Código muda com número e histórico.");
  Serial.println();
  Serial.println("tabela de migrações:");
  Serial.println("  numero  nome                  aplicada  reversivel");
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    Serial.printf("  %6d  %-20s  %-8s  %s\n",
                  MIGRACOES[i].numero, MIGRACOES[i].nome,
                  MIGRACOES[i].aplicada ? "sim" : "PENDENTE",
                  MIGRACOES[i].reversivel ? "sim" : "nao");
  }
  Serial.println();
  Serial.printf("  versão aplicada agora: %d\n", versaoAplicada());
  Serial.printf("  migrações pendentes:    %d\n", contarPendentes());
  Serial.println();

  Serial.println("o que cada uma faz e o que ela desfaz:");
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    Serial.printf("  %d %s\n    faz:    %s\n    desfaz: %s\n",
                  MIGRACOES[i].numero, MIGRACOES[i].nome,
                  MIGRACOES[i].faz, MIGRACOES[i].desfaz);
  }
  Serial.println();
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  Serial.println("--- o que acontece se rodar a 003 e a 004 fora de ordem ---");
  Serial.println("  se a 004 rodar antes da 003, a coluna que o backfill");
  Serial.println("  deveria preencher não existe mais. O UPDATE falha e a");
  Serial.println("  migração para no meio, com a 003 e a 004 aplicadas.");
  Serial.println("  O banco fica num estado que nenhuma versão do código usa.");
  Serial.println();
  mostrarOrdenacao();
  Serial.println("--- o rollback, linha a linha ---");
  for (int i = 0; i < TOTAL_MIGRACOES; i++) {
    Serial.printf("  %d %-20s ", MIGRACOES[i].numero, MIGRACOES[i].nome);
    if (podeDesfazer(MIGRACOES[i])) {
      Serial.print("pode desfazer: ");
      Serial.println(MIGRACOES[i].desfaz);
    }
  }
  Serial.println();
  Serial.println("--- o custo do backfill, medido ---");
  int mil = 1000, dezMil = 10000, umMilhao = 1000000;
  Serial.print("     1.000 linhas: ");
  Serial.print(mil / LINHAS_POR_SEGUNDO);
  Serial.println(" s");
  Serial.print("    10.000 linhas: ");
  Serial.print(dezMil / LINHAS_POR_SEGUNDO);
  Serial.println(" s");
  Serial.print("  1.000.000 linhas: ");
  Serial.print(umMilhao / LINHAS_POR_SEGUNDO);
  Serial.println(" s");
  Serial.println();
  Serial.println("  é esse número que decide se a migração roda com o ar");
  Serial.println("  no ar ou precisa de duas etapas, que é a aula 2.");
  Serial.println();

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_MS);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia06 aula1.

Aula 2 — Preparar a migração antes de ela quebrar a produção

Objetivos

  • Reconhecer a migração que quebra, e dizer em qual das duas direções do código ela quebra primeiro.
  • Aplicar a ordem de expandir e migrar, e explicar por que código antigo com coluna nova e código novo sem coluna são estados diferentes.
  • Escrever a migração de NOT NULL em duas etapas, e medir o downtime de cada uma.
  • Rodar a migração em cópia do banco de dados antes de rodar na de verdade, e comparar os dois resultados.
  • Escrever o procedimento de backup antes de migrar com nome de arquivo, e o ler o schema que evita migração escrita contra banco errado.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 LED da placa no GPIO2, com o resistor de 330 ohm da bancada
  • 1 computador com o projeto do 2o trimestre aberto, com dois bancos: o de teste e uma cópia dele
  • Folha de papel por dupla, com o desenho das duas etapas
  • 1 computador com o monitor serial aberto a 115200
  • Projetor, para o terminal com os dois schemas lado a lado

Conceitos

A migração que quebra tem uma direção

Uma migração que quebra é a que produz um schema que o código existente não sabe usar, ou que o banco recusa. A frase importante é que ela tem direção: existe um estado anterior e um estado novo, e o defeito aparece na transição entre os dois, em uma ordem específica.

O caso que abre a aula é o mais comum: transformar a coluna nova em NOT NULL. O comando é sintaticamente correto e a intenção é legítima — a coluna é obrigatória. O banco recusa, e a mensagem diz que existem linhas com nulo. O que o banco está fazendo é proteger a integridade do dado: a coluna nova tem valor padrão desde o backfill, mas o backfill rodou em mil linhas e a tabela tem duzentas mil.

Coluna NOT NULL em tabela cheia é o nome técnico do problema, e ele tem uma solução que não é "tirar o NOT NULL para sempre". A solução é a migração em duas etapas: primeiro, em lote, preencher os nulos que restaram; depois, aplicar o NOT NULL. São duas migrações, dois números, e a segunda só é segura porque a primeira terminou.

A diferença entre as duas etapas é o que o professor mede na lousa, e é o número que decide a estratégia. Preencher em lote de mil linhas custa milissegundos de escrita e não trava a tabela. Aplicar o NOT NULL custa uma varredura para verificar que não há nulos, e trava a tabela durante a verificação — que é proporcional ao tamanho. Em tabela de duzentas mil linhas, a verificação leva segundos; em tabela de cinco milhões, leva minutos. O downtime é o nome do que a equipe perde enquanto a tabela está travada, e ele é a métrica que decide se a migração entra no horário de ninguém.

O O default em coluna nova é o que torna a etapa um possível, e o professor volta nele com uma frase: coluna nova com DEFAULT nunca tem nulo, porque o banco preenche as linhas existentes com o valor padrão. Se há nulo, ou o DEFAULT não foi aplicado, ou o backfill não rodou, ou o DEFAULT foi removido depois. Nulo em coluna que deveria ter padrão é sintoma, e o sintoma tem três causas e a turma precisa saber quais são antes de consertar.

Os quatro estados da migração, e só um funciona

Expandir e migrar é o nome da ordem que existe porque, durante a troca de schema, o sistema passa por estados que antes não existiam. São quatro combinações de código e coluna, e o professor desenha as quatro na lousa antes de qualquer comando:

EstadoCódigo antigoCódigo novoO que acontece
inicialsem colunasem colunatudo normal
expandircom colunasem colunao código antigo ignora a coluna extra
migrarcom colunacom colunaambos escrevem, o backfill preenche
contrairsem colunacom colunaquebra: o código novo não acha a coluna

A linha "expandir" é a que responde à pergunta código antigo com coluna nova, e ela é segura por um motivo que vale a pena entender: uma coluna extra não atrapalha um INSERT que não a menciona. O código antigo continua gravando nas colunas que conhece, e a coluna nova fica esperando. O único efeito é que a coluna nova não recebe dado daquele caminho — e é exatamente por isso que o backfill vem depois.

A linha "contrair" é o estado de código novo sem coluna, e ela é a que quebra. O código novo faz INSERT com a coluna nova, a coluna não existe, e toda requisição falha. Zerar downtime é reduzir a janela de quebra, que é do tamanho do tempo entre publicar o código e aplicar a migração — que é exatamente o tempo que zerar downtime tenta reduzir.

A ordem correta, e ela é a sequência que o professor escreve na lousa como regra do dia:

  1. Expandir — adicionar a coluna nova, com DEFAULT, e a coluna NOT NULL só se a nova também puder nascer preenchida.
  2. Migrar — backfill em lotes, e depois o código novo passa a escrever na coluna nova.
  3. Contrair — só depois que todo o código lê e escreve na coluna nova, remover a coluna antiga.

A A compatibilidade entre código e schema não é garantida por ninguém: ela é resultado da ordem. Um código novo assume que a coluna existe; um código antigo não sabe que ela existe. As duas suposições só são verdadeiras ao mesmo tempo no meio da sequência, e é por isso que cada etapa é uma migração separada e versionada.

Ler o schema, e a migração escrita contra o banco errado

O erro que a equipe do dia 6 paga mais caro não é a migração lenta. É a migração escrita contra o banco errado, e ele começa com uma pergunta que ninguém faz: qual é o schema deste banco agora?

Ler o schema é a operação que responde isso, e ela é a primeira coisa a fazer antes de escrever qualquer migração. Não é a mesma coisa que ler o script de criação, porque o script diz o que o banco *deveria* ter e o banco diz o que ele *tem*. Entre os dois há todas as mudanças que alguém aplicou direto no terminal, todas as migrações que rodaram em outra máquina e todas as divergências acumuladas desde o primeiro deploy.

A fonte da verdade para o schema é o banco, e a fonte da verdade para a história é a tabela de migrações. São duas leituras, e a segunda é a que responde "por que o banco está assim". A turma precisa das duas, e a aula entrega o hábito de fazer as duas antes de escrever.

Testar migração antes de rodar em produção tem quatro partes, e o professor as pede na ordem:

  1. Rodar migração em cópia — um banco clonado, nunca o de verdade.
  2. Rodar a migração na cópia e medir o tempo.
  3. Comparar o schema resultante com o que o código novo espera.
  4. Só então rodar na de verdade.

O quarto passo tem uma condição que o professor destaca: rodar na de verdade só depois que a cópia passou. E a cópia tem que ser de hoje, não da semana passada, porque o schema muda todo dia e a cópia desatualizada testa a migração contra um banco que não existe mais.

O que a cópia pega, e que a leitura do schema não pega: migração que depende de dados específicos, índice que não existe no banco de teste, coluna com tipo diferente do que o script declara. São três defeitos que só aparecem com dado dentro, e é por isso que a cópia precisa da quantidade de dado também — uma cópia com dez linhas não exercita o backfill de verdade.

Backup antes de migrar, com nome de arquivo

O backup antes de migrar é a rede debaixo de todas as outras redes, e o professor trata como procedimento, não como sugestão. A regra que ele escreve: antes de rodar em produção, existe um arquivo de backup, com nome, com caminho, com a data, e alguém sabe onde ele está.

Migração irreversível é a que justifica o backup, e ela se reconhece pela resposta da pergunta do dia anterior: "se eu desfizer isso, o que o sistema perde?". Perder coluna, perder tabela, transformar dado — são as três respostas que exigem backup. Adicionar coluna com DEFAULT não exige, e é por isso que a equipe pode correr com a etapa um.

O backup que serve não é o do arquivo compactado de três horas atrás. É o de antes da migração, com o instante mais próximo possível, e ele só é confiável se a equipe sabe restaurá-lo. E há um detalhe que o professor aponta e que ninguém testa: restaurar um backup em produção é uma operação que trava o serviço, e uma restauração que nunca foi testada é uma hipótese. O procedimento tem uma linha a mais que quase nenhum projeto escreve: quem restaura, e em quanto tempo.

O que a migração em duas etapas permite que a migração de uma etapa não permite é justamente rollback. Com a coluna nova adicionada e o código ainda antigo, o rollback é remover a coluna — e o dado que o código novo tinha escrito nela é perdido, o que é aceitável porque o código novo ainda não rodou. Com tudo de uma vez, o rollback exigiria desfazer a remoção, e o dado removido não volta. São as duas frases que o professor pede no fim da aula.

A última coisa que a aula entrega é a lista do que precisa estar escrito antes de o procedimento existir, e ela cabe em cinco itens que o professor enumera na lousa: backup feito e testado, cópia do banco de hoje, tempo medido, ordem dos passos, e quem faz. Sem os cinco, o procedimento é um deseo, e o incidente do dia seguinte é o que escreve a versão real dele.

Atividade

Montagem:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • Cabo USB conectado, monitor serial em 115200.
  • Dois bancos: o de teste e uma cópia dele feita agora, com duas mil linhas em ambos.
  • Folha de papel com os quatro estados da migração e a ordem das etapas.
  1. Ler o schema dos dois bancos e colar as duas saídas aqui. Eles são iguais? Se não, qual é a diferença e como ela apareceu?
  2. Escreva no papel os quatro estados da migração em duas etapas, com a coluna e o código em cada um. Em qual deles o sistema quebra? Escreva a linha do seu código que quebra nesse estado.
  3. Tente rodar a migração de NOT NULL na cópia, com a tabela cheia. O que o banco respondeu? Quantas linhas têm nulo? Copie a mensagem do banco.
  4. Escreva a migração de preencher em lote — mil linhas por lote — e meça três números: o tempo de cada lote, o número de lotes e o tempo total. Anote o tempo que a tabela fica travada por lote.
  5. Agora rode a migração de NOT NULL depois do preenchimento. Mede o tempo de novo. Compare os dois tempos: o do preenchimento e o do NOT NULL. Qual dos dois é o downtime real da operação?
  6. Escreva a rollback da etapa um e da etapa dois. O que cada uma perde? Marque qual delas é irreversível e escreva por quê.
  7. Rode a migração no banco de verdade, com o servidor Node parado, e meça o tempo. Depois rode com o servidor no ar, rodando, e meça de novo. Os dois números são iguais? O que muda quando há escrita concorrente durante o preenchimento?
  8. Escreva, no seu projeto, o procedimento de deploy de schema em cinco linhas: backup, cópia, migra a cópia, compara, migra a de verdade. Quem executa cada linha, e o que acontece se a linha 3 falhar?

Nota: 12 pontos. Critério de fim: as duas leituras de schema coladas e comparadas, os três tempos do item 4 com o tempo de travada por lote, e os dois tempos do item 7 — banco parado e banco com escrita concorrente.

Resolucao

O sketch da aula está em codigo/t3/dia06/aula2.ino.

#include <Arduino.h>

// ===========================================================================
// dia 6, aula 2: preparar a migracao antes de ela quebrar a producao.
//
// A placa NAO tem banco e NAO roda migracao. O que ela faz e o que importa:
// mostrar os QUATRO estados que o sistema atravessa durante a troca de schema,
// e medir o custo de cada etapa do preenchimento em lote.
//
// O numero de linhas por segundo e o mesmo do dia 6 aula 1, e e ele que
// transforma "a tabela tem muito dado" em "a migracao leva este tempo".
// ===========================================================================

const int PIN_LED = 2;
const unsigned long INTERVALO_MS = 5000;

// ---------------------------------------------------------------------------
// OS QUATRO ESTADOS. Coluna e codigo sao duas dimensoes, e a combinacao que
// quebra e a ultima: codigo novo sem coluna.
//
// `funciona` e o que o professor pede para a turma prever antes de rodar.
// ---------------------------------------------------------------------------
struct Estado {
  const char* nome;
  bool colunaNovaExiste;
  bool codigoNovoEstaNoAr;
  bool funciona;
  const char* oQue Acontece;
};

const Estado ESTADOS[] = {
  {"inicial",     false, false, true,  "tabela como o codigo espera"},
  {"expandir",    true,  false, true,  "codigo antigo ignora a coluna extra"},
  {"migrar",      true,  true,  true,  "ambos gravam; o backfill preenche"},
  {"contrair",    false, true,  false, "codigo novo pede coluna que sumiu"}
};
const int TOTAL_ESTADOS = 4;

// ---------------------------------------------------------------------------
// O CUSTO DE CADA ETAPA. Estes sao os numeros que o professor mediu na copia
// do banco, e a ordem deles e a ordem do downtime.
// ---------------------------------------------------------------------------

// O que o preenchimento em lote segura a tabela, por lote.
const int TAMANHO_LOTE = 1000;
const int LINHAS_POR_SEGUNDO = 1000;

// O NOT NULL nao preenche nada: ele VARRE a tabela para verificar que nao ha
// nulo. E por isso que ele e o downtime grande, e nao o preenchimento.
const int LINHAS_POR_SEGUNDO_VERIFICACAO = 5000;

int tempoDePreenchimento(int linhas) {
  return (linhas + LINHAS_POR_SEGUNDO - 1) / LINHAS_POR_SEGUNDO;
}

int tempoDeVerificacao(int linhas) {
  return (linhas + LINHAS_POR_SEGUNDO_VERIFICACAO - 1) / LINHAS_POR_SEGUNDO_VERIFICACAO;
}

void mostrarEstados() {
  Serial.println("  estado      coluna  codigo novo  funciona  o que acontece");
  for (int i = 0; i < TOTAL_ESTADOS; i++) {
    Serial.printf("  %-11s %-7s %-12s %-9s %s\n",
                  ESTADOS[i].nome,
                  ESTADOS[i].colunaNovaExiste ? "sim" : "nao",
                  ESTADOS[i].codigoNovoEstaNoAr ? "sim" : "nao",
                  ESTADOS[i].funciona ? "sim" : "NAO",
                  ESTADOS[i].oQue Acontece);
  }
  Serial.println();
  Serial.println("  so o ultimo quebra: codigo novo sem a coluna.");
  Serial.println("  e por isso que a ordem e banco primeiro, codigo depois.");
  Serial.println();
}

void mostrarDuasEtapas(int linhas) {
  Serial.println("--- a migracao que quebra, em duas etapas ---");
  Serial.print("  linhas na tabela: ");
  Serial.println(linhas);
  Serial.println();

  Serial.println("  ETAPA 1: preencher os nulos, em lote");
  Serial.print("    lotes de ");
  Serial.print(TAMANHO_LOTE);
  Serial.println(" linhas");
  Serial.print("    tempo total:          ");
  Serial.print(tempoDePreenchimento(linhas));
  Serial.println(" s");
  Serial.print("    tempo por lote:       ");
  Serial.print(tempoDePreenchimento(TAMANHO_LOTE));
  Serial.println(" ms  <- a tabela nao fica travada por mais que isso");
  Serial.println();

  Serial.println("  ETAPA 2: aplicar o NOT NULL");
  Serial.println("    nao preenche nada: so varre para confirmar que nao ha nulo");
  Serial.print("    tempo total:          ");
  Serial.print(tempoDeVerificacao(linhas));
  Serial.println(" s  <- este e o downtime de verdade");
  Serial.println();

  Serial.println("  se as duas fossem uma so, o downtime seria a soma:");
  Serial.print("    ");
  Serial.print(tempoDePreenchimento(linhas) + tempoDeVerificacao(linhas));
  Serial.println(" s, e o risco do NOT NULL viria junto.");
  Serial.println();
}

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("=== dia 6, aula 2: preparar a migração antes de quebrar algo ===");
  Serial.println("Quatro estados, uma ordem, e o downtime medido de cada etapa.");
  Serial.println();
  Serial.println("os quatro estados que o sistema atravessa:");
  mostrarEstados();
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  mostrarDuasEtapas(2000);
  Serial.println("--- e com a tabela de um milhão de linhas ---");
  mostrarDuasEtapas(1000000);

  Serial.println("--- o que precisa estar escrito antes do procedimento ---");
  Serial.println("  1. backup feito, e o restore testado");
  Serial.println("  2. copia do banco de HOJE, com a mesma quantidade de dado");
  Serial.println("  3. tempo medido na copia");
  Serial.println("  4. ordem dos passos escrita");
  Serial.println("  5. quem executa cada passo");
  Serial.println();
  Serial.println("  sem os cinco, o procedimento e um desejo, e o incidente");
  Serial.println("  do dia seguinte escreve a versao real dele.");
  Serial.println();

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_MS);
}

Por que assim e não de outro jeito. A placa não tem banco e não roda migração, e o professor começa dizendo isso porque a pergunta vem rápida: "onde está o ALTER TABLE?". A resposta é que o sketch não executa nada — ele mostra os quatro estados e mede o downtime, que são as duas coisas que a equipe precisa saber antes de sentar na frente do banco de verdade.

Os quatro estados estão no arranjo porque a combinação código-coluna é uma grade, e uma grade precisa ser percorrida inteira para ser vista. O campo funciona é o que o professor pede para a turma preencher antes de rodar qualquer coisa: preencher essa coluna é escrever a previsão, e comparar com o que aconteceu depois é o teste de um plano de migração. Uma migração preparada é uma migração com previsão escrita.

O estado "contrair" tem funciona igual a nao, e é o único com nao na tabela. O professor deixa isso visível e pede a explicação à turma antes de dar: o código novo pede a coluna, a coluna não existe, toda requisição falha. Os outros três estados são aceitáveis porque são transitórios e o código em produção ainda é o antigo; esse é o estado que ninguém quer atravessar, e a ordem das etapas existe para passar de "migrar" para "contrair" só depois que o código antigo saiu do ar.

A separação entre tempoDePreenchimento e tempoDeVerificacao é a parte que mais importa na aula, e ela está em duas funções com duas constantes diferentes. O preenchimento escreve mil linhas por segundo e o NOT NULL varre cinco mil por segundo — e a razão é que as duas operações fazem coisas diferentes. Preencher é escrever; verificar é ler e conferir. O professor insiste nos dois números porque é a soma deles que aparece quando alguém faz as duas etapas em um comando só, e é essa soma que vira downtime de produção.

O TAMANHO_LOTE com mil linhas é a migração em duas etapas no eixo prático: cada lote trava a tabela por um milissegundo e devolve a escrita ao sistema, e o total continua sendo o mesmo. O professor mede o antes e o depois no banco de teste, e o que a turma descobre é que o tempo total praticamente não muda, enquanto a janela em que a tabela fica travada encolhe em três ordens de grandeza. Essa é a diferença entre uma migração que derruba o sistema e uma que ninguém percebe.

A mostrarDuasEtapas ser chamada duas vezes, com dois mil e com um milhão de linhas, é o que transforma a aula em medição. Com a constante no código e não o resultado colado à mão, o aluno muda o número, roda, e vê os dois tempos mudarem juntos. É a diferença entre uma tabela que demonstra e uma tabela que responde a pergunta "e se a tabela for maior?".

O LED acende e apaga a cada volta, e o intervalo de cinco segundos dá ao professor o tempo de ler a tabela de estados e as duas tabelas de tempos sem pausar. Como nas outras aulas, é escolha de sala e não do sistema.

Criterios de correcao

CritérioPontos
As duas leituras de schema coladas, com a comparação escrita2 pontos
Os quatro estados da migração no papel, com o que quebra e a linha do código2 pontos
A mensagem do banco ao tentar o NOT NULL com nulo na tabela, colada1 ponto
Os três números do preenchimento em lote, com o tempo de travada por lote2 pontos
Os dois tempos comparados — preenchimento e NOT NULL — e o downtime escolhido2 pontos
Os dois rollbacks escritos, com o que cada um perde e qual é irreversível2 pontos
Os dois tempos do item 7, banco parado e com escrita concorrente1 ponto

Erros comuns

ErroComo apareceCorreção
NOT NULL em coluna com nuloO banco recusa e a equipe culpa o comando"Não é erro de sintaxe: é integridade. Primeiro preenche os nulos em lote, depois aplica o NOT NULL. São duas migrações, dois números."
Fazer as duas etapas em um comandoUPDATE e ALTER na mesma transação, com downtime longo"A soma dos tempos é o downtime. Separando, o preenchimento trava a tabela por milissegundos por lote e só a verificação trava de verdade."
Publicar o código antes da migraçãoO código novo sobe, pede a coluna, e todo pedido falha"Ordem é banco, depois código. O estado 'código novo sem coluna' é o único que quebra, e a janela é do tamanho do deploy."
Fazer a migração no banco de verdade primeiroO time descobre o problema com o sistema no ar"Rode na cópia de hoje, com a mesma quantidade de dado. Dez linhas não exercitam o backfill, e a cópia da semana passada testa um banco que não existe mais."
Backup de três horas atrás"Temos backup" e ninguém sabe restaurar"O backup que serve é o de antes da migração, e o que vale precisa ter o restore testado. Backup nunca restaurado é hipótese, não rede."
Migrar sem ler o schemaA migração roda no banco de teste e falha no de verdade"Leia o schema dos dois antes. O script diz o que o banco deveria ter; o banco diz o que tem. São coisas diferentes desde a primeira mudança aplicada na mão."
Coluna nova sem DEFAULTA criação falha em tabela com linhas"Coluna nova nasce com valor padrão. Sem ele, não há como criar em tabela cheia, e o backfill vira obrigatório em vez de opcional."

Desafio extra

Meça o tempo real da migração de verificação no seu banco de teste com mil linhas, com dez mil e com cem mil, e construa a curva do tempo pelo tamanho da tabela. Depois escreva a versão que cria o índice em segundo plano, se o seu banco oferecer a opção, meça de novo, e compare com o tempo da criação normal. Escreva na frente os dois gráficos em texto, com os números, e diga em que tamanho de tabela a diferença deixa de ser aceitável para um horário de produção. Por fim, escreva o procedimento de restaurar um backup com o tempo estimado de restauração, e diga o que a equipe precisa decidir antes de precisar dele.

>

A resolucao, compilada

// Aula 2 do dia 6 do 3o trimestre de ESP32 e IoT.
// Gerado a partir da secao ## Resolucao de conteudo/t3/dia06/aula2.md.
// Se mudar o codigo, mude no .md e regere: o .ino e copia do .md.

#include <Arduino.h>

// ===========================================================================
// dia 6, aula 2: preparar a migracao antes de ela quebrar a producao.
//
// A placa NAO tem banco e NAO roda migracao. O que ela faz e o que importa:
// mostrar os QUATRO estados que o sistema atravessa durante a troca de schema,
// e medir o custo de cada etapa do preenchimento em lote.
//
// O numero de linhas por segundo e o mesmo do dia 6 aula 1, e e ele que
// transforma "a tabela tem muito dado" em "a migracao leva este tempo".
// ===========================================================================

const int PIN_LED = 2;
const unsigned long INTERVALO_MS = 5000;

// ---------------------------------------------------------------------------
// OS QUATRO ESTADOS. Coluna e codigo sao duas dimensoes, e a combinacao que
// quebra e a ultima: codigo novo sem coluna.
//
// `funciona` e o que o professor pede para a turma prever antes de rodar.
// ---------------------------------------------------------------------------
struct Estado {
  const char* nome;
  bool colunaNovaExiste;
  bool codigoNovoEstaNoAr;
  bool funciona;
  const char* oQueAcontece;
};

const Estado ESTADOS[] = {
  {"inicial",     false, false, true,  "tabela como o codigo espera"},
  {"expandir",    true,  false, true,  "codigo antigo ignora a coluna extra"},
  {"migrar",      true,  true,  true,  "ambos gravam; o backfill preenche"},
  {"contrair",    false, true,  false, "codigo novo pede coluna que sumiu"}
};
const int TOTAL_ESTADOS = 4;

// ---------------------------------------------------------------------------
// O CUSTO DE CADA ETAPA. Estes sao os numeros que o professor mediu na copia
// do banco, e a ordem deles e a ordem do downtime.
// ---------------------------------------------------------------------------

// O que o preenchimento em lote segura a tabela, por lote.
const int TAMANHO_LOTE = 1000;
const int LINHAS_POR_SEGUNDO = 1000;

// O NOT NULL nao preenche nada: ele VARRE a tabela para verificar que nao ha
// nulo. E por isso que ele e o downtime grande, e nao o preenchimento.
const int LINHAS_POR_SEGUNDO_VERIFICACAO = 5000;

int tempoDePreenchimento(int linhas) {
  return (linhas + LINHAS_POR_SEGUNDO - 1) / LINHAS_POR_SEGUNDO;
}

int tempoDeVerificacao(int linhas) {
  return (linhas + LINHAS_POR_SEGUNDO_VERIFICACAO - 1) / LINHAS_POR_SEGUNDO_VERIFICACAO;
}

void mostrarEstados() {
  Serial.println("  estado      coluna  codigo novo  funciona  o que acontece");
  for (int i = 0; i < TOTAL_ESTADOS; i++) {
    Serial.printf("  %-11s %-7s %-12s %-9s %s\n",
                  ESTADOS[i].nome,
                  ESTADOS[i].colunaNovaExiste ? "sim" : "nao",
                  ESTADOS[i].codigoNovoEstaNoAr ? "sim" : "nao",
                  ESTADOS[i].funciona ? "sim" : "NAO",
                  ESTADOS[i].oQueAcontece);
  }
  Serial.println();
  Serial.println("  so o ultimo quebra: codigo novo sem a coluna.");
  Serial.println("  e por isso que a ordem e banco primeiro, codigo depois.");
  Serial.println();
}

void mostrarDuasEtapas(int linhas) {
  Serial.println("--- a migracao que quebra, em duas etapas ---");
  Serial.print("  linhas na tabela: ");
  Serial.println(linhas);
  Serial.println();

  Serial.println("  ETAPA 1: preencher os nulos, em lote");
  Serial.print("    lotes de ");
  Serial.print(TAMANHO_LOTE);
  Serial.println(" linhas");
  Serial.print("    tempo total:          ");
  Serial.print(tempoDePreenchimento(linhas));
  Serial.println(" s");
  Serial.print("    tempo por lote:       ");
  Serial.print(tempoDePreenchimento(TAMANHO_LOTE));
  Serial.println(" ms  <- a tabela nao fica travada por mais que isso");
  Serial.println();

  Serial.println("  ETAPA 2: aplicar o NOT NULL");
  Serial.println("    nao preenche nada: so varre para confirmar que nao ha nulo");
  Serial.print("    tempo total:          ");
  Serial.print(tempoDeVerificacao(linhas));
  Serial.println(" s  <- este e o downtime de verdade");
  Serial.println();

  Serial.println("  se as duas fossem uma so, o downtime seria a soma:");
  Serial.print("    ");
  Serial.print(tempoDePreenchimento(linhas) + tempoDeVerificacao(linhas));
  Serial.println(" s, e o risco do NOT NULL viria junto.");
  Serial.println();
}

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("=== dia 6, aula 2: preparar a migração antes de quebrar algo ===");
  Serial.println("Quatro estados, uma ordem, e o downtime medido de cada etapa.");
  Serial.println();
  Serial.println("os quatro estados que o sistema atravessa:");
  mostrarEstados();
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  mostrarDuasEtapas(2000);
  Serial.println("--- e com a tabela de um milhão de linhas ---");
  mostrarDuasEtapas(1000000);

  Serial.println("--- o que precisa estar escrito antes do procedimento ---");
  Serial.println("  1. backup feito, e o restore testado");
  Serial.println("  2. copia do banco de HOJE, com a mesma quantidade de dado");
  Serial.println("  3. tempo medido na copia");
  Serial.println("  4. ordem dos passos escrita");
  Serial.println("  5. quem executa cada passo");
  Serial.println();
  Serial.println("  sem os cinco, o procedimento e um desejo, e o incidente");
  Serial.println("  do dia seguinte escreve a versao real dele.");
  Serial.println();

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_MS);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia06 aula2.