DELETE заява суперечить обмеженню ДОВІДКИ


10

Моя ситуація виглядає приблизно так:

Таблиця STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

МІСЦЕ ТАБЛИЦІ:

ID *[PK]*
LOCATION_NAME

Таблиця WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

Таблиця INVENTORY_ITEMS:

ID *[PK]*
ITEM_NAME
STOCK_ARTICLE *[FK]*
LOCATION *[FK]*
WORK_PLACE *[FK]*

3 FK в INVENTORY_ITEMS, очевидно, посилаються на стовпці "ID" у відповідних інших таблицях.

Відповідні таблиці тут: STOCK_ARTICLE та INVENTORY_ITEMS.

Тепер існує завдання SQL, що складається з декількох етапів (SQL-скриптів), які "синхронізують" базу даних, згадану вище, з іншою базою даних (OTHER_DB). Один із кроків у цій роботі - це "очищення". Він видаляє всі записи з STOCK_ITEMS, де в іншій базі даних немає відповідного запису з тим самим ідентифікатором. Це виглядає приблизно так:

DELETE FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID)

Але цей крок завжди не вдається:

Заява DELETE суперечить обмеженню REFERENCE "FK_INVENTORY_ITEMS_STOCK_ARTICLES". Конфлікт стався в базі даних "FIRST_DB", таблиці "dbo.INVENTORY_ITEMS", стовпці "STOCK_ARTICLES". [SQLSTATE 23000] (Помилка 547) Заява припинено. [SQLSTATE 01000] (Помилка 3621). Крок не вдався.

Тому проблема полягає в тому, що він не може видалити записи зі STOCK_ARTICLES, коли на них посилається INVENTORY_ITEMS. Але ця очистка потребує роботи. Це означає, що мені, мабуть, доведеться розширити сценарій очищення, щоб він спочатку ідентифікував записи, які слід видалити з STOCK_ITEMS, але не може, тому що відповідний ідентифікатор посилається з INVENTORY_ITEMS. Потім слід спочатку видалити ці записи всередині INVENTORY_ITEMS, а потім видалити записи всередині STOCK_ARTICLES. Чи правий я? Як би виглядав код SQL тоді?

Дякую.

Відповіді:


13

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

Є два варіанти:

  1. Видаліть ряди INVENTORY_ITEMSспочатку, потім рядки з STOCK_ARTICLES.
  2. Використовуйте ON DELETE CASCADEдля визначення в ключі.

1: Видалення у правильному порядку

Найефективніший спосіб зробити це залежить від складності запиту, який вирішує, які рядки видалити. Загальною схемою може бути:

BEGIN TRANSACTION
SET XACT_ABORT ON
DELETE INVENTORY_ITEMS WHERE STOCK_ARTICLE IN (<select statement that returns stock_article.id for the rows you are about to delete>)
DELETE STOCK_ARTICLES WHERE <the rest of your current delete statement>
COMMIT TRANSACTION

Це добре для простих запитів або для видалення одного товарного товару, але, враховуючи, що виписка заявки містить WHERE NOT EXISTSпункт введення, який WHERE INможе створити дуже неефективний план, тому протестуйте з реалістичним розміром набору даних і переставляйте запит, якщо це потрібно.

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

2: Використання ON DELETE CASCADE

Якщо ви додасте параметр каскаду до свого зовнішнього ключа, тоді SQL Server автоматично зробить це за вас, видаляючи рядки з, INVENTORY_ITEMSщоб задовольнити обмеження, що нічого не повинно стосуватися рядків, які ви видаляєте. Просто додайте ON DELETE CASCADEдо визначення FK так:

ALTER TABLE <child_table> WITH CHECK 
ADD CONSTRAINT <fk_name> FOREIGN KEY(<column(s)>)
REFERENCES <parent_table> (<column(s)>)
ON DELETE CASCADE

Перевагою тут є те, що видалення - це одне атомне твердження, що зменшує (хоча, як завжди, не на 100% видалення) необхідності турбуватися про налаштування транзакцій та блокування. Каскад може працювати навіть на кількох рівнях батьків / дитини / онука / дитини / ..., якщо між батьком та всіма нащадками є лише один шлях (шукайте "декілька каскадних шляхів" для прикладів, коли це може не працювати).

ПРИМІТКА: Я та багато інших вважаю каскадні делети небезпечними, тому якщо ви використовуєте цю опцію, будьте дуже обережні, щоб правильно її задокументувати у вашій базі даних, щоб ви та інші розробники не подолали небезпеку пізніше . Я уникаю каскадних делетів, де це можливо, з цієї причини.

Поширена проблема, спричинена каскадними видаленнями, коли хтось оновлює дані, скидаючи та відтворюючи рядки замість використання UPDATEабо MERGE. Це часто можна побачити там, де потрібно "оновити рядки, які вже існують, вставити ті, які не роблять" (іноді їх називають операцією UPSERT), а людям, які не знають про MERGEце, зробити це легше:

DELETE <all rows that match IDs in the new data>
INSERT <all rows from the new data>

ніж

-- updates
UPDATE target 
SET    <col1> = source.<col1>
  ,    <col2> = source.<col2>
       ...
  ,    <colN> = source.<colN>
FROM   <target_table> AS target JOIN <source_table_or_view_or_statement> AS source ON source.ID = target.ID
-- inserts
INSERT  <target_table>
SELECT  *
FROM    <source_table_or_other> AS source
LEFT OUTER JOIN
        <target_table> AS target
        ON target.ID = source.ID
WHERE   target.ID IS NULL

Проблема тут полягає в тому, що оператор delete буде каскадом дочірніх рядків, а оператор вставлення не відтворить їх, тому, оновлюючи батьківську таблицю, ви випадково втрачаєте дані з дочірніх таблиць.

Підсумок

Так, потрібно спочатку видалити дочірні рядки.

Існує ще один варіант: ON DELETE CASCADE.

Але це ON DELETE CASCADEможе бути небезпечно , тому використовуйте обережно.

Бічна примітка: використовуйте MERGE(або UPDATE-і- INSERTтам, де MERGEце недоступно), коли вам потрібна UPSERTоперація, а не DELETE -надайте-замініть, INSERTщоб уникнути потрапляння в пастки, встановлені іншими людьми, які використовують ON DELETE CASCADE.


2

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

Ця операція не повинна провалюватися:

SELECT sa.ID INTO #StockToDelete
FROM STOCK_ARTICLES sa
LEFT JOIN [OTHER_DB].[dbo].[OtherTable] other ON other.ObjectID = sa.OTHER_DB_ID
WHERE other.ObjectID IS NULL

DELETE ii
FROM INVENTORY_ITEMS ii
JOIN #StockToDelete std ON ii.STOCK_ARTICLE = std.ID

DELETE sa
FROM STOCK_ARTICLES sa
JOIN #StockToDelete std ON sa.ID = std.ID

2
Хоча якщо видалити велику кількість рядків STOCK_ARTICLES, це, швидше за все, буде гіршим, ніж інші параметри, через побудову таблиці темп (для невеликої кількості рядків різниця навряд чи буде суттєвою). Також слід подбати про використання відповідних директив транзакцій, щоб забезпечити, що три оператори виконуються як атомна одиниця, якщо паралельний доступ не є неможливим, інакше ви можете побачити помилки як нові, INVENTORY_ITEMSщо додаються між двома DELETEs.
Девід Спіллетт

1

Я також зіткнувся з цим питанням, і мені вдалося його вирішити. Ось моя ситуація:

У моєму випадку у мене є база даних, яка використовується для звітності про аналітику (MYTARGET_DB), яка витягується з вихідної системи (MYSOURCE_DB). Деякі таблиці 'MYTARGET_DB' унікальні для цієї системи, і там дані створюються та керуються ними; Більшість таблиць є з 'MYSOURCE_DB', і є завдання, яке видаляє / вставляє дані в 'MYTARGET_DB' з 'MYSOURCE_DB'.

Одна з таблиць пошуку [ПРОДУКТ] є з ДЖЕРЕЛА, і там є таблиця даних [InventoryOutsourced], що зберігається в ЦІЛІ. Існує референтна цілісність, розроблена в таблиці. Тому коли я намагаюся запустити видалити / вставити, я отримую це повідомлення.

Msg 50000, Level 16, State 1, Procedure uspJobInsertAllTables_AM, Line 249
The DELETE statement conflicted with the REFERENCE constraint "FK_InventoryOutsourced_Product". The conflict occurred in database "ProductionPlanning", table "dbo.InventoryOutsourced", column 'ProdCode'.

Я створив обхідне рішення - вставити дані в змінну таблиці [@tempTable] з [InventoryOutsourced], видалити дані в [InventoryOutsourced], запустити завдання синхронізації, вставити в [InventoryOutsourced] з [@tempTable]. Це зберігає цілісність на місці, а також зберігається унікальний збір даних. Що найкраще в обох світах. Сподіваюсь, це допомагає.

BEGIN TRY
    BEGIN TRANSACTION InsertAllTables_AM

        DECLARE
        @BatchRunTime datetime = getdate(),
        @InsertBatchId bigint
            select @InsertBatchId = max(IsNull(batchid,0)) + 1 from JobRunStatistic 

        --<DataCaptureTmp/> Capture the data tables unique to this database, before deleting source system reference tables
            --[InventoryOutsourced]
            DECLARE @tmpInventoryOutsourced as table (
                [ProdCode]      VARCHAR (12)    NOT NULL,
                [WhseCode]      VARCHAR (4)     NOT NULL,
                [Cases]          NUMERIC (8)     NOT NULL,
                [Weight]         NUMERIC (10, 2) NOT NULL,
                [Date] DATE NOT NULL, 
                [SourcedFrom] NVARCHAR(50) NOT NULL, 
                [User] NCHAR(50) NOT NULL, 
                [ModifiedDatetime] DATETIME NOT NULL
                )

            INSERT INTO @tmpInventoryOutsourced (
                [ProdCode]
               ,[WhseCode]
               ,[Cases]
               ,[Weight]
               ,[Date]
               ,[SourcedFrom]
               ,[User]
               ,[ModifiedDatetime]
               )
            SELECT 
                [ProdCode]
                ,[WhseCode]
                ,[Cases]
                ,[Weight]
                ,[Date]
                ,[SourcedFrom]
                ,[User]
                ,[ModifiedDatetime]
            FROM [dbo].[InventoryOutsourced]

            DELETE FROM [InventoryOutsourced]
        --</DataCaptureTmp> 

... Delete Processes
... Delete Processes    

        --<DataCaptureInsert/> Capture the data tables unique to this database, before deleting source system reference tables
            --[InventoryOutsourced]
            INSERT INTO [dbo].[InventoryOutsourced] (
                [ProdCode]
               ,[WhseCode]
               ,[Cases]
               ,[Weight]
               ,[Date]
               ,[SourcedFrom]
               ,[User]
               ,[ModifiedDatetime]
               )
            SELECT 
                [ProdCode]
                ,[WhseCode]
                ,[Cases]
                ,[Weight]
                ,[Date]
                ,[SourcedFrom]
                ,[User]
                ,[ModifiedDatetime]
            FROM @tmpInventoryOutsourced
            --</DataCaptureInsert> 

    COMMIT TRANSACTION InsertAllTables_AM
END TRY

0

Я не пройшов повну перевірку, але щось подібне повинно працювати.

--cte of Stock Articles to be deleted
WITH StockArticlesToBeDeleted AS
(
SELECT ID FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID)
)
--delete from INVENTORY_ITEMS where we have a match on deleted STOCK_ARTICLE
DELETE a FROM INVENTORY_ITEMS a join
StockArticlesToBeDeleted b on
    b.ID = a.STOCK_ARTICLE;

--now, delete from STOCK_ARTICLES
DELETE FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.