SQL FOREIGN KEY
Na Loja Aurora, cada pedido diz de quem ele é com um número: cliente_id = 3 quer dizer "este pedido é da cliente 3". E se alguém gravar um pedido com cliente_id = 999, de um cliente que não existe? O pedido ficaria "órfão": ninguém para entregar, ninguém para cobrar. A chave estrangeira (foreign key) é a regra que impede isso: ela obriga o número a apontar para uma linha que existe de verdade em outra tabela. Nesta aula você vai ver essa regra funcionando, a pegadinha do SQLite (ela vem desligada no terminal) e o que fazer com os filhos quando o "pai" é apagado.
- O que é uma chave estrangeira e como ela liga a tabela "filha" à tabela "mãe".
- As duas formas de escrever:
REFERENCESna coluna eFOREIGN KEY (…) REFERENCES …no fim da tabela. - O erro real
FOREIGN KEY constraint failedao inserir, mudar e apagar. - O
PRAGMA foreign_keys: por que no editor do guia a regra vale e no terminal dosqlite3ela vem desligada. - Como achar linhas órfãs com
PRAGMA foreign_key_check. ON DELETE CASCADE,SET NULLeRESTRICT: o que acontece com os filhos quando o pai some.
Antes, vale ter visto PRIMARY KEY e Tabelas que se Relacionam. Todos os exemplos rodam de verdade no seu navegador, e cada Rodar começa do banco limpo: pode apagar à vontade.
A ideia: a comanda e o fichário
Pense numa lojinha de bairro com um fichário de clientes, cada ficha com um número. Quando alguém faz uma encomenda, o vendedor anota numa comanda: "cliente nº 3, dois cadernos". A comanda não repete o nome nem o endereço: ela só aponta para a ficha 3.
Agora imagine que a dona da loja tem uma regra: "não se aceita comanda com número de ficha que não está no fichário". E outra: "não se joga fora a ficha de um cliente que ainda tem comanda". É exatamente isso que a chave estrangeira faz no banco:
| Na lojinha | No banco | Na Loja Aurora |
|---|---|---|
| O fichário | A tabela mãe (ou "pai", parent) | clientes |
| O número da ficha | A chave primária da mãe | clientes.id |
| As comandas | A tabela filha (child) | pedidos |
| O número anotado na comanda | A chave estrangeira | pedidos.cliente_id |
"Estrangeira" porque é uma chave que vem de fora: cliente_id mora na tabela pedidos, mas o valor dele é a chave de outra tabela.
1. As chaves estrangeiras da Loja Aurora
O banco da loja já foi criado com chaves estrangeiras. Peça ao SQLite o CREATE TABLE da tabela produtos; ele fica guardado na tabela interna sqlite_schema:
SELECT sql FROM sqlite_schema WHERE name = 'produtos';
Repare nas duas linhas com REFERENCES:
- categoria_id
- A coluna da tabela filha (
produtos) que guarda o número da categoria. - INTEGER NOT NULL
- O tipo e a regra de não ficar vazio (aula NOT NULL).
- REFERENCES
- "Faz referência a": aqui começa a chave estrangeira.
- categorias(id)
- A tabela mãe e, entre parênteses, a coluna dela para onde o número aponta. Essa coluna tem de ser a chave primária (ou uma coluna
UNIQUE) da mãe.
O SQLite também mostra a lista de chaves estrangeiras de uma tabela, já desmontada, com PRAGMA foreign_key_list:
PRAGMA foreign_key_list(produtos);
As colunas que importam: table (a mãe), from (a coluna desta tabela), to (a coluna da mãe) e on_delete/on_update (o que fazer se a mãe mudar; NO ACTION é o padrão, e você vai entender na seção 5).
2. A regra funcionando
Inserir um filho sem mãe
A Loja Aurora tem 8 categorias, numeradas de 1 a 8. A Ana tenta cadastrar uma caneta na categoria 99:
INSERT INTO produtos (nome, categoria_id, fornecedor_id, preco)
VALUES ('Caneta Gel Azul', 99, 1, 4.5);
A resposta do SQLite: FOREIGN KEY constraint failed, "a regra de chave estrangeira falhou". O produto não foi gravado. Repare que a mensagem não diz qual coluna deu problema: com duas chaves estrangeiras (categoria_id e fornecedor_id), você tem de conferir as duas. Com a categoria 1 (Papelaria), que existe, o mesmo INSERT passa:
INSERT INTO produtos (nome, categoria_id, fornecedor_id, preco)
VALUES ('Caneta Gel Azul', 1, 1, 4.5);
SELECT id, nome, categoria_id
FROM produtos
WHERE id = 46;
Experimente: rode o código e veja o erro. A Ana queria cadastrar o regador na categoria Jardim. Consulte SELECT id, nome FROM categorias; para descobrir o número certo e conserte o INSERT.
INSERT INTO produtos (nome, categoria_id, fornecedor_id, preco)
VALUES ('Regador 5 litros', 80, 1, 29.9);
INSERT INTO produtos (nome, categoria_id, fornecedor_id, preco)
VALUES ('Regador 5 litros', 8, 1, 29.9);
Mudar o número para um que não existe
A regra vale também para o UPDATE. Trocar a categoria do produto 1 para 50 deixaria o produto apontando para o vazio:
UPDATE produtos SET categoria_id = 50 WHERE id = 1;
Apagar uma mãe que tem filhos
Agora o outro lado da regra. A categoria 1, Papelaria, tem vários produtos. Se a Ana apagar a categoria, esses produtos ficariam apontando para nada:
DELETE FROM categorias WHERE id = 1;
O mesmo erro. Mas apagar um cliente que nunca fez pedido funciona, porque ninguém aponta para ele. O cliente 27, Paulo Sérgio Andrade, é um dos quatro que nunca compraram:
DELETE FROM clientes WHERE id = 27; SELECT COUNT(*) AS clientes FROM clientes;
Já o cliente 1, que tem pedidos, não sai: DELETE FROM clientes WHERE id = 1; dá FOREIGN KEY constraint failed. Para apagá-lo, você teria de apagar (ou passar para outro cliente) os pedidos dele primeiro, e antes ainda os itens desses pedidos, que apontam para os pedidos. Ou deixar o banco fazer isso sozinho, com ON DELETE CASCADE (seção 5).
Uma chave estrangeira NULL não aponta para lugar nenhum, e isso é permitido: quer dizer "não tem". Na tabela funcionarios, a gerente Marta Lustosa tem chefe_id NULL, porque não tem chefe. Se o "não tem" não fizer sentido (todo pedido tem cliente), junte NOT NULL à coluna, como a Loja Aurora fez em pedidos.cliente_id.
3. A pegadinha do SQLite: PRAGMA foreign_keys
Aqui o SQLite é diferente de quase todos os outros bancos. A documentação diz: "as chaves estrangeiras vêm desligadas por padrão (por compatibilidade com versões antigas), e precisam ser ligadas separadamente em cada conexão com o banco". Quem liga e desliga é o comando PRAGMA foreign_keys. Um PRAGMA é um comando especial do SQLite para ver e mudar configurações (aula PRAGMAs Úteis).
No editor do guia: ligado
Pergunte ao editor como está a regra:
PRAGMA foreign_keys;
O 1 quer dizer ligado. Isso acontece porque o arquivo que monta a Loja Aurora a cada Rodar começa com PRAGMA foreign_keys = ON;. Foi por isso que todos os erros acima apareceram.
No terminal: desligado
O banco da Ana no computador dela, ~/loja.db, tem exatamente as mesmas tabelas e as mesmas chaves estrangeiras. Mas o ligado/desligado não fica gravado no arquivo: cada vez que o programa sqlite3 abre o banco, a regra começa desligada. Veja o que acontece com a caneta da categoria 99:
$ 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 produtos (nome, categoria_id, fornecedor_id, preco) VALUES ('Caneta Fantasma', 99, 1, 2.5);
sqlite> SELECT id, nome, categoria_id FROM produtos WHERE id = 46;
46|Caneta Fantasma|99
sqlite> .quit
O 0 quer dizer desligado, e o INSERT passou calado. Agora o banco tem um produto órfão, de uma categoria que não existe. Com a regra ligada logo no começo da sessão (a Ana já tinha apagado a caneta fantasma):
$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> PRAGMA foreign_keys = ON;
sqlite> PRAGMA foreign_keys;
1
sqlite> INSERT INTO produtos (nome, categoria_id, fornecedor_id, preco) VALUES ('Caneta Fantasma', 99, 1, 2.5);
Runtime error: FOREIGN KEY constraint failed (19)
sqlite> .quit
$ sqlite3 loja.db 'PRAGMA foreign_keys;'
0 « abriu de novo: voltou a ficar desligado
- PRAGMA foreign_keys = ON;
- Liga a regra nesta conexão, até fechar o programa.
= OFFdesliga. - Runtime error:
- "Erro na execução": é como o terminal do
sqlite3mostra um erro que aconteceu ao rodar (e não ao ler) o comando. - (19)
- O código numérico do erro. 19 é o código de "regra violada" (constraint), o mesmo do
UNIQUEe doNOT NULL. O editor do guia mostra só a frase.
Quem usa o SQLite num programa (em PHP, Python…) precisa mandar PRAGMA foreign_keys = ON; logo depois de abrir o banco, toda vez. Esquecer é um dos erros mais comuns com o SQLite: as chaves estrangeiras ficam escritas no CREATE TABLE, parecem funcionar, e não protegem nada. E um detalhe: dentro de uma transação aberta com BEGIN (aula Transações), o pragma não tem efeito nenhum, e também não dá erro. Ligue antes do BEGIN.
Achar os órfãos: PRAGMA foreign_key_check
E se o banco já foi usado com a regra desligada? O PRAGMA foreign_key_check ("conferir chaves estrangeiras") procura as linhas que apontam para o vazio, mesmo com a regra desligada. No terminal, logo depois de gravar a caneta fantasma, a Ana veria:
sqlite> PRAGMA foreign_key_check; produtos|46|categorias|1
Dá para ver o mesmo no editor: desligue a regra, faça o estrago e confira.
PRAGMA foreign_keys = OFF; DELETE FROM categorias WHERE id = 1; PRAGMA foreign_key_check;
- table
- A tabela filha que tem o problema:
produtos. - rowid
- O número da linha órfã: os produtos 1 a 6 eram todos da Papelaria.
- parent
- A tabela mãe que deveria ter a linha:
categorias. - fkid
- Qual das chaves estrangeiras da tabela falhou, pelo número que aparece no
iddoPRAGMA foreign_key_list. Aqui, 1 =categoria_id.
Resultado vazio quer dizer "nenhum órfão". No banco limpo da Loja Aurora, o PRAGMA foreign_key_check não devolve nenhuma linha.
4. Criando a sua chave estrangeira
A Ana quer guardar as avaliações que os clientes dão aos produtos. Cada avaliação aponta para um produto e para um cliente. Há duas formas de escrever.
Forma 1: REFERENCES na própria coluna
CREATE TABLE avaliacoes (
id INTEGER PRIMARY KEY,
produto_id INTEGER NOT NULL REFERENCES produtos(id),
cliente_id INTEGER NOT NULL REFERENCES clientes(id),
nota INTEGER NOT NULL
);
INSERT INTO avaliacoes (produto_id, cliente_id, nota)
VALUES (1, 3, 5), (2, 8, 4);
SELECT * FROM avaliacoes;
Forma 2: FOREIGN KEY no fim da tabela
CREATE TABLE avaliacoes (
id INTEGER PRIMARY KEY,
produto_id INTEGER NOT NULL,
cliente_id INTEGER NOT NULL,
nota INTEGER NOT NULL,
FOREIGN KEY (produto_id) REFERENCES produtos(id),
FOREIGN KEY (cliente_id) REFERENCES clientes(id)
);
INSERT INTO avaliacoes (produto_id, cliente_id, nota)
VALUES (1, 99, 5);
- FOREIGN KEY (cliente_id)
- "Chave estrangeira": qual coluna desta tabela é a chave. Fica depois de todas as colunas, separada por vírgula.
- REFERENCES clientes(id)
- Para onde ela aponta, igual à forma 1.
As duas formas fazem a mesma coisa; o exemplo de cima falhou de propósito, porque não existe o cliente 99. A forma 2 tem duas vantagens: deixa todas as ligações juntas no fim, fáceis de achar, e é a única que serve para chaves de mais de uma coluna (por exemplo, FOREIGN KEY (pedido_id, produto_id) REFERENCES …). Também é a forma que aparece na documentação de todos os bancos grandes.
| Forma | Escrita | Quando usar |
|---|---|---|
| Na coluna | cliente_id INTEGER REFERENCES clientes(id) | Tabelas pequenas, uma coluna por chave. É a que a Loja Aurora usa. |
| No fim da tabela | FOREIGN KEY (cliente_id) REFERENCES clientes(id) | Chave de várias colunas, ou quando o mesmo SQL vai rodar em outro banco. |
5. Quando a mãe é apagada: ON DELETE
Até aqui, apagar uma mãe com filhos dava erro. Esse é o comportamento padrão, chamado NO ACTION ("nenhuma ação"): o banco não faz nada com os filhos e recusa a mudança que os deixaria órfãos. Mas você pode escolher outra coisa, escrevendo ON DELETE ("ao apagar") depois do REFERENCES.
ON DELETE CASCADE: os filhos vão junto
A Ana vai guardar os endereços de entrega dos clientes. Um endereço não tem sentido sem o cliente: se o cliente for apagado, os endereços dele podem ir junto. Isso é CASCADE ("em cascata"):
CREATE TABLE enderecos (
id INTEGER PRIMARY KEY,
cliente_id INTEGER NOT NULL
REFERENCES clientes(id) ON DELETE CASCADE,
rua TEXT NOT NULL
);
INSERT INTO enderecos (cliente_id, rua) VALUES
(27, 'Rua das Flores, 10'),
(27, 'Av. Frei Serafim, 200'),
(28, 'Rua Areolino de Abreu, 55');
DELETE FROM clientes WHERE id = 27;
SELECT * FROM enderecos;
- ON DELETE
- "Ao apagar" a linha da mãe (o cliente)…
- CASCADE
- …apague também os filhos (os endereços dele).
O DELETE só falava de clientes, mas os dois endereços do cliente 27 sumiram junto; sobrou só o do cliente 28. É poderoso e perigoso na mesma medida: um DELETE errado numa tabela mãe pode apagar muita coisa em outras tabelas sem você ver. Use CASCADE só onde o filho realmente não vive sem o pai (endereços de um cliente, itens de um pedido), nunca em coisas que precisam ficar guardadas, como os pedidos.
ON DELETE SET NULL: o filho fica, sem pai
Para as avaliações, a Ana prefere outra coisa: se o cliente sair da loja, a avaliação continua valendo para o produto, só que "de autor desconhecido":
CREATE TABLE avaliacoes (
id INTEGER PRIMARY KEY,
produto_id INTEGER NOT NULL REFERENCES produtos(id),
cliente_id INTEGER
REFERENCES clientes(id) ON DELETE SET NULL,
nota INTEGER NOT NULL
);
INSERT INTO avaliacoes (produto_id, cliente_id, nota)
VALUES (1, 27, 5), (2, 28, 4);
DELETE FROM clientes WHERE id = 27;
SELECT * FROM avaliacoes;
A avaliação 1 ficou com cliente_id NULL. Repare que, para isso funcionar, a coluna não pode ser NOT NULL.
Todas as opções
| Ação | Ao apagar (ou mudar o id de) uma mãe com filhos | Exemplo na loja |
|---|---|---|
NO ACTION (padrão) | Recusa com FOREIGN KEY constraint failed. | Categoria com produtos, cliente com pedidos. |
RESTRICT | Também recusa. A diferença para o NO ACTION só aparece com regras adiadas para o fim da transação, assunto avançado. | Igual ao de cima. |
CASCADE | Apaga os filhos junto (ou, no ON UPDATE, muda o número neles também). | Endereços de um cliente. |
SET NULL | Põe NULL na chave estrangeira dos filhos. | Avaliação de um cliente que saiu. |
SET DEFAULT | Põe o valor DEFAULT da coluna; esse valor tem de existir na mãe. | Produtos de uma categoria apagada vão para uma categoria "Outros". |
O mesmo vale para ON UPDATE ("ao mudar"), quando o id da mãe muda. Na Loja Aurora, nenhuma chave tem ação, então UPDATE clientes SET id = 100 WHERE id = 1; também dá FOREIGN KEY constraint failed. Mudar a chave primária de uma linha é raro; é mais um motivo para usar números sem significado como chave.
Quando dá errado
É o erro desta aula, e ele aparece em três situações. Ele nunca diz qual coluna nem qual valor; você tem de investigar:
| Você tentou | O que conferir |
|---|---|
INSERT ou UPDATE na filha | Cada chave estrangeira da linha aponta para um id que existe? (SELECT id FROM categorias;) |
DELETE ou UPDATE do id na mãe | Alguém ainda aponta para ela? (SELECT COUNT(*) FROM produtos WHERE categoria_id = 1;) |
DROP TABLE da mãe | Mesma coisa: veja abaixo. |
DROP TABLE categorias;
A mensagem: FOREIGN KEY constraint failed. Com a regra ligada, o DROP TABLE primeiro apaga todas as linhas da tabela, e isso esbarra nos produtos que apontam para as categorias. A tabela continua lá. Já DROP TABLE itens_pedido; funciona: ninguém aponta para os itens. Mais na aula DROP TABLE.
A Ana quis ligar pelo nome do cliente:
CREATE TABLE recados (
cliente_nome TEXT REFERENCES clientes(nome),
texto TEXT
);
INSERT INTO recados VALUES ('Ana Beatriz Sousa', 'Ligar amanhã');
A mensagem: foreign key mismatch - "recados" referencing "clientes", "chave estrangeira incompatível: recados fazendo referência a clientes". Repare que o CREATE TABLE passou; o erro só aparece no primeiro INSERT. A coluna da mãe tem de ser a chave primária ou ter UNIQUE (aula UNIQUE), porque a chave estrangeira precisa apontar para uma linha só, e podem existir dois clientes com o mesmo nome.
Como consertar: aponte para o id: cliente_id INTEGER REFERENCES clientes(id).
CREATE TABLE recados (
cliente_id INTEGER REFERENCES cliente(id),
texto TEXT
);
INSERT INTO recados VALUES (3, 'Ligar amanhã');
A mensagem: no such table: main.cliente, "não existe a tabela main.cliente" (main é o nome do banco principal aberto). Faltou o s: a tabela é clientes. De novo, o CREATE TABLE aceitou sem conferir, e o erro só apareceu no INSERT.
Você criou tudo com REFERENCES, gravou um pedido de cliente inexistente e o banco aceitou. Quase sempre é o PRAGMA foreign_keys desligado (seção 3). Confira com PRAGMA foreign_keys;: se der 0, mande PRAGMA foreign_keys = ON; logo depois de abrir o banco, e use PRAGMA foreign_key_check; para achar o que já entrou errado.
No MySQL (com o motor de tabelas InnoDB, o padrão), no PostgreSQL e no SQL Server, as chaves estrangeiras valem sempre, sem pragma nenhum. No MySQL existe a variável foreign_key_checks, que vem ligada e pode ser desligada para importar dados. A forma "no fim da tabela" funciona em todos (trecho ilustrativo, no estilo do MySQL):
CREATE TABLE enderecos (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
cliente_id INT NOT NULL,
rua VARCHAR(200) NOT NULL,
FOREIGN KEY (cliente_id) REFERENCES clientes(id) ON DELETE CASCADE
);
CASCADE, SET NULL e NO ACTION existem nos três. Uma diferença: no MySQL, segundo a documentação, as tabelas InnoDB recusam SET DEFAULT.
Referência rápida
| Escrita | O que faz |
|---|---|
coluna INTEGER REFERENCES mae(id) | Chave estrangeira na própria coluna. |
FOREIGN KEY (coluna) REFERENCES mae(id) | Chave estrangeira no fim da tabela (serve para várias colunas). |
… ON DELETE CASCADE | Apagar a mãe apaga os filhos. |
… ON DELETE SET NULL | Apagar a mãe põe NULL nos filhos. |
… ON UPDATE CASCADE | Mudar o id da mãe muda o número nos filhos. |
PRAGMA foreign_keys; | Mostra se a regra está ligada (1) ou desligada (0). |
PRAGMA foreign_keys = ON; | Liga a regra nesta conexão (fora de transação). |
PRAGMA foreign_key_list(tabela); | Lista as chaves estrangeiras de uma tabela. |
PRAGMA foreign_key_check; | Lista as linhas órfãs de todo o banco. |
| Mensagem | Quer dizer |
|---|---|
FOREIGN KEY constraint failed | A mudança deixaria uma linha órfã (filha sem mãe). |
Runtime error: FOREIGN KEY constraint failed (19) | O mesmo, no terminal do sqlite3. |
foreign key mismatch - "recados" referencing "clientes" | A coluna da mãe não é chave primária nem UNIQUE. |
no such table: main.cliente | A tabela mãe do REFERENCES não existe. |
Exercícios
1. Ligando a regra
Complete o comando que liga as chaves estrangeiras no terminal do SQLite:
2. Os endereços vão junto
Complete para que os endereços sejam apagados junto com o cliente:
3. Na tabela pedidos, qual é a tabela mãe da chave estrangeira cliente_id?
4. A Ana abriu o loja.db no terminal e, sem rodar nenhum pragma, inseriu um produto da categoria 99. O que aconteceu?
5. A chave avaliacoes.cliente_id tem ON DELETE SET NULL. O que acontece com a avaliação quando o cliente é apagado?
6. Qual destes comandos funciona no editor do guia (com a regra ligada), sem erro?
Missão final: endereços que vão junto
Missão: o código cria a tabela de endereços, grava dois endereços do cliente 27 e um do 28, e tenta apagar o cliente 27. Rode e veja o erro. Mude só a criação da tabela para que, ao apagar o cliente, os endereços dele sejam apagados junto. No fim, deve sobrar 1 endereço.
CREATE TABLE enderecos (
id INTEGER PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES clientes(id),
rua TEXT NOT NULL
);
INSERT INTO enderecos (cliente_id, rua) VALUES
(27, 'Rua das Flores, 10'),
(27, 'Av. Frei Serafim, 200'),
(28, 'Rua Areolino de Abreu, 55');
DELETE FROM clientes WHERE id = 27;
SELECT * FROM enderecos;
CREATE TABLE enderecos (
id INTEGER PRIMARY KEY,
cliente_id INTEGER NOT NULL
REFERENCES clientes(id) ON DELETE CASCADE,
rua TEXT NOT NULL
);
INSERT INTO enderecos (cliente_id, rua) VALUES
(27, 'Rua das Flores, 10'),
(27, 'Av. Frei Serafim, 200'),
(28, 'Rua Areolino de Abreu, 55');
DELETE FROM clientes WHERE id = 27;
SELECT * FROM enderecos;
Resumo
- Uma chave estrangeira é uma coluna da tabela filha que aponta para a chave de uma tabela mãe:
pedidos.cliente_id→clientes.id. - Escreva
REFERENCES mae(id)na coluna ouFOREIGN KEY (coluna) REFERENCES mae(id)no fim da tabela. - Com a regra ligada, inserir um filho sem mãe, mudar o número para um que não existe ou apagar uma mãe com filhos dá
FOREIGN KEY constraint failed. - No SQLite, a regra vem desligada em cada conexão: no terminal,
PRAGMA foreign_keys;dá0. Ligue comPRAGMA foreign_keys = ON;toda vez que abrir o banco. No editor do guia ela já vem ligada. PRAGMA foreign_key_check;acha as linhas órfãs que entraram com a regra desligada.ON DELETE CASCADEapaga os filhos junto;ON DELETE SET NULLos deixa sem pai; o padrão (NO ACTION) recusa.