SQL Tabelas que se Relacionam

Abra a tabela de pedidos da Loja Aurora e olhe o pedido 7: lá está cliente_id = 7 e funcionario_id = 2. Não tem nome de cliente, nem de vendedor, só números. Quem é o cliente 7? A resposta está em outra tabela, a de clientes, na linha cujo id é 7. Esse jeito de uma tabela "apontar" para outra é o que dá o nome de banco de dados relacional ao SQLite, ao MySQL, ao PostgreSQL e aos outros. Nesta aula você vai entender essas ligações antes de aprender, na próxima, o comando que as segue sozinho: o JOIN.

Você vai aprender
  • Por que um banco guarda clientes, produtos e pedidos em tabelas separadas, e não numa planilha só.
  • O que é chave primária (o "número de identidade" de cada linha) e chave estrangeira (a coluna que aponta para outra tabela).
  • Como seguir uma ligação "à mão", com duas consultas.
  • Os tipos de relação: um-para-muitos, muitos-para-muitos (com tabela no meio), um-para-um e a tabela que aponta para ela mesma.
  • O mapa completo da Loja Aurora, e como ver as ligações de uma tabela no terminal.
  • O que o SQLite faz quando alguém tenta apontar para uma linha que não existe: o erro FOREIGN KEY constraint failed.

Os exemplos rodam de verdade no seu navegador, no banco da Loja Aurora, e o quadro "Resultado" é o SQLite respondendo.


A ideia: por que não uma tabela só?

Imagine que a Ana anotasse os pedidos numa planilha só, com tudo na mesma linha. O cliente José Ribamar Silva fez 4 pedidos, então os dados dele apareceriam 4 vezes:

pedidodataclienteemail do clientecidade
12024-01-05José Ribamar Silvajose.ribamar@exemplo.com.brTeresina
272024-10-29José Ribamar Silvajose.ribamar@exemplo.com.brTeresina
532025-09-03José Ribamar Silvajose.ribamar@exemplo.com.brTeresina
792026-06-29José Ribamar Silvajose.ribamar@exemplo.com.brTeresina

Parece inofensivo, mas traz três problemas:

  1. Repetição. O nome, o email e a cidade estão escritos 4 vezes. Com 1.000 pedidos, seriam 1.000 cópias de dados de cliente.
  2. Mudar dá trabalho. Se o José trocar de email, a Ana tem de corrigir 4 linhas. Se esquecer uma, a planilha passa a ter dois emails para a mesma pessoa, e ninguém sabe qual é o certo.
  3. Quem nunca comprou some. Um cliente que se cadastrou e ainda não fez pedido não tem linha nenhuma para aparecer.

A solução do banco relacional é guardar cada coisa num lugar só: os dados do cliente ficam uma vez só na tabela clientes, e cada pedido guarda apenas o número do cliente. Funciona como o prontuário de um posto de saúde: a ficha da paciente fica no arquivo, com um número; cada consulta anotada no caderno diz só "paciente nº 7". Para saber quem é, você vai ao arquivo e procura a ficha 7.


1. A chave primária: o número de cada linha

Toda tabela da Loja Aurora tem uma coluna id, um número que nunca se repete dentro da tabela. É a chave primária (primary key): ela identifica cada linha sem confusão, mesmo que dois clientes tenham o mesmo nome.

SELECT id, nome, cidade
FROM clientes
WHERE id = 7;

Existe exatamente uma linha com id = 7, e sempre vai existir só uma. Como criar uma chave primária numa tabela nova é assunto da aula PRIMARY KEY.


2. A chave estrangeira: a coluna que aponta

Agora o pedido 7:

SELECT id, cliente_id, funcionario_id, data
FROM pedidos
WHERE id = 7;

A coluna cliente_id guarda um número que é o id de alguém na tabela clientes. Uma coluna assim, que guarda a chave primária de outra tabela, se chama chave estrangeira (foreign key): "estrangeira" porque a chave é de fora, pertence à outra tabela. Veja a ligação lado a lado:

pedidos
idcliente_iddata
662024-02-26
772024-03-08
882024-03-15
clientes
idnomecidade
6Rafael MonteiroSão Paulo
7Luís Otávio BritoParnaíba
8Fernanda CarvalhoFortaleza
pedidos.id
A chave primária do pedido: identifica o pedido 7.
pedidos.cliente_id
A chave estrangeira: o número 7 aqui quer dizer "o cliente cujo id é 7". O nome segue um costume muito usado: nome da tabela no singular + _id.
clientes.id
A chave primária do cliente, para onde a chave estrangeira aponta.

Repare que os dois 7 são coincidência: o pedido 7 foi do cliente 7, mas o pedido 27 também é do cliente 1. O que importa é o valor dentro de cliente_id. A escrita tabela.coluna (pedidos.cliente_id) é como se diz em SQL "a coluna tal daquela tabela"; você vai usá-la muito nas próximas aulas.


3. Seguindo a ligação à mão

Sem nenhum comando novo, dá para descobrir quem fez o pedido 7 e quem o atendeu: basta pegar os números do pedido e procurar nas outras tabelas. A consulta de cima mostrou cliente_id = 7 e funcionario_id = 2. Agora as duas fichas:

SELECT id, nome, cidade FROM clientes WHERE id = 7;
SELECT id, nome, cargo FROM funcionarios WHERE id = 2;

O pedido 7 foi do Luís Otávio Brito, de Parnaíba, e quem atendeu foi o vendedor Caio Teles. Funciona, mas foram três consultas para responder uma pergunta, e a Ana teve de copiar os números de uma para a outra. Imagine fazer isso para os 90 pedidos. É exatamente esse trabalho que o JOIN faz sozinho, numa consulta só: a próxima aula, O que é um JOIN, começa daqui.

Experimente: descubra o nome do funcionário que atendeu o pedido 50. Rode a consulta como está, veja o funcionario_id do pedido e depois troque-a por uma consulta na tabela funcionarios que mostre o nome dele.

SELECT id, cliente_id, funcionario_id, data
FROM pedidos
WHERE id = 50;
SELECT id, nome, cargo
FROM funcionarios
WHERE id = 3;

4. Um-para-muitos: um cliente, vários pedidos

O José Ribamar Silva (cliente 1) aparece em quantos pedidos?

SELECT id, cliente_id, data
FROM pedidos
WHERE cliente_id = 1;

Quatro. Um cliente pode ter muitos pedidos, mas cada pedido tem um cliente só (a coluna cliente_id guarda um número, não uma lista). Essa é a relação um-para-muitos, a mais comum de todas. A chave estrangeira fica sempre do lado "muitos": é o pedido que aponta para o cliente, nunca o contrário.

Na Loja Aurora há várias relações assim:

Um……tem muitosChave estrangeira
clientepedidospedidos.cliente_id → clientes.id
funcionáriopedidos atendidospedidos.funcionario_id → funcionarios.id
categoriaprodutosprodutos.categoria_id → categorias.id
fornecedorprodutosprodutos.fornecedor_id → fornecedores.id

Com o que você aprendeu em GROUP BY, dá para contar os pedidos de cada cliente pela chave estrangeira:

SELECT cliente_id, COUNT(*) AS pedidos
FROM pedidos
GROUP BY cliente_id;

São 26 linhas, mas a loja tem 30 clientes. Quem faltou? Os clientes 27, 28, 29 e 30 (Paulo Sérgio Andrade, Tainá Soares, Elias Furtado e Rosângela Dias) se cadastraram e nunca fizeram pedido. Como eles não aparecem em nenhuma linha de pedidos, uma consulta só na tabela de pedidos nunca vai mostrá-los. Mostrar "todos os clientes, inclusive quem nunca comprou" é o trabalho do LEFT JOIN. Guarde esses quatro nomes: eles vão aparecer de novo.


5. Muitos-para-muitos: pedidos e produtos

Um pedido pode ter vários produtos. E um produto pode estar em vários pedidos. Os dois lados são "muitos". Onde fica a chave estrangeira, então? Não dá para pôr um produto_id em pedidos (seria um produto só por pedido), nem um pedido_id em produtos (seria um pedido só por produto).

A solução é uma terceira tabela, no meio, com uma linha para cada par "este produto está neste pedido". Na Loja Aurora ela se chama itens_pedido, e tem duas chaves estrangeiras:

SELECT *
FROM itens_pedido
WHERE pedido_id = 1;
pedido_id
Chave estrangeira para pedidos.id: o pedido a que o item pertence.
produto_id
Chave estrangeira para produtos.id: o produto comprado.
quantidade, preco_unitario
Dados que não são nem do pedido nem do produto, mas do par: quantas unidades daquele produto entraram naquele pedido, e por quanto cada uma.

O pedido 1 tem 3 itens: os produtos 1, 2 e 3 (caderno, caneta e lápis). E o caminho inverso, em quais pedidos o produto 1 aparece:

SELECT pedido_id, quantidade
FROM itens_pedido
WHERE produto_id = 1;

O Caderno Pautado foi vendido em 6 pedidos. A relação muitos-para-muitos é, na verdade, duas relações um-para-muitos: um pedido tem muitos itens, e um produto aparece em muitos itens. A aula Relação Muitos-para-Muitos mostra como desenhar uma dessas do zero.

Produtos que nunca saíram

A loja tem 45 produtos, mas só 42 aparecem em itens_pedido. Os produtos 43, 44 e 45 (Vaso de Cerâmica Médio, Tesoura de Poda e Luvas de Jardinagem) nunca foram vendidos. É o mesmo caso dos clientes sem pedido, do outro lado do banco, e você vai encontrá-los com o LEFT JOIN.


6. A tabela que aponta para ela mesma

Na tabela funcionarios, a coluna chefe_id guarda o id de outro funcionário, o chefe:

SELECT id, nome, cargo, chefe_id
FROM funcionarios;

A gerente Marta Lustosa (1) não tem chefe: o chefe_id dela é NULL, "sem valor" (aula O Valor NULL). Ela chefia os funcionários 2, 3, 4 e 5; a supervisora Luana Castelo (5) chefia o 6, o 7 e o 8. A chave estrangeira chefe_id aponta para a chave primária da própria tabela. Para juntar cada funcionário com o nome do chefe, existe o Self Join.


7. Um-para-um

Existe ainda a relação um-para-um: cada linha de uma tabela corresponde a no máximo uma linha da outra. Por exemplo, se a loja guardasse os dados bancários de cada funcionário numa tabela separada (para pouca gente ter acesso), cada funcionário teria uma linha só lá. A Loja Aurora não tem nenhuma relação assim; ela é bem menos comum que as outras, e costuma existir por motivo de organização ou de segurança.

TipoExemplo na Loja AuroraOnde fica a chave estrangeira
Um-para-muitosum cliente, muitos pedidosno lado "muitos" (pedidos.cliente_id)
Muitos-para-muitospedidos e produtosnuma tabela no meio, com duas chaves (itens_pedido)
Com ela mesmafuncionário e chefena própria tabela (funcionarios.chefe_id)
Um-para-um(não há)em qualquer um dos lados, sem repetir

8. O mapa da Loja Aurora

Juntando tudo, este é o mapa das ligações do banco. Cada caixa é uma tabela (com as colunas principais), e cada seta sai de uma chave estrangeira e aponta para a chave primária que ela referencia. Um desenho assim se chama diagrama de entidade-relacionamento (DER); a aula Diagramas (DER) ensina a desenhar um.

Mapa das tabelas da Loja Aurora: produtos aponta para categorias e fornecedores; itens_pedido aponta para produtos e pedidos; pedidos aponta para clientes e funcionarios; funcionarios aponta para ela mesma pelo chefe_id. categorias fornecedores produtos itens_pedido pedidos funcionarios clientes id PK nome id PK nome cidade, pais… id PK categoria_id FK fornecedor_id FK nome, preco… (45 linhas) pedido_id FK produto_id FK quantidade preco_unitario id PK cliente_id FK funcionario_id FK data, frete… id PK nome, cargo chefe_id FK id PK nome, email cidade, estado…

PK marca a chave primária (primary key) e FK, a chave estrangeira (foreign key). Siga uma seta qualquer: pedidos.cliente_id desce até clientes.id; itens_pedido.produto_id volta até produtos.id; e funcionarios.chefe_id dá a volta e aponta para a própria tabela.

Vendo as ligações de uma tabela

Quem criou a tabela deixou as ligações registradas: a palavra REFERENCES ("faz referência a") na definição de cada chave estrangeira. No terminal do SQLite, o comando de ponto .schema mostra essa definição (aula Comandos de Ponto):

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> .schema pedidos
CREATE TABLE pedidos (
    id             INTEGER PRIMARY KEY,
    cliente_id     INTEGER NOT NULL REFERENCES clientes(id),
    funcionario_id INTEGER NOT NULL REFERENCES funcionarios(id),
    data           TEXT NOT NULL,
    entregue_em    TEXT,
    frete          REAL NOT NULL DEFAULT 0
);

A linha cliente_id INTEGER NOT NULL REFERENCES clientes(id) se lê: "a coluna cliente_id guarda um número inteiro, não pode ficar vazia e faz referência à coluna id da tabela clientes". No editor do guia não existem comandos de ponto, mas o SQLite tem um comando SQL que lista as chaves estrangeiras de uma tabela, o PRAGMA foreign_key_list (os PRAGMAs têm aula própria: PRAGMAs Úteis):

PRAGMA foreign_key_list(pedidos);

Cada linha é uma chave estrangeira: a coluna from ("de") é a coluna de pedidos, e table e to ("para") dizem para qual tabela e coluna ela aponta. As outras colunas tratam do que fazer quando a linha apontada muda ou é apagada, assunto da aula FOREIGN KEY.

Experimente: troque o nome da tabela para ver as chaves estrangeiras de produtos. Para quais tabelas ela aponta?

PRAGMA foreign_key_list(pedidos);
PRAGMA foreign_key_list(produtos);

Formas diferentes de chegar ao mesmo dado

Para responder "qual o nome do cliente do pedido 7?", existem três caminhos. Você já conhece o primeiro; os outros dois são os próximos assuntos do guia:

FormaComo éQuando usar
Seguir à mãoUma consulta em pedidos, copiar o cliente_id, outra consulta em clientesPara entender as ligações, ou conferir um caso só
JOINUma consulta só, que junta as linhas das duas tabelas pela ligaçãoO jeito normal: listas, relatórios, qualquer coisa com mais de um caso
SubconsultaUma consulta dentro da outra: WHERE id = (SELECT cliente_id FROM pedidos WHERE id = 7)Quando você só precisa filtrar por algo da outra tabela

Quando dá errado

Um pedido para um cliente que não existe

A loja tem clientes de 1 a 30. O que acontece se alguém registrar um pedido para o cliente 99?

INSERT INTO pedidos (cliente_id, funcionario_id, data, frete)
VALUES (99, 2, '2026-10-18', 10.0);

A mensagem: FOREIGN KEY constraint failed, "a restrição de chave estrangeira falhou". O SQLite conferiu a ligação, não achou nenhum cliente com id = 99 e recusou o pedido. Sem essa conferência, o banco ganharia um pedido órfão: um pedido de ninguém, que some de qualquer relatório por cliente.

Como consertar: use o id de um cliente que existe ou, se for um cliente novo, cadastre-o antes em clientes (aula INSERT INTO) e só depois o pedido.

Apagar um cliente que tem pedidos
DELETE FROM clientes
WHERE id = 1;

A mensagem: a mesma, FOREIGN KEY constraint failed. O José Ribamar Silva tem 4 pedidos apontando para ele. Se a ficha dele sumisse, esses 4 pedidos ficariam órfãos. O SQLite protege a ligação dos dois lados: nem se cria uma seta para o nada, nem se apaga a linha para onde setas apontam.

No editor, cada Rodar parte do banco limpo, então pode testar sem medo.

No terminal, a conferência vem desligada

No editor do guia, a conferência está ligada (o banco da Loja Aurora começa com PRAGMA foreign_keys = ON). No programa sqlite3, porém, ela vem desligada a cada vez que você abre o banco. A documentação do SQLite explica: as chaves estrangeiras são desligadas por padrão, por compatibilidade com programas antigos, e precisam ser ligadas em cada conexão. Veja a Ana tentando o mesmo pedido no terminal:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> PRAGMA foreign_keys;
0
sqlite> INSERT INTO pedidos (cliente_id, funcionario_id, data, frete) VALUES (99, 2, '2026-10-18', 10.0);
sqlite> PRAGMA foreign_key_check;
pedidos|91|clientes|1
sqlite> PRAGMA foreign_keys = ON;
sqlite> INSERT INTO pedidos (cliente_id, funcionario_id, data, frete) VALUES (98, 2, '2026-10-18', 10.0);
Runtime error: FOREIGN KEY constraint failed (19)
sqlite> .quit
PRAGMA foreign_keys;
Pergunta se a conferência está ligada: 0 = desligada.
INSERT … (99, …)
Sem conferência, o pedido para o cliente 99 entrou calado. O banco agora tem um órfão.
PRAGMA foreign_key_check;
Procura órfãos: a resposta diz que, na tabela pedidos, a linha 91 aponta para um clientes que não existe.
PRAGMA foreign_keys = ON;
Liga a conferência (só até fechar o sqlite3).
Runtime error: FOREIGN KEY constraint failed (19)
Com ela ligada, o pedido para o cliente 98 foi recusado. No terminal a mensagem vem com o prefixo Runtime error: ("erro durante a execução") e o código do erro, 19.

Como evitar: rode PRAGMA foreign_keys = ON; logo depois de abrir o banco no terminal. Para limpar o órfão que entrou, apague-o: DELETE FROM pedidos WHERE id = 91;.

Nos outros bancos

PostgreSQL e SQL Server conferem as chaves estrangeiras sempre, sem precisar ligar nada; no MySQL, o motor de tabelas padrão (InnoDB) também. Essa conferência desligada por padrão é uma particularidade do SQLite. As mensagens de erro mudam de banco para banco, mas a ideia é a mesma: o banco não deixa uma chave estrangeira apontar para o nada.


Referência rápida

TermoO que éNa Loja Aurora
Chave primária (PK)Coluna que identifica cada linha; nunca se repeteclientes.id, pedidos.id…
Chave estrangeira (FK)Coluna que guarda a chave primária de outra tabelapedidos.cliente_id → clientes.id
Um-para-muitosUma linha de A, várias de B; a FK fica em Bcliente → pedidos; categoria → produtos
Muitos-para-muitosTabela no meio com duas FKsitens_pedido (pedidos ↔ produtos)
Auto-relacionamentoA FK aponta para a própria tabelafuncionarios.chefe_id
REFERENCES tabela(coluna)Como a FK aparece na definição da tabela.schema pedidos no terminal
PRAGMA foreign_key_list(t)Lista as FKs da tabela tfunciona no editor
PRAGMA foreign_keys = ONLiga a conferência das FKs (no terminal vem desligada)já ligada no editor
FOREIGN KEY constraint failedTentou criar uma ligação para o nada, ou apagar uma linha que tem ligaçõespedido para o cliente 99

Exercícios

1. Qual é a chave estrangeira?

Na tabela produtos, qual coluna aponta para a tabela categorias?

produtos. → categorias.id

2. A tabela do meio

Qual tabela da Loja Aurora liga pedidos e produtos numa relação muitos-para-muitos?

3. Na relação "um cliente tem muitos pedidos", onde fica a chave estrangeira?

4. Por que a consulta SELECT cliente_id, COUNT(*) FROM pedidos GROUP BY cliente_id; mostra 26 clientes, se a loja tem 30?

5. No editor, o que acontece com DELETE FROM clientes WHERE id = 1;?

Missão final: os produtos do pedido 3

Missão: a consulta abaixo mostra os itens do pedido 3, mas só com os números dos produtos. Rode-a, anote os três produto_id e troque-a por uma consulta na tabela produtos que mostre o nome e o preço desses três produtos (dica: WHERE id IN (…), da aula IN).

SELECT pedido_id, produto_id, quantidade
FROM itens_pedido
WHERE pedido_id = 3;
SELECT id, nome, preco
FROM produtos
WHERE id IN (7, 8, 9);

Deu certo, mas você precisou de duas consultas e copiou números de uma para a outra. Na próxima aula, O que é um JOIN, o SQL faz essa ponte sozinho.


Resumo

  • Um banco relacional guarda cada coisa num lugar só (clientes numa tabela, pedidos em outra) e liga as tabelas por números, em vez de repetir dados.
  • A chave primária (id) identifica cada linha. A chave estrangeira (cliente_id) guarda a chave primária de outra tabela: ela "aponta" para lá.
  • Um-para-muitos: a chave estrangeira fica no lado "muitos". Muitos-para-muitos: uma tabela no meio com duas chaves estrangeiras (itens_pedido). Uma tabela pode apontar para ela mesma (chefe_id).
  • Na Loja Aurora, 4 clientes nunca pediram (27 a 30) e 3 produtos nunca venderam (43 a 45): eles não aparecem quando você olha só a tabela do outro lado.
  • O SQLite recusa ligações para o nada com FOREIGN KEY constraint failed, mas só se a conferência estiver ligada: no editor ela está; no terminal, ligue com PRAGMA foreign_keys = ON;.