CASCADE DELETE apenas uma vez

200

Eu tenho um banco de dados Postgresql no qual desejo fazer algumas exclusões em cascata. No entanto, as tabelas não são configuradas com a regra ON DELETE CASCADE. Existe alguma maneira de executar uma exclusão e dizer ao Postgresql para fazer cascata apenas desta vez? Algo equivalente a

DELETE FROM some_table CASCADE;

As respostas para essa pergunta mais antiga fazem parecer que essa solução não existe, mas imaginei que faria essa pergunta explicitamente apenas para ter certeza.

Eli Courtwright
fonte
Por favor, veja minha função personalizada abaixo. É possível com certas restrições.
31520 Joe

Respostas:

175

Não. Para fazer isso apenas uma vez, basta escrever a instrução delete para a tabela que você deseja colocar em cascata.

DELETE FROM some_child_table WHERE some_fk_field IN (SELECT some_id FROM some_Table);
DELETE FROM some_table;
cavalo pálido
fonte
12
Isso não funciona necessariamente, pois pode haver outras chaves estrangeiras em cascata da cascata original (recursão). Você pode até entrar em um loop em que a tabela a se refere a b que se refere a a. Para conseguir isso em um sentido geral, veja minha tabela abaixo, mas há algumas restrições. Se você tiver uma configuração de tabela simples, tente o código acima, é mais fácil compreender o que está fazendo.
31513 Joe
2
Simples, seguro. Você deve executá-los em uma única transação se tiver inserções de densidade.
İsmail Yavuz
39

Se você realmente deseja o DELETE FROM some_table CASCADE; que significa " remover todas as linhas da tabelasome_table ", pode usar em TRUNCATEvez de DELETEe CASCADEsempre é suportado. No entanto, se você deseja usar a exclusão seletiva com uma wherecláusula, isso TRUNCATEnão é bom o suficiente.

USE WITH CARE - Isso eliminará todas as linhas de todas as tabelas que têm uma restrição de chave estrangeira some_tablee todas as tabelas que têm restrições nessas tabelas, etc.

O Postgres suporta CASCADEcom o comando TRUNCATE :

TRUNCATE some_table CASCADE;

Facilmente, isso é transacional (isto é, pode ser revertido), embora não esteja totalmente isolado de outras transações simultâneas e tenha várias outras advertências. Leia os documentos para obter detalhes.

DanC
fonte
226
claramente "algumas exclusões em cascata" ≠ descartando todos os dados da tabela ...
lensovet 14/02/12
33
Isso eliminará todas as linhas de todas as tabelas que possuem uma restrição de chave estrangeira em some_table e todas as tabelas que têm restrições nessas tabelas, etc ... isso é potencialmente muito perigoso.
AJP
56
cuidado. Esta é uma resposta imprudente.
Jordan Arseno
4
Alguém sinalizou esta resposta para exclusão - provavelmente porque discorda dela. O curso de ação correto nesse caso é reduzir a votação, não sinalizar.
Wai Ha Lee
7
Ele tem o aviso no topo. Se você optar por ignorar isso, ninguém poderá ajudá-lo. Eu acho que seus usuários "copyPaste" são o verdadeiro perigo aqui.
BluE
28

Eu escrevi uma função (recursiva) para excluir qualquer linha com base em sua chave primária. Eu escrevi isso porque não queria criar minhas restrições como "ao excluir cascata". Eu queria poder excluir conjuntos complexos de dados (como um DBA), mas não permitir que meus programadores pudessem excluir em cascata a exclusão sem pensar em todas as repercussões. Ainda estou testando essa função, portanto pode haver erros nela - mas não tente se o seu banco de dados tiver chaves primárias de várias colunas (e, portanto, estrangeiras). Além disso, todas as chaves devem poder ser representadas na forma de cadeia, mas podem ser escritas de uma maneira que não tenha essa restrição. De qualquer maneira, eu uso essa função MUITO POUCOSAMENTE, eu dou muito valor aos meus dados para permitir as restrições em cascata de tudo. Basicamente, essa função é passada no esquema, nome da tabela e valor primário (em forma de cadeia), e começará encontrando as chaves estrangeiras nessa tabela e assegurando que os dados não existam - se existir, chama-se recursivamente aos dados encontrados. Ele usa uma matriz de dados já marcados para exclusão para evitar loops infinitos. Teste e informe-me como funciona para você. Nota: é um pouco lento. Eu chamo assim: select delete_cascade('public','my_table','1');

create or replace function delete_cascade(p_schema varchar, p_table varchar, p_key varchar, p_recursion varchar[] default null)
 returns integer as $$
declare
    rx record;
    rd record;
    v_sql varchar;
    v_recursion_key varchar;
    recnum integer;
    v_primary_key varchar;
    v_rows integer;
begin
    recnum := 0;
    select ccu.column_name into v_primary_key
        from
        information_schema.table_constraints  tc
        join information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name and ccu.constraint_schema=tc.constraint_schema
        and tc.constraint_type='PRIMARY KEY'
        and tc.table_name=p_table
        and tc.table_schema=p_schema;

    for rx in (
        select kcu.table_name as foreign_table_name, 
        kcu.column_name as foreign_column_name, 
        kcu.table_schema foreign_table_schema,
        kcu2.column_name as foreign_table_primary_key
        from information_schema.constraint_column_usage ccu
        join information_schema.table_constraints tc on tc.constraint_name=ccu.constraint_name and tc.constraint_catalog=ccu.constraint_catalog and ccu.constraint_schema=ccu.constraint_schema 
        join information_schema.key_column_usage kcu on kcu.constraint_name=ccu.constraint_name and kcu.constraint_catalog=ccu.constraint_catalog and kcu.constraint_schema=ccu.constraint_schema
        join information_schema.table_constraints tc2 on tc2.table_name=kcu.table_name and tc2.table_schema=kcu.table_schema
        join information_schema.key_column_usage kcu2 on kcu2.constraint_name=tc2.constraint_name and kcu2.constraint_catalog=tc2.constraint_catalog and kcu2.constraint_schema=tc2.constraint_schema
        where ccu.table_name=p_table  and ccu.table_schema=p_schema
        and TC.CONSTRAINT_TYPE='FOREIGN KEY'
        and tc2.constraint_type='PRIMARY KEY'
)
    loop
        v_sql := 'select '||rx.foreign_table_primary_key||' as key from '||rx.foreign_table_schema||'.'||rx.foreign_table_name||'
            where '||rx.foreign_column_name||'='||quote_literal(p_key)||' for update';
        --raise notice '%',v_sql;
        --found a foreign key, now find the primary keys for any data that exists in any of those tables.
        for rd in execute v_sql
        loop
            v_recursion_key=rx.foreign_table_schema||'.'||rx.foreign_table_name||'.'||rx.foreign_column_name||'='||rd.key;
            if (v_recursion_key = any (p_recursion)) then
                --raise notice 'Avoiding infinite loop';
            else
                --raise notice 'Recursing to %,%',rx.foreign_table_name, rd.key;
                recnum:= recnum +delete_cascade(rx.foreign_table_schema::varchar, rx.foreign_table_name::varchar, rd.key::varchar, p_recursion||v_recursion_key);
            end if;
        end loop;
    end loop;
    begin
    --actually delete original record.
    v_sql := 'delete from '||p_schema||'.'||p_table||' where '||v_primary_key||'='||quote_literal(p_key);
    execute v_sql;
    get diagnostics v_rows= row_count;
    --raise notice 'Deleting %.% %=%',p_schema,p_table,v_primary_key,p_key;
    recnum:= recnum +v_rows;
    exception when others then recnum=0;
    end;

    return recnum;
end;
$$
language PLPGSQL;
Joe Love
fonte
Isso acontece o tempo todo, especialmente com tabelas de auto-referência. Considere uma empresa com diferentes níveis de gerenciamento em diferentes departamentos ou uma taxonomia hierárquica genérica. Sim, concordo que essa função não é a melhor coisa desde pão fatiado, mas é uma ferramenta útil na situação certa.
Joe Amor
Se você reescrevê-lo, aceite a matriz de IDs e também gere consultas que usarão o INoperador com sub-seleções em vez de =(portanto, passo para usar a lógica de conjuntos), isso se tornaria muito mais rápido.
Hubbitus
2
Obrigado pela sua solução. Escrevi alguns testes e precisava excluir um registro e estava tendo problemas para fazer uma cascata dessa exclusão. Sua função funcionou muito bem!
Fernando Camargo
1
@JoeLove que problema de velocidade você tem? Nessa situação, a recursão é a única solução correta em minha mente.
Hubbitus
1
@ arthur você provavelmente poderia usar alguma versão da linha -> json -> texto para fazê-lo, no entanto, eu não fui tão longe. Ao longo dos anos, descobri que uma chave primária singular (com possíveis chaves secundárias) é boa por vários motivos.
Joe Love
17

Se bem entendi, você poderá fazer o que deseja, eliminando a restrição de chave estrangeira, adicionando uma nova (que irá cascatear), executando suas tarefas e recriando a restrição de chave estrangeira restritiva.

Por exemplo:

testing=# create table a (id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "a_pkey" for table "a"
CREATE TABLE
testing=# create table b (id integer references a);
CREATE TABLE

-- put some data in the table
testing=# insert into a values(1);
INSERT 0 1
testing=# insert into a values(2);
INSERT 0 1
testing=# insert into b values(2);
INSERT 0 1
testing=# insert into b values(1);
INSERT 0 1

-- restricting works
testing=# delete from a where id=1;
ERROR:  update or delete on table "a" violates foreign key constraint "b_id_fkey" on table "b"
DETAIL:  Key (id)=(1) is still referenced from table "b".

-- find the name of the constraint
testing=# \d b;
       Table "public.b"
 Column |  Type   | Modifiers 
--------+---------+-----------
 id     | integer | 
Foreign-key constraints:
    "b_id_fkey" FOREIGN KEY (id) REFERENCES a(id)

-- drop the constraint
testing=# alter table b drop constraint b_a_id_fkey;
ALTER TABLE

-- create a cascading one
testing=# alter table b add FOREIGN KEY (id) references a(id) on delete cascade; 
ALTER TABLE

testing=# delete from a where id=1;
DELETE 1
testing=# select * from a;
 id 
----
  2
(1 row)

testing=# select * from b;
 id 
----
  2
(1 row)

-- it works, do your stuff.
-- [stuff]

-- recreate the previous state
testing=# \d b;
       Table "public.b"
 Column |  Type   | Modifiers 
--------+---------+-----------
 id     | integer | 
Foreign-key constraints:
    "b_id_fkey" FOREIGN KEY (id) REFERENCES a(id) ON DELETE CASCADE

testing=# alter table b drop constraint b_id_fkey;
ALTER TABLE
testing=# alter table b add FOREIGN KEY (id) references a(id) on delete restrict; 
ALTER TABLE

Obviamente, você deve abstrair coisas assim em um procedimento, para o bem da sua saúde mental.

Ryszard Szopa
fonte
4
Supondo que a chave estrangeira evite fazer coisas que tornem o banco de dados inconsistente, não é esse o caminho. Você pode excluir a entrada "desagradável", mas agora você está deixando um monte de cacos de zumbis wich poderia causar problemas no futuro
Sprinterfreak
1
Que fragmentos você quer dizer exatamente? os registros serão excluídos via cascata; não deve haver inconsistência.
Pedro Borges
1
em vez de se preocupar com "fragmentos desagradáveis" (as restrições em cascata ainda serão consistentes), eu ficaria mais preocupado com a cascata não ir longe o suficiente - se os registros excluídos exigirem mais registros excluídos, essas restrições precisarão ser alteradas para garantir a cascata também. (ou use a função que eu escrevi acima para evitar esse cenário) ... Uma última recomendação em qualquer caso: USE UMA TRANSAÇÃO para que você possa revertê-la se der errado.
317 Joe Joe
7

Não posso comentar a resposta de Palehorse, então adicionei minha própria resposta. A lógica do Palehorse está correta, mas a eficiência pode ser ruim com grandes conjuntos de dados.

DELETE FROM some_child_table sct 
 WHERE exists (SELECT FROM some_Table st 
                WHERE sct.some_fk_fiel=st.some_id);

DELETE FROM some_table;

É mais rápido se você tiver índices em colunas e o conjunto de dados for maior que alguns registros.

Grzegorz Grabek
fonte
7

Sim, como outros já disseram, não há um conveniente 'DELETE FROM my_table ... CASCADE' (ou equivalente). Para excluir registros filhos protegidos por chave estrangeira não em cascata e seus ancestrais referenciados, suas opções incluem:

  • Execute todas as exclusões explicitamente, uma consulta de cada vez, começando com tabelas filho (embora isso não funcione se você tiver referências circulares); ou
  • Execute todas as exclusões explicitamente em uma única consulta (potencialmente massiva); ou
  • Supondo que suas restrições de chave estrangeira não em cascata foram criadas como 'ON DELETE NO ACTION DEFERRABLE', execute todas as exclusões explicitamente em uma única transação; ou
  • Solte temporariamente as restrições 'sem ação' e 'restrinja' a chave estrangeira no gráfico, recrie-as como CASCADE, exclua os ancestrais ofensivos, elimine as restrições de chave estrangeira novamente e, por fim, recrie-as como estavam originalmente (enfraquecendo temporariamente a integridade de seus dados); ou
  • Algo provavelmente igualmente divertido.

É de propósito que contornar restrições de chave estrangeira não é conveniente, presumo; mas entendo por que, em circunstâncias específicas, você deseja fazer isso. Se é algo que você fará com alguma frequência, e se estiver disposto a desprezar a sabedoria dos DBAs em todos os lugares, convém automatizá-lo com um procedimento.

Eu vim aqui há alguns meses atrás, procurando uma resposta para a pergunta "CASCADE DELETE apenas uma vez" (originalmente feita há mais de uma década!). Eu obtive alguma milhagem da solução inteligente de Joe Love (e da variante de Thomas CG de Vilhena), mas no final meu caso de uso tinha requisitos específicos (manipulação de referências circulares dentro da mesa, por exemplo) que me forçaram a adotar uma abordagem diferente. Essa abordagem acabou se tornando recursivamente_delete (PG 10.10).

Estou usando o recursively_delete na produção há um tempo e agora me sinto (cautelosamente) confiante o suficiente para disponibilizá-lo a outras pessoas que possam acabar aqui procurando idéias. Assim como na solução de Joe Love, ele permite excluir gráficos inteiros de dados como se todas as restrições de chave estrangeira no seu banco de dados fossem momentaneamente definidas para CASCADE, mas oferece alguns recursos adicionais:

  • Fornece uma visualização ASCII do destino de exclusão e seu gráfico de dependentes.
  • Executa a exclusão em uma única consulta usando CTEs recursivas.
  • Lida com dependências circulares, intra e inter tabela.
  • Lida com chaves compostas.
  • Ignora as restrições 'definir padrão' e 'definir nulo'.
TRL
fonte
Estou recebendo um erro: ERRO: a matriz deve ter um número par de elementos Onde: PL / pgSQL function _recursively_delete (regclass, text [], integer, jsonb, integer, text [], jsonb, jsonb) linha 15 na instrução SQL de atribuição "SELECT * FROM _recursively_delete (ARG_table, VAR_pk_col_names)" Função PL / pgSQL recursively_delete (regclass, anyelement, boolean) linha 73 na instrução SQL
Joe Love
Ei, @JoeLove. Obrigado por experimentar. Você pode me dar passos para reproduzir? E qual é a sua versão do PG?
TRL
Não tenho certeza se isso ajudará. mas acabei de criar suas funções e, em seguida, executei o seguinte código: select recursively_delete ('dallas.vendor', 1094, false) Após algumas depurações, acho que isso acaba logo de cara - ou seja, parece que é a primeira chamada para a função, não depois de fazer várias coisas. Para referência, estou executando o PG 10.8
Joe Love
@JoeLove, Por favor, tente o ramo trl-fix-array_must_have_even_number_of_element ( github.com/trlorenz/PG-recursively_delete/pull/2 ).
TRL
Tentei esse ramo e corrigiu o erro original. Infelizmente, não é mais rápido que a minha versão original (que pode não ter sido o seu objetivo ao escrever isso em primeiro lugar). Estou trabalhando em outra tentativa que cria chaves estrangeiras duplicadas com "on delete cascade", excluindo o registro original e largando todas as chaves estrangeiras recém-criadas.
Joe Love
3

Você pode usar para automatizar isso, você pode definir a restrição de chave estrangeira com ON DELETE CASCADE.
Cito o manual de restrições de chave estrangeira :

CASCADE especifica que quando uma linha referenciada é excluída, as linhas que fazem referência a ela também devem ser excluídas automaticamente.

atiruz
fonte
1
Embora isso não aborde o OP, é bom planejar quando as linhas com chaves estrangeiras precisam ser excluídas. Como Ben Franklin disse, "uma grama de prevenção vale um quilo de cura".
Jesuisme
1
Descobri que essa solução pode ser bastante perigosa se o aplicativo excluir o registro a com vários irmãos e, em vez de um pequeno erro, você excluir permanentemente um grande conjunto de dados.
Joe Amor
2

Peguei a resposta de Joe Love e a reescrevi usando o INoperador com sub-selects em vez de =acelerar a função (de acordo com a sugestão de Hubbitus):

create or replace function delete_cascade(p_schema varchar, p_table varchar, p_keys varchar, p_subquery varchar default null, p_foreign_keys varchar[] default array[]::varchar[])
 returns integer as $$
declare

    rx record;
    rd record;
    v_sql varchar;
    v_subquery varchar;
    v_primary_key varchar;
    v_foreign_key varchar;
    v_rows integer;
    recnum integer;

begin

    recnum := 0;
    select ccu.column_name into v_primary_key
        from
        information_schema.table_constraints  tc
        join information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name and ccu.constraint_schema=tc.constraint_schema
        and tc.constraint_type='PRIMARY KEY'
        and tc.table_name=p_table
        and tc.table_schema=p_schema;

    for rx in (
        select kcu.table_name as foreign_table_name, 
        kcu.column_name as foreign_column_name, 
        kcu.table_schema foreign_table_schema,
        kcu2.column_name as foreign_table_primary_key
        from information_schema.constraint_column_usage ccu
        join information_schema.table_constraints tc on tc.constraint_name=ccu.constraint_name and tc.constraint_catalog=ccu.constraint_catalog and ccu.constraint_schema=ccu.constraint_schema 
        join information_schema.key_column_usage kcu on kcu.constraint_name=ccu.constraint_name and kcu.constraint_catalog=ccu.constraint_catalog and kcu.constraint_schema=ccu.constraint_schema
        join information_schema.table_constraints tc2 on tc2.table_name=kcu.table_name and tc2.table_schema=kcu.table_schema
        join information_schema.key_column_usage kcu2 on kcu2.constraint_name=tc2.constraint_name and kcu2.constraint_catalog=tc2.constraint_catalog and kcu2.constraint_schema=tc2.constraint_schema
        where ccu.table_name=p_table  and ccu.table_schema=p_schema
        and TC.CONSTRAINT_TYPE='FOREIGN KEY'
        and tc2.constraint_type='PRIMARY KEY'
)
    loop
        v_foreign_key := rx.foreign_table_schema||'.'||rx.foreign_table_name||'.'||rx.foreign_column_name;
        v_subquery := 'select "'||rx.foreign_table_primary_key||'" as key from '||rx.foreign_table_schema||'."'||rx.foreign_table_name||'"
             where "'||rx.foreign_column_name||'"in('||coalesce(p_keys, p_subquery)||') for update';
        if p_foreign_keys @> ARRAY[v_foreign_key] then
            --raise notice 'circular recursion detected';
        else
            p_foreign_keys := array_append(p_foreign_keys, v_foreign_key);
            recnum:= recnum + delete_cascade(rx.foreign_table_schema, rx.foreign_table_name, null, v_subquery, p_foreign_keys);
            p_foreign_keys := array_remove(p_foreign_keys, v_foreign_key);
        end if;
    end loop;

    begin
        if (coalesce(p_keys, p_subquery) <> '') then
            v_sql := 'delete from '||p_schema||'."'||p_table||'" where "'||v_primary_key||'"in('||coalesce(p_keys, p_subquery)||')';
            --raise notice '%',v_sql;
            execute v_sql;
            get diagnostics v_rows = row_count;
            recnum := recnum + v_rows;
        end if;
        exception when others then recnum=0;
    end;

    return recnum;

end;
$$
language PLPGSQL;
Thomas CG de Vilhena
fonte
2
Vou ter que olhar para isso e ver como ele funciona com restrições de auto-referência e coisas do gênero. Tentei fazer algo semelhante, mas não consegui fazê-lo funcionar totalmente. Se sua solução funcionar para mim, eu vou implementá-la. Essa é uma das muitas ferramentas de dba que devem ser empacotadas e colocadas no github ou algo assim.
317 Joe Joe
Eu tenho bancos de dados de tamanho médio para um CMS com vários inquilinos (todos os clientes compartilham as mesmas tabelas). Minha versão (sem o "in") parece estar bastante lenta para excluir todos os vestígios de um cliente antigo ... Estou interessado em tentar isso com alguns dados de maquete para comparar velocidades. Você tinha algo a dizer sobre a diferença de velocidade observada nos casos de uso?
31519 Joe Joe
Para o meu caso de uso, notei uma velocidade na ordem de 10x ao usar o inoperador e as subconsultas.
Thomas CG de Vilhena
1

A opção excluir com cascata se aplica apenas a tabelas com chaves estrangeiras definidas. Se você excluir, e ele disser que não pode, porque violaria a restrição de chave estrangeira, a cascata fará com que ele exclua as linhas incorretas.

Se você deseja excluir as linhas associadas dessa maneira, primeiro precisará definir as chaves estrangeiras. Além disso, lembre-se de que, a menos que você o instrua explicitamente para iniciar uma transação ou altere os padrões, ele fará uma confirmação automática, o que pode levar muito tempo para ser limpo.

Grant Johnson
fonte
2
A resposta de Grant está parcialmente errada - o Postgresql não suporta CASCADE em consultas DELETE. postgresql.org/docs/8.4/static/dml-delete.html
Fredrik Wendt
Alguma idéia de por que isso não é suportado na consulta de exclusão?
Teifion 29/07
2
não há como "excluir com cascata" em uma tabela que não foi configurada adequadamente, ou seja, para a qual a restrição de chave estrangeira não foi definida como ON DELETE CASCADE, que era originalmente a questão.
Lensovet
Como resposta a esta pergunta, isso está completamente errado. Não há como CASCADE uma vez.
26418 Jeremy