Die DELETE-Anweisung steht in Konflikt mit der REFERENCE-Einschränkung

10

Meine Situation sieht so aus:

Tabelle STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

Tabelle STANDORT:

ID *[PK]*
LOCATION_NAME

Tabelle WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

Tabelle INVENTORY_ITEMS:

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

Die 3 FKs in INVENTORY_ITEMS verweisen offensichtlich auf die "ID" -Spalten in den jeweiligen anderen Tabellen.

Die relevanten Tabellen hier sind STOCK_ARTICLE und INVENTORY_ITEMS.

Jetzt gibt es einen SQL-Job, der aus mehreren Schritten (SQL-Skripten) besteht, die die oben erwähnte Datenbank mit einer anderen Datenbank (OTHER_DB) "synchronisieren" . Einer der Schritte in diesem Job ist das "Aufräumen". Es löscht alle Datensätze aus STOCK_ITEMS, wenn in der anderen Datenbank kein entsprechender Datensatz mit derselben ID vorhanden ist. Es sieht aus wie das:

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)

Dieser Schritt schlägt jedoch immer fehl mit:

Die DELETE-Anweisung stand in Konflikt mit der REFERENCE-Einschränkung "FK_INVENTORY_ITEMS_STOCK_ARTICLES". Der Konflikt trat in der Datenbank "FIRST_DB", Tabelle "dbo.INVENTORY_ITEMS", Spalte 'STOCK_ARTICLES' auf. [SQLSTATE 23000] (Fehler 547) Die Anweisung wurde beendet. [SQLSTATE 01000] (Fehler 3621). Der Schritt ist fehlgeschlagen.

Das Problem ist also, dass keine Datensätze aus STOCK_ARTICLES gelöscht werden können, wenn sie von INVENTORY_ITEMS referenziert werden. Aber diese Bereinigung muss funktionieren. Das bedeutet, dass ich das Bereinigungsskript wahrscheinlich erweitern muss, damit es zuerst die Datensätze identifiziert, die aus STOCK_ITEMS gelöscht werden sollen, dies jedoch nicht kann, da auf die entsprechende ID in INVENTORY_ITEMS verwiesen wird. Dann sollte es zuerst diese Datensätze in INVENTORY_ITEMS löschen und danach die Datensätze in STOCK_ARTICLES löschen. Habe ich recht? Wie würde der SQL-Code dann aussehen?

Danke.

derwodaso
quelle

Antworten:

13

Das ist der springende Punkt bei Fremdschlüsseleinschränkungen: Sie verhindern, dass Sie Daten löschen, auf die an anderer Stelle verwiesen wird, um die referenzielle Integrität aufrechtzuerhalten.

Es gibt zwei Möglichkeiten:

  1. Löschen Sie zuerst die Zeilen von INVENTORY_ITEMSund dann die Zeilen von STOCK_ARTICLES.
  2. Verwenden Sie ON DELETE CASCADEfür die in der Schlüsseldefinition.

1: Löschen in der richtigen Reihenfolge

Der effizienteste Weg, dies zu tun, hängt von der Komplexität der Abfrage ab, die entscheidet, welche Zeilen gelöscht werden sollen. Ein allgemeines Muster könnte sein:

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

Dies ist in Ordnung für einfache Abfragen oder zum Löschen eines einzelnen Lagerartikels. Da Ihre Löschanweisung jedoch eine WHERE NOT EXISTSKlausel enthält, die eine Verschachtelung enthält WHERE IN, kann dies zu einem sehr ineffizienten Plan führen. Testen Sie daher mit einer realistischen Datensatzgröße und ordnen Sie die Abfrage bei Bedarf neu an.

Beachten Sie auch die Transaktionsanweisungen: Sie möchten sicherstellen, dass beide Löschvorgänge abgeschlossen sind oder keiner von beiden. Wenn der Vorgang bereits innerhalb einer Transaktion ausgeführt wird, müssen Sie dies offensichtlich ändern, um ihn an Ihren aktuellen Transaktions- und Fehlerbehandlungsprozess anzupassen.

2: Verwenden ON DELETE CASCADE

Wenn Sie Ihrem Fremdschlüssel die Kaskadenoption hinzufügen, erledigt SQL Server dies automatisch für Sie und entfernt Zeilen aus INVENTORY_ITEMS, um die Einschränkung zu erfüllen, dass sich nichts auf die zu löschenden Zeilen beziehen sollte. Fügen Sie ON DELETE CASCADEder FK-Definition einfach Folgendes hinzu :

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

Ein Vorteil hierbei ist, dass das Löschen eine atomare Anweisung ist, die die Notwendigkeit verringert, sich um Transaktions- und Sperreinstellungen zu kümmern (obwohl dies wie üblich nicht zu 100% entfernt wird). Die Kaskade kann sogar über mehrere Ebenen Eltern / Kind / Enkel / ... betrieben werden, wenn nur ein Pfad zwischen Eltern und allen Nachkommen vorhanden ist (suchen Sie nach "mehreren Kaskadenpfaden", um Beispiele dafür zu finden, wo dies möglicherweise nicht funktioniert).

HINWEIS: Ich und viele andere halten kaskadierte Löschvorgänge für gefährlich. Wenn Sie diese Option verwenden, müssen Sie sie in Ihrem Datenbankdesign sorgfältig dokumentieren, damit Sie und andere Entwickler später nicht über die Gefahr stolpern . Aus diesem Grund vermeide ich das Kaskadieren von Löschungen, wo immer dies möglich ist.

Ein häufiges Problem bei kaskadierten Löschvorgängen besteht darin, dass jemand Daten aktualisiert, indem er Zeilen löscht und neu erstellt, anstatt UPDATEoder zu verwenden MERGE. Dies tritt häufig dort auf, wo "Aktualisieren der bereits vorhandenen Zeilen, Einfügen der nicht vorhandenen" (manchmal als UPSERT-Operation bezeichnet) erforderlich ist und Personen, die die MERGEAnweisung nicht kennen, dies einfacher tun:

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

als

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

Das Problem hierbei ist, dass die delete-Anweisung in untergeordnete Zeilen kaskadiert und die insert-Anweisung diese nicht neu erstellt. Wenn Sie also die übergeordnete Tabelle aktualisieren, verlieren Sie versehentlich Daten aus den untergeordneten Tabellen.

Zusammenfassung

Ja, Sie müssen zuerst die untergeordneten Zeilen löschen.

Es gibt noch eine andere Option : ON DELETE CASCADE.

Aber ON DELETE CASCADEkann gefährlich sein , so mit Vorsicht verwendet werden .

Randnotiz: Verwenden Sie MERGE(oder UPDATE-und- INSERTwo MERGEnicht verfügbar), wenn Sie eine UPSERTOperation benötigen, und ersetzen Sie sie nicht DELETE durch, um INSERTzu vermeiden, dass Sie in Fallen fallen, die von anderen Personen gelegt wurden ON DELETE CASCADE.

David Spillett
quelle
2

Sie können IDs nur einmal löschen lassen, sie in einer temporären Tabelle speichern und zum Löschen von Vorgängen verwenden. Dann haben Sie eine bessere Kontrolle darüber, was Sie löschen.

Dieser Vorgang sollte nicht fehlschlagen:

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
quelle
2
Wenn Sie jedoch eine große Anzahl von STOCK_ARTICLES-Zeilen löschen, ist die Leistung aufgrund der Erstellung der temporären Tabelle wahrscheinlich schlechter als bei den anderen Optionen (bei einer kleinen Anzahl von Zeilen ist der Unterschied wahrscheinlich nicht signifikant). Achten Sie auch darauf, geeignete Transaktionsanweisungen zu verwenden, um sicherzustellen, dass die drei Anweisungen als atomare Einheit ausgeführt werden, wenn ein gleichzeitiger Zugriff nicht unmöglich ist. Andernfalls können Fehler als neu INVENTORY_ITEMSzwischen den beiden DELETEs hinzugefügt werden .
David Spillett
1

Ich bin auch auf dieses Problem gestoßen und konnte es beheben. Hier ist meine Situation:

In meinem Fall habe ich eine Datenbank, die zum Melden einer Analyse verwendet wird (MYTARGET_DB), die aus einem Quellsystem (MYSOURCE_DB) abgerufen wird. Einige der 'MYTARGET_DB'-Tabellen sind für dieses System eindeutig, und Daten werden dort erstellt und verwaltet. Die meisten Tabellen stammen aus 'MYSOURCE_DB' und es gibt einen Job, der die Daten aus 'MYSOURCE_DB' in 'MYTARGET_DB' löscht / einfügt.

Eine der Nachschlagetabellen [PRODUCT] stammt aus der SOURCE, und im TARGET ist eine Datentabelle [InventoryOutsourced] gespeichert. In den Tabellen ist eine referenzielle Integrität vorgesehen. Wenn ich also versuche, das Löschen / Einfügen auszuführen, wird diese Meldung angezeigt.

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

Die von mir erstellte Problemumgehung besteht darin, Daten aus [InventoryOutsourced] in die Tabellenvariable [@tempTable] einzufügen, Daten in [InventoryOutsourced] zu löschen, die Synchronisierungsjobs auszuführen und aus [@tempTable] in [InventoryOutsourced] einzufügen. Dadurch bleibt die Integrität erhalten, und die eindeutige Datenerfassung bleibt ebenfalls erhalten. Welches ist das Beste aus beiden Welten. Hoffe das hilft.

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

Ich habe noch nicht vollständig getestet, aber so etwas sollte funktionieren.

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