级联删除一次


199

我有一个Postgresql数据库,我想在该数据库上进行一些级联删除。但是,未使用ON DELETE CASCADE规则设置表。有什么办法可以执行一次删除并告诉Postgresql仅一次将此级联吗?相当于

DELETE FROM some_table CASCADE;

这个老问题的答案似乎似乎不存在这样的解决方案,但我想我想明确地问这个问题只是为了确定。


请在下面查看我的自定义功能。有一定的限制是可能的。
Joe Love

Answers:


175

只需一次,您只需为要级联的表编写delete语句即可。

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

12
这不一定有效,因为原始级联(递归)中可能还有其他外键级联。您甚至可以进入一个循环,其中表a引用b,后者引用a。一般而言,要实现此目的,请参见下面的表格,但它有一些限制。如果您有一个简单的表设置,请尝试上面的代码,这会更容易理解您正在做的事情。
Joe Love

2
简单,安全。如果有密度插入,则应在单个事务中运行它们。
伊斯梅尔Yavuz的

40

如果您确实要 DELETE FROM some_table CASCADE;表示“ 从表中删除所有行some_table ”,则可以使用TRUNCATE代替,DELETE并且CASCADE始终受支持。但是,如果要对where子句使用选择性删除,TRUNCATE则不够好。

与护理一起使用 -这将删除所有具有外键约束的表的所​​有行some_table以及对该表具有约束的所有表,等等。

Postgres的支持CASCADETRUNCATE命令

TRUNCATE some_table CASCADE;

方便地,这是事务性的(即可以回滚),尽管它与其他并发事务没有完全隔离,并且还有其他一些警告。阅读文档以获取详细信息。


226
显然是“一些级联删除”≠从表中删除所有数据…
lensovet 2012年

33
这将删除在some_table上具有外键约束的所有表的所有行,以及在那些表上具有约束的所有表,等等...这可能非常危险。
AJP

56
谨防。这是鲁re的回答。
乔丹·阿塞诺

4
有人将这个答案标记为删除 -大概是因为他们不同意该答案。在这种情况下,正确的做法是降票而不是标记。
Wai Ha Lee

7
他上面有警告。如果您选择忽略这一点,那么没有人可以帮助您。我认为您的“ copyPaste”用户是这里的真正危险。
BluE

28

我编写了一个(递归)函数,以根据其主键删除任何行。我写这篇文章是因为我不想将约束创建为“在删除级联上”。我希望能够删除复杂的数据集(作为DBA),但不允许我的程序员能够级联删除而不考虑所有影响。我仍在测试此功能,因此其中可能存在错误-但如果您的数据库具有多列主键(因此具有外键),请不要尝试。同样,所有键都必须能够以字符串形式表示,但是可以用没有这种限制的方式来编写。无论如何,我非常谨慎地使用此功能,我太重视数据了,无法对所有内容启用级联约束。基本上,此函数是通过模式,表名和主值(以字符串形式)传递的,并从查找该表上的任何外键开始,并确保数据不存在-如果存在,则对找到的数据进行递归调用。它使用已标记为删除的数据数组来防止无限循环。请测试一下,让我知道它如何为您工作。注意:有点慢。我这样称呼它: 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

如果你重写它接受ID的阵列,也产生将使用查询IN与子选择,而不是运营商=(所以步骤使用了一组逻辑),将成为快。
哈比图斯'17

2
感谢您的解决方案。我编写了一些测试,我需要删除一条记录,但无法级联该删除。您的功能运行得非常好!
费尔南多·卡玛戈

1
@JoeLove您遇到什么速度问题?在那种情况下,递归是我心中唯一正确的解决方案。
哈比图斯'18

1
@arthur,您可能可以使用某些版本的row-> json-> text来完成它,但是,我还没走那么远。多年来,Ive发现,出于多种原因,单一的主键(具有潜在的辅助键)是好的。
Joe Love

17

如果我理解正确,那么您应该能够通过删除外键约束,添加新的(将层叠的),完成您的工作并重新创建限制性外键约束来做您想要的事情。

例如:

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

当然,为了您的心理健康,您应该将类​​似的内容抽象到一个过程中。


4
假定外键应该防止服务器工作使数据库不一致,那么这不是处理的方法。您现在可以删除“讨厌的”条目,但是您留下的僵尸碎片可能会在将来引起问题
Sprinterfreak 2015年

1
您是什么意思的碎片?记录将通过级联删除,应该没有不一致的地方。
Pedro Borges

1
与其担心“讨厌的碎片”(级联约束仍将是一致的),不如担心级联不够远-如果删除的记录需要进一步删除的记录,那么这些约束将需要更改以确保级联。(或使用我在上面编写的函数来避免这种情况)...在任何情况下,最后一个建议是:使用事务,以便在出现问题时可以将其回滚。
乔·爱

7

我无法评论Palehorse的答案,因此我添加了自己的答案。Palehorse的逻辑还可以,但是大数据集的效率可能很差。

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;

如果在列上具有索引并且数据集大于少数记录,则速度会更快。


7

是的,正如其他人所说,没有方便的“从my_table ... CASCADE删除”(或等效功能)。要删除非级联的受外键保护的子记录及其引用的祖先,您可以选择以下选项:

  • 从子表开始,显式地执行所有删除操作,一次一次查询(尽管如果有循环引用,这将无法执行);要么
  • 在单个(可能是大量的)查询中明确执行所有删除操作;要么
  • 假设您的非级联外键约束创建为“ ON DELETE NO ACTION DEFERRABLE”,则在单个事务中显式执行所有删除操作;要么
  • 暂时删除图中的“无操作”和“限制”外键约束,将它们重新创建为CASCADE,删除有问题的祖先,再次删除外键约束,最后重新创建它们的原始位置(因此暂时削弱了完整性)您的数据);要么
  • 可能同样有趣。

我认为绕开外键约束的目的不是很方便。但我确实了解您为什么要在特定情况下这样做。如果您会以某种频率执行某项操作,并且愿意在所有地方炫耀DBA的智慧,那么您可能希望使用一个过程使其自动化。

几个月前,我来这里寻求“仅一次CASCADE DELETE”问题的答案(最初是在十年前提出的!)。我从Joe Love的聪明解决方案(以及Thomas CG de Vilhena的变体)中获得了一些收获,但是最终,我的用例有特殊的要求(对于一个表内循环引用的处理),迫使我不得不采用另一种方法。该方法最终成为递归删除(PG 10.10)。

现在,我已经在生产中使用了recursively_delete一段时间了,终于(谨慎地)有足够的信心将其提供给其他可能会在这里寻找想法的人。与Joe Love的解决方案一样,它允许您删除整个数据图,就好像数据库中的所有外键约束都被暂时设置为CASCADE一样,但是它还提供了一些附加功能:

  • 提供删除目标及其依存关系图的ASCII预览。
  • 使用递归CTE在单个查询中执行删除。
  • 处理表内和表间的循环依赖关系。
  • 处理复合键。
  • 跳过“设置默认”和“设置为空”约束。

我收到一个错误:错误:数组必须具有偶数个元素其中:PL / pgSQL函数_recursively_delete(regclass,text [],integer,jsonb,integer,text [],jsonb,jsonb)在赋值SQL语句的第15行“ SELECT * FROM _recursively_delete(ARG_table,VAR_pk_col_names)” PL / pgSQL函数recursively_delete(regclass,anyelement,boolean)第73行,SQL语句
Joe Love

嘿,@ JoeLove。感谢您尝试。你能给我重现步骤吗?您的PG版本是什么?
TRL

我不确定这会有所帮助。但是我只是创建了您的函数,然后运行了以下代码:select recursively_delete('dallas.vendor',1094,false)经过一些调试后,我发现这很快就消失了-意思是,这似乎是第一次调用功能,而不是在做多件事之后。供参考,我正在运行PG 10.8
Joe Love

@JoeLove,请尝试使用分支trl-fix-array_must_have_even_number_of_element(github.com/trlorenz/PG-recursively_delete/pull/2)。
TRL

尝试了该分支,它确实修复了原始错误。可悲的是,它并没有比我的原始版本快(最初可能不是您写这篇文章的意思)。我正在尝试另一种尝试,即使用“在删除级联上”创建重复的外键,然后删除原始记录,然后删除所有新创建的外键,
Joe Love

3

您可以用来自动执行此操作,也可以使用定义外键约束ON DELETE CASCADE
我引用外键约束手册

CASCADE 指定删除引用的行时,引用该行的行也应自动删除。


1
尽管这不能解决操作问题,但它是计划何时删除具有外键的行的良好计划。正如本富兰克林所说,“一分预防胜过一磅治疗。”
Jesuisme

1
我发现,如果您的应用删除具有很多同级记录的记录,并且永久删除了一个巨大的数据集,而没有一个小错误,那么此解决方案将非常危险。
Joe Love

2

我接受了Joe Love的回答,并使用IN带有子选择的运算符重写了它,而不是=使函数更快(根据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
我将不得不研究此问题,并查看它在使用自引用约束等条件下的效果如何。我尝试做类似的事情,但没有使其完全起作用。如果您的解决方案对我有用,我将实施它。这是应该打包并放在github之类的许多dba工具之一。
Joe Love

我有一个用于多租户CMS的中型数据库(客户端均共享相同的表)。我的版本(不带“ in”)似乎在删除旧客户端的所有痕迹时运行得相当慢...我有兴趣尝试使用一些样机数据来比较速度。关于用例中发现的速度差异,您有什么想说的吗?
Joe Love

对于我的用例,当我使用in运算符和子查询时,我注意到速度提高了10倍。
Thomas CG de Vilhena,

1

带级联选项的删除仅适用于已定义外键的表。如果您执行删除操作,但说不能,因为它会违反外键约束,则级联将导致它删除有问题的行。

如果要以这种方式删除关联的行,则需要首先定义外键。另外,请记住,除非您明确指示它开始事务,或者更改默认值,否则它将执行自动提交,这可能非常耗时。


2
Grant的答案部分是错误的-Postgresql在DELETE查询中不支持CASCADE。postgresql.org/docs/8.4/static/dml-delete.html
Fredrik Wendt,2009年

知道为什么删除查询不支持它吗?
Teifion 2010年

2
没有办法在尚未进行相应设置的表上“以级联删除”,即,对于该表,外键约束尚未定义为ON DELETE CASCADE,这就是问题的初衷。
lensovet

为了回答这个问题,这是完全错误的。无法进行一次CASCADE。
杰里米(Jeremy)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.