BORRAR EN CASCADA solo una vez

200

Tengo una base de datos Postgresql en la que quiero hacer algunas eliminaciones en cascada. Sin embargo, las tablas no están configuradas con la regla ON DELETE CASCADE. ¿Hay alguna manera de que pueda eliminar y decirle a Postgresql que lo conecte en cascada solo por esta vez? Algo equivalente a

DELETE FROM some_table CASCADE;

Las respuestas a esta pregunta anterior hacen que parezca que no existe tal solución, pero pensé que haría esta pregunta explícitamente solo para estar seguro.

Eli Courtwright
fuente
Por favor vea mi función personalizada a continuación. Es posible con ciertas restricciones.
Joe Love

Respuestas:

175

No. Para hacerlo solo una vez, simplemente escribiría la declaración de eliminación para la tabla que desea en cascada.

DELETE FROM some_child_table WHERE some_fk_field IN (SELECT some_id FROM some_Table);
DELETE FROM some_table;
caballo pálido
fuente
12
Esto no necesariamente funciona, ya que podría haber otras claves externas en cascada desde la cascada original (recursividad). Incluso puede entrar en un bucle donde la tabla a se refiere a b que se refiere a a. Para lograr esto en un sentido general, vea mi tabla a continuación, pero tiene algunas restricciones. Si tiene una configuración de tabla simple, pruebe el código anterior, es más fácil comprender lo que está haciendo.
Joe Love
2
Simple, seguro Debe ejecutarlos en una sola transacción si tiene inserciones de densidad.
Ismail Yavuz
39

Si realmente quiere lo DELETE FROM some_table CASCADE; que significa " eliminar todas las filas de la tablasome_table ", puede usar en TRUNCATElugar de DELETEy CASCADEsiempre es compatible. Sin embargo, si desea utilizar la eliminación selectiva con una wherecláusula, TRUNCATEno es suficiente.

USO CON CUIDADO : esto eliminará todas las filas de todas las tablas que tienen una restricción de clave externa some_tabley todas las tablas que tienen restricciones en esas tablas, etc.

Postgres es compatible CASCADEcon el comando TRUNCATE :

TRUNCATE some_table CASCADE;

Prácticamente esto es transaccional (es decir, puede revertirse), aunque no está completamente aislado de otras transacciones concurrentes y tiene varias otras advertencias. Lea los documentos para más detalles.

DanC
fuente
226
claramente "algunas eliminaciones en cascada" ≠ soltando todos los datos de la tabla ...
lensovet
33
Esto eliminará todas las filas de todas las tablas que tienen una restricción de clave externa en some_table y todas las tablas que tienen restricciones en esas tablas, etc. Esto es potencialmente muy peligroso.
AJP
56
tener cuidado. Esta es una respuesta imprudente.
Jordan Arseno
44
Alguien ha marcado esta respuesta para eliminarla , presumiblemente porque no está de acuerdo con ella. El curso de acción correcto en ese caso es hacer un voto negativo, no marcar.
Wai Ha Lee
77
Él tiene la advertencia en la parte superior. Si eliges ignorar eso, nadie puede ayudarte. Creo que sus usuarios "copyPaste" son el verdadero peligro aquí.
BluE
28

Escribí una función (recursiva) para eliminar cualquier fila en función de su clave principal. Escribí esto porque no quería crear mis restricciones como "en cascada de eliminación". Quería poder eliminar conjuntos complejos de datos (como un DBA) pero no permitir que mis programadores puedan eliminar en cascada sin pensar en todas las repercusiones. Todavía estoy probando esta función, por lo que puede haber errores, pero no lo intente si su base de datos tiene claves principales de varias columnas (y, por lo tanto, externas). Además, todas las claves deben poder representarse en forma de cadena, pero podrían escribirse de una manera que no tenga esa restricción. Utilizo esta función MUY ESPACIOSAMENTE de todos modos, valoro mis datos demasiado para permitir las restricciones en cascada en todo. Básicamente, esta función se pasa en el esquema, el nombre de la tabla y el valor primario (en forma de cadena), y comenzará por encontrar cualquier clave externa en esa tabla y se asegurará de que los datos no existan; si es así, se llama recursivamente a los datos encontrados. Utiliza una matriz de datos ya marcados para su eliminación para evitar bucles infinitos. Por favor, pruébelo y dígame cómo funciona para usted. Nota: es un poco lento. Yo lo llamo así: 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
fuente
Sucede todo el tiempo, especialmente con las tablas de autorreferencia. Considere una empresa con diferentes niveles de gestión en diferentes departamentos, o una taxonomía jerárquica genérica. Sí, estoy de acuerdo en que esta función no es lo mejor desde el pan rebanado, pero es una herramienta útil en la situación correcta.
Joe Love
Si lo reescribe, acepta una matriz de ID y también genera consultas que usarán el INoperador con sub-selecciones en lugar de =(por lo tanto, el paso para usar la lógica de conjuntos) sería mucho más rápido.
Hubbitus
2
Gracias por tu solución. Escribí algunas pruebas y necesitaba eliminar un registro y estaba teniendo problemas para poner en cascada esa eliminación. ¡Tu función funcionó muy bien!
Fernando Camargo
1
@JoeLove, ¿qué problema de velocidad tienes? En esa situación, la recursión es la única solución correcta en mi mente.
Hubbitus
1
@arthur probablemente podría usar alguna versión de fila -> json -> texto para hacerlo, sin embargo, no he ido tan lejos. A través de los años he descubierto que una clave primaria singular (con claves secundarias potenciales) es buena por muchas razones.
Joe Love
17

Si lo entiendo correctamente, debería ser capaz de hacer lo que quiera al soltar la restricción de clave externa, agregar una nueva (que en cascada), hacer sus cosas y recrear la restricción de clave externa restrictiva.

Por ejemplo:

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

Por supuesto, debe resumir cosas como esas en un procedimiento, por el bien de su salud mental.

Ryszard Szopa
fuente
44
En el supuesto de que la clave foránea evite hacer cosas que hacen que la base de datos sea inconsistente, esta no es la forma de tratar. Puede eliminar la entrada "desagradable" ahora, pero está dejando muchos fragmentos de zombis que podrían causar problemas en el futuro
Sprinterfreak
1
¿A qué fragmentos te refieres exactamente? los registros se eliminarán en cascada, no debería haber inconsistencias.
Pedro Borges
1
en lugar de preocuparme por los "fragmentos desagradables" (las restricciones en cascada seguirán siendo consistentes), estaría MÁS preocupado por que la conexión en cascada no llegue lo suficientemente lejos: si los registros eliminados requieren más registros eliminados, entonces esas restricciones deberán ser alteradas para garantizar también la conexión en cascada. (o use la función que escribí anteriormente para evitar este escenario) ... Una última recomendación en cualquier caso: USE UNA TRANSACCIÓN para que pueda deshacerla si sale mal.
Joe Love
7

No puedo comentar la respuesta de Palehorse, así que agregué mi propia respuesta. La lógica de Palehorse está bien, pero la eficiencia puede ser mala con grandes conjuntos de datos.

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;

Es más rápido si tiene índices en columnas y el conjunto de datos es mayor que unos pocos registros.

Grzegorz Grabek
fuente
7

Sí, como han dicho otros, no hay un conveniente 'DELETE FROM my_table ... CASCADE' (o equivalente). Para eliminar registros secundarios protegidos por clave externa no en cascada y sus antepasados ​​referenciados, sus opciones incluyen:

  • Realice todas las eliminaciones explícitamente, una consulta a la vez, comenzando con las tablas secundarias (aunque esto no funcionará si tiene referencias circulares); o
  • Realice todas las eliminaciones explícitamente en una sola consulta (potencialmente masiva); o
  • Suponiendo que sus restricciones de clave externa no en cascada se crearon como 'AL BORRAR SIN ACCIÓN DEFERRABLE', realice todas las eliminaciones explícitamente en una sola transacción; o
  • Elimine temporalmente las restricciones de clave externa 'no acción' y 'restrinja' en el gráfico, vuelva a crearlas como CASCADE, elimine los antepasados ​​ofensivos, elimine las restricciones de clave externa nuevamente y finalmente vuelva a crearlas como estaban originalmente (debilitando así temporalmente la integridad de tu información); o
  • Algo probablemente igualmente divertido.

Supongo que es deliberado eludir las restricciones de clave externa; pero entiendo por qué en circunstancias particulares querrías hacerlo. Si es algo que hará con cierta frecuencia, y si está dispuesto a ignorar la sabiduría de los DBA en todas partes, es posible que desee automatizarlo con un procedimiento.

Vine aquí hace unos meses en busca de una respuesta a la pregunta "BORRADO EN CASCADA solo una vez" (¡originalmente hecha hace más de una década!). Obtuve algo de la solución inteligente de Joe Love (y la variante de Thomas CG de Vilhena), pero al final mi caso de uso tenía requisitos particulares (manejo de referencias circulares intra-tabla, por ejemplo) que me obligaron a adoptar un enfoque diferente. Ese enfoque finalmente se convirtió en recursive_delete (PG 10.10).

He estado usando recursively_delete en producción durante un tiempo, ahora, y finalmente me siento (cautelosamente) lo suficientemente seguro como para ponerlo a disposición de otros que podrían terminar buscando ideas. Al igual que con la solución de Joe Love, le permite eliminar gráficos completos de datos como si todas las restricciones de clave externa en su base de datos estuvieran configuradas momentáneamente en CASCADA, pero ofrece un par de características adicionales:

  • Proporciona una vista previa ASCII del objetivo de eliminación y su gráfico de dependientes.
  • Realiza la eliminación en una sola consulta utilizando CTE recursivos.
  • Maneja dependencias circulares, intra e inter-tabla.
  • Maneja llaves compuestas.
  • Omite las restricciones 'set default' y 'set null'.
TRL
fuente
Recibo un error: ERROR: la matriz debe tener un número par de elementos Donde: función PL / pgSQL _recursively_delete (regclass, text [], integer, jsonb, integer, text [], jsonb, jsonb) línea 15 en la instrucción SQL de asignación "SELECT * FROM _recursively_delete (ARG_table, VAR_pk_col_names)" Función PL / pgSQL recursively_delete (regclass, anyelement, boolean) línea 73 en la declaración SQL
Joe Love
Hola, Joe Joe. Gracias por intentarlo. ¿Me puede dar pasos para reproducir? ¿Y cuál es tu versión de PG?
TRL
No estoy seguro de que esto ayude. pero acabo de crear sus funciones y luego ejecuté el siguiente código: select recursively_delete ('dallas.vendor', 1094, false) Después de algunas depuraciones, encuentro que esto muere de inmediato, lo que significa que parece que es la primera llamada a la función, no después de hacer varias cosas. Como referencia, estoy ejecutando PG 10.8
Joe Love
@JoeLove, intente bifurcar trl-fix-array_must_have_even_number_of_element ( github.com/trlorenz/PG-recursively_delete/pull/2 ).
TRL
Probé esa rama y solucionó el error original. Lamentablemente, no es más rápido que mi versión original (que puede no haber sido tu punto al escribir esto en primer lugar). Estoy trabajando en otro intento que crea claves externas duplicadas con "on delete cascade", luego elimino el registro original y luego elimino todas las claves externas recién creadas,
Joe Love
3

Puede utilizar para automatizar esto, puede definir la restricción de clave externa con ON DELETE CASCADE.
Cito el manual de restricciones de clave externa :

CASCADE especifica que cuando se elimina una fila referenciada, las filas que hacen referencia a ella también deberían eliminarse automáticamente.

atiruz
fuente
1
Aunque esto no aborda el OP, es una buena planificación para cuando las filas con claves externas deben eliminarse. Como dijo Ben Franklin, "una onza de prevención vale una libra de cura".
Jesuisme
1
Descubrí que esta solución puede ser bastante peligrosa si su aplicación elimina un registro con muchos hermanos y, en lugar de un error menor, ha eliminado permanentemente un gran conjunto de datos.
Joe Love
2

Tomé la respuesta de Joe Love y la reescribí usando el INoperador con sub-selecciones en lugar de =hacer la función más rápida (según la sugerencia 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
fuente
2
Voy a tener que ver esto y ver qué tan bien funciona con las restricciones de autorreferencia y similares. Intenté hacer algo similar, pero no pude hacerlo funcionar por completo. Si su solución funciona para mí, la voy a implementar. Esta es una de las muchas herramientas de dba que deben empaquetarse y colocarse en github o algo así.
Joe Love
Tengo bases de datos de tamaño mediano para un CMS multiinquilino (todos los clientes comparten las mismas tablas). Mi versión (sin el "in") parece correr bastante lento para eliminar todos los rastros de un cliente antiguo ... Estoy interesado en probar esto con algunos datos de maquetas para comparar velocidades. ¿Tenía algo que pudiera decir sobre la diferencia de velocidad que notó en su (s) caso (s) de uso?
Joe Love
Para mi caso de uso, noté una aceleración del orden de 10x al usar el inoperador y las subconsultas.
Thomas CG de Vilhena
1

La opción Eliminar con la cascada solo se aplica a las tablas con claves externas definidas. Si hace una eliminación, y dice que no puede porque violaría la restricción de clave externa, la cascada hará que elimine las filas ofensivas.

Si desea eliminar las filas asociadas de esta manera, primero deberá definir las claves externas. Además, recuerde que a menos que le indique explícitamente que comience una transacción, o que cambie los valores predeterminados, realizará una confirmación automática, lo que podría llevar mucho tiempo limpiar.

Grant Johnson
fuente
2
La respuesta de Grant es en parte incorrecta: Postgresql no admite CASCADE en las consultas DELETE. postgresql.org/docs/8.4/static/dml-delete.html
Fredrik Wendt
¿Alguna idea de por qué no es compatible con la consulta de eliminación?
Teifion
2
no hay forma de "eliminar con cascada" en una tabla que no se ha configurado en consecuencia, es decir, para la cual la restricción de clave externa no se ha definido como ON DELETE CASCADE, que es de lo que se trataba originalmente la pregunta.
lensovet
Como respuesta a esta pregunta, esto está completamente mal. No hay forma de hacer CASCADA una vez.
Jeremy