Uma chave estrangeira faz mais do que marcar uma relacao entre tabelas: ela dita o que o banco faz com as linhas filhas quando voce apaga o pai. Essa reacao se chama acao referencial e voce a escolhe com a clausula ON DELETE. Escolha errado e voce ou recebe um erro a cada delete ou perde dados em silencio.
Um esquema base e um vinculo
Pegue tres tabelas conhecidas. orders tem uma chave estrangeira para users, e employees referencia a si mesma por meio de manager_id.
CREATE TABLE users (
id bigint PRIMARY KEY,
email text UNIQUE NOT NULL,
name text,
country text,
created_at timestamptz DEFAULT now()
);
CREATE TABLE orders (
id bigint PRIMARY KEY,
user_id bigint NOT NULL,
amount numeric(12,2),
status text,
created_at timestamptz DEFAULT now(),
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
Agora DELETE FROM users WHERE id = 7 remove automaticamente todos os pedidos daquele usuario. Sem a clausula ON DELETE, a acao padrao e NO ACTION: apagar um pai que ainda tem filhos simplesmente falha com um erro.
As quatro acoes de exclusao
CASCADE apaga as linhas filhas junto com o pai. Otimo para dados estritamente dependentes (itens dentro de um pedido), perigoso para qualquer coisa com valor proprio.
SET NULL zera a referencia na linha filha. A coluna FK precisa aceitar nulos. Util quando o filho sobrevive ao pai: voce apaga o usuario mas mantem os pedidos com user_id = NULL.
RESTRICT impede apagar o pai enquanto existir alguma linha filha. A verificacao dispara de imediato.
NO ACTION e o mesmo comportamento padrao, mas a verificacao pode ser adiada para o fim da transacao em uma restricao deferrable.
ALTER TABLE orders
DROP CONSTRAINT fk_orders_user,
ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
Existe tambem o ON UPDATE: as mesmas acoes disparam quando a propria chave primaria do pai muda. Na pratica voce quase nunca altera uma PK, entao ON UPDATE CASCADE raramente e necessario.
Quando o cascade e perigoso
ON DELETE CASCADE e tentador porque "tudo se limpa sozinho". Mas ele tambem apaga dados sem confirmacao e sem rastro na aplicacao.
DELETE FROM users WHERE country = 'RU';
Pegadinha: os cascades encadeiam. Se orders tem uma filha order_items tambem com CASCADE, apagar um usuario varre tres niveis. Em escala isso e uma transacao longa e bloqueante e um WAL inflado. Para dados financeiros ou de auditoria prefira RESTRICT ou um soft delete (deleted_at), e deixe o DELETE real para rotinas de limpeza.
Indexar a coluna da chave estrangeira
O PostgreSQL indexa automaticamente a chave primaria para a qual a FK aponta, mas NAO cria um indice na coluna que referencia. Sem ele, cada exclusao ou atualizacao de um pai dispara um scan sequencial da tabela filha para encontrar as linhas dependentes.
CREATE INDEX idx_orders_user_id ON orders (user_id);
Isso importa principalmente com CASCADE e SET NULL, porque a verificacao dos filhos roda a cada exclusao do pai. A mesma regra vale para a tabela autorreferenciada:
CREATE INDEX idx_employees_manager ON employees (manager_id);
Restricoes adiaveis e autorreferencias
As vezes voce precisa quebrar a integridade temporariamente dentro de uma transacao, por exemplo para trocar linhas de lugar ou carregar dados com referencias circulares. Para isso voce declara a restricao DEFERRABLE:
CREATE TABLE employees (
id bigint PRIMARY KEY,
name text,
manager_id bigint,
dept text,
salary numeric(12,2),
CONSTRAINT fk_emp_manager
FOREIGN KEY (manager_id) REFERENCES employees(id)
ON DELETE SET NULL
DEFERRABLE INITIALLY DEFERRED
);
Com INITIALLY DEFERRED a verificacao e adiada ate o COMMIT, entao voce pode manter referencias penduradas dentro da transacao. Lembre das diferencas entre os bancos:
- MySQL (InnoDB) suporta
CASCADE, SET NULL, RESTRICT e NO ACTION, mas nao tem DEFERRABLE: toda verificacao e imediata.
- ClickHouse nao tem chaves estrangeiras: a integridade e tarefa da aplicacao e as relacoes sao modeladas com
JOIN.
A regra base: cascade para dados estritamente dependentes, SET NULL para filhos que sobrevivem ao pai, RESTRICT para tudo que tem valor, e sempre indexe a coluna FK.
Uma chave estrangeira faz mais do que marcar uma relacao entre tabelas: ela dita o que o banco faz com as linhas filhas quando voce apaga o pai. Essa reacao se chama acao referencial e voce a escolhe com a clausula
ON DELETE. Escolha errado e voce ou recebe um erro a cada delete ou perde dados em silencio.Um esquema base e um vinculo
Pegue tres tabelas conhecidas.
orderstem uma chave estrangeira parausers, eemployeesreferencia a si mesma por meio demanager_id.CREATE TABLE users ( id bigint PRIMARY KEY, email text UNIQUE NOT NULL, name text, country text, created_at timestamptz DEFAULT now() ); CREATE TABLE orders ( id bigint PRIMARY KEY, user_id bigint NOT NULL, amount numeric(12,2), status text, created_at timestamptz DEFAULT now(), CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );Agora
DELETE FROM users WHERE id = 7remove automaticamente todos os pedidos daquele usuario. Sem a clausulaON DELETE, a acao padrao eNO ACTION: apagar um pai que ainda tem filhos simplesmente falha com um erro.As quatro acoes de exclusao
CASCADEapaga as linhas filhas junto com o pai. Otimo para dados estritamente dependentes (itens dentro de um pedido), perigoso para qualquer coisa com valor proprio.SET NULLzera a referencia na linha filha. A coluna FK precisa aceitar nulos. Util quando o filho sobrevive ao pai: voce apaga o usuario mas mantem os pedidos comuser_id = NULL.RESTRICTimpede apagar o pai enquanto existir alguma linha filha. A verificacao dispara de imediato.NO ACTIONe o mesmo comportamento padrao, mas a verificacao pode ser adiada para o fim da transacao em uma restricao deferrable.-- orders keep living, the link is cleared ALTER TABLE orders DROP CONSTRAINT fk_orders_user, ADD CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;Existe tambem o
ON UPDATE: as mesmas acoes disparam quando a propria chave primaria do pai muda. Na pratica voce quase nunca altera uma PK, entaoON UPDATE CASCADEraramente e necessario.Quando o cascade e perigoso
ON DELETE CASCADEe tentador porque "tudo se limpa sozinho". Mas ele tambem apaga dados sem confirmacao e sem rastro na aplicacao.-- one innocent-looking statement DELETE FROM users WHERE country = 'RU'; -- and every order of every matching user is gone tooPegadinha: os cascades encadeiam. Se
orderstem uma filhaorder_itemstambem comCASCADE, apagar um usuario varre tres niveis. Em escala isso e uma transacao longa e bloqueante e um WAL inflado. Para dados financeiros ou de auditoria prefiraRESTRICTou um soft delete (deleted_at), e deixe oDELETEreal para rotinas de limpeza.Indexar a coluna da chave estrangeira
O PostgreSQL indexa automaticamente a chave primaria para a qual a FK aponta, mas NAO cria um indice na coluna que referencia. Sem ele, cada exclusao ou atualizacao de um pai dispara um scan sequencial da tabela filha para encontrar as linhas dependentes.
CREATE INDEX idx_orders_user_id ON orders (user_id);Isso importa principalmente com
CASCADEeSET NULL, porque a verificacao dos filhos roda a cada exclusao do pai. A mesma regra vale para a tabela autorreferenciada:CREATE INDEX idx_employees_manager ON employees (manager_id);Restricoes adiaveis e autorreferencias
As vezes voce precisa quebrar a integridade temporariamente dentro de uma transacao, por exemplo para trocar linhas de lugar ou carregar dados com referencias circulares. Para isso voce declara a restricao
DEFERRABLE:CREATE TABLE employees ( id bigint PRIMARY KEY, name text, manager_id bigint, dept text, salary numeric(12,2), CONSTRAINT fk_emp_manager FOREIGN KEY (manager_id) REFERENCES employees(id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED );Com
INITIALLY DEFERREDa verificacao e adiada ate oCOMMIT, entao voce pode manter referencias penduradas dentro da transacao. Lembre das diferencas entre os bancos:CASCADE,SET NULL,RESTRICTeNO ACTION, mas nao temDEFERRABLE: toda verificacao e imediata.JOIN.A regra base: cascade para dados estritamente dependentes,
SET NULLpara filhos que sobrevivem ao pai,RESTRICTpara tudo que tem valor, e sempre indexe a coluna FK.