【问题标题】:CASCADE DELETE just once级联删除一次
【发布时间】:2010-09-12 20:34:19
【问题描述】:

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

DELETE FROM some_table CASCADE;

this older question 的答案看起来似乎不存在这样的解决方案,但我想我会明确地问这个问题以确保。

【问题讨论】:

  • 请看下面我的自定义函数。有一定的限制是可能的。
  • 任何来到这里的人请注意,在大多数情况下,级联删除可能很危险。它是您的数据库,您知道参考文献请保留一个方便的顺序脚本传递 ID 并删除内容。最初有点工作,但除非您非常确定,否则不要在删除级联上添加插件

标签: postgresql


【解决方案1】:

没有。只需为要级联的表编写删除语句即可。

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

【讨论】:

  • 这不一定有效,因为可能有其他外键从原始级联(递归)级联。您甚至可以进入一个循环,其中表 a 引用 b,而 b 引用 a。要在一般意义上实现这一点,请参阅下表,但它有一些限制。如果你有一个简单的表格设置,那么试试上面的代码,更容易理解你在做什么。
  • 简单、安全。如果你有密度插入,你应该在一个事务中运行它们。
【解决方案2】:

如果你真的想要DELETE FROM some_table CASCADE;,意思是“从表some_table中删除所有行”,你可以使用TRUNCATE而不是DELETE和@始终支持 987654326@。但是,如果您想使用带有where 子句的选择性删除,TRUNCATE 还不够好。

谨慎使用 - 这将删除所有表的所有行,这些表对some_table 有外键约束,以及所有对这些表有约束的表,等等.

Postgres 支持CASCADETRUNCATE command

TRUNCATE some_table CASCADE;

这是事务性的(即可以回滚),尽管它没有与其他并发事务完全隔离,并且还有其他一些注意事项。阅读文档了解详情。

【讨论】:

  • 显然“一些级联删除”≠从表中删除所有数据……
  • 这将删除对 some_table 具有外键约束的所有表的所有行以及对这些表有约束的所有表,等等......这可能非常危险。
  • 我认为原来的问题是有问题的。它要求一种方法来执行一些级联删除,但随后又询问是否存在“相当于DELETE FROM some_table CASCADE; 的东西”。这个答案是关于后一个问题,但很少有人在现实中想做的事情。
  • 我从字面上搜索了“从 some_table CASCADE 删除”,因为这就是我想要做的,并找到了最准确的答案。从我这里投票。
  • 如果我有这个用例,我会找出需要截断的表的列表,并为每个表编写一个单独的 TRUNCATE。为了非常明确地了解所有正在进行的核武器。
【解决方案3】:

我编写了一个(递归)函数来根据其主键删除任何行。我写这个是因为我不想将我的约束创建为“删除级联”。我希望能够删除复杂的数据集(作为 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;

【讨论】:

  • 这种情况经常发生,尤其是在自引用表中。考虑一家在不同部门拥有不同管理层的公司,或者一个通用的分层分类法。是的,我同意这个功能不是切片面包之后绝对最好的东西,但它在适当的情况下是一个有用的工具。
  • 如果你重写它接受 ID 数组并生成查询,这些查询将使用带有子选择的 IN 运算符而不是 =(所以使用集合逻辑的步骤)它将变成 快得多
  • 感谢您的解决方案。我写了一些测试,我需要删除一条记录,我在级联删除时遇到了麻烦。你的函数运行得非常好!
  • @JoeLove 你有什么速度问题?在这种情况下,我认为递归是唯一正确的解决方案。
  • @arthur 您可能可以使用某些版本的 row -> json -> text 来完成它,但是,我还没有走那么远。多年来,我发现单数主键(带有潜在的辅助键)有很多好处。
【解决方案4】:

如果我理解正确,您应该能够通过删除外键约束、添加一个新约束(将级联)、执行您的操作并重新创建限制性外键约束来做您想做的事情。

例如:

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

当然,为了你的心理健康,你应该把类似的东西抽象成一个程序。

【讨论】:

  • 假设外键应该阻止做导致数据库不一致的事情,这不是处理的方法。您现在可以删除“讨厌的”条目,但您会留下很多僵尸碎片,将来可能会导致问题
  • 你到底指的是什么碎片?记录将通过级联删除,应该没有不一致。
  • 而不是关心“讨厌的碎片”(级联约束仍然是一致的),我更关心级联不够远——如果删除的记录需要进一步删除的记录,那么这些约束也需要改变以确保级联。 (或者使用我上面写的函数来避免这种情况)......无论如何最后一个建议:使用一个事务,这样你就可以在出错时回滚它。
【解决方案5】:

我无法评论 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;

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

【讨论】:

    【解决方案6】:

    是的,正如其他人所说,没有方便的'DELETE FROM my_table ... CASCADE'(或等效项)。要删除非级联外键保护的子记录及其引用的祖先,您的选项包括:

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

    我认为,绕过外键约束并不方便,这是故意的;但我确实理解为什么在特定情况下你想这样做。如果这是您经常做的事情,并且如果您愿意在任何地方蔑视 DBA 的智慧,那么您可能希望通过一个过程来自动化它。

    几个月前我来到这里寻找“CASCADE DELETE just once”问题的答案(最初是在十多年前提出的!)。我从 Joe Love 的巧妙解决方案(以及 Thomas C. G. de Vilhena 的变体)中获得了一些成果,但最终我的用例有特殊要求(例如处理表内循环引用),这迫使我采取了不同的方法。这种方法最终变成了recursively_delete (PG 10.10)。

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

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

    【讨论】:

    • 我收到一个错误:错误:数组必须有偶数个元素其中:PL/pgSQL 函数 _recursively_delete(regclass,text[],integer,jsonb,integer,text[],jsonb, jsonb) line 15 at assignment SQL statement "SELECT * FROM _recursively_delete(ARG_table, VAR_pk_col_names)" PL/pgSQL function recursively_delete(regclass,anyelement,boolean) line 73 at SQL statement
    • 嘿,@JoeLove。感谢您试用。你能给我重现的步骤吗?你的 PG 版本是什么?
    • 我不确定这会有所帮助。但是我刚刚创建了您的函数,然后运行了以下代码: select recursively_delete('dallas.vendor',1094,false) 经过一些调试后,我发现它马上就死了——意思是,这似乎是第一次调用到功能,而不是在做多件事之后。作为参考,我正在运行 PG 10.8
    • @JoeLove,请尝试分支 trl-fix-array_must_have_even_number_of_element (github.com/trlorenz/PG-recursively_delete/pull/2)。
    • 试过那个分支,它确实修复了原来的错误。可悲的是,它并不比我的原始版本快(这可能不是你写这篇文章的初衷)。我正在尝试使用“on delete cascade”创建重复的外键,然后删除原始记录,然后删除所有新创建的外键,
    【解决方案7】:

    您可以使用它来自动执行此操作,您可以使用 ON DELETE CASCADE 定义外键约束。
    我引用the manual of foreign key constraints

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

    【讨论】:

    • 虽然这并没有解决 OP,但是当需要删除具有外键的行时,它是一个很好的计划。正如本富兰克林所说,“一盎司的预防胜过一磅的治疗。”
    • 我发现,如果您的应用删除了包含许多同级的记录,并且您已经永久删除了一个巨大的数据集,而不是一个小错误,那么这个解决方案可能会非常危险。
    【解决方案8】:

    我接受了 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;
    

    【讨论】:

    • 我将不得不看看这个,看看它在自引用约束等方面的表现如何。我试图做类似的事情,但没有让它充分发挥作用。如果您的解决方案对我有用,我将实施它。这是许多 dba 工具之一,应该打包并放在 github 上。
    • 我有一个用于多租户 CMS 的中型数据库(客户端都共享相同的表)。我的版本(没有“in”)似乎运行相当慢,以删除旧客户端的所有痕迹......我有兴趣尝试使用一些模型数据来比较速度。对于您在用例中发现的速度差异,您有什么想说的吗?
    • 对于我的用例,我注意到使用 in 运算符和子查询时速度提高了大约 10 倍。
    【解决方案9】:

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

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

    【讨论】:

    • Grant 的回答部分错误 - Postgresql 不支持 DELETE 查询的 CASCADE。 postgresql.org/docs/8.4/static/dml-delete.html
    • 知道为什么删除查询不支持它吗?
    • 没有办法在没有相应设置的表上“使用级联删除”,即没有将外键约束定义为 ON DELETE CASCADE,这就是问题本来就是关于的。
    • 作为对this问题的回答,这是完全错误的。没有办法一次级联。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-24
    • 2011-06-29
    • 2010-10-05
    • 2011-09-21
    • 2012-02-27
    相关资源
    最近更新 更多