CASCADE DELETE une seule fois

200

J'ai une base de données Postgresql sur laquelle je veux faire quelques suppressions en cascade. Cependant, les tables ne sont pas configurées avec la règle ON DELETE CASCADE. Existe-t-il un moyen de supprimer un fichier et de demander à Postgresql de le mettre en cascade une seule fois? Quelque chose d'équivalent à

DELETE FROM some_table CASCADE;

Les réponses à cette question plus ancienne donnent l'impression qu'aucune telle solution n'existe, mais j'ai pensé que je poserais cette question explicitement juste pour être sûr.

Eli Courtwright
la source
Veuillez voir ma fonction personnalisée ci-dessous. C'est possible avec certaines restrictions.
Joe Love

Réponses:

175

Non. Pour le faire une seule fois, il vous suffit d'écrire l'instruction delete pour la table que vous souhaitez mettre en cascade.

DELETE FROM some_child_table WHERE some_fk_field IN (SELECT some_id FROM some_Table);
DELETE FROM some_table;
cheval pâle
la source
12
Cela ne fonctionne pas nécessairement car il pourrait y avoir d'autres clés étrangères en cascade à partir de la cascade d'origine (récursivité). Vous pouvez même entrer dans une boucle où la table a fait référence à b qui fait référence à a. Pour y parvenir dans un sens général, voir mon tableau ci-dessous, mais il a quelques restrictions. Si vous avez une configuration de table simple, essayez le code ci-dessus, il est plus facile de comprendre ce que vous faites.
Joe Love
2
Simple, sûr. Vous devez les exécuter en une seule transaction si vous avez des insertions de densité.
İsmail Yavuz
39

Si vous voulez vraiment DELETE FROM some_table CASCADE; ce qui signifie " supprimer toutes les lignes du tableausome_table ", vous pouvez utiliser à la TRUNCATEplace de DELETEet CASCADEest toujours pris en charge. Cependant, si vous souhaitez utiliser la suppression sélective avec une whereclause, ce TRUNCATEn'est pas suffisant.

UTILISER AVEC SOIN - Cela supprimera toutes les lignes de toutes les tables qui ont une contrainte de clé étrangère some_tableet toutes les tables qui ont des contraintes sur ces tables, etc.

Postgres prend CASCADEen charge avec la commande TRUNCATE :

TRUNCATE some_table CASCADE;

Il s'agit d'un processus transactionnel (c'est-à-dire qu'il peut être annulé), bien qu'il ne soit pas complètement isolé des autres transactions simultanées et comporte plusieurs autres mises en garde. Lisez les documents pour plus de détails.

DanC
la source
226
clairement "quelques suppressions en cascade" ≠ laissant tomber toutes les données de la table…
lensovet
33
Cela supprimera toutes les lignes de toutes les tables qui ont une contrainte de clé étrangère sur some_table et toutes les tables qui ont des contraintes sur ces tables, etc ... c'est potentiellement très dangereux.
AJP
56
il faut se méfier. c'est une réponse imprudente.
Jordan Arseno
4
Quelqu'un a signalé cette réponse pour suppression - probablement parce qu'il n'était pas d'accord. La ligne de conduite correcte dans ce cas est de voter contre, et non de signaler.
Wai Ha Lee
7
Il a l'avertissement en haut. Si vous choisissez d'ignorer cela, personne ne peut vous aider. Je pense que vos utilisateurs "copyPaste" sont le vrai danger ici.
BluE
28

J'ai écrit une fonction (récursive) pour supprimer n'importe quelle ligne en fonction de sa clé primaire. J'ai écrit ceci parce que je ne voulais pas créer mes contraintes comme "sur la cascade de suppression". Je voulais pouvoir supprimer des ensembles de données complexes (en tant que DBA) mais ne pas permettre à mes programmeurs de pouvoir supprimer en cascade sans réfléchir à toutes les répercussions. Je teste toujours cette fonction, donc il peut y avoir des bogues - mais s'il vous plaît ne l'essayez pas si votre base de données a des clés primaires (et donc étrangères) multi-colonnes. De plus, les clés doivent toutes pouvoir être représentées sous forme de chaîne, mais elles peuvent être écrites d'une manière qui n'a pas cette restriction. J'utilise cette fonction TRÈS PEU de toute façon, j'apprécie trop mes données pour permettre les contraintes en cascade sur tout. Fondamentalement, cette fonction est passée dans le schéma, le nom de la table et la valeur principale (sous forme de chaîne), et il commencera par trouver toutes les clés étrangères sur cette table et s'assurera que les données n'existent pas - si c'est le cas, il s'appellera récursivement sur les données trouvées. Il utilise un tableau de données déjà marquées pour suppression pour éviter les boucles infinies. Veuillez le tester et faites-moi savoir comment cela fonctionne pour vous. Remarque: c'est un peu lent. Je l'appelle comme ça: 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
la source
Cela arrive tout le temps, en particulier avec les tables auto-référencées. Considérez une entreprise avec différents niveaux de gestion dans différents départements, ou une taxonomie hiérarchique générique. Oui, je suis d'accord que cette fonction n'est pas la meilleure chose absolue depuis le pain tranché, mais c'est un outil utile dans la bonne situation.
Joe Love
Si vous le réécrivez, acceptez un tableau d'ID et générez également des requêtes qui utiliseront l' INopérateur avec des sous-sélections au lieu de =(donc utilisez la logique des ensembles), cela deviendrait beaucoup plus rapide.
Hubbitus
2
Merci pour votre solution. J'écris des tests et j'avais besoin de supprimer un enregistrement et j'avais du mal à cascader cette suppression. Votre fonction a très bien fonctionné!
Fernando Camargo
1
@JoeLove quel problème de vitesse avez-vous? Dans cette situation, la récursivité est la seule solution correcte dans mon esprit.
Hubbitus
1
@arthur, vous pourriez probablement utiliser une version de row -> json -> text pour le faire, cependant, je ne suis pas allé aussi loin. J'ai trouvé au fil des ans qu'une clé primaire singulière (avec des clés secondaires potentielles) est bonne pour de nombreuses raisons.
Joe Love
17

Si je comprends bien, vous devriez pouvoir faire ce que vous voulez en supprimant la contrainte de clé étrangère, en en ajoutant une nouvelle (qui se mettra en cascade), en faisant votre travail et en recréant la contrainte de clé étrangère restrictive.

Par exemple:

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

Bien sûr, vous devez résumer des trucs comme ça dans une procédure, pour votre santé mentale.

Ryszard Szopa
la source
4
Dans l'hypothèse où la clé étrangère devrait empêcher de faire des choses qui rendent la base de données incohérente, ce n'est pas la façon de traiter. Vous pouvez supprimer l'entrée "méchante" maintenant mais vous laissez beaucoup de fragments de zombies qui pourraient causer des problèmes à l'avenir
Sprinterfreak
1
Quels fragments voulez-vous dire exactement? les enregistrements seront supprimés via cascade, il ne devrait y avoir aucune incohérence.
Pedro Borges le
1
plutôt que d'être préoccupé par les "éclats désagréables" (les contraintes en cascade seront toujours cohérentes), je serais PLUS préoccupé par le fait que la cascade n'aille pas assez loin - si les enregistrements supprimés nécessitent d'autres enregistrements supprimés, alors ces contraintes devront être modifiées pour assurer également la mise en cascade. (ou utilisez la fonction que j'ai écrite ci-dessus pour éviter ce scénario) ... Une dernière recommandation dans tous les cas: UTILISEZ UNE TRANSACTION pour pouvoir la faire reculer en cas de problème.
Joe Love
7

Je ne peux pas commenter la réponse de Palehorse, j'ai donc ajouté ma propre réponse. La logique de Palehorse est correcte mais l'efficacité peut être mauvaise avec des ensembles de données volumineux.

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;

C'est plus rapide si vous avez des index sur les colonnes et que l'ensemble de données est plus grand que quelques enregistrements.

Grzegorz Grabek
la source
7

Oui, comme d'autres l'ont dit, il n'y a pas de 'DELETE FROM my_table ... CASCADE' (ou équivalent). Pour supprimer les enregistrements enfants protégés par une clé étrangère non en cascade et leurs ancêtres référencés, vos options incluent:

  • Effectuez toutes les suppressions de manière explicite, une requête à la fois, en commençant par les tables enfants (bien que cela ne vole pas si vous avez des références circulaires); ou
  • Effectuer toutes les suppressions explicitement dans une seule requête (potentiellement massive); ou
  • En supposant que vos contraintes de clé étrangère non en cascade ont été créées comme «ON DELETE NO ACTION DEFERRABLE», effectuez toutes les suppressions explicitement en une seule transaction; ou
  • Supprimez temporairement les contraintes de clé étrangère `` aucune action '' et `` restreindre '' dans le graphique, recréez-les en tant que CASCADE, supprimez les ancêtres incriminés, supprimez à nouveau les contraintes de clé étrangère et enfin recréez-les telles qu'elles étaient à l'origine (affaiblissant ainsi temporairement l'intégrité de vos données); ou
  • Quelque chose de probablement aussi amusant.

C'est à dessein que contourner les contraintes de clés étrangères n'est pas commode, je suppose; mais je comprends pourquoi, dans des circonstances particulières, vous voudriez le faire. Si c'est quelque chose que vous ferez avec une certaine fréquence, et si vous êtes prêt à bafouer la sagesse des DBA partout, vous voudrez peut-être l'automatiser avec une procédure.

Je suis venu ici il y a quelques mois à la recherche d'une réponse à la question "CASCADE DELETE juste une fois" (initialement posée il y a plus d'une décennie!). J'ai tiré un certain kilométrage de la solution intelligente de Joe Love (et de la variante de Thomas CG de Vilhena), mais à la fin mon cas d'utilisation avait des exigences particulières (gestion des références circulaires intra-table, par exemple) qui m'ont forcé à adopter une approche différente. Cette approche est finalement devenue récursivement supprimée (PG 10.10).

J'utilise recursively_delete en production depuis un certain temps, maintenant, et finalement je me sens (prudemment) suffisamment confiant pour le mettre à la disposition de ceux qui pourraient se retrouver ici à la recherche d'idées. Comme avec la solution de Joe Love, elle vous permet de supprimer des graphiques entiers de données comme si toutes les contraintes de clé étrangère dans votre base de données étaient momentanément définies sur CASCADE, mais offre quelques fonctionnalités supplémentaires:

  • Fournit un aperçu ASCII de la cible de suppression et son graphique des dépendants.
  • Effectue la suppression dans une seule requête à l'aide de CTE récursifs.
  • Gère les dépendances circulaires, intra et inter-table.
  • Gère les clés composites.
  • Ignore les contraintes 'set default' et 'set null'.
TRL
la source
Je reçois une erreur: ERREUR: le tableau doit avoir un nombre pair d'éléments Où: fonction PL / pgSQL _recursively_delete (regclass, text [], integer, jsonb, integer, text [], jsonb, jsonb) ligne 15 à l'affectation instruction SQL "SELECT * FROM _recursively_delete (ARG_table, VAR_pk_col_names)" Fonction PL / pgSQL recursively_delete (regclass, anyelement, boolean) ligne 73 à l'instruction SQL
Joe Love
Hé, @JoeLove. Merci de l'avoir essayé. Pouvez-vous me donner des étapes pour reproduire? Et quelle est votre version de PG?
TRL
Je ne suis pas sûr que cela vous aidera. mais je viens de créer vos fonctions, puis j'ai exécuté le code suivant: select recursively_delete ('dallas.vendor', 1094, false) Après un débogage, je trouve que cela s'éteint immédiatement - ce qui signifie qu'il semble que c'est le premier appel à la fonction, pas après avoir fait plusieurs choses. Pour référence, je lance PG 10.8
Joe Love
@JoeLove, veuillez essayer la branche trl-fix-array_must_have_even_number_of_element ( github.com/trlorenz/PG-recursively_delete/pull/2 ).
TRL
J'ai essayé cette branche et cela a corrigé l'erreur d'origine. Malheureusement, ce n'est pas plus rapide que ma version originale (ce qui n'était peut-être pas votre objectif en écrivant cela en premier lieu). Je travaille sur une autre tentative qui crée des clés étrangères en double avec "on delete cascade", puis en supprimant l'enregistrement d'origine, puis en supprimant toutes les clés étrangères nouvellement créées,
Joe Love
3

Vous pouvez utiliser pour automatiser cela, vous pouvez définir la contrainte de clé étrangère avec ON DELETE CASCADE.
Je cite le manuel des contraintes de clé étrangère :

CASCADE spécifie que lorsqu'une ligne référencée est supprimée, la ou les lignes qui la référencent doivent également être supprimées automatiquement.

atiruz
la source
1
Bien que cela ne traite pas de l'OP, il est bon de planifier le moment où les lignes avec des clés étrangères doivent être supprimées. Comme l'a dit Ben Franklin, "une once de prévention vaut mieux que guérir".
Jesuisme
1
J'ai trouvé que cette solution peut être assez dangereuse si votre application supprime un enregistrement avec beaucoup de frères et sœurs et au lieu d'une erreur mineure, vous avez définitivement supprimé un énorme ensemble de données.
Joe Love
2

J'ai pris la réponse de Joe Love et l'ai réécrite en utilisant l' INopérateur avec des sous-sélections au lieu de =rendre la fonction plus rapide (selon la suggestion 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
la source
2
Je vais devoir regarder cela et voir à quel point cela fonctionne avec les contraintes d'auto-référencement et autres. J'ai essayé de faire quelque chose de similaire, mais j'ai arrêté de le faire fonctionner pleinement. Si votre solution fonctionne pour moi, je vais la mettre en œuvre. C'est l'un des nombreux outils dba qui doivent être emballés et installés sur github ou quelque chose.
Joe Love
J'ai des bases de données de taille moyenne pour un CMS multi-locataire (les clients partagent tous les mêmes tables). Ma version (sans le "in") semble fonctionner assez lentement pour supprimer toutes les traces d'un ancien client ... Je suis intéressé à essayer cela avec quelques données de maquette pour comparer les vitesses. Avez-vous quelque chose à dire sur la différence de vitesse que vous avez remarquée dans vos cas d'utilisation?
Joe Love
Pour mon cas d'utilisation, j'ai remarqué une accélération de l'ordre de 10x lors de l'utilisation de l' inopérateur et des sous-requêtes.
Thomas CG de Vilhena
1

L'option de suppression avec cascade s'applique uniquement aux tables avec des clés étrangères définies. Si vous effectuez une suppression, et il est dit que vous ne pouvez pas, car cela violerait la contrainte de clé étrangère, la cascade entraînera la suppression des lignes incriminées.

Si vous souhaitez supprimer les lignes associées de cette manière, vous devrez d'abord définir les clés étrangères. N'oubliez pas non plus que, sauf si vous lui demandez explicitement de commencer une transaction ou si vous modifiez les valeurs par défaut, il effectuera une validation automatique, ce qui pourrait prendre beaucoup de temps à nettoyer.

Grant Johnson
la source
2
La réponse de Grant est partiellement fausse - Postgresql ne prend pas en charge CASCADE sur les requêtes DELETE. postgresql.org/docs/8.4/static/dml-delete.html
Fredrik Wendt
Une idée pourquoi elle n'est pas prise en charge sur la requête de suppression?
Teifion
2
il n'y a aucun moyen de "supprimer avec cascade" sur une table qui n'a pas été configurée en conséquence, c'est-à-dire pour laquelle la contrainte de clé étrangère n'a pas été définie comme ON DELETE CASCADE, ce qui était à l'origine la question.
lensovet
En réponse à cette question, c'est complètement faux. Il n'y a aucun moyen de CASCADE une fois.
Jeremy