CASCADE DELETE solo una volta


200

Ho un database Postgresql su cui voglio fare alcune eliminazioni a cascata. Tuttavia, le tabelle non sono impostate con la regola ON DELETE CASCADE. Esiste un modo per eseguire una cancellazione e dire a Postgresql di collegarla in cascata solo una volta? Qualcosa di equivalente a

DELETE FROM some_table CASCADE;

Le risposte a questa domanda più vecchia fanno sembrare che non esista tale soluzione, ma ho pensato che avrei posto questa domanda esplicitamente solo per essere sicuro.


Si prega di vedere la mia funzione personalizzata di seguito. È possibile con alcune restrizioni.
Joe Love,

Risposte:


175

No. Per farlo solo una volta dovresti semplicemente scrivere l'istruzione delete per la tabella che vuoi mettere in cascata.

DELETE FROM some_child_table WHERE some_fk_field IN (SELECT some_id FROM some_Table);
DELETE FROM some_table;

12
Questo non funziona necessariamente in quanto potrebbero esserci altre chiavi esterne a cascata dalla cascata originale (ricorsione). Puoi persino entrare in un ciclo in cui la tabella a si riferisce a b che si riferisce a. Per raggiungere questo obiettivo in senso generale, vedere la mia tabella di seguito, ma presenta alcune restrizioni. Se hai una semplice configurazione della tabella, prova il codice sopra, è più facile capire cosa stai facendo.
Joe Love,

2
Semplice, sicuro Dovresti eseguirli in una singola transazione se hai inserimenti di densità.
Invia Yavuz

39

Se vuoi davvero DELETE FROM some_table CASCADE; che significa " rimuovi tutte le righe dalla tabellasome_table ", puoi usare al TRUNCATEposto di DELETEed CASCADEè sempre supportato. Tuttavia, se si desidera utilizzare l'eliminazione selettiva con una whereclausola, TRUNCATEnon è abbastanza buono.

UTILIZZARE CON CURA - Questo eliminerà tutte le righe di tutte le tabelle che hanno un vincolo di chiave esterna su some_tablee tutte le tabelle che hanno vincoli su quelle tabelle, ecc.

Postgres supporta CASCADEcon il comando TRUNCATE :

TRUNCATE some_table CASCADE;

Praticamente questo è transazionale (cioè può essere eseguito il rollback), anche se non è completamente isolato da altre transazioni simultanee e ha molti altri avvertimenti. Leggi i documenti per i dettagli.


226
chiaramente "alcune eliminazioni a cascata" ≠ eliminando tutti i dati dalla tabella ...
lensovet

33
Questo eliminerà tutte le righe di tutte le tabelle che hanno un vincolo di chiave esterna su some_table e tutte le tabelle che hanno vincoli su quelle tabelle, ecc ... questo è potenzialmente molto pericoloso.
AJP,

56
state attenti. questa è una risposta spericolata.
Jordan Arseno,

4
Qualcuno ha segnalato questa risposta per la cancellazione , presumibilmente perché non sono d'accordo con essa. In questo caso, il corretto modo di agire è di votare, non di contrassegnare.
Wai Ha Lee,

7
Ha l'avvertimento in cima. Se scegli di ignorarlo, nessuno può aiutarti. Penso che i tuoi utenti di "copyPaste" siano il vero pericolo qui.
BluE

28

Ho scritto una funzione (ricorsiva) per eliminare qualsiasi riga in base alla sua chiave primaria. Ho scritto questo perché non volevo creare i miei vincoli come "on delete cascade". Volevo essere in grado di eliminare complessi set di dati (come un DBA) ma non consentire ai miei programmatori di essere in grado di eliminare a cascata senza pensare a tutte le ripercussioni. Sto ancora testando questa funzione, quindi potrebbero esserci dei bug, ma per favore non provarlo se il tuo DB ha chiavi primarie a più colonne (e quindi esterne). Inoltre, tutte le chiavi devono essere in grado di essere rappresentate in forma di stringa, ma potrebbero essere scritte in un modo che non ha questa limitazione. Uso comunque questa funzione MOLTO RISPARMIO, apprezzo troppo i miei dati per abilitare i vincoli a cascata su tutto. Fondamentalmente questa funzione viene passata nello schema, nel nome della tabella e nel valore primario (in forma di stringa), e inizierà trovando tutte le chiavi esterne su quella tabella e assicurandosi che i dati non esistano - in caso affermativo, si chiama ricorsivamente sui dati trovati. Utilizza una matrice di dati già contrassegnati per l'eliminazione per evitare loop infiniti. Per favore, provalo e fammi sapere come funziona per te. Nota: è un po 'lento. Lo chiamo così: 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;

Succede sempre, specialmente con le tabelle autoreferenziali. Prendi in considerazione una società con diversi livelli di gestione in diversi dipartimenti o una tassonomia gerarchica generica. Sì, sono d'accordo sul fatto che questa funzione non è la cosa migliore in assoluto dopo il pane a fette, ma è uno strumento utile nella giusta situazione.
Joe Love

Se lo riscrivi accetti un array di ID e generi anche query che useranno l' INoperatore con sottoselezioni invece di =(quindi passo per usare la logica degli insiemi) diventerebbe molto più veloce.
Hubbitus,

2
Grazie per la tua soluzione Scrivo alcuni test e avevo bisogno di cancellare un record e stavo avendo problemi a cascata quell'eliminazione. La tua funzione ha funzionato davvero bene!
Fernando Camargo,

1
@JoeLove che problema hai con la velocità? In quella situazione, la ricorsione è l'unica soluzione corretta nella mia mente.
Hubbitus,

1
@arthur potresti probabilmente usare una versione di row -> json -> text per farlo, tuttavia, non sono andato così lontano. Ho scoperto negli anni che una chiave primaria singolare (con potenziali chiavi secondarie) è buona per molte ragioni.
Joe Love,

17

Se ho capito bene, dovresti essere in grado di fare ciò che vuoi eliminando il vincolo di chiave esterna, aggiungendone uno nuovo (che verrà messo in cascata), facendo le tue cose e ricreando il vincolo di restrizione di chiave esterna.

Per esempio:

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

Naturalmente, dovresti astrarre cose del genere in una procedura, per il bene della tua salute mentale.


4
Partendo dal presupposto che la chiave esterna potrebbe impedire di fare cose che rendono incoerente il database, questo non è il modo di gestirlo. Ora puoi cancellare la voce "cattiva" ma stai lasciando molti frammenti di zombi che potrebbero causare problemi in futuro
Sprinterfreak,

1
Quali frammenti intendi esattamente? i record verranno eliminati in cascata, non dovrebbero esserci incoerenze.
Pedro Borges,

1
piuttosto che essere preoccupato per i "frammenti cattivi" (i vincoli a cascata saranno ancora coerenti), sarei PIÙ preoccupato che il collegamento a cascata non vada abbastanza lontano-- se i record eliminati richiedono ulteriori record eliminati, allora quei vincoli dovranno essere modificati per garantire anche il collegamento a cascata. (o utilizzare la funzione che ho scritto sopra per evitare questo scenario) ... Un'ultima raccomandazione in ogni caso: UTILIZZARE UNA TRANSAZIONE in modo da poterla ripristinare se non funziona correttamente.
Joe Love,

7

Non posso commentare la risposta di Palehorse, quindi ho aggiunto la mia risposta. La logica di Palehorse è ok ma l'efficienza può essere negativa con i set di big data.

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;

È più veloce se hai indici su colonne e il set di dati è più grande di pochi record.


7

Sì, come altri hanno già detto, non c'è conveniente 'ELIMINA DA my_table ... CASCADE' (o equivalente). Per eliminare i record figlio protetti da chiave esterna non a cascata e i loro antenati referenziati, le opzioni includono:

  • Esegui tutte le eliminazioni in modo esplicito, una query alla volta, iniziando con tabelle figlio (anche se questo non volerà se hai riferimenti circolari); o
  • Esegue esplicitamente tutte le eliminazioni in una singola query (potenzialmente massiccia); o
  • Supponendo che i vincoli della chiave esterna non in cascata siano stati creati come "ON DELETE NO ACTION DEFERRABLE", eseguire tutte le eliminazioni in modo esplicito in un'unica transazione; o
  • Elimina temporaneamente i "nessun'azione" e "limita" i vincoli di chiave esterna nel grafico, ricrearli come CASCADE, eliminare gli antenati, rilasciare nuovamente i vincoli di chiave esterna e infine ricrearli come erano in origine (indebolendo così temporaneamente l'integrità di i tuoi dati); o
  • Qualcosa probabilmente altrettanto divertente.

È appunto che aggirare i vincoli di chiave esterna non sia reso conveniente, presumo; ma capisco perché in determinate circostanze vorresti farlo. Se è qualcosa che farai con una certa frequenza e se sei disposto a smentire la saggezza dei DBA ovunque, potresti voler automatizzare con una procedura.

Sono venuto qui qualche mese fa in cerca di una risposta alla domanda "ELIMINA CASCATA una sola volta" (originariamente posta oltre un decennio fa!). Ho ottenuto un po 'di chilometraggio dalla soluzione intelligente di Joe Love (e dalla variante di Thomas CG de Vilhena), ma alla fine il mio caso d'uso aveva requisiti particolari (gestione dei riferimenti circolari all'interno della tabella, per esempio) che mi hanno costretto ad adottare un approccio diverso. Questo approccio alla fine divenne recursively_delete (PG 10.10).

Sto usando recursively_delete in produzione per un po ', ora, e finalmente mi sento (con cautela) abbastanza sicuro da renderlo disponibile ad altri che potrebbero finire qui in cerca di idee. Come con la soluzione di Joe Love, ti consente di eliminare interi grafici di dati come se tutti i vincoli di chiave esterna nel tuo database fossero momentaneamente impostati su CASCADE, ma offre un paio di funzionalità aggiuntive:

  • Fornisce un'anteprima ASCII del target di eliminazione e il relativo grafico delle persone a carico.
  • Esegue la cancellazione in una singola query utilizzando CTE ricorsivi.
  • Gestisce dipendenze circolari, interne e inter-tabella.
  • Gestisce chiavi composite.
  • Salta i vincoli 'set default' e 'set null'.

Ricevo un errore: ERRORE: l'array deve avere un numero pari di elementi Dove: funzione PL / pgSQL _recursively_delete (regclass, text [], intero, jsonb, intero, text [], jsonb, jsonb) riga 15 all'istruzione SQL dell'assegnazione "SELECT * FROM _recursively_delete (ARG_table, VAR_pk_col_names)" Funzione PL / pgSQL recursively_delete (regclass, anyelement, boolean) riga 73 all'istruzione SQL
Joe Love

Ehi, @JoeLove. Grazie per averci provato. Puoi darmi dei passaggi per riprodurlo? E qual è la tua versione di PG?
TRL

Non sono sicuro che questo aiuterà. ma ho appena creato le tue funzioni e quindi ho eseguito il seguente codice: select recursively_delete ('dallas.vendor', 1094, false) Dopo un po 'di debug, trovo che questo muoia subito dopo la mazza, il che significa che sembra che sia la prima chiamata alla funzione, non dopo aver fatto più cose. Per riferimento sto eseguendo PG 10.8
Joe Love

@JoeLove, prova cortesemente il ramo trl-fix-array_must_have_even_number_of_element ( github.com/trlorenz/PG-recursively_delete/pull/2 ).
TRL

Ho provato quel ramo e ha risolto l'errore originale. Purtroppo, non è più veloce della mia versione originale (che potrebbe non essere stato il tuo punto nello scrivere questo in primo luogo). Sto lavorando a un altro tentativo che crea duplicati di chiavi esterne con "on delete cascade", quindi eliminando il record originale, quindi rilasciando tutte le chiavi esterne appena create,
Joe Love

3

È possibile utilizzare per automatizzare questo, è possibile definire il vincolo di chiave esterna con ON DELETE CASCADE.
Cito il manuale dei vincoli di chiave esterna :

CASCADE specifica che quando una riga a cui viene fatto riferimento viene eliminata, anche le righe a cui fa riferimento devono essere eliminate automaticamente.


1
Sebbene ciò non riguardi l'OP, è una buona pianificazione per quando è necessario eliminare le righe con chiavi esterne. Come ha detto Ben Franklin, "un'oncia di prevenzione vale una libbra di cura".
Jesuisme,

1
Ho scoperto che questa soluzione può essere piuttosto pericolosa se la tua app elimina un record con molti fratelli e invece di un errore minore, hai eliminato definitivamente un enorme set di dati.
Joe Love,

2

Ho preso la risposta di Joe Love e l'ho riscritta usando l' INoperatore con =sottoselezioni invece di rendere più veloce la funzione (secondo il suggerimento di 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;

2
Dovrò guardare a questo e vedere come funziona bene con vincoli autoreferenziali e simili. Ho tentato di fare qualcosa di simile ma ho smesso di farlo funzionare completamente. Se la tua soluzione funziona per me, la implementerò. Questo è uno dei tanti strumenti dba che dovrebbero essere impacchettati e messi su github o qualcosa del genere.
Joe Love,

Ho database di medie dimensioni per un CMS multi-tenant (i client condividono tutti le stesse tabelle). La mia versione (senza "in") sembra funzionare abbastanza lentamente per eliminare tutte le tracce di un vecchio client ... Sono interessato a provarlo con alcuni dati di mockup per confrontare le velocità. Hai avuto qualcosa che potresti dire sulla differenza di velocità che hai notato nei tuoi casi d'uso?
Joe Love,

Nel mio caso d'uso ho notato una velocità dell'ordine di 10 volte quando si utilizzavano l' inoperatore e le query secondarie.
Thomas CG de Vilhena,

1

L'eliminazione con l'opzione a cascata si applicava solo alle tabelle con le chiavi esterne definite. Se si esegue un'eliminazione e si dice che non è possibile perché violerebbe il vincolo di chiave esterna, la cascata causerà l'eliminazione delle righe offensive.

Se si desidera eliminare le righe associate in questo modo, sarà necessario prima definire le chiavi esterne. Inoltre, ricorda che, a meno che non gli venga esplicitamente richiesto di iniziare una transazione o di modificare le impostazioni predefinite, verrà eseguito un commit automatico, che potrebbe richiedere molto tempo per ripulire.


2
La risposta di Grant è in parte errata: Postgresql non supporta CASCADE sulle query DELETE. postgresql.org/docs/8.4/static/dml-delete.html
Fredrik Wendt

Qualche idea sul perché non sia supportato nella query di eliminazione?
Teifion,

2
non c'è modo di "cancellare con cascata" su una tabella che non è stata impostata di conseguenza, vale a dire per la quale il vincolo di chiave esterna non è stato definito come ON DELETE CASCADE, che è la questione originaria della questione.
Lensovet,

Come risposta a questa domanda, questo è completamente sbagliato. Non c'è modo di CASCADE una volta.
Jeremy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.