Оператор DELETE конфликтует с ограничением REFERENCE

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?

Спасибо.

derwodaso
источник

Ответы:

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

Если вы добавите опцию 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% удаление) необходимость беспокоиться о настройках транзакции и блокировки. Каскад может даже работать на нескольких уровнях parent / child / grand-child / ..., если существует только один путь между parent и всеми потомками (ищите «несколько путей каскада» для примеров, где это может не сработать).

ПРИМЕЧАНИЕ. Я и многие другие полагаем, что каскадное удаление опасно, поэтому, если вы используете эту опцию, будьте очень осторожны, чтобы правильно документировать ее в своей структуре базы данных, чтобы вы и другие разработчики не опрокинули опасность позже . По этой причине я избегаю каскадного удаления, где это возможно.

Распространенная проблема, связанная с каскадным удалением, - это когда кто-то обновляет данные, удаляя и воссоздавая строки вместо использования 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.

Одна из справочных таблиц [PRODUCT] взята из SOURCE, и в TARGET хранится таблица данных [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
SherlockSpreadsheets
источник
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);
Скотт Ходжин
источник