КАСКАДИ ВІДКЛЮЧИТИ лише один раз


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
Простий, безпечний. Ви повинні запустити їх в одній транзакції, якщо у вас є вставки щільності.
Ізмаїл Явуз

40

Якщо ви дійсно хочете, DELETE FROM some_table CASCADE; що означає " видалити всі рядки з таблиціsome_table ", ви можете використовувати TRUNCATEзамість DELETEі CASCADEзавжди підтримується. Однак, якщо ви хочете використовувати вибіркове видалення за допомогою whereпункту, TRUNCATEце недостатньо добре.

ВИКОРИСТАННЯ ДЛЯ ДОГЛЯДИ - Це скине всі рядки всіх таблиць, на яких є обмеження на зовнішній ключ, some_tableі всіх таблиць, які мають обмеження для цих таблиць тощо.

Postgres підтримує CASCADEз командою TRUNCATE :

TRUNCATE some_table CASCADE;

Це зручно - це транзакція (тобто може бути повернута назад), хоча вона не є повністю ізольованою від інших одночасних транзакцій і має кілька інших застережень. Детально прочитайте документи.


226
чітко "кілька каскадних видаляє" ≠ скидання всіх даних із таблиці…
lensovet

33
Це скине всі рядки всіх таблиць, які мають обмеження на зовнішній ключ для some_table, і всі таблиці, які мають обмеження для цих таблиць тощо. Це потенційно дуже небезпечно.
AJP

56
остерігайся. це необачна відповідь.
Йордан Арсено

4
Хтось позначив цю відповідь для видалення - імовірно, тому, що вони не згодні з нею. Правильний хід дій у такому випадку полягає в тому, щоб схилити, а не прапор.
Вай Ха Лі

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;

Це відбувається постійно, особливо з таблицями, що самостійно посилаються. Розглянемо компанію з різними рівнями управління в різних відділах, або загальну ієрархічну систематику. Так, я погоджуюся, що ця функція не є абсолютно найкращою справою з нарізаного хліба, але це корисний інструмент у правильній ситуації.
Джо Лав

Якщо ви перезапишете його, прийміть масив ідентифікаторів, а також генеруйте запити, які використовуватимуть INоператор із під-виборами замість =(так що крок використовувати логіку наборів), це стане набагато швидше.
Губбіт

2
Дякую за ваше рішення. Я пишу кілька тестів, і мені потрібно було видалити запис, і я мав проблеми з каскадом цього видалення. Ваша функція спрацювала дуже добре!
Фернандо Камарго

1
@JoeLove, яка у тебе проблема швидкості? У цій ситуації рекурсія є єдиним правильним рішенням для мене.
Губбіт

1
@arthur ви, ймовірно, можете скористатися якоюсь версією рядка -> json -> тексту, щоб виконати це, однак я ще не пішов так далеко. Через роки я виявив, що єдиний первинний ключ (з потенційними вторинними ключами) корисний з багатьох причин.
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

1
Які осколки ви точно маєте на увазі? записи буде видалено каскадом, не повинно бути непослідовности.
Педро Борхес

1
замість того, щоб перейматися "неприємними осколками" (каскадні обмеження все ще будуть узгоджені), я був би ДУЖЕ стурбований тим, що каскад не заходить досить далеко - якщо для видалених записів потрібні подальші видалені записи, то ці обмеження потрібно буде змінити щоб також забезпечити каскад. (або скористайтеся функцією, про яку я писав вище, щоб уникнути цього сценарію) ... Остання остання рекомендація в будь-якому випадку: ВИКОРИСТОВУЙТЕ ПЕРЕКЛАД, щоб ви могли відкотити її назад, якщо вона помилиться.
Джо Лав

7

Я не можу коментувати відповідь Палехорса, тому я додав власну відповідь. Логіка 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

Так, як уже говорили інші, немає зручного "ВИДАЛИТИ З МОЯ_Таблиця ... КАСКАД" (або еквівалент). Щоб видалити нескаказні закордонні записи, що захищаються від законів, та їх посилання,

  • Виконайте всі видалення явно, по одному запиту за раз, починаючи з дочірніх таблиць (хоча це не пролетить, якщо у вас кругові посилання); або
  • Виконувати всі видалення явно в одному (потенційно масивному) запиті; або
  • Якщо припустити, що ваші нескаказні обмеження в іноземних ключах були створені як "НА ВИДАЛЕННЯ НЕ ДІЯЛЬНОСТІ ДІЇ", виконайте всі видалення явно в одній транзакції; або
  • Тимчасово скиньте в графіку обмеження "без дії" та "обмежте" зовнішні ключові обмеження, відтворіть їх як CASCADE, видаліть предків, які порушили правопорушення, знову скасуйте обмеження іноземних ключів і, нарешті, відтворіть їх, як вони були спочатку (тимчасово послабляючи цілісність ваші дані); або
  • Щось, мабуть, однаково весело.

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

Я приїхав сюди кілька місяців тому, шукаючи відповідь на питання "КАСКАДИ ВИДАЛИТИ тільки один раз" (спочатку задавали більше десяти років тому!). З розумного рішення Джо Лав (і варіанту Томаса Г.Г. де Вільхена) я пробіг деякий пробіг, але врешті-решт у моєму випадку використання були особливі вимоги (обробка циркулярних посилань на внутрішньому столі, для одного), що змусило мене застосувати інший підхід. Такий підхід врешті-решт став рекурсивно_захищеним (PG 10.10).

Я вже деякий час використовую rekursively_delete у виробництві, і, нарешті, відчуваю себе (насторожено) досить впевнено, щоб зробити його доступним для інших, хто може повернутися сюди, шукаючи ідеї. Як і рішення Джо Лав, воно дозволяє видаляти цілі графіки даних, як ніби всі зовнішні ключові обмеження у вашій базі даних були на мить встановлені на CASCADE, але пропонує ще кілька додаткових функцій:

  • Забезпечує попередній перегляд ASCII цілі видалення та її графіка залежних.
  • Виконує видалення в одному запиті, використовуючи рекурсивні CTE.
  • Обробляє кругові залежності, внутрішньо- та міжстолові.
  • Обробляє складові ключі.
  • Пропускає обмеження "встановити за замовчуванням" та "встановити нульові".

Я отримую помилку: ПОМИЛКА: масив повинен мати парну кількість елементів Де: PL / pgSQL функція _recursively_delete (regclass, text [], integer, jsonb, integer, text [], jsonb, jsonb) рядок 15 при призначенні оператора SQL "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

Спробував цю гілку, і вона виправила початкову помилку. На жаль, це не швидше моєї оригінальної версії (яка, можливо, не була вашою думкою в написанні цього в першу чергу). Я працюю над черговою спробою, яка створює дублікати закордонних ключів з "каскадом на видалення", потім видаляю оригінальний запис, потім викидаю всі новостворені закордонні ключі,
Джо Лав

3

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

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


1
Хоча це не стосується ОП, добре планувати, коли рядки із сторонніми ключами потрібно видалити. Як сказав Бен Франклін, "унція профілактики вартує фунта лікування".
Єзуїзм

1
Я виявив, що це рішення може бути досить небезпечним, якщо ваш додаток видаляє запис з великою кількістю побратимів і замість незначної помилки ви назавжди видалили величезний набір даних.
Джо Лав

2

Я взяв відповідь Джо Лав і переписав його за допомогою INоператора з підбором замість того, =щоб зробити функцію швидшою (за пропозицією Губбіта):

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 з кількома орендарями (всі клієнти діляться в одних і тих же таблицях). Моя версія (без "в"), здається, працює досить повільно, щоб видалити всі сліди старого клієнта ... Мені цікаво спробувати це з деякими макетними даними для порівняння швидкостей. Чи було у вас щось, що ви могли сказати про різницю швидкості, яку помітили у ваших випадках використання?
Джо Лав

Для мого використання я помітив прискорення в 10 разів під час використання inоператора та підзапитів.
Thomas CG de Vilhena

1

Опція видалення за допомогою каскаду застосовується лише до таблиць із визначеними зовнішніми ключами. Якщо ви видалите, і воно говорить, що ви не можете, оскільки це порушило б обмеження іноземного ключа, каскад призведе до видалення порушень рядків.

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


2
Відповідь Гранта частково неправильна - Postgresql не підтримує CASCADE на DELETE-запити. postgresql.org/docs/8.4/static/dml-delete.html
Фредрік Вендт

Будь-яка ідея, чому він не підтримується в запиті на видалення?
Teifion

2
немає способу "видалити за допомогою каскаду" на таблиці, яка не була встановлена ​​відповідно, тобто для якої обмеження зовнішнього ключа не було визначено як ON DELETE CASCADE, про що спочатку йшлося в цьому питанні.
лінзовет

Як відповідь на це питання, це абсолютно неправильно. Одного разу до КАСКАДУ немає способу.
Джеремі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.