SQL Modelando um Banco do Zero
Até aqui você usou o banco da Loja Aurora, que já veio pronto. Mas alguém teve de decidir que existiriam as tabelas clientes, pedidos e itens_pedido, e quais colunas cada uma teria. Esse trabalho se chama modelagem. Nesta aula você acompanha a Ana modelando, do zero, o banco da clínica veterinária da prima dela, que hoje anota tudo num caderno. São sete passos, sempre os mesmos, que servem para qualquer banco: uma escola, uma biblioteca, as suas finanças.
- Por que "uma planilha só" vira problema, com um exemplo real.
- Achar as entidades (as coisas que viram tabelas) e os atributos (as colunas).
- Escolher a chave primária de cada tabela.
- Ligar as tabelas: relacionamentos 1 para N, e em que lado vai a chave estrangeira.
- Transformar as regras do negócio em
NOT NULL,CHECK,UNIQUEeREFERENCES. - Escrever os
CREATE TABLE, pôr dados de teste e conferir se o banco responde às perguntas do dia a dia. - As mensagens reais de erro, e a armadilha das chaves estrangeiras desligadas no terminal.
Os exemplos rodam de verdade no seu navegador, no SQLite 3.50. O banco da clínica não existe no editor: cada exemplo cria as tabelas e põe os dados antes de consultar, porque cada Rodar parte do banco limpo (aula O Editor do Guia). Os nomes, telefones e registros da clínica são todos inventados.
A ideia: do caderno às fichas
Na recepção da clínica tem um caderno. Cada consulta marcada vira uma linha:
06/10 09:00 Rex (cachorro, nasc. 10/04/2019) — João Pereira, (86) 90000-0001 — Dr. Paulo — vacina antirrábica — R$ 80 06/10 10:30 Mingau (gato) — Ana Lima, (86) 90000-0002 — Dra. Lúcia — check-up anual — R$ 120 13/10 14:00 Rex (cachorro) — João Pereira, (86) 90000-0001 — Dra. Lúcia — dor na pata — R$ 150 20/10 08:30 Pipoca (ave) — Rita Sales, (86) 90000-0003 — Dr. Paulo — corte de unhas — R$ 40 20/10 09:00 Thor (cachorro, nasc. 22/01/2023) — João Pereira, (86) 90000-0001 — Dr. Paulo — vacina V10 — R$ 90
Repare: o telefone do João está escrito três vezes. O Rex aparece duas. Cada linha mistura quatro assuntos: quando foi a consulta, qual animal, de quem ele é e quem atendeu. Modelar é separar esses assuntos em fichas diferentes (uma pasta de fichas de tutores, uma de animais, uma de veterinários, uma de consultas) e ligar as fichas pelo número de cada uma. Cada pasta vira uma tabela.
Passo 0: o que dá errado com uma tabela só
A primeira ideia de quem sai do caderno é copiar o caderno numa tabela só, com uma coluna para cada pedaço da linha:
CREATE TABLE caderno (
data_hora TEXT,
animal TEXT,
especie TEXT,
tutor TEXT,
telefone TEXT,
veterinario TEXT,
motivo TEXT,
valor REAL
);
INSERT INTO caderno VALUES
('2026-10-06 09:00', 'Rex', 'cachorro', 'João Pereira', '(86) 90000-0001', 'Paulo Viana', 'Vacina antirrábica', 80),
('2026-10-06 10:30', 'Mingau', 'gato', 'Ana Lima', '(86) 90000-0002', 'Lúcia Rocha', 'Check-up anual', 120),
('2026-10-13 14:00', 'Rex', 'cachorro', 'João Pereira', '(86) 90000-0001', 'Lúcia Rocha', 'Dor na pata', 150),
('2026-10-20 08:30', 'Pipoca', 'ave', 'Rita Sales', '(86) 90000-0003', 'Paulo Viana', 'Corte de unhas', 40),
('2026-10-20 09:00', 'Thor', 'cachorro', 'João Pereira', '(86) 90000-0001', 'Paulo Viana', 'Vacina V10', 90);
-- o João trocou de telefone; a recepcionista corrigiu só a consulta de hoje
UPDATE caderno SET telefone = '(86) 90000-0009'
WHERE tutor = 'João Pereira' AND data_hora LIKE '2026-10-20%';
SELECT DISTINCT tutor, telefone FROM caderno ORDER BY tutor;
Agora o banco diz que o João tem dois telefones, e ninguém sabe qual é o certo. Isso acontece porque a mesma informação (o telefone do João) está guardada em várias linhas. E há outros problemas escondidos:
- Para cadastrar um animal novo, que ainda não teve consulta, não há onde: toda linha é uma consulta.
- Se a consulta da Pipoca for desmarcada e apagada, a clínica perde o telefone da Rita, que só estava ali.
- Nada impede alguém de escrever "cachoro" ou "Joao Pereira", e aí o "mesmo" tutor vira dois.
Esses defeitos têm nome e regra para evitar, e são o assunto da aula Normalização. Aqui, a solução vem pelos passos.
Passo 1: as coisas (entidades)
Leia o caderno e sublinhe os substantivos, as "coisas" de que a clínica precisa lembrar. Cada coisa que tem vida própria (existe e tem dados mesmo sem as outras) é uma entidade, e cada entidade vira uma tabela:
| Coisa | Tem vida própria? | Vira |
|---|---|---|
| Tutor (o dono do animal) | Sim: tem nome e telefone, mesmo sem consulta marcada | tabela tutores |
| Animal | Sim: tem nome, espécie, nascimento | tabela animais |
| Veterinário | Sim: tem nome e registro profissional | tabela veterinarios |
| Consulta | Sim: um acontecimento, com data, motivo e valor | tabela consultas |
| Telefone, espécie, valor… | Não: são detalhes de uma das coisas acima | colunas (passo 2) |
Um truque para não errar: se você consegue dizer "um tutor", "dois animais", "cinco consultas", é uma entidade. Ninguém diz "dois telefones do caderno" como coisa separada; é o telefone de alguém.
Passo 2: os detalhes (atributos)
Para cada entidade, a Ana pergunta à prima: "o que você precisa saber sobre um tutor? E sobre um animal?". Cada resposta é um atributo, que vira uma coluna:
| Tabela | Colunas |
|---|---|
tutores | nome, telefone, email |
animais | nome, espécie, nascimento |
veterinarios | nome, CRMV (o registro no conselho de veterinária) |
consultas | data e hora, motivo, valor |
Três cuidados nesta hora:
- Um valor por coluna. Nada de uma coluna "telefones" com
'(86) 90000-0001, (86) 90000-0005': você não conseguiria buscar por um deles. Se o tutor pode ter vários telefones, telefone vira uma tabela própria. - Não guarde o que se calcula. A idade do animal muda todo ano; guarde o nascimento e calcule a idade na consulta (aula Funções de Data).
- Cada detalhe na tabela da coisa a que ele pertence. O telefone é do tutor, não da consulta. Por isso ele fica em
tutores, uma vez só.
Passo 3: a chave de cada tabela
Cada linha precisa de um jeito único e que nunca muda de ser apontada: a chave primária (aula PRIMARY KEY). Qual coluna usar?
| Candidata | Serve? | Por quê |
|---|---|---|
| O nome do tutor | Não | Pode haver dois "João Pereira"; e se ele corrigir a grafia, todas as ligações quebram |
| O telefone | Não | Muda (o João acabou de mudar o dele) |
| O CRMV do veterinário | Quase | É único, mas é um texto comprido para repetir em cada consulta, e tem gente que ainda não tem |
Um número inventado: id INTEGER PRIMARY KEY | Sim | Único, curto, nunca muda e o SQLite gera sozinho (aula AUTOINCREMENT) |
A Ana segue o mesmo padrão da Loja Aurora: toda tabela começa com id INTEGER PRIMARY KEY. O CRMV continua guardado, como coluna UNIQUE (passo 5): único, mas não é ele que liga as tabelas.
Passo 4: as ligações (relacionamentos)
Agora, como as fichas se ligam. Para cada par de entidades, faça a mesma pergunta nos dois sentidos:
| Pergunta | Resposta | Tipo |
|---|---|---|
| Um tutor tem quantos animais? E um animal tem quantos tutores? | Um tutor, vários animais (o João tem o Rex e o Thor). Um animal, um tutor. | 1 para N |
| Um animal tem quantas consultas? E uma consulta é de quantos animais? | Um animal, várias consultas. Uma consulta, um animal. | 1 para N |
| Um veterinário faz quantas consultas? E uma consulta é feita por quantos? | Um veterinário, várias. Uma consulta, um veterinário. | 1 para N |
A regra do 1 para N (lê-se "um para ene", ou "um para muitos") é sempre a mesma: a chave estrangeira vai no lado do N, o lado "dos muitos". Cada animal guarda o tutor_id do seu tutor; cada consulta guarda o animal_id e o veterinario_id. É como em cada ficha de animal escrever "tutor: ficha nº 1".
tutores (1) ──────< (N) animais (1) ──────< (N) consultas (N) >────── (1) veterinarios
id id id id
nome tutor_id → tutores animal_id → animais nome
telefone nome veterinario_id → veterin. crmv
email especie data_hora
nascimento motivo
valor
O < na ponta da linha marca o lado "dos muitos". Esse desenho, com as tabelas e as ligações, é um diagrama do banco, e tem um jeito padrão de desenhar, com o "pé de galinha" no lado N: é a aula Diagramas (DER).
Se uma consulta pudesse ter vários veterinários (uma cirurgia com dois), e cada veterinário várias consultas, seria uma relação N para N. Aí a chave estrangeira não cabe em nenhum dos dois lados, e entra uma terceira tabela, de ligação, como a itens_pedido da Loja Aurora, que liga pedidos e produtos. Esse caso tem aula própria: Relação Muitos-para-Muitos. Na clínica da prima da Ana, por enquanto, cada consulta tem um veterinário só.
Passo 5: as regras
Cada frase da prima do tipo "tem de", "não pode" ou "só pode" vira uma regra que o próprio banco garante. Assim, um erro de digitação é barrado na entrada, e não descoberto meses depois:
| A prima disse… | No banco |
|---|---|
| "Todo tutor tem de deixar um telefone." | telefone TEXT NOT NULL (aula NOT NULL) |
| "Email, só se a pessoa quiser." | email TEXT, sem NOT NULL: aceita NULL |
| "Atendemos cachorro, gato e ave; o resto é 'outro'." | CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')) (aula CHECK) |
| "Não existem dois veterinários com o mesmo CRMV." | crmv TEXT NOT NULL UNIQUE (aula UNIQUE) |
| "Todo animal tem um tutor cadastrado." | tutor_id INTEGER NOT NULL REFERENCES tutores(id) (aula FOREIGN KEY) |
| "Valor de consulta não pode ser negativo." | CHECK (valor >= 0) |
Passo 6: os CREATE TABLE
Com tudo decidido, escrever as tabelas é quase copiar os passos. A ordem importa: uma tabela só pode apontar para outra que já existe, então primeiro as que não dependem de ninguém (tutores, veterinarios), depois animais, por último consultas.
CREATE TABLE tutores (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
telefone TEXT NOT NULL,
email TEXT
);
CREATE TABLE veterinarios (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
crmv TEXT NOT NULL UNIQUE
);
CREATE TABLE animais (
id INTEGER PRIMARY KEY,
tutor_id INTEGER NOT NULL REFERENCES tutores(id),
nome TEXT NOT NULL,
especie TEXT NOT NULL CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')),
nascimento TEXT
);
CREATE TABLE consultas (
id INTEGER PRIMARY KEY,
animal_id INTEGER NOT NULL REFERENCES animais(id),
veterinario_id INTEGER NOT NULL REFERENCES veterinarios(id),
data_hora TEXT NOT NULL,
motivo TEXT NOT NULL,
valor REAL NOT NULL CHECK (valor >= 0)
);
SELECT name FROM sqlite_schema WHERE type = 'table' AND name IN ('tutores', 'veterinarios', 'animais', 'consultas');
- nomes no plural e em minúsculas
tutores,animais: uma tabela guarda vários. Sem acento e sem espaço (veterinarios, nãoveterinários), para não precisar de aspas em todo comando.- id INTEGER PRIMARY KEY
- A chave do passo 3, sempre a primeira coluna.
- tutor_id, animal_id, veterinario_id
- As chaves estrangeiras do passo 4, com o nome da tabela apontada no singular mais
_id. Quem lêanimal_idsabe na hora para onde ela aponta. - data_hora TEXT
- O SQLite não tem um tipo de data: datas vão como texto no formato
'2026-10-20 09:00', que se ordena certo e funciona com as funções de data (aula Datas e Horas). - NOT NULL, CHECK, UNIQUE, REFERENCES
- As regras do passo 5.
- SELECT name FROM sqlite_schema …
- Confere que as quatro tabelas foram criadas. A
sqlite_schemaé a tabela onde o SQLite guarda a lista de tudo o que existe no banco.
Missão: nesta versão, alguém guardou o nome do tutor dentro de animais, numa coluna tutor, e a consulta de baixo dá erro. Conserte o modelo: troque a coluna tutor por tutor_id apontando para tutores(id) e cadastre os animais com o número do tutor (João é o 1, Ana é a 2, Rita é a 3).
CREATE TABLE tutores ( id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT ); INSERT INTO tutores (id, nome, telefone, email) VALUES (1, 'João Pereira', '(86) 90000-0001', 'joao.pereira@exemplo.com.br'), (2, 'Ana Lima', '(86) 90000-0002', NULL), (3, 'Rita Sales', '(86) 90000-0003', 'rita.sales@exemplo.com.br'); CREATE TABLE animais ( id INTEGER PRIMARY KEY, tutor TEXT, nome TEXT NOT NULL, especie TEXT NOT NULL ); INSERT INTO animais (id, tutor, nome, especie) VALUES (1, 'João Pereira', 'Rex', 'cachorro'), (2, 'João Pereira', 'Thor', 'cachorro'), (3, 'Ana Lima', 'Mingau', 'gato'), (4, 'Rita Sales', 'Pipoca', 'ave'); SELECT a.nome AS animal, t.nome AS tutor FROM animais a JOIN tutores t ON t.id = a.tutor_id ORDER BY a.id;
CREATE TABLE tutores ( id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT ); INSERT INTO tutores (id, nome, telefone, email) VALUES (1, 'João Pereira', '(86) 90000-0001', 'joao.pereira@exemplo.com.br'), (2, 'Ana Lima', '(86) 90000-0002', NULL), (3, 'Rita Sales', '(86) 90000-0003', 'rita.sales@exemplo.com.br'); CREATE TABLE animais ( id INTEGER PRIMARY KEY, tutor_id INTEGER NOT NULL REFERENCES tutores(id), nome TEXT NOT NULL, especie TEXT NOT NULL ); INSERT INTO animais (id, tutor_id, nome, especie) VALUES (1, 1, 'Rex', 'cachorro'), (2, 1, 'Thor', 'cachorro'), (3, 2, 'Mingau', 'gato'), (4, 3, 'Pipoca', 'ave'); SELECT a.nome AS animal, t.nome AS tutor FROM animais a JOIN tutores t ON t.id = a.tutor_id ORDER BY a.id;
Passo 7: dados de teste e as perguntas
Um modelo só está pronto quando responde às perguntas de quem vai usá-lo. A Ana pediu à prima três perguntas que ela faz toda semana, cadastrou as linhas do caderno e escreveu uma consulta para cada pergunta:
CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT);
CREATE TABLE veterinarios (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, crmv TEXT NOT NULL UNIQUE);
CREATE TABLE animais (id INTEGER PRIMARY KEY, tutor_id INTEGER NOT NULL REFERENCES tutores(id),
nome TEXT NOT NULL, especie TEXT NOT NULL CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')), nascimento TEXT);
CREATE TABLE consultas (id INTEGER PRIMARY KEY, animal_id INTEGER NOT NULL REFERENCES animais(id),
veterinario_id INTEGER NOT NULL REFERENCES veterinarios(id), data_hora TEXT NOT NULL,
motivo TEXT NOT NULL, valor REAL NOT NULL CHECK (valor >= 0));
INSERT INTO tutores VALUES
(1, 'João Pereira', '(86) 90000-0001', 'joao.pereira@exemplo.com.br'),
(2, 'Ana Lima', '(86) 90000-0002', NULL),
(3, 'Rita Sales', '(86) 90000-0003', 'rita.sales@exemplo.com.br');
INSERT INTO veterinarios VALUES (1, 'Paulo Viana', 'CRMV-PI 1001'), (2, 'Lúcia Rocha', 'CRMV-PI 1002');
INSERT INTO animais VALUES
(1, 1, 'Rex', 'cachorro', '2019-04-10'), (2, 1, 'Thor', 'cachorro', '2023-01-22'),
(3, 2, 'Mingau', 'gato', '2021-08-03'), (4, 3, 'Pipoca', 'ave', NULL);
INSERT INTO consultas VALUES
(1, 1, 1, '2026-10-06 09:00', 'Vacina antirrábica', 80.0),
(2, 3, 2, '2026-10-06 10:30', 'Check-up anual', 120.0),
(3, 1, 2, '2026-10-13 14:00', 'Dor na pata', 150.0),
(4, 4, 1, '2026-10-20 08:30', 'Corte de unhas', 40.0),
(5, 2, 1, '2026-10-20 09:00', 'Vacina V10', 90.0);
-- 1. Qual é a agenda do dia 20/10?
SELECT c.data_hora, a.nome AS animal, t.nome AS tutor, t.telefone, v.nome AS veterinario, c.motivo
FROM consultas c
JOIN animais a ON a.id = c.animal_id
JOIN tutores t ON t.id = a.tutor_id
JOIN veterinarios v ON v.id = c.veterinario_id
WHERE c.data_hora >= '2026-10-20' AND c.data_hora < '2026-10-21'
ORDER BY c.data_hora;
-- 2. Qual é o histórico do Rex?
SELECT c.data_hora, c.motivo, v.nome AS veterinario, c.valor
FROM consultas c
JOIN veterinarios v ON v.id = c.veterinario_id
WHERE c.animal_id = 1
ORDER BY c.data_hora;
-- 3. Quanto cada tutor já gastou?
SELECT t.nome, COUNT(c.id) AS consultas, SUM(c.valor) AS total
FROM tutores t
JOIN animais a ON a.tutor_id = t.id
LEFT JOIN consultas c ON c.animal_id = a.id
GROUP BY t.id
ORDER BY total DESC;
As três perguntas foram respondidas, e o telefone do João aparece certinho na agenda, guardado uma vez só. Se ele trocar de número, um UPDATE tutores SET telefone = … WHERE id = 1 muda uma linha, e todas as consultas passam a mostrar o número novo. O problema do passo 0 sumiu.
Se uma pergunta da prima não tivesse resposta ("quais animais estão com a vacina atrasada?"), seria sinal de que falta alguma coisa no modelo. É assim que se descobre, por exemplo, que a clínica precisa de uma tabela de vacinas: é a missão do fim da aula.
Experimente: faça outras perguntas ao modelo da clínica: quantas consultas cada veterinário atendeu, quais animais nunca vieram (pista: LEFT JOIN com IS NULL), qual a idade de cada animal hoje.
CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT);
CREATE TABLE veterinarios (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, crmv TEXT NOT NULL UNIQUE);
CREATE TABLE animais (id INTEGER PRIMARY KEY, tutor_id INTEGER NOT NULL REFERENCES tutores(id),
nome TEXT NOT NULL, especie TEXT NOT NULL CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')), nascimento TEXT);
CREATE TABLE consultas (id INTEGER PRIMARY KEY, animal_id INTEGER NOT NULL REFERENCES animais(id),
veterinario_id INTEGER NOT NULL REFERENCES veterinarios(id), data_hora TEXT NOT NULL,
motivo TEXT NOT NULL, valor REAL NOT NULL CHECK (valor >= 0));
INSERT INTO tutores VALUES
(1, 'João Pereira', '(86) 90000-0001', 'joao.pereira@exemplo.com.br'),
(2, 'Ana Lima', '(86) 90000-0002', NULL),
(3, 'Rita Sales', '(86) 90000-0003', 'rita.sales@exemplo.com.br');
INSERT INTO veterinarios VALUES (1, 'Paulo Viana', 'CRMV-PI 1001'), (2, 'Lúcia Rocha', 'CRMV-PI 1002');
INSERT INTO animais VALUES
(1, 1, 'Rex', 'cachorro', '2019-04-10'), (2, 1, 'Thor', 'cachorro', '2023-01-22'),
(3, 2, 'Mingau', 'gato', '2021-08-03'), (4, 3, 'Pipoca', 'ave', NULL);
INSERT INTO consultas VALUES
(1, 1, 1, '2026-10-06 09:00', 'Vacina antirrábica', 80.0),
(2, 3, 2, '2026-10-06 10:30', 'Check-up anual', 120.0),
(3, 1, 2, '2026-10-13 14:00', 'Dor na pata', 150.0),
(4, 4, 1, '2026-10-20 08:30', 'Corte de unhas', 40.0),
(5, 2, 1, '2026-10-20 09:00', 'Vacina V10', 90.0);
SELECT v.nome, COUNT(*) AS consultas
FROM consultas c
JOIN veterinarios v ON v.id = c.veterinario_id
GROUP BY v.id;
Formas diferentes de fazer a mesma coisa
Modelar é escolher. Algumas escolhas que aparecem em todo projeto:
| Decisão | Opção A | Opção B | O que a Ana escolheu, e por quê |
|---|---|---|---|
| Chave primária | Um número inventado (id), a chamada chave substituta | Um dado do mundo real (CPF, CRMV), a chave natural | id: nunca muda e é curto. O dado real fica numa coluna UNIQUE |
| Data e hora | Uma coluna data_hora: '2026-10-20 09:00' | Duas colunas, data e hora | Uma só: ordena e compara faixas com um só WHERE |
| Dinheiro | REAL (80.0) | INTEGER em centavos (8000) | REAL, como na Aurora. Em sistemas com muitas contas, centavos inteiros evitam as "caudas" de ponto flutuante (aula Funções Numéricas) |
| Espécie | Texto com CHECK (… IN …) | Uma tabela especies e um especie_id | CHECK: são quatro valores que quase nunca mudam. Se a lista crescer muito ou ganhar detalhes, vira tabela |
| Nomes | tutores, tutor_id | Tutor, TutorID | Minúsculas com _, como no resto do guia. O importante é escolher um padrão e seguir em todas as tabelas |
Quando dá errado
Os erros abaixo são as regras do passo 5 fazendo o trabalho delas. Em todos, o banco já tinha as quatro tabelas e os dados do passo 7.
CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT);
CREATE TABLE veterinarios (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, crmv TEXT NOT NULL UNIQUE);
CREATE TABLE animais (id INTEGER PRIMARY KEY, tutor_id INTEGER NOT NULL REFERENCES tutores(id),
nome TEXT NOT NULL, especie TEXT NOT NULL CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')), nascimento TEXT);
CREATE TABLE consultas (id INTEGER PRIMARY KEY, animal_id INTEGER NOT NULL REFERENCES animais(id),
veterinario_id INTEGER NOT NULL REFERENCES veterinarios(id), data_hora TEXT NOT NULL,
motivo TEXT NOT NULL, valor REAL NOT NULL CHECK (valor >= 0));
INSERT INTO tutores VALUES (1, 'João Pereira', '(86) 90000-0001', NULL);
INSERT INTO veterinarios VALUES (1, 'Paulo Viana', 'CRMV-PI 1001');
INSERT INTO animais VALUES (1, 1, 'Rex', 'cachorro', '2019-04-10');
INSERT INTO consultas (animal_id, veterinario_id, data_hora, motivo, valor)
VALUES (99, 1, '2026-10-21 10:00', 'Banho', 50);
A mensagem: FOREIGN KEY constraint failed, "a regra da chave estrangeira falhou". Não existe animal 99. O mesmo erro aparece se alguém tentar apagar um tutor que ainda tem animais (DELETE FROM tutores WHERE id = 1): os animais ficariam apontando para ninguém.
A Ana carregou o mesmo modelo, com os dados do passo 7, no sqlite3 (com o comando .read, que roda um arquivo .sql) e repetiu o INSERT do animal 99:
$ sqlite3 clinica.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> .read clinica.sql sqlite> .tables animais consultas tutores veterinarios sqlite> PRAGMA foreign_keys; 0 sqlite> INSERT INTO consultas (animal_id, veterinario_id, data_hora, motivo, valor) VALUES (99, 1, '2026-10-21 10:00', 'Banho', 50); sqlite> SELECT id, animal_id, motivo FROM consultas WHERE id = 6; 6|99|Banho sqlite> PRAGMA foreign_key_check; consultas|6|animais|1 sqlite> DELETE FROM consultas WHERE id = 6; sqlite> PRAGMA foreign_keys = ON; sqlite> INSERT INTO consultas (animal_id, veterinario_id, data_hora, motivo, valor) VALUES (99, 1, '2026-10-21 10:00', 'Banho', 50); Runtime error: FOREIGN KEY constraint failed (19)
O que aconteceu: no SQLite, a verificação das chaves estrangeiras vem desligada (PRAGMA foreign_keys respondeu 0), por compatibilidade com bancos antigos. O REFERENCES estava no modelo, mas ninguém conferia, e a consulta do animal 99 entrou. O PRAGMA foreign_key_check acusou o problema depois do fato: a linha 6 de consultas aponta para um animal que não existe. Com PRAGMA foreign_keys = ON, o erro aparece na hora (o (19) é o número do erro no SQLite).
Como consertar: rode PRAGMA foreign_keys = ON; toda vez que abrir o banco (no terminal ou no programa). Ele vale só para aquela conexão, não fica gravado no arquivo. No editor do guia ele já vem ligado, por isso o erro apareceu no exemplo anterior. Mais em PRAGMAs Úteis.
CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT);
CREATE TABLE animais (id INTEGER PRIMARY KEY, tutor_id INTEGER NOT NULL REFERENCES tutores(id),
nome TEXT NOT NULL, especie TEXT NOT NULL CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')), nascimento TEXT);
INSERT INTO tutores VALUES (1, 'João Pereira', '(86) 90000-0001', NULL);
INSERT INTO animais (tutor_id, nome, especie) VALUES (1, 'Bidu', 'cão');
A mensagem: CHECK constraint failed: especie IN ('cachorro', 'gato', 'ave', 'outro'). A recepcionista escreveu "cão", e a regra só aceita os quatro valores combinados. Sem a regra, "cão" e "cachorro" virariam duas espécies diferentes nos relatórios.
CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT);
CREATE TABLE animais (id INTEGER PRIMARY KEY, tutor_id INTEGER NOT NULL REFERENCES tutores(id),
nome TEXT NOT NULL, especie TEXT NOT NULL CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')), nascimento TEXT);
INSERT INTO tutores VALUES (1, 'João Pereira', '(86) 90000-0001', NULL);
INSERT INTO animais (tutor_id, nome, especie) VALUES (1, NULL, 'cachorro');
A mensagem: NOT NULL constraint failed: animais.nome: a coluna nome de animais não aceita vazio. Do mesmo jeito, cadastrar um segundo veterinário com o CRMV 'CRMV-PI 1001' dá UNIQUE constraint failed: veterinarios.crmv, e uma consulta com valor −5 dá CHECK constraint failed: valor >= 0.
CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT); CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT);
A mensagem: table tutores already exists, "a tabela tutores já existe". Acontece quando você roda o arquivo do modelo de novo num banco que já tem as tabelas.
Como consertar: CREATE TABLE IF NOT EXISTS tutores (…) cria só se não existir. Para recomeçar do zero de propósito, apague antes com DROP TABLE (aula DROP TABLE), começando pelas tabelas que apontam para as outras.
Os passos da modelagem são os mesmos em qualquer banco. O que muda são os tipos e a forma de gerar o id. O trecho é ilustrativo (PostgreSQL) e não roda no SQLite:
-- PostgreSQL (ilustrativo) CREATE TABLE consultas ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, animal_id INTEGER NOT NULL REFERENCES animais(id), veterinario_id INTEGER NOT NULL REFERENCES veterinarios(id), data_hora TIMESTAMP NOT NULL, motivo VARCHAR(200) NOT NULL, valor NUMERIC(10, 2) NOT NULL CHECK (valor >= 0) );
- Datas ganham tipos próprios (
DATE,TIMESTAMP), e dinheiro, um tipo decimal exato (NUMERIC/DECIMAL). Veja Tipos de Dados. - O
idautomático se escreveGENERATED … AS IDENTITYouSERIALno PostgreSQL,AUTO_INCREMENTno MySQL eIDENTITYno SQL Server. - Nesses bancos, as chaves estrangeiras são conferidas sem precisar de um
PRAGMA.
Referência rápida: os sete passos
| Passo | Pergunta | Vira |
|---|---|---|
| 0. O problema | Como é feito hoje? O que se repete? | A lista de dados e de problemas |
| 1. Entidades | De que "coisas" preciso lembrar? Dá para contar "um, dois…"? | Tabelas (no plural) |
| 2. Atributos | O que preciso saber de cada coisa? Um valor por coluna? É calculável? | Colunas |
| 3. Chaves | Como apontar cada linha sem ambiguidade, para sempre? | id INTEGER PRIMARY KEY |
| 4. Relacionamentos | Um X tem quantos Y? E um Y, quantos X? | 1:N → chave estrangeira no lado N; N:N → tabela de ligação |
| 5. Regras | O que "tem de", "não pode", "só pode"? | NOT NULL, CHECK, UNIQUE, REFERENCES, DEFAULT |
6. CREATE TABLE | Em que ordem criar? | Primeiro quem não depende de ninguém |
| 7. Testar | O banco responde às perguntas do dia a dia? | Dados de teste e consultas; o que faltar volta para o passo 1 |
Exercícios
1. Onde vai a chave estrangeira
Uma escola: uma turma tem vários alunos; um aluno está em uma turma. Complete a coluna da tabela alunos:
2. Ligar a verificação
Complete o comando que liga a verificação das chaves estrangeiras no terminal:
3. Numa biblioteca, qual destes é um atributo, e não uma entidade?
4. Por que a Ana guardou a data de nascimento do animal, e não a idade?
5. Por que o telefone do tutor não fica na tabela consultas?
6. No terminal, um INSERT com uma chave estrangeira inexistente entrou sem erro. O mais provável é:
Missão final: as vacinas
Missão: a prima da Ana quer saber quais vacinas cada animal tomou. Um animal toma várias vacinas; cada registro de vacina é de um animal. Crie a tabela vacinas (com id, o animal, o nome da vacina e a data, todos obrigatórios), cadastre as duas vacinas do Rex (animal 1): "Antirrábica" em 2026-10-06 e "V10" em 2025-10-02, e termine com uma consulta que mostre o nome do animal, a vacina e a data.
CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT);
CREATE TABLE animais (id INTEGER PRIMARY KEY, tutor_id INTEGER NOT NULL REFERENCES tutores(id),
nome TEXT NOT NULL, especie TEXT NOT NULL CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')), nascimento TEXT);
INSERT INTO tutores VALUES (1, 'João Pereira', '(86) 90000-0001', 'joao.pereira@exemplo.com.br');
INSERT INTO animais VALUES (1, 1, 'Rex', 'cachorro', '2019-04-10'), (2, 1, 'Thor', 'cachorro', '2023-01-22');
-- crie aqui a tabela vacinas e cadastre as vacinas do Rex
SELECT a.nome AS animal, v.nome AS vacina, v.data
FROM vacinas v
JOIN animais a ON a.id = v.animal_id
ORDER BY v.data;
CREATE TABLE tutores (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, telefone TEXT NOT NULL, email TEXT);
CREATE TABLE animais (id INTEGER PRIMARY KEY, tutor_id INTEGER NOT NULL REFERENCES tutores(id),
nome TEXT NOT NULL, especie TEXT NOT NULL CHECK (especie IN ('cachorro', 'gato', 'ave', 'outro')), nascimento TEXT);
INSERT INTO tutores VALUES (1, 'João Pereira', '(86) 90000-0001', 'joao.pereira@exemplo.com.br');
INSERT INTO animais VALUES (1, 1, 'Rex', 'cachorro', '2019-04-10'), (2, 1, 'Thor', 'cachorro', '2023-01-22');
CREATE TABLE vacinas (
id INTEGER PRIMARY KEY,
animal_id INTEGER NOT NULL REFERENCES animais(id),
nome TEXT NOT NULL,
data TEXT NOT NULL
);
INSERT INTO vacinas (animal_id, nome, data) VALUES
(1, 'Antirrábica', '2026-10-06'),
(1, 'V10', '2025-10-02');
SELECT a.nome AS animal, v.nome AS vacina, v.data
FROM vacinas v
JOIN animais a ON a.id = v.animal_id
ORDER BY v.data;
Repare que você acabou de fazer os sete passos em miniatura: achou uma entidade nova (vacina), os atributos dela, a chave, a ligação 1 para N com animais (a chave estrangeira do lado N), as regras (NOT NULL), o CREATE TABLE e o teste.
Resumo
- Modelar é separar os assuntos misturados num caderno ou numa planilha em tabelas ligadas entre si. Uma tabela só repete dados, e dados repetidos acabam discordando.
- Entidades (coisas que se contam: tutor, animal, consulta) viram tabelas; atributos (detalhes de cada coisa) viram colunas, um valor por coluna, sem guardar o que se calcula.
- Cada tabela ganha
id INTEGER PRIMARY KEY; dados reais únicos (CRMV, CPF) ficam comoUNIQUE. - Em 1 para N, a chave estrangeira vai no lado N (
animais.tutor_id). Em N para N, entra uma tabela de ligação. - As regras do negócio viram
NOT NULL,CHECK,UNIQUEeREFERENCES. No terminal do SQLite, liguePRAGMA foreign_keys = ON. - Crie primeiro as tabelas que não dependem de ninguém, ponha dados de teste e confira se o modelo responde às perguntas reais.