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.

Você vai aprender
  • 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, UNIQUE e REFERENCES.
  • 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:

CoisaTem vida própria?Vira
Tutor (o dono do animal)Sim: tem nome e telefone, mesmo sem consulta marcadatabela tutores
AnimalSim: tem nome, espécie, nascimentotabela animais
VeterinárioSim: tem nome e registro profissionaltabela veterinarios
ConsultaSim: um acontecimento, com data, motivo e valortabela consultas
Telefone, espécie, valor…Não: são detalhes de uma das coisas acimacolunas (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:

TabelaColunas
tutoresnome, telefone, email
animaisnome, espécie, nascimento
veterinariosnome, CRMV (o registro no conselho de veterinária)
consultasdata 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?

CandidataServe?Por quê
O nome do tutorNãoPode haver dois "João Pereira"; e se ele corrigir a grafia, todas as ligações quebram
O telefoneNãoMuda (o João acabou de mudar o dele)
O CRMV do veterinárioQuaseÉ ú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 KEYSimÚ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:

PerguntaRespostaTipo
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).

E se os dois lados forem "vários"?

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ão veteriná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_id sabe 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ãoOpção AOpção BO que a Ana escolheu, e por quê
Chave primáriaUm número inventado (id), a chamada chave substitutaUm dado do mundo real (CPF, CRMV), a chave naturalid: nunca muda e é curto. O dado real fica numa coluna UNIQUE
Data e horaUma coluna data_hora: '2026-10-20 09:00'Duas colunas, data e horaUma só: ordena e compara faixas com um só WHERE
DinheiroREAL (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écieTexto com CHECK (… IN …)Uma tabela especies e um especie_idCHECK: são quatro valores que quase nunca mudam. Se a lista crescer muito ou ganhar detalhes, vira tabela
Nomestutores, tutor_idTutor, TutorIDMinú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.

Consulta de um animal que não existe
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.

No terminal, a mesma consulta… entrou!

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.

Espécie fora da lista
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.

Animal sem nome, CRMV repetido
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.

Rodou o script duas vezes
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.

Nos outros bancos

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 id automático se escreve GENERATED … AS IDENTITY ou SERIAL no PostgreSQL, AUTO_INCREMENT no MySQL e IDENTITY no SQL Server.
  • Nesses bancos, as chaves estrangeiras são conferidas sem precisar de um PRAGMA.

Referência rápida: os sete passos

PassoPerguntaVira
0. O problemaComo é feito hoje? O que se repete?A lista de dados e de problemas
1. EntidadesDe que "coisas" preciso lembrar? Dá para contar "um, dois…"?Tabelas (no plural)
2. AtributosO que preciso saber de cada coisa? Um valor por coluna? É calculável?Colunas
3. ChavesComo apontar cada linha sem ambiguidade, para sempre?id INTEGER PRIMARY KEY
4. RelacionamentosUm X tem quantos Y? E um Y, quantos X?1:N → chave estrangeira no lado N; N:N → tabela de ligação
5. RegrasO que "tem de", "não pode", "só pode"?NOT NULL, CHECK, UNIQUE, REFERENCES, DEFAULT
6. CREATE TABLEEm que ordem criar?Primeiro quem não depende de ninguém
7. TestarO 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:

CREATE TABLE alunos (id INTEGER PRIMARY KEY, nome TEXT NOT NULL, INTEGER REFERENCES turmas(id));

2. Ligar a verificação

Complete o comando que liga a verificação das chaves estrangeiras no terminal:

PRAGMA = ON;

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 como UNIQUE.
  • 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, UNIQUE e REFERENCES. No terminal do SQLite, ligue PRAGMA 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.