Competições Senac RS · Seletiva 26
Módulo 03 · Apostila teórica
Módulo 03 — Banco de dados
Onde esse módulo entra. No Módulo B da prova (Banco + API, 2h30), a primeira coisa que se faz é modelar o banco. Ele é a fundação: se o modelo de dados está torto, a API vem torta, o front vem torto, e não tem acabamento que salve. O banco vale 10% direto, mas contamina os outros 60% de back e front. Aqui a natureza é 🔄 revisão, porque as duas já modelaram banco na Regional. Só que agora a revisão desce fundo: não é decorar comando, é entender por que cada decisão de modelagem existe, escolher certo em situações diferentes e conseguir defender a escolha na apresentação. Modelar certo, rápido e sabendo o porquê. Esse é o nível 3.
1. Tipos de banco de dados: relacional, não relacional e por que a prova é relacional
1.1 O que é um SGBD
Banco de dados é o dado organizado. SGBD (Sistema Gerenciador de Banco de Dados) é o software que administra esse dado: recebe comandos, garante as regras, controla acesso simultâneo, protege contra corrupção. MySQL, PostgreSQL, SQL Server e Oracle são SGBDs. Quando a gente fala "o banco barrou o insert", quem barrou foi o SGBD aplicando uma regra que você definiu.
O SQL (Structured Query Language) nasceu nos anos 70 na IBM (projeto System R, baseado no modelo relacional que o Edgar Codd propôs em 1970) e virou padrão ANSI/ISO nos anos 80. Por isso o grosso do SQL funciona igual em qualquer banco relacional: o SELECT que você escreve no MySQL é praticamente o mesmo do PostgreSQL. O que muda são detalhes de função e de tipo, e a gente vai trabalhar sempre no dialeto do MySQL 8, que é o da prova.
1.2 Relacional: tabelas, relações e garantias
No modelo relacional o dado mora em tabelas (linhas e colunas), as tabelas se ligam por chaves, e o SGBD garante um pacote de propriedades conhecido como ACID:
| Propriedade | O que garante | Exemplo no Wedding Pass |
|---|---|---|
| Atomicidade | Operação em grupo ou acontece inteira ou não acontece | Criar convidado + convite juntos: se o convite falhar, o convidado não fica órfão |
| Consistência | O banco nunca fica num estado que viola as regras | Nenhum convite aponta pra convidado inexistente |
| Isolamento | Duas operações simultâneas não se atropelam | Dois check-ins do mesmo código ao mesmo tempo: só um passa |
| Durabilidade | Confirmou, tá gravado, mesmo se faltar luz | O check-in feito às 19h32 não some |
Essas quatro letras são exatamente o que um sistema de gestão precisa. Guarda esse quadro: ele volta na seção de transações (17) e no check-in do módulo 07.
1.3 Não relacional (NoSQL): o que existe do outro lado
O plano de curso pede o comparativo, e o mercado usa os dois. NoSQL não é "melhor" nem "pior": é um conjunto de modelos diferentes pra problemas diferentes.
| Família | Como guarda | Exemplo | Brilha quando |
|---|---|---|---|
| Documento | JSON aninhado, sem esquema fixo | MongoDB | Dados semiestruturados que mudam de forma (catálogo de produtos com atributos variados) |
| Chave-valor | Um valor por chave, acesso direto | Redis | Cache, sessão, contador em tempo real |
| Colunar | Colunas agrupadas, escrita massiva | Cassandra | Volume gigante de escrita (telemetria, logs) |
| Grafo | Nós e arestas | Neo4j | Relacionamentos profundos (rede social, recomendação) |
O trade-off central: NoSQL costuma abrir mão de parte das garantias ACID e dos JOINs pra ganhar flexibilidade e escala horizontal. Num sistema de gestão (convidados, convites, mesas, check-ins), essa troca é péssima: os dados são estruturados, se relacionam o tempo todo, e os relatórios (dashboard!) nascem de cruzamento de tabelas.
🎯 Por que a prova é relacional. Todo Projeto-Teste da nossa área é um sistema de gestão: entidades bem definidas, regras de integridade, relatórios agregados. Isso é o caso de uso clássico do relacional. Saber explicar isso em uma frase ("escolhi relacional porque os dados são estruturados e se relacionam, e eu preciso de integridade e de relatórios com JOIN") é repertório de apresentação que impressiona banca. NoSQL na prova é isso: repertório pra conversa, não ferramenta pra usar.
2. Os três níveis de modelagem: conceitual → lógico → físico
Modelar não é um passo só. O caminho profissional (e o que o plano de curso cobra) tem três níveis, cada um respondendo uma pergunta diferente:
| Nível | Pergunta | Produto | Se preocupa com |
|---|---|---|---|
| Conceitual | O que o sistema precisa lembrar? | MER (entidades, atributos, relacionamentos) | O negócio. Zero tecnologia |
| Lógico | Como isso vira tabelas? | Tabelas, chaves, cardinalidades resolvidas | Estrutura relacional. N:N vira tabela associativa aqui |
| Físico | Como isso roda no MySQL? | Script SQL com tipos exatos, constraints, índices | O SGBD escolhido |
2.1 O mesmo caminho numa situação diferente: sistema de escola
Pra provar que o método serve pra qualquer enunciado (e a Prova Surpresa vai trazer um enunciado que ninguém conhece), vamos modelar uma escola do zero:
Conceitual. Lendo o enunciado imaginário ("a escola tem turmas, alunos se matriculam em turmas, cada turma tem um professor responsável e as notas dos alunos ficam registradas"), sublinho os substantivos que o sistema precisa lembrar: aluno, turma, professor, matrícula, nota. Relações: professor 1:N turma (um professor responde por várias turmas), aluno N:N turma (um aluno em várias turmas, uma turma com vários alunos), e a nota pertence... a quê? Não ao aluno sozinho nem à turma sozinha: à matrícula (aquele aluno naquela turma). Isso é achado de modelagem: a nota é atributo do relacionamento.
Lógico. O N:N aluno-turma vira a tabela matricula (aluno_id, turma_id), e a nota vira coluna dela (ou tabela avaliacao ligada à matrícula, se forem várias notas). turma ganha professor_id. Todas as tabelas ganham chave primária.
Físico. Agora sim: matricula.nota vira DECIMAL(4,1), aluno.nome vira VARCHAR(100) NOT NULL, matricula ganha UNIQUE(aluno_id, turma_id) pra ninguém se matricular duas vezes na mesma turma, e as FKs ganham regra de deleção.
2.2 O mesmo padrão em pedidos de e-commerce
Outro cenário clássico, porque ele ensina uma decisão que cai em prova: pedido e produto são N:N (um pedido tem vários produtos, um produto aparece em vários pedidos), então nasce item_pedido(pedido_id, produto_id, quantidade, preco_unitario). Repara no preco_unitario dentro do item: o preço do produto muda com o tempo, mas o pedido precisa lembrar quanto custava na hora da compra. Copiar o preço ali não é erro de normalização, é snapshot histórico de propósito (a gente volta nisso na seção 5.5).
🛠️ Gambiarra boa: os primeiros 10 minutos do Módulo B são de papel. Conceitual no rascunho: caixas, linhas, cardinalidades. Só depois abre o MySQL. Quem modela direto no SQL descobre a tabela faltando no meio do CREATE e refaz tudo. Dez minutos de papel economizam trinta de retrabalho, e o rascunho ainda vira roteiro da apresentação ("essa foi minha modelagem inicial").
⚠️ Pega-ratão: pular o nível conceitual "porque o sistema é pequeno". O sistema da prova nunca é tão pequeno quanto parece: o enunciado esconde entidades (o RSVP esconde um status com data, o check-in esconde um registro com hora). O conceitual é onde essas coisas aparecem antes de custar caro.
3. Entidade, atributo, relacionamento e cardinalidade: a leitura fina
3.1 Achar entidades num enunciado
Entidade é uma coisa sobre a qual o sistema guarda informação com identidade própria. O truque de leitura: substantivos que o sistema precisa lembrar depois. No Wedding Pass: usuário, casamento, convidado, convite, mesa, check-in. "Relatório" não é entidade (é uma consulta). "Status" sozinho não é entidade (é atributo de alguém).
3.2 Tipos de atributo (e uma regra de ouro)
- Simples vs composto: endereço é composto (rua, número, cidade). No físico, geralmente vira colunas separadas.
- Obrigatório vs opcional: nome do convidado é obrigatório (
NOT NULL); acompanhantes pode ser opcional. - Derivado (calculado): idade deriva da data de nascimento; lotação da mesa deriva da contagem de convidados nela. Regra de ouro: não guardar o que dá pra calcular. Guardar
idadenuma coluna cria dado que envelhece errado. Guardadata_nascimentoe calcula. A exceção consciente é o snapshot histórico (opreco_unitarioda seção 2.2).
3.3 Cardinalidade com mínimo e máximo
Além do 1:1, 1:N e N:N, o modelo fino marca o mínimo: a relação é obrigatória ou opcional?
- Convidado (1,1) → casamento: todo convidado pertence a exatamente um casamento. FK
NOT NULL. - Convidado (0,1) → mesa: convidado pode não ter mesa ainda. FK que aceita
NULL. - Casamento (0,N) → convidado: um casamento pode ter zero convidados (recém-criado).
Essa leitura de mínimo é o que decide, lá no físico, se a FK é NOT NULL ou não. E é ela que evita a armadilha da obrigatoriedade dos dois lados: se "todo departamento tem ao menos um funcionário" e "todo funcionário tem departamento", como é que se insere o primeiro registro num banco vazio? (Segura essa pergunta: ela é a porta de entrada da ligação circular, seção 6.)
3.4 Resolvendo cada cardinalidade
- 1:N (o caso mais comum): a FK mora no lado N. Muitos convidados, um casamento →
convidado.casamento_id. - N:N: tabela associativa no meio, com as duas FKs. Se o relacionamento tem informação própria (nota da matrícula, quantidade do item), ela mora na associativa.
- 1:1: raro, e quase sempre é decisão de projeto: separar dados sensíveis (
usuarioeusuario_credencial) ou dados opcionais grandes. Na prova, 1:1 costuma indicar que dava pra ser uma tabela só. Use quando tiver motivo pra contar.
⚠️ Pega-ratão: resolver N:N com lista dentro de campo ("João, Maria, Ana" numa coluna). Quebra busca, quebra contagem, quebra integridade, quebra 1FN. N:N pede tabela associativa, sempre. Se você se pegar querendo guardar uma lista numa coluna, tá faltando uma tabela.
🎯 Isso é a habilidade da Prova Surpresa. O Módulo A dá um enunciado desconhecido e 2h. Quem tem o método (substantivos → entidades → cardinalidades → FK no lado certo) modela qualquer domínio em minutos. O módulo 07 transforma isso num passo a passo completo (enunciado → entidades → endpoints → telas).
4. Chaves: o sistema de identidade do banco
- Chave candidata: qualquer coluna (ou combinação) que identifica a linha sem repetir. Na tabela de convidados: o id, o CPF, talvez o e-mail. Todas são candidatas; uma vira primária.
- Chave primária (PK): a candidata escolhida. Identidade oficial da linha. Nunca repete, nunca é nula.
- Chave natural vs substituta (surrogate): CPF é chave natural (existe no mundo real).
id INT AUTO_INCREMENTé substituta (inventada pelo banco). Na prática e na prova, surrogate ganha: chave natural muda (pessoa corrige o CPF digitado errado, troca de e-mail) e mudar PK propaga dor em todas as FKs. O padrão de mercado éidnumérico como PK e o dado natural comoUNIQUEao lado. - Chave estrangeira (FK): coluna que aponta pra PK de outra tabela. É o que materializa o relacionamento e habilita a integridade referencial (seção 7).
- Chave composta: PK formada por mais de uma coluna. Habitat natural: tabela associativa (
matriculacom PK(aluno_id, turma_id)). Alternativa igualmente válida:idpróprio +UNIQUE(aluno_id, turma_id). As duas garantem a mesma regra; a segunda deixa a tabela mais fácil de referenciar depois. - Código de negócio: o código do convite que o convidado digita no check-in não é a PK. É uma coluna
codigo VARCHAR(8) UNIQUE, gerada aleatória. PK é assunto interno do banco; código de negócio é interface com humano. Separar os dois é maturidade de modelagem (e segurança: código sequencial na URL deixa qualquer um adivinhar o convite do vizinho).
🎯 Foco de prova: a banca verifica objetivamente se as tabelas estão de fato relacionadas por FK no banco, ou só "de mentira" (colunas soltas com nome parecido). FOREIGN KEY declarado é sim/não no CIS. E a escolha surrogate + natural como UNIQUE é um "porquê" pronto pra apresentação.
5. Normalização: 1FN, 2FN e 3FN com a tabela errada na mesa
Normalizar é eliminar redundância e dependência mal colocada. A teoria fica clara quando se parte do erro. Eis a "tabelona" que todo mundo já fez na vida (uma planilha virada tabela):
convidados_geral
| id | convidado | telefones | casamento | data_casamento | mesa | capacidade_mesa |
| 1 | Ana Souza | 5199..., 5198... | Júlia & Pedro | 2026-11-21 | 5 | 8 |
| 2 | Bruno Lima | 5197... | Júlia & Pedro | 2026-11-21 | 5 | 8 |
| 3 | Carla Dias | 5196..., 5195... | Júlia & Pedro | 2026-11-21 | 2 | 10 |
5.1 As três anomalias que essa tabela cria
- Anomalia de atualização: a data do casamento mudou? Tem que atualizar em TODAS as linhas. Esqueceu uma, o banco mente.
- Anomalia de inserção: quero cadastrar a mesa 7 que ainda não tem convidado. Não dá: mesa só existe grudada num convidado.
- Anomalia de exclusão: apaguei a Carla (única da mesa 2)... e perdi junto a informação de que a mesa 2 tem capacidade 10.
Normalização é o antídoto dessas três.
5.2 1FN: valores atômicos, sem grupos repetidos
Cada célula guarda um valor. telefones com dois números viola. E a variação "resolvida" com telefone1, telefone2, telefone3 também viola (grupo repetido: e o quarto telefone?).
Correção: telefone vira tabela filha.
CREATE TABLE telefone (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
convidado_id INT UNSIGNED NOT NULL,
numero VARCHAR(15) NOT NULL,
FOREIGN KEY (convidado_id) REFERENCES convidado(id)
);
(Na prova, se o enunciado pede um telefone por convidado, uma coluna resolve. 1FN pega quando são vários.)
5.3 2FN: todo campo depende da chave inteira
Só faz sentido com chave composta. Exemplo: tabela de presença com PK (convidado_id, evento_id):
presenca
| convidado_id | evento_id | nome_convidado | status |
status depende do par completo (aquele convidado naquele evento): ok. nome_convidado depende só de convidado_id: violação de 2FN. O nome já mora na tabela convidado; aqui é cópia que vai dessincronizar.
Correção: tirar nome_convidado e buscar por JOIN quando precisar.
5.4 3FN: nenhum campo depende de outro campo não chave
Na tabelona: capacidade_mesa depende de mesa, e mesa não é chave. Isso é dependência transitiva (id → mesa → capacidade). Resultado: a capacidade da mesa 5 repetida em toda linha de convidado da mesa 5, e a anomalia de exclusão da Carla.
Correção: mesa vira tabela própria (id, numero, capacidade), convidado ganha mesa_id.
5.5 O resultado normalizado (e quando parar)
casamento(id, nome, data_evento, ...)
mesa(id, casamento_id, numero, capacidade)
convidado(id, casamento_id, mesa_id NULL, nome, ...)
telefone(id, convidado_id, numero)
Cada informação mora num lugar só. As três anomalias morreram.
Desnormalizar de propósito é ferramenta de gente grande, não desculpa de preguiça: o preco_unitario no item do pedido (snapshot histórico) e contadores cacheados em sistemas de altíssimo volume são casos legítimos. A diferença entre desnormalização e erro é uma só: você sabe dizer por quê.
🛠️ Gambiarra boa (a regra de bolso). Na hora da prova ninguém recita definição de forma normal. O instinto que elas geram é o que se aplica: cada informação mora num lugar só. Se você está copiando o mesmo dado em várias linhas, falta uma tabela. Se um campo não fala sobre a "coisa" daquela tabela, ele está no lugar errado. Mas a definição precisa estar na cabeça também, porque banca pergunta ("por que você separou a mesa?" → "dependência transitiva, terceira forma normal" é resposta de nível 3).
⚠️ Pega-ratão do outro extremo: normalizar demais. Vinte tabelas microscópicas numa prova de 2h30 atrasam o CREATE, complicam cada JOIN e não pontuam mais por isso. 3FN com bom senso é o equilíbrio: organizado e prático.
6. Ligação circular: o que é, quando é problema e como resolver
Ligação circular (referência circular) é quando as FKs formam um ciclo: seguindo as setas de dependência, você volta pro ponto de partida. Existem três formas, e elas têm veredictos diferentes. Isso importa porque a pergunta "isso é errado?" não tem uma resposta só: tem três.
6.1 Auto-relacionamento (a tabela aponta pra ela mesma): legítimo e elegante
CREATE TABLE funcionario (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
gerente_id INT UNSIGNED NULL,
FOREIGN KEY (gerente_id) REFERENCES funcionario(id)
);
Todo funcionário pode ter um gerente, que também é funcionário. Isso modela hierarquia com uma tabela só: organograma, categoria com subcategoria, comentário com resposta, menu com submenu. Não é erro nenhum; é padrão clássico.
Os dois cuidados que fazem ele funcionar:
- A FK aceita
NULL. Alguém está no topo (a diretora não tem gerente). SemNULLpermitido, ninguém consegue ser o primeiro inserido. - Consultar hierarquia profunda exige recursão. "Quem responde pra Ana, direta e indiretamente?" não sai com um JOIN simples. Um nível sai com self-join (seção 14); a árvore inteira pede CTE recursiva (
WITH RECURSIVE, que o MySQL 8 tem). Na prova, quase sempre um nível basta.
6.2 Ciclo entre duas tabelas (A → B e B → A): sinal amarelo, resolvível
O caso clássico:
-- departamento tem um chefe (que é funcionário)
-- funcionário pertence a um departamento
departamento(id, nome, chefe_id → funcionario.id)
funcionario(id, nome, departamento_id → departamento.id)
Aqui nasce o problema do ovo e da galinha: pra inserir o departamento preciso do chefe, pra inserir o chefe preciso do departamento. Com as duas FKs NOT NULL, o banco vazio trava: nenhum insert é possível. E o espelho disso acontece no DELETE: com RESTRICT dos dois lados, nenhuma das duas linhas consegue ser apagada primeiro.
Como resolver (em ordem de preferência):
- Quebrar o ciclo deixando uma FK aceitar
NULL.departamento.chefe_id NULL: cria o departamento sem chefe, insere o funcionário, faz o UPDATE apontando o chefe. Duas etapas, problema resolvido. Escolhe pra ser anulável o lado cuja ausência temporária faz sentido no negócio (departamento sem chefe por uns dias é normal; funcionário sem departamento talvez não). - Repensar se as duas FKs precisam existir. Muitas vezes uma delas é derivável ou modelável de outro jeito: um
cargona tabela de funcionário ("chefe") + a FK de departamento resolve sem ciclo nenhum. Ciclo que dá pra dissolver é ciclo que deveria ser dissolvido. - Última instância (fora da prova): inserir os dois dentro de uma transação com verificação adiada. O MySQL/InnoDB não suporta constraint adiada (
DEFERRABLE, coisa do PostgreSQL), então no MySQL essa porta nem existe. Mais um motivo pra resolver no modelo.
6.3 Ciclo longo (A → B → C → A): cheiro forte de modelagem errada
Quando o ciclo atravessa três ou mais tabelas, quase sempre alguma daquelas FKs não representa um relacionamento real, e sim uma tentativa de atalho ("vou guardar o casamento_id no check-in pra não fazer JOIN"). O check-in já chega no casamento via convidado; a FK extra é redundante, pode dessincronizar e ainda fecha o ciclo. Regra prática: cada fato do negócio entra no modelo uma vez. Se uma FK é derivável seguindo outras FKs, ela provavelmente não deveria existir.
6.4 No Wedding Pass
O modelo natural é acíclico: casamento ← mesa ← convidado ← convite / checkin, tudo apontando "pra cima" sem voltar. Um ciclo apareceria se, por exemplo, a mesa ganhasse um convidado_responsavel_id (mesa → convidado → mesa). Se o enunciado pedir "responsável pela mesa", a solução da seção 6.2 se aplica direto: convidado_responsavel_id NULL, preenchido depois que os convidados existem.
🎯 Resposta pronta pra banca (e pro Diogo 🙂): "ligação circular é errado?" Auto-relacionamento não é errado, é o padrão certo pra hierarquia. Ciclo entre tabelas é sinal amarelo: trava inserts e deletes, e se resolve quebrando o ciclo com uma FK anulável ou repensando o modelo. Ciclo longo quase sempre denuncia FK redundante. Saber essa resposta em três frases é diferencial de apresentação, porque a maioria só sabe dizer "acho que é ruim".
⚠️ Pega-ratão: obrigatoriedade nos dois lados do relacionamento (todo X tem Y e todo Y tem X, ambos NOT NULL). Mesmo sem FK circular declarada, isso cria o ovo-e-galinha na prática. Sempre pergunte: "com o banco vazio, qual linha entra primeiro?" Se a resposta é "nenhuma", o modelo tem um nó.
7. Integridade referencial: CASCADE, SET NULL e RESTRICT com casos reais
Integridade referencial é a garantia de que nenhuma FK aponta pro vazio. O banco cuida disso reagindo quando alguém apaga ou altera a linha "pai". A reação é você quem escolhe, na declaração da FK:
FOREIGN KEY (convidado_id) REFERENCES convidado(id)
ON DELETE CASCADE
ON UPDATE CASCADE
| Regra | Ao apagar o pai... | Use quando o filho... |
|---|---|---|
RESTRICT / NO ACTION |
O banco barra a exclusão | Não pode perder o pai (registro histórico, financeiro) |
CASCADE |
Os filhos somem junto | Só existe em função do pai (convite sem convidado não significa nada) |
SET NULL |
A FK do filho vira NULL |
Sobrevive sem o pai (a FK precisa aceitar NULL) |
(No MySQL/InnoDB, RESTRICT e NO ACTION se comportam igual, e o padrão quando você não escreve nada é esse. ON UPDATE CASCADE cobre o caso raro de a PK do pai mudar; com AUTO_INCREMENT isso quase não acontece, mas declarar não custa.)
7.1 As decisões do Wedding Pass, uma a uma
| FK | Regra | Porquê (esse é o texto da tua apresentação) |
|---|---|---|
convite.convidado_id |
CASCADE |
Convite é papel do convidado. Apagou o convidado, o convite perde o sentido |
convidado.mesa_id |
SET NULL |
Apagou a mesa, o convidado continua existindo, só fica sem lugar marcado |
convidado.casamento_id |
CASCADE (ou RESTRICT) |
Apagar o casamento = limpar o evento inteiro (CASCADE); ou proibir apagar casamento com convidados (RESTRICT). As duas defendem; escolhe e justifica |
checkin.convidado_id |
RESTRICT |
Check-in é registro de fato: aquela pessoa entrou. Apagar convidado que já entrou silenciosamente some com histórico. Barrar e exigir decisão explícita é o comportamento seguro |
mesa.casamento_id |
CASCADE |
Mesa só existe dentro de um casamento |
Repara que não existe UMA resposta certa pra tudo: existe resposta defendida. "Escolhi RESTRICT no check-in porque é registro histórico" é frase de nível 3.
⚠️ Pega-ratão: o CASCADE em cadeia. casamento CASCADE→ convidado CASCADE→ convite/checkin: um DELETE FROM casamento WHERE id = 1 varre o evento inteiro em silêncio. Às vezes é exatamente o que se quer; às vezes é meio banco sumindo por um clique. Antes de pôr CASCADE, siga a cadeia até o fim e pergunte "tudo isso deve sumir junto?". E no front, ação com CASCADE embaixo SEMPRE pede confirmação destrutiva (módulo 08).
🎯 Foco de prova: o descritivo cita integridade referencial explicitamente, e ela é verificável de forma objetiva (a banca tenta apagar um pai com filhos e observa). Fazer o banco impedir estado inválido é mais forte que confiar que a aplicação nunca erra. Essa frase também é da apresentação.
8. Constraints: o banco dizendo "não" (defesa em profundidade)
Além das chaves, o banco impõe regras nos campos. São a última linha de defesa: mesmo que a API tenha bug, mesmo que alguém insira na mão, a regra segura.
CREATE TABLE mesa (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
casamento_id INT UNSIGNED NOT NULL, -- NOT NULL: obrigatório
numero INT UNSIGNED NOT NULL,
capacidade INT UNSIGNED NOT NULL,
CONSTRAINT chk_capacidade CHECK (capacidade BETWEEN 1 AND 50), -- CHECK: regra de valor
CONSTRAINT uq_mesa_numero UNIQUE (casamento_id, numero), -- UNIQUE composto
FOREIGN KEY (casamento_id) REFERENCES casamento(id) ON DELETE CASCADE
);
- NOT NULL: obrigatório. Decidido lá na cardinalidade mínima (seção 3.3).
- UNIQUE: não repete. Pode ser composto:
UNIQUE (casamento_id, numero)diz "o número da mesa não repete dentro do mesmo casamento" (em casamentos diferentes pode). Essa nuance de UNIQUE composto cai muito em prova. - DEFAULT: valor quando ninguém informa.
status ENUM(...) DEFAULT 'pendente',criado_em DATETIME DEFAULT CURRENT_TIMESTAMP. - CHECK: regra de valor. Desde o MySQL 8.0.16 o CHECK é aplicado de verdade (antes o MySQL aceitava a sintaxe e ignorava; em versão velha, cuidado).
CHECK (capacidade > 0),CHECK (acompanhantes <= 5).
O UNIQUE que ganha jogo: UNIQUE (convidado_id) na tabela checkin é o bloqueio de dupla entrada garantido no nível mais baixo. Nem bug de API, nem dois cliques rápidos, nem duas abas abertas furam. O módulo 07 constrói a feature completa em cima dessa constraint.
🛠️ Gambiarra boa: deixa o banco trabalhar por você. Toda regra que vira constraint é regra que nunca será furada e que a aplicação valida "de graça" (a API só precisa traduzir o erro do banco pra mensagem bonita). Constraint é a defesa mais barata e mais confiável do sistema inteiro. A validação da API (módulo 04) existe pra dar mensagem boa; a constraint existe pra garantir a verdade.
9. Tipos de dados MySQL: escolher certo é pontuar de graça
Tipo errado funciona no dia 1 e explode no dia 30 (ou na frente da banca). A tabela de decisão:
| Dado | Tipo certo | Porquê | Erro comum |
|---|---|---|---|
| id | INT UNSIGNED AUTO_INCREMENT |
4 bytes, até ~4,29 bi sem sinal; sobra pra prova | BIGINT à toa (dobra o espaço de todo índice) |
| Nome, e-mail | VARCHAR(100) / VARCHAR(150) |
Tamanho variável, só ocupa o que usa | TEXT pra campo curto (não pode ter DEFAULT, índice pior) |
| CPF, telefone, CEP | VARCHAR |
Tem zero à esquerda e ninguém soma CPF | INT (come o zero à esquerda: CPF 012... vira 12...) |
| Dinheiro | DECIMAL(10,2) |
Exato. 0.10 é 0.10 | FLOAT/DOUBLE: binário não representa 0.1 direito, e centavos somem em soma grande |
| Data do evento | DATE |
Só a data, compara e calcula | VARCHAR "21/11/2026" (não ordena, não entra em DATEDIFF) |
| Momento do check-in | DATETIME |
Data + hora, range gigante | — |
| criado_em / atualizado_em | DATETIME DEFAULT CURRENT_TIMESTAMP (e ON UPDATE CURRENT_TIMESTAMP no segundo) |
Auditoria de graça | Preencher na aplicação e esquecer em algum INSERT |
| Status | ENUM('pendente','confirmado','recusado') |
Legível, compacto, valida sozinho | VARCHAR livre (aceita "confirmadu") |
| Sim/não | TINYINT(1) (o BOOLEAN do MySQL) |
Padrão da casa | VARCHAR "sim"/"não" |
| Texto longo (observações) | TEXT |
Até 64KB | — |
DATETIME vs TIMESTAMP, porque banca gosta: os dois guardam data+hora. TIMESTAMP converte pro fuso do servidor e só vai até janeiro de 2038 (4 bytes com época Unix); DATETIME guarda literal o que você mandou e vai do ano 1000 ao 9999. Regra prática do nosso contexto: DATETIME em tudo, com DEFAULT CURRENT_TIMESTAMP onde precisar de carimbo automático. Simples e sem surpresa de fuso.
ENUM vs tabela de domínio, a decisão honesta: ENUM é perfeito quando a lista é pequena e estável (status de convite não ganha valores novos toda semana). Se a lista cresce ou tem dados próprios (categorias com descrição e cor), aí é tabela + FK. Na prova, status é ENUM e segue o jogo; saber verbalizar o trade-off é o bônus.
⚠️ Pega-ratão: FLOAT pra dinheiro. É o erro de tipo mais clássico que existe, e é dos que a banca conhece de cor. DECIMAL(10,2) custa o mesmo pra escrever e é a resposta certa em qualquer sistema com valor monetário. Grava como reflexo.
10. O script do banco: DDL completo do Wedding Pass
Tudo que veio até aqui, materializado. Esse script é referência de estudo e esqueleto pra treinar (a prova terá outro domínio, mas o formato é esse):
DROP DATABASE IF EXISTS wedding_pass;
CREATE DATABASE wedding_pass CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE wedding_pass;
-- Ordem de criação: pais antes de filhos (a FK exige que o alvo exista)
CREATE TABLE usuario (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL,
senha_hash VARCHAR(255) NOT NULL, -- hash bcrypt, NUNCA a senha (seção 18.3)
perfil ENUM('admin','organizador','recepcao') NOT NULL DEFAULT 'recepcao',
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_usuario_email UNIQUE (email)
) ENGINE=InnoDB;
CREATE TABLE casamento (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(150) NOT NULL, -- "Júlia & Pedro"
data_evento DATETIME NOT NULL,
local VARCHAR(200) NOT NULL,
capacidade_total INT UNSIGNED NOT NULL,
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_capacidade_total CHECK (capacidade_total > 0)
) ENGINE=InnoDB;
CREATE TABLE mesa (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
casamento_id INT UNSIGNED NOT NULL,
numero INT UNSIGNED NOT NULL,
capacidade INT UNSIGNED NOT NULL,
CONSTRAINT chk_mesa_capacidade CHECK (capacidade BETWEEN 1 AND 50),
CONSTRAINT uq_mesa_numero UNIQUE (casamento_id, numero),
FOREIGN KEY (casamento_id) REFERENCES casamento(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE convidado (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
casamento_id INT UNSIGNED NOT NULL,
mesa_id INT UNSIGNED NULL, -- (0,1): pode não ter mesa ainda
nome VARCHAR(100) NOT NULL,
email VARCHAR(150) NULL,
telefone VARCHAR(15) NULL,
acompanhantes TINYINT UNSIGNED NOT NULL DEFAULT 0,
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_acompanhantes CHECK (acompanhantes <= 5),
FOREIGN KEY (casamento_id) REFERENCES casamento(id) ON DELETE CASCADE,
FOREIGN KEY (mesa_id) REFERENCES mesa(id) ON DELETE SET NULL
) ENGINE=InnoDB;
CREATE TABLE convite (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
convidado_id INT UNSIGNED NOT NULL,
codigo CHAR(8) NOT NULL, -- código de negócio, aleatório, não sequencial
status ENUM('pendente','confirmado','recusado') NOT NULL DEFAULT 'pendente',
enviado_em DATETIME NULL,
respondido_em DATETIME NULL,
CONSTRAINT uq_convite_codigo UNIQUE (codigo),
CONSTRAINT uq_convite_convidado UNIQUE (convidado_id), -- um convite por convidado
FOREIGN KEY (convidado_id) REFERENCES convidado(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE checkin (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
convidado_id INT UNSIGNED NOT NULL,
registrado_por INT UNSIGNED NOT NULL, -- qual usuário da recepção registrou
registrado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_checkin_convidado UNIQUE (convidado_id), -- 🔒 bloqueio de dupla entrada
FOREIGN KEY (convidado_id) REFERENCES convidado(id) ON DELETE RESTRICT,
FOREIGN KEY (registrado_por) REFERENCES usuario(id) ON DELETE RESTRICT
) ENGINE=InnoDB;
Anatomia das decisões (cada uma é um "porquê" de apresentação):
utf8mb4: o UTF-8 completo do MySQL (outf8velho trunca emoji e alguns caracteres). Padrão do MySQL 8, mas declarar mostra intenção.ENGINE=InnoDB: o engine que suporta FK e transação (o MyISAM antigo não tinha nenhum dos dois). É o default do MySQL 8; escrever é documentação.DROP ... IF EXISTSno topo: torna o script idempotente (roda de novo do zero sem erro). Na prova, recomeçar limpo em 5 segundos vale ouro.- Ordem: pais antes de filhos. A FK exige que a tabela alvo exista. E o espelho: pra dropar na mão, filhos antes dos pais (o
DROP DATABASEresolve tudo de uma vez).
🛠️ Gambiarra boa: um arquivo schema.sql versionado no Git desde o dia 1. Banco não se cria clicando no Workbench sem registro: se cria por script, que roda inteiro, do zero, quantas vezes precisar. O script é a memória, a evidência pra banca e o botão de pânico (deu ruim? dropa e roda de novo, 10 segundos).
11. Dicionário de dados: o mapa que acompanha o modelo
Dicionário de dados é a documentação tabular do banco: cada tabela, cada coluna, tipo, regra e significado. O plano de curso pede, e a banca reconhece na hora. Formato enxuto, exemplo pra convite:
| Coluna | Tipo | Nulo? | Regra | Descrição |
|---|---|---|---|---|
| id | INT UNSIGNED | não | PK, auto | Identificador interno |
| convidado_id | INT UNSIGNED | não | FK → convidado, UNIQUE, CASCADE | Dono do convite (um por convidado) |
| codigo | CHAR(8) | não | UNIQUE, gerado aleatório | Código que o convidado usa no RSVP e check-in |
| status | ENUM | não | pendente/confirmado/recusado, DEFAULT pendente | Situação da resposta |
| enviado_em | DATETIME | sim | — | Quando o convite foi disparado |
| respondido_em | DATETIME | sim | — | Quando o convidado respondeu |
🛠️ Gambiarra boa: o dicionário de prova se escreve em 5 minutos DEPOIS que o schema.sql está pronto, praticamente transcrevendo o script pra tabela. Custo mínimo, cara de documentação profissional, e ainda organiza a tua própria cabeça pra apresentação. Se o tempo apertar demais, o script comentado (como o da seção 10) já cumpre meio papel.
12. Manipulação: INSERT, UPDATE e DELETE sem sustos
-- INSERT simples
INSERT INTO convidado (casamento_id, nome, email, telefone)
VALUES (1, 'Ana Souza', 'ana@email.com', '51999990000');
-- INSERT múltiplo (a base do seed): uma instrução, várias linhas
INSERT INTO mesa (casamento_id, numero, capacidade) VALUES
(1, 1, 8), (1, 2, 8), (1, 3, 10), (1, 4, 6);
-- INSERT ... SELECT: inserir a partir de consulta (gerar convites pra quem não tem)
INSERT INTO convite (convidado_id, codigo)
SELECT c.id, UPPER(SUBSTRING(MD5(RAND()), 1, 8))
FROM convidado c
LEFT JOIN convite cv ON cv.convidado_id = c.id
WHERE cv.id IS NULL;
-- UPDATE: sempre com WHERE
UPDATE convite SET status = 'confirmado', respondido_em = NOW()
WHERE codigo = 'A3F9K2LM';
-- DELETE: sempre com WHERE
DELETE FROM convidado WHERE id = 42;
- DELETE vs TRUNCATE:
DELETE FROM tabelaapaga linha a linha (respeitando FKs, disparando regras);TRUNCATE TABLEzera a tabela inteira e reseta o AUTO_INCREMENT, mas é barrado se existir FK apontando pra ela. Pra "zerar e recomeçar", o caminho da prova é rodar o schema.sql de novo.
⚠️ Pega-ratão (o clássico dos clássicos): UPDATE/DELETE sem WHERE. Atualiza ou apaga a tabela INTEIRA, sem confirmação, sem volta. Na pressa da prova é um desastre real.
🛠️ Gambiarra boa: o ritual do SELECT antes. Antes de qualquer UPDATE/DELETE delicado, roda um SELECT * FROM tabela WHERE <mesma condição> e olha o que volta. É exatamente o conjunto que vai ser afetado. Conferiu, troca o SELECT * por UPDATE ... SET ou DELETE. Dez segundos que blindam contra o pior erro possível.
13. Consultas: SELECT do básico ao afiado
13.1 Filtros
SELECT nome, email FROM convidado
WHERE casamento_id = 1
AND mesa_id IS NULL -- NULL se testa com IS, nunca com =
AND nome LIKE 'A%' -- começa com A ('%A%' = contém)
AND acompanhantes BETWEEN 1 AND 3
AND id IN (SELECT convidado_id FROM convite WHERE status = 'pendente')
ORDER BY nome ASC
LIMIT 10 OFFSET 0; -- página 1 com 10 itens (paginação, módulo 04)
Pontos finos: NULL não é igual a nada, nem a outro NULL (WHERE mesa_id = NULL não acha NADA; é IS NULL). LIKE '%texto%' com curinga no começo não usa índice (seção 16), mas na escala da prova não dói. AND tem precedência sobre OR: misturou os dois, usa parênteses.
13.2 Funções que resolvem prova
SELECT
CONCAT(nome, ' (mesa ', COALESCE(mesa_id, 'sem mesa'), ')') AS etiqueta,
UPPER(nome) AS nome_maiusculo,
COALESCE(email, 'sem e-mail') AS contato, -- primeiro valor não nulo
IF(acompanhantes > 0, 'com acompanhante', 'sozinho') AS tipo,
CASE
WHEN acompanhantes = 0 THEN 'individual'
WHEN acompanhantes <= 2 THEN 'família pequena'
ELSE 'família grande'
END AS faixa
FROM convidado;
13.3 Datas: o grupo de função que mais cai
SELECT
NOW(), -- data e hora agora
CURDATE(), -- só a data de hoje
DATEDIFF(ca.data_evento, CURDATE()) AS dias_faltando,
DATE_FORMAT(ca.data_evento, '%d/%m/%Y %H:%i') AS data_br,
DATE_ADD(ca.data_evento, INTERVAL -7 DAY) AS prazo_rsvp
FROM casamento ca;
-- Filtrar por período (check-ins da última hora)
SELECT * FROM checkin
WHERE registrado_em >= NOW() - INTERVAL 1 HOUR;
DATEDIFF devolve dias; pra outras unidades, TIMESTAMPDIFF(MINUTE, inicio, fim). DATE_FORMAT formata pra exibir (%d/%m/%Y é o formato BR), mas guarda-se SEMPRE no tipo de data nativo: formatação é saída, não armazenamento.
14. JOINs: onde o relacional mostra a que veio
JOIN junta linhas de tabelas relacionadas, casando pela condição do ON (quase sempre FK = PK).
-- INNER JOIN: só quem tem correspondência dos dois lados
-- "convidados COM convite, mostrando o status"
SELECT c.nome, cv.codigo, cv.status
FROM convidado c
INNER JOIN convite cv ON cv.convidado_id = c.id;
-- LEFT JOIN: todos da esquerda, com ou sem par na direita
-- "TODOS os convidados, e o convite de quem tiver"
SELECT c.nome, cv.status -- cv.status vem NULL pra quem não tem convite
FROM convidado c
LEFT JOIN convite cv ON cv.convidado_id = c.id;
-- O padrão-ouro "o que está FALTANDO": LEFT JOIN + IS NULL
-- "convidados SEM convite" (é a query que alimentou o INSERT...SELECT da seção 12)
SELECT c.nome
FROM convidado c
LEFT JOIN convite cv ON cv.convidado_id = c.id
WHERE cv.id IS NULL;
-- Três tabelas (vai encadeando): convidado + mesa + status do convite
SELECT c.nome, m.numero AS mesa, cv.status
FROM convidado c
LEFT JOIN mesa m ON m.id = c.mesa_id
LEFT JOIN convite cv ON cv.convidado_id = c.id
ORDER BY m.numero, c.nome;
-- Self-join (o auto-relacionamento da seção 6.1 em ação)
SELECT f.nome AS funcionario, g.nome AS gerente
FROM funcionario f
LEFT JOIN funcionario g ON g.id = f.gerente_id;
Guia rápido de escolha: INNER quando só interessa quem tem par. LEFT quando a lista da esquerda precisa vir completa (lista de convidados com dados opcionais ao lado: quase sempre é LEFT). RIGHT existe, mas todo RIGHT vira um LEFT invertendo a ordem das tabelas; o mercado escreve LEFT e pronto. UNION empilha resultados de dois SELECTs com as mesmas colunas (UNION ALL mantém duplicados e é mais rápido; UNION deduplica).
⚠️ Pega-ratão: esquecer a condição do JOIN (ou errar ela). Sem ON correto o banco cruza todo mundo com todo mundo (produto cartesiano): 50 convidados × 50 convites = 2500 linhas de lixo. Se uma consulta devolver muito mais linha que o esperado, o primeiro suspeito é o ON.
🛠️ Gambiarra boa: apelido curto pra toda tabela (convidado c, convite cv) e coluna sempre prefixada (c.nome). Em JOIN de 3+ tabelas com colunas de nome igual (id, nome), isso evita o erro "ambiguous column" e deixa a query legível pra banca.
15. Agrupamento e agregação: o SQL que vira dashboard
Funções de agregação resumem linhas: COUNT, SUM, AVG, MIN, MAX. Com GROUP BY, o resumo sai por grupo. E aqui mora o dashboard inteiro do Wedding Pass:
-- Os números do topo do dashboard
SELECT
COUNT(*) AS total_convidados,
SUM(acompanhantes) AS total_acompanhantes,
COUNT(*) + SUM(acompanhantes) AS pessoas_esperadas
FROM convidado WHERE casamento_id = 1;
-- Convites por status (o gráfico de pizza)
SELECT cv.status, COUNT(*) AS quantidade
FROM convite cv
JOIN convidado c ON c.id = cv.convidado_id
WHERE c.casamento_id = 1
GROUP BY cv.status;
-- Ocupação por mesa (COUNT(c.id), não COUNT(*): mesa vazia conta 0)
SELECT m.numero, m.capacidade,
COUNT(c.id) AS ocupacao,
ROUND(COUNT(c.id) / m.capacidade * 100) AS pct
FROM mesa m
LEFT JOIN convidado c ON c.mesa_id = m.id
WHERE m.casamento_id = 1
GROUP BY m.id, m.numero, m.capacidade;
-- Só as mesas acima de 90% (o ALERTA do enunciado): HAVING filtra o grupo
SELECT m.numero, COUNT(c.id) / m.capacidade * 100 AS pct
FROM mesa m
LEFT JOIN convidado c ON c.mesa_id = m.id
GROUP BY m.id, m.numero, m.capacidade
HAVING pct >= 90;
-- Lotação em tempo real: quem já entrou (check-ins) sobre a capacidade
SELECT COUNT(ck.id) AS presentes, ca.capacidade_total,
ROUND(COUNT(ck.id) / ca.capacidade_total * 100, 1) AS lotacao_pct
FROM casamento ca
LEFT JOIN convidado c ON c.casamento_id = ca.id
LEFT JOIN checkin ck ON ck.convidado_id = c.id
WHERE ca.id = 1
GROUP BY ca.id, ca.capacidade_total;
-- Check-ins por hora (o gráfico de linha da festa)
SELECT DATE_FORMAT(registrado_em, '%H:00') AS hora, COUNT(*) AS entradas
FROM checkin
GROUP BY hora
ORDER BY hora;
WHERE vs HAVING, de uma vez por todas: WHERE filtra linhas antes de agrupar; HAVING filtra grupos depois de agregar. "Convidados do casamento 1" é WHERE; "mesas com mais de 90%" é HAVING (porque o % só existe depois do COUNT). E a regra do GROUP BY: toda coluna do SELECT que não está dentro de função de agregação precisa estar no GROUP BY (o MySQL 8 reclama se não estiver, e ele está certo).
Subquery quando o filtro depende de outro resultado: WHERE acompanhantes > (SELECT AVG(acompanhantes) FROM convidado) (convidados acima da média). Subquery no FROM cria uma "tabela temporária" pra agregar em cima de agregação. São ferramentas de canivete: use quando o JOIN não expressa a pergunta.
🎯 O dashboard nasce no SQL, e essa seção É o dashboard. Cada query acima corresponde a um número ou gráfico da tela do módulo 07. Quem sai daqui com essas seis queries no músculo monta o dashboard do Módulo C em minutos, porque a parte difícil (a pergunta certa em SQL) já está pronta. O front só pinta o resultado.
16. Views e índices: o tunning que a UC3 pede
16.1 View: consulta com nome
View é uma consulta salva que se usa como tabela:
CREATE VIEW vw_ocupacao_mesa AS
SELECT m.id, m.casamento_id, m.numero, m.capacidade,
COUNT(c.id) AS ocupacao,
ROUND(COUNT(c.id) / m.capacidade * 100) AS pct
FROM mesa m
LEFT JOIN convidado c ON c.mesa_id = m.id
GROUP BY m.id, m.casamento_id, m.numero, m.capacidade;
-- E agora o dashboard vira consulta simples:
SELECT * FROM vw_ocupacao_mesa WHERE casamento_id = 1 AND pct >= 90;
O que a view compra: a query cabeluda mora num lugar só (mudou a regra, muda na view, todo consumidor ganha de graça), e o código da API fica limpo. O que ela NÃO compra: velocidade (a view roda a consulta por baixo toda vez; não é cache). Na prova, view no dashboard é organização que a banca vê e a UC3 pede nominalmente.
16.2 Índice: o atalho de leitura
Sem índice, buscar WHERE email = 'ana@...' obriga o banco a ler a tabela INTEIRA (full scan). Índice é uma estrutura ordenada ao lado da tabela (uma árvore B-tree) que leva direto às linhas certas, como o índice remissivo de um livro.
O que já vem indexado sem você pedir: PK (sempre), UNIQUE (sempre) e, no InnoDB, toda FK declarada ganha índice automaticamente. Ou seja: o schema da seção 10 já nasce bem indexado pros JOINs.
Quando criar na mão: coluna que aparece com frequência em WHERE, ORDER BY ou JOIN e que não é PK/UNIQUE/FK. No Wedding Pass, o candidato real é a busca por nome:
CREATE INDEX idx_convidado_nome ON convidado(nome);
Índice composto (mais de uma coluna) segue a regra da esquerda: um índice em (casamento_id, nome) serve pra filtrar por casamento_id sozinho e por casamento_id + nome, mas NÃO serve pra filtrar só por nome (o MySQL lê o índice da esquerda pra direita e não pula coluna). Por isso a ordem das colunas importa: a coluna de igualdade mais usada vai primeiro.
O custo que ninguém conta: todo índice desacelera escrita (cada INSERT/UPDATE atualiza a tabela E cada índice) e ocupa espaço. Índice em tudo é tão errado quanto índice em nada.
E a ferramenta de conferência: EXPLAIN SELECT ... mostra o plano do banco. Na coluna type, ALL significa full scan (sem índice); ref/range significa que o índice entrou. Não precisa dominar EXPLAIN pra prova; precisa saber que ele existe e o que ALL denuncia.
🛠️ Gambiarra boa (a política de índice da prova): confia nos automáticos (PK, UNIQUE, FK) e adiciona no máximo um ou dois na mão, nas colunas de busca do usuário (nome, código). Nessa escala de dados a diferença de performance é invisível, mas a frase "criei índice no nome porque é o campo de busca da recepção" na apresentação mostra que você conhece a ferramenta e o critério. É dos pontos de tunning mais baratos do CIS.
17. Transações: tudo ou nada
Transação agrupa operações numa unidade atômica (o A do ACID): ou tudo confirma, ou tudo desfaz.
START TRANSACTION;
INSERT INTO convidado (casamento_id, nome, email)
VALUES (1, 'Duda Reis', 'duda@email.com');
INSERT INTO convite (convidado_id, codigo)
VALUES (LAST_INSERT_ID(), 'K2M8XP4Q'); -- LAST_INSERT_ID(): o id do insert acima
COMMIT; -- confirma os dois juntos
-- ROLLBACK; -- ou desfaz os dois juntos
Sem transação, se o segundo INSERT falhar (código duplicado, por exemplo), o convidado fica criado sem convite: estado meio-termo que ninguém pediu. Com transação, falhou qualquer parte → ROLLBACK → banco como se nada tivesse acontecido.
Quando usar na prova: toda operação que escreve em mais de uma tabela e precisa ser coerente. Criar convidado + convite. Registrar check-in + qualquer atualização de contagem. No módulo 04 a transação reaparece dentro do código da API (nas duas stacks), e no módulo 07 ela se junta com o UNIQUE do check-in pra fechar a solução da condição de corrida (dois check-ins simultâneos do mesmo código: o isolamento + a constraint garantem que só um passa).
🎯 Vocabulário de nível 3: "essa operação é transacional porque escreve em duas tabelas" é o tipo de frase que, dita naturalmente na apresentação, vale mais que dez minutos de tela bonita. Banca técnica reconhece na hora quem entende atomicidade.
18. Backup, restore, import e export: a política de recuperação
18.1 Dump e restore (a dupla que salva provas)
# Backup (export): o banco inteiro vira um arquivo .sql com CREATEs e INSERTs
mysqldump -u root -p wedding_pass > backup_wedding_2026-08-14.sql
# Restore (import): o arquivo recria tudo
mysql -u root -p wedding_pass < backup_wedding_2026-08-14.sql
O mysqldump gera um script SQL completo: estrutura + dados. Serve de backup, de transporte entre máquinas (a dupla treina em casa e restaura no Senac) e de entrega (se a prova pedir "exporte o banco", é isso).
18.2 A política de recuperação (o conceito que a UC3 pede)
Política de recuperação responde três perguntas: o que copiar (o banco da prova), quando copiar (nos marcos: terminou o schema, terminou o seed, terminou uma feature grande) e como voltar (restore testado, não só backup guardado). Backup que nunca foi restaurado é promessa, não política.
Na competição, a política é simples e real: mysqldump ao fechar cada etapa do Módulo B + o schema.sql + seed.sql versionados no Git. Qualquer desastre (drop errado, UPDATE sem WHERE, máquina travou) se resolve em menos de um minuto.
Import/export de dados avulsos: SELECT ... INTO OUTFILE exporta CSV, LOAD DATA INFILE importa CSV em massa (é a "carga massiva" citada na UC9). Bom saber que existem; na prova, o caminho normal é o dump.
🛠️ Gambiarra boa: o dump é a máquina do tempo. Vai fazer uma mexida grande e arriscada no banco no meio da prova? 15 segundos: mysqldump antes. Deu ruim? Restaura e ninguém soube. Essa disciplina tira o medo de mexer, e quem não tem medo de mexer trabalha mais rápido. (E o commit no Git faz o mesmo pelo código: módulo 02.)
18.3 Senha no banco: hash, nunca texto
- Senha nunca é guardada como digitada. Guarda-se um hash: transformação de mão única, sem volta.
- Login: aplica-se o mesmo processo na senha digitada e comparam-se os hashes.
- Hash de senha usa algoritmo próprio pra senha (bcrypt/argon2): já embute o "sal" (tempero aleatório que faz duas senhas iguais virarem hashes diferentes) e é propositalmente lento, pra inviabilizar força bruta. MD5/SHA1 são rápidos demais e NÃO servem pra senha.
- O hash não se gera no SQL: gera-se na aplicação (bcrypt no Node,
password_hash()no PHP: código pronto no módulo 04) e o banco só guarda a string (por issosenha_hash VARCHAR(255)).
🎯 Avaliação objetiva direta (sim/não): a banca abre a tabela de usuários e olha. Ou tem hash, ou é zero nesse item. E é zero dos dolorosos, porque custava duas linhas de código.
⚠️ Pega-ratão: senha em texto puro "só pra testar" e esquecer de trocar. Na correria acontece. A vacina: o seed já cria o usuário com a senha hasheada desde o primeiro dia, e ninguém nunca insere usuário na mão.
19. Seed: povoar o banco com massa realista
Seed é o script que carrega os dados iniciais, de forma automatizada e repetível. O descritivo pede, e a demo depende dele.
O que um seed de nível 3 tem:
- Idempotência em dupla com o schema: rodou
schema.sql(que dropa e recria) +seed.sql, o banco está SEMPRE no mesmo estado conhecido. - Usuários dos três perfis (admin, organizadora, recepção), com senha hasheada.
- Volume que parece real: 30 a 50 convidados, não 3. Lista com 3 convidados não demonstra busca, nem filtro, nem paginação, nem dashboard.
- Variedade de estados de propósito: confirmados, pendentes, recusados; com e sem mesa; alguns já com check-in; uma mesa lotada acima de 90% (pro alerta do dashboard ter o que alertar!). Cada estado do sistema que a demo mostra precisa existir no seed.
-- seed.sql (trecho): multi-row INSERT é a forma
INSERT INTO usuario (nome, email, senha_hash, perfil) VALUES
('Admin', 'admin@wp.com', '$2b$10$hash...aqui', 'admin'),
('Marina', 'marina@wp.com', '$2b$10$hash...aqui', 'organizador'),
('Portaria','porta@wp.com', '$2b$10$hash...aqui', 'recepcao');
INSERT INTO convidado (casamento_id, mesa_id, nome, email, acompanhantes) VALUES
(1, 1, 'Ana Souza', 'ana@email.com', 1),
(1, 1, 'Bruno Lima', 'bruno@email.com',0),
(1, NULL, 'Carla Dias','carla@email.com',2),
-- ... 30+ linhas, estados variados
(1, 3, 'Zeca Prado', 'zeca@email.com', 0);
Pra gerar massa grande sem digitar: o INSERT ... SELECT da seção 12 gera os convites de todos de uma vez, e um loop pequeno na aplicação (ou uma planilha que monta as linhas de INSERT com fórmula de CONCAT) gera os convidados. Essa é a "carga massiva" da UC9 no nosso tamanho.
🛠️ Gambiarra boa: seed é o palco pronto. Chega no Módulo C com o banco cheio e variado, e o dashboard nasce com números de verdade na primeira renderização. Chega no Módulo D (apresentação) com uma história pra contar em cima de dados que parecem reais. Seed bom se prepara no Módulo B pensando na demo do D: é o mesmo arquivo trabalhando três módulos.
20. Autoavaliação do módulo
- Quando um banco não relacional seria melhor escolha que o relacional? E por que o sistema da prova é o caso oposto?
- Explica a diferença entre modelo conceitual, lógico e físico, e o que acontece com um N:N em cada nível.
- No sistema da escola, por que a nota é atributo da matrícula e não do aluno? Acha um exemplo do mesmo padrão no Wedding Pass.
- Qual a diferença entre chave candidata, primária e substituta? Por que o CPF não deve ser PK?
- Monta de memória a "tabelona" errada e mostra qual forma normal cada problema dela viola.
- O que são as três anomalias (inserção, atualização, exclusão)? Dá um exemplo de cada.
- Ligação circular: quais os três tipos, qual deles é legítimo e como se quebra um ciclo entre duas tabelas?
- Com o banco vazio, que pergunta você faz pra detectar um nó de obrigatoriedade mútua no modelo?
- Justifica a regra de deleção de cada FK do Wedding Pass (CASCADE, SET NULL ou RESTRICT) como se estivesse na apresentação.
- Por que
UNIQUE (convidado_id)na tabela de check-in é o bloqueio de dupla entrada mais confiável do sistema? - Por que dinheiro é
DECIMALe nuncaFLOAT? E por que CPF éVARCHARe nuncaINT? - Qual a diferença entre WHERE e HAVING? Escreve a query das mesas acima de 90%.
- Escreve, sem olhar, a query "convidados sem convite" (LEFT JOIN + IS NULL) e explica por que INNER JOIN não resolveria.
- O que uma view compra e o que ela não compra? E qual a regra da esquerda do índice composto?
- Quando uma operação precisa de transação? Dá dois exemplos do Wedding Pass.
- Descreve a política de recuperação da prova: o que, quando e como voltar.
- Lista quatro características de um seed de nível 3.
Referências do módulo
Do plano de curso (UC3): - MySQL 8.0 Reference Manual — downloads.mysql.com/docs/refman-8.0-en.pdf (a fonte da verdade do dialeto da prova; treinar scanning nela é módulo 09 acontecendo) - ELMASRI, R.; NAVATHE, S. Sistemas de banco de dados. Pearson. (modelagem, formas normais e ACID a fundo) - TEOREY, T. et al. Projeto e modelagem de banco de dados. Elsevier. (o caminho conceitual → lógico → físico)
Do mercado: - Multiple-Column Indexes — MySQL Reference Manual (a regra da esquerda, da fonte) - Composite Indexes — MySQL for Developers, PlanetScale (curso gratuito, o melhor material prático de índice que existe) - Modelagem de Bancos Relacionais: boas práticas — DevMedia (inclui a armadilha da obrigatoriedade dos dois lados) - Modelagem de Bancos de Dados sem Segredos — Albert Eije, Medium (série em português, boa pra revisão)
Fundação de dados fechada, e fechada com porquês. Agora a gente constrói a camada que serve esses dados pro mundo, nas duas stacks: 04 — Back-end e API RESTful.