Instrukcja DELETE była w konflikcie z ograniczeniem REFERENCE

10

Moja sytuacja wygląda następująco:

Tabela STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

Tabela LOKALIZACJA:

ID *[PK]*
LOCATION_NAME

Tabela WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

Tabela INVENTORY_ITEMS:

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

Oczywiście 3 FK w INVENTORY_ITEMS odnoszą się do kolumn „ID” w odpowiednich innych tabelach.

Odpowiednie tabele tutaj to STOCK_ARTICLE i INVENTORY_ITEMS.

Teraz jest zadanie SQL składające się z kilku kroków (skrypty SQL), które „synchronizują” wspomnianą wyżej bazę danych z inną bazą danych (OTHER_DB). Jednym z kroków w tym zadaniu jest „oczyszczenie”. Usuwa wszystkie rekordy ze STOCK_ITEMS, w których nie ma odpowiedniego rekordu w innej bazie danych o tym samym identyfikatorze. To wygląda tak:

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)

Ale ten krok zawsze kończy się niepowodzeniem:

Instrukcja DELETE była w konflikcie z ograniczeniem REFERENCE „FK_INVENTORY_ITEMS_STOCK_ARTICLES”. Konflikt wystąpił w bazie danych „FIRST_DB”, tabeli „dbo.INVENTORY_ITEMS”, kolumnie „STOCK_ARTICLES”. [SQLSTATE 23000] (Błąd 547) Instrukcja została zakończona. [SQLSTATE 01000] (Błąd 3621). Krok nie powiódł się.

Problem polega na tym, że nie można usunąć rekordów ze STOCK_ARTICLES, gdy odwołują się do nich INVENTORY_ITEMS. Ale to czyszczenie musi działać. Co oznacza, że ​​prawdopodobnie muszę rozszerzyć skrypt czyszczenia, aby najpierw zidentyfikował rekordy, które powinny zostać usunięte z STOCK_ITEMS, ale nie może, ponieważ do odpowiedniego identyfikatora odwołuje się od wewnątrz INVENTORY_ITEMS. Następnie powinien najpierw usunąć te rekordy w INVENTORY_ITEMS, a następnie usunąć rekordy w STOCK_ARTICLES. Czy mam rację? Jak wyglądałby wtedy kod SQL?

Dziękuję Ci.

derwodaso
źródło

Odpowiedzi:

13

To jest sedno ograniczeń klucza obcego: powstrzymują cię przed usuwaniem danych, o których mowa w innym miejscu w celu zachowania integralności referencyjnej.

Istnieją dwie opcje:

  1. Usuń wiersze od INVENTORY_ITEMSpierwszego, a następnie wiersze od STOCK_ARTICLES.
  2. Użyj ON DELETE CASCADEdla definicji klucza.

1: Usuwanie we właściwej kolejności

Najbardziej efektywny sposób wykonania tego zadania różni się w zależności od złożoności zapytania, które decyduje o tym, które wiersze należy usunąć. Ogólny wzór może być:

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

Jest to przydatne w przypadku prostych zapytań lub do usunięcia pojedynczego elementu zapasowego, ale biorąc pod uwagę, że instrukcja delete zawiera WHERE NOT EXISTSzagnieżdżoną klauzulę, która WHERE INmoże stworzyć bardzo nieefektywny plan, więc przetestuj z realistycznym rozmiarem zestawu danych i w razie potrzeby zmień kolejność zapytania.

Zwróć także uwagę na wyciągi z transakcji: chcesz się upewnić, że oba usunięcia zostały zakończone, lub żadne z nich nie wykona. Jeśli operacja już się dzieje w ramach transakcji, oczywiście musisz to zmienić, aby dopasować ją do bieżącej transakcji i procesu obsługi błędów.

2: Użyj ON DELETE CASCADE

Jeśli dodasz opcję kaskadową do klucza obcego, SQL Server zrobi to automatycznie, usuwając wiersze, INVENTORY_ITEMSaby spełnić ograniczenie, że nic nie powinno odnosić się do usuwanych wierszy. Po prostu dodaj ON DELETE CASCADEdo definicji FK tak:

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

Zaletą tego rozwiązania jest to, że usunięcie jest jedną instrukcją atomową, która zmniejsza (choć jak zwykle nie usuwa w 100%) potrzebę martwienia się o ustawienia transakcji i blokady. Kaskada może nawet działać na wielu poziomach rodzic / dziecko / wnuczek / ..., jeśli istnieje tylko jedna ścieżka między rodzicem a wszystkimi potomkami (wyszukaj „wiele ścieżek kaskady”, aby zobaczyć przykłady, w których może to nie działać).

UWAGA: Ja i wielu innych uważam, że usuwanie kaskadowe jest niebezpieczne, więc jeśli użyjesz tej opcji, bardzo ostrożnie udokumentuj ją w projekcie bazy danych, abyś Ty i inni programiści nie potknęli się o to później . Z tego powodu unikam usuwania kaskadowego w miarę możliwości.

Częstym problemem powodowanym przez kaskadowe usuwanie jest sytuacja, gdy ktoś aktualizuje dane, upuszczając i odtwarzając wiersze zamiast przy użyciu UPDATElub MERGE. Jest to często widoczne, gdy „zaktualizuj wiersze, które już istnieją, wstaw te, które nie” (czasami nazywane operacją UPSERT), a osobom nieświadomym MERGEinstrukcji łatwiej jest to zrobić:

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

niż

-- 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

Problem polega na tym, że instrukcja delete będzie kaskadowana do wierszy podrzędnych, a instrukcja insert nie odtworzy ich, więc podczas aktualizacji tabeli nadrzędnej przypadkowo tracisz dane z tabel podrzędnych.

Podsumowanie

Tak, najpierw musisz usunąć wiersze potomne.

Nie ma innej opcji: ON DELETE CASCADE.

Ale ON DELETE CASCADEmoże być niebezpieczny , więc używaj go ostrożnie.

Uwaga dodatkowa: używaj MERGE( UPDATElub - i INSERTgdzie MERGEnie jest dostępne), gdy potrzebujesz UPSERToperacji, a nie DELETE - a następnie - zamień - z, INSERTaby uniknąć wpadnięcia w pułapki ustanowione przez inne osoby ON DELETE CASCADE.

David Spillett
źródło
2

Możesz uzyskać identyfikatory do usunięcia tylko raz, zapisać je w tabeli tymczasowej i użyć do usunięcia operacji. Dzięki temu masz lepszą kontrolę nad tym, co usuwasz.

Ta operacja nie powinna zakończyć się niepowodzeniem:

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
Paweł Tajs
źródło
2
Chociaż usunięcie dużej liczby wierszy STOCK_ARTICLES prawdopodobnie spowoduje gorsze wyniki niż inne opcje ze względu na zbudowanie tabeli tymczasowej (w przypadku małej liczby wierszy różnica prawdopodobnie nie będzie znacząca). Należy również stosować odpowiednie dyrektywy dotyczące transakcji, aby upewnić się, że trzy instrukcje są wykonywane jako jednostka atomowa, jeśli równoczesny dostęp nie jest niemożliwy, w przeciwnym razie można zobaczyć błędy jako nowe INVENTORY_ITEMSdodawane między dwiema DELETEs.
David Spillett
1

Zetknąłem się również z tym problemem i udało mi się go rozwiązać. Oto moja sytuacja:

W moim przypadku mam bazę danych służącą do raportowania analizy (MYTARGET_DB), która pobiera z systemu źródłowego (MYSOURCE_DB). Niektóre tabele „MYTARGET_DB” są unikalne dla tego systemu, a dane są tam tworzone i zarządzane; Większość tabel pochodzi z „MYSOURCE_DB” i istnieje zadanie, które usuwa / wstawia dane do „MYTARGET_DB” z „MYSOURCE_DB”.

Jedna z tabel przeglądowych [PRODUKT] pochodzi z ŹRÓDŁA, aw tabeli TARGET znajduje się tabela danych [InventoryOutsourced]. Tabele zawierają integralność referencyjną. Więc kiedy próbuję uruchomić usuwanie / wstawianie, otrzymuję ten komunikat.

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'.

Obejście, które stworzyłem, polega na wstawianiu danych do zmiennej tabeli [@tempTable] z [InventoryOutsourced], usuwaniu danych w [InventoryOutsourced], uruchamianiu zadań synchronizacji, wstawianiu do [InventoryOutsourced] z [@tempTable]. Utrzymuje to integralność na miejscu, a unikalne gromadzenie danych jest również zachowywane. Który jest najlepszy z obu światów. Mam nadzieję że to pomoże.

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
Arkusze Sherlocka
źródło
0

Nie w pełni przetestowałem, ale coś takiego powinno działać.

--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);
Scott Hodgin
źródło