КАСКАД-УДАЛИТЬ только один раз

200

У меня есть база данных Postgresql, в которой я хочу сделать несколько каскадных удалений. Однако таблицы не настраиваются с правилом ON DELETE CASCADE. Есть ли способ, которым я могу выполнить удаление и сказать Postgresql каскадировать это только один раз? Что-то эквивалентное

DELETE FROM some_table CASCADE;

Ответы на этот старый вопрос создают впечатление, что такого решения не существует, но я решил, что задам этот вопрос явно, просто чтобы быть уверенным.

Эли Кортрайт
источник
Пожалуйста, смотрите мои пользовательские функции ниже. Это возможно с определенными ограничениями.
Джо Лав

Ответы:

175

Нет. Чтобы сделать это только один раз, вы просто напишите оператор удаления для таблицы, которую вы хотите каскадировать.

DELETE FROM some_child_table WHERE some_fk_field IN (SELECT some_id FROM some_Table);
DELETE FROM some_table;
бледная лошадь
источник
12
Это не обязательно работает, так как могут быть другие внешние ключи, каскадные из исходного каскадирования (рекурсия). Вы даже можете попасть в цикл, где таблица a ссылается на b, который ссылается на a. Чтобы достичь этого в общем смысле, смотрите мою таблицу ниже, но она имеет некоторые ограничения. Если у вас простая настройка таблицы, попробуйте приведенный выше код, вам будет проще понять, что вы делаете.
Джо Лав
2
Просто, безопасно. Вы должны запустить их в одной транзакции, если у вас есть вставки плотности.
Исмаил Явуз
39

Если вы действительно хотите, DELETE FROM some_table CASCADE; что означает « удалить все строки из таблицыsome_table », вы можете использовать TRUNCATEвместо DELETEи CASCADEвсегда поддерживается. Однако, если вы хотите использовать выборочное удаление с whereпредложением, TRUNCATEэтого недостаточно.

ИСПОЛЬЗОВАТЬ С УХОДОМ - Это удалит все строки всех таблиц, для которых есть ограничение внешнего ключа, some_tableи всех таблиц, которые имеют ограничения для этих таблиц и т. Д.

Postgres поддерживает CASCADEс командой TRUNCATE :

TRUNCATE some_table CASCADE;

Удобно, что это транзакционный (то есть может быть откат), хотя он не полностью изолирован от других одновременных транзакций и имеет несколько других предостережений. Прочитайте документы для деталей.

DanC
источник
226
явно «несколько каскадных удалений», отбрасывая все данные из таблицы…
Ленсовет
33
Это приведет к удалению всех строк всех таблиц, которые имеют ограничение внешнего ключа для some_table, и всех таблиц, которые имеют ограничения для этих таблиц и т. Д. ... это потенциально очень опасно.
AJP
56
берегитесь. это безрассудный ответ.
Джордан Арсено
4
Кто-то пометил этот ответ для удаления - вероятно, потому что он не согласен с ним. Правильный курс действий в этом случае - понижать, а не сигнализировать.
Вай Ха Ли
7
У него есть предупреждение наверху. Если вы решите игнорировать это, никто не сможет вам помочь. Я думаю, что ваши пользователи "copyPaste" представляют реальную опасность здесь.
BluE
28

Я написал (рекурсивную) функцию для удаления любой строки на основе ее первичного ключа. Я написал это, потому что я не хотел создавать свои ограничения как «на каскаде удаления». Я хотел иметь возможность удалять сложные наборы данных (в качестве администратора баз данных), но не позволять моим программистам иметь возможность каскадного удаления, не продумывая все последствия. Я все еще тестирую эту функцию, поэтому в ней могут быть ошибки - но, пожалуйста, не пытайтесь ее использовать, если ваша БД имеет многоколонные первичные (и, следовательно, внешние) ключи. Кроме того, все ключи должны быть представлены в виде строки, но они могут быть написаны так, чтобы не иметь этого ограничения. В любом случае, я использую эту функцию ОЧЕНЬ ИСКЛЮЧИТЕЛЬНО, я слишком дорожу своими данными, чтобы включить каскадные ограничения для всего. В основном эта функция передается в схеме, имени таблицы и первичном значении (в виде строки), и он начнет с поиска любых внешних ключей в этой таблице и удостоверится, что данные не существуют - если это произойдет, он рекурсивно вызывает себя для найденных данных. Он использует массив данных, уже помеченных для удаления, чтобы предотвратить бесконечные циклы. Пожалуйста, проверьте это и дайте мне знать, как это работает для вас. Примечание: это немного медленно. Я называю это так: 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;
Джо Лав
источник
Это происходит все время, особенно с самообращающимися таблицами. Рассмотрим компанию с разными уровнями управления в разных отделах или общую иерархическую таксономию. Да, я согласен с тем, что эта функция не самая лучшая вещь после нарезанного хлеба, но это полезный инструмент в правильной ситуации.
Джо Лав
Если переписать его, примите массив идентификаторов, а также сгенерируйте запросы, которые будут использовать INоператор с вложенными выборками вместо =(так что шаг для использования логики множеств), это стало бы намного быстрее.
Hubbitus
2
Спасибо за ваше решение. Я пишу несколько тестов, и мне нужно было удалить запись, и у меня были проблемы с каскадным удалением этой записи. Ваша функция сработала очень хорошо!
Фернандо Камарго
1
@JoeLove, какая у тебя проблема со скоростью? В этой ситуации рекурсия - единственное правильное решение для меня.
Hubbitus
1
@arthur, возможно, вы могли бы использовать какую-то версию строки -> json -> text, но я не зашел так далеко. В течение многих лет я обнаружил, что единственный первичный ключ (с потенциальными вторичными ключами) хорош по многим причинам.
Джо Лав,
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
1
Какие осколки вы имеете в виду именно? записи будут удалены через каскад, не должно быть противоречий.
Педро Борхес
1
вместо того, чтобы беспокоиться о «неприятных осколках» (каскадные ограничения по-прежнему будут согласованы), я бы БОЛЬШЕ беспокоился о том, что каскадирование не заходит достаточно далеко - если для удаленных записей требуются дополнительные удаленные записи, то эти ограничения необходимо будет изменить чтобы обеспечить каскадирование, а также. (или используйте функцию, которую я написал выше, чтобы избежать этого сценария) ... Последняя рекомендация в любом случае: ИСПОЛЬЗУЙТЕ СДЕЛКУ, чтобы вы могли откатить ее обратно, если она ошиблась.
Джо Лав
7

Я не могу комментировать ответ Палехорса, поэтому я добавил свой собственный ответ. Логика Палехорса в порядке, но эффективность может быть плохой с большими наборами данных.

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, удалите предков-нарушителей, снова удалите ограничения внешнего ключа и, наконец, воссоздайте их такими, какими они были изначально (таким образом временно ослабив целостность ваши данные); или
  • Что-то, наверное, одинаково весело.

Я предполагаю, что намеренно обходить ограничения внешнего ключа не удобно; но я понимаю, почему в определенных обстоятельствах вы захотите это сделать. Если это то, чем вы будете заниматься с какой-то частотой, и если вы хотите повсюду пренебрегать мудростью администраторов баз данных, вы можете автоматизировать это с помощью процедуры.

Я приехал сюда несколько месяцев назад в поисках ответа на вопрос «УДАЛИТЬ КАСКАД только один раз» (первоначально задавался более десяти лет назад!). Я получил некоторое преимущество от умного решения Джо Лава (и варианта Томаса К.Г. де Вильены), но, в конце концов, в моем случае использования были особые требования (например, обработка циклических ссылок внутри таблицы), что заставило меня выбрать другой подход. Этот подход в конечном итоге стал recursively_delete (PG 10.10).

Я уже некоторое время использую recursively_delete в производстве и, наконец, чувствую себя (достаточно осторожно) достаточно уверенно, чтобы сделать его доступным для других, которые могут оказаться здесь в поисках идей. Как и в случае решения Joe Love, оно позволяет вам удалять целые графы данных, как если бы все ограничения внешнего ключа в вашей базе данных были на мгновение установлены на CASCADE, но предлагает пару дополнительных функций:

  • Обеспечивает предварительный просмотр ASCII цели удаления и ее графика зависимостей.
  • Выполняет удаление в одном запросе с использованием рекурсивных CTE.
  • Обрабатывает круговые зависимости, внутри и между таблицами.
  • Ручки составные ключи.
  • Пропускает ограничения 'set default' и 'set null'.
TRL
источник
Я получаю сообщение об ошибке: ОШИБКА: массив должен иметь четное количество элементов Где: PL / pgSQL функция _recursively_delete (regclass, text [], целое число, jsonb, целое число, текст [], jsonb, jsonb) строка 15 при назначении оператора SQL «SELECT * FROM _recursively_delete (ARG_table, VAR_pk_col_names)» PL / pgSQL функция recursively_delete (regclass, anyelement, boolean) строка 73 в операторе SQL
Джо Лав
Привет, @JoeLove. Спасибо за попытку. Можете ли вы дать мне шаги для воспроизведения? А какая у тебя версия PG?
TRL
Я не уверен, что это поможет. но я просто создал ваши функции, а затем запустил следующий код: 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 ).
TRL
Попробовал ту ветку и она исправила первоначальную ошибку. К сожалению, это не намного быстрее, чем моя оригинальная версия (которая, возможно, не была вашей целью при написании этого в первую очередь). Я работаю над другой попыткой, которая создает дубликаты внешних ключей с помощью «каскада удаления», затем удаляет исходную запись, а затем отбрасывает все вновь созданные внешние ключи,
Джо Лав
3

Вы можете использовать для автоматизации этого, вы можете определить ограничение внешнего ключа с помощью ON DELETE CASCADE.
Я цитирую руководство по ограничениям внешнего ключа :

CASCADE указывает, что при удалении ссылочной строки строки, ссылающиеся на нее, также должны автоматически удаляться.

atiruz
источник
1
Несмотря на то, что это не относится к OP, хорошо планировать, когда нужно удалять строки с внешними ключами. Как сказал Бен Франклин, «унция профилактики стоит фунта лечения».
Иезуизм
1
Я обнаружил, что это решение может быть довольно опасным, если ваше приложение удаляет запись с большим количеством братьев и сестер, и вместо небольшой ошибки вы навсегда удалили огромный набор данных.
Джо Лав
2

Я взял ответ Джо Лав и переписал его, используя 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
Я собираюсь взглянуть на это и посмотреть, насколько хорошо он работает с самореферентными ограничениями и тому подобным. Я попытался сделать что-то подобное, но остановился на том, чтобы заставить его работать полностью. Если ваше решение работает для меня, я собираюсь его реализовать. Это один из многих инструментов dba, которые должны быть упакованы и помещены на github или что-то в этом роде.
Джо Лав
У меня есть базы данных среднего размера для мультитенантной CMS (все клиенты используют одни и те же таблицы). Моя версия (без "in"), кажется, работает довольно медленно, чтобы удалить все следы старого клиента ... Мне интересно попробовать это с некоторыми данными макета для сравнения скоростей. Можете ли вы что-нибудь сказать о разнице в скорости, которую вы заметили в своих случаях?
Джо Лав
Для моего случая использования я заметил ускорение порядка 10x при использовании inоператора и подзапросов.
Томас КГ де Вильена
1

Удаление с опцией каскада применяется только к таблицам с определенными внешними ключами. Если вы делаете удаление, и он говорит, что вы не можете, потому что это нарушило бы ограничение внешнего ключа, каскад заставит его удалить ошибочные строки.

Если вы хотите удалить связанные строки таким способом, вам нужно сначала определить внешние ключи. Кроме того, помните, что если вы явно не дадите ему указание начать транзакцию или не измените значения по умолчанию, он выполнит автоматическую фиксацию, которая может занять очень много времени для очистки.

Грант Джонсон
источник
2
Ответ Гранта частично неверен - Postgresql не поддерживает CASCADE для запросов DELETE. postgresql.org/docs/8.4/static/dml-delete.html
Фредрик Вендт,
Есть идеи, почему это не поддерживается в запросе на удаление?
Teifion
2
нет способа «удалить с помощью каскада» для таблицы, которая не была настроена соответствующим образом, то есть для которой ограничение внешнего ключа не было определено как ON DELETE CASCADE, о чем изначально был вопрос.
Ленсовет
Как ответ на этот вопрос, это совершенно неправильно. Нет способа КАСКАД один раз.
Джереми