Istruzione DELETE in conflitto con il vincolo REFERENCE


10

La mia situazione è simile a questa:

Tabella STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

Tabella UBICAZIONE:

ID *[PK]*
LOCATION_NAME

Tabella WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

Tabella INVENTORY_ITEMS:

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

I 3 FK in INVENTORY_ITEMS fanno ovviamente riferimento alle colonne "ID" nelle rispettive altre tabelle.

Le tabelle pertinenti qui sono STOCK_ARTICLE e INVENTORY_ITEMS.

Ora esiste un processo SQL composto da diversi passaggi (script SQL) che "sincronizza" il database sopra menzionato con un altro database (OTHER_DB). Uno dei passaggi all'interno di questo lavoro è per la "pulizia". Elimina tutti i record da STOCK_ITEMS in cui non esiste alcun record corrispondente nell'altro database con lo stesso ID. Sembra così:

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)

Ma questo passaggio fallisce sempre con:

L'istruzione DELETE è in conflitto con il vincolo REFERENCE "FK_INVENTORY_ITEMS_STOCK_ARTICLES". Il conflitto si è verificato nel database "FIRST_DB", tabella "dbo.INVENTORY_ITEMS", colonna "STOCK_ARTICLES". [SQLSTATE 23000] (Errore 547) L'istruzione è stata terminata. [SQLSTATE 01000] (Errore 3621). Il passo è fallito.

Quindi il problema è che non può eliminare i record da STOCK_ARTICLES quando sono indicati da INVENTORY_ITEMS. Ma questa pulizia deve funzionare. Ciò significa che probabilmente dovrò estendere lo script di cleanup in modo che identifichi prima i record che dovrebbero essere eliminati da STOCK_ITEMS, ma non posso perché l'ID corrispondente è referenziato dall'interno di INVENTORY_ITEMS. Quindi dovrebbe prima eliminare quei record all'interno di INVENTORY_ITEMS e successivamente eliminare i record all'interno di STOCK_ARTICLES. Ho ragione? Come sarebbe il codice SQL allora?

Grazie.

Risposte:


13

Questo è il punto centrale dei vincoli delle chiavi esterne: ti impediscono di eliminare i dati a cui si fa riferimento altrove per mantenere l'integrità referenziale.

Esistono due opzioni:

  1. Elimina prima le righe INVENTORY_ITEMS, quindi le righe da STOCK_ARTICLES.
  2. Utilizzare ON DELETE CASCADEper la definizione chiave.

1: eliminazione nell'ordine corretto

Il modo più efficiente per farlo varia in base alla complessità della query che decide quali righe eliminare. Un modello generale potrebbe essere:

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

Questo va bene per semplici query o per eliminare un singolo articolo di scorta, ma dato la tua dichiarazione di eliminazione contiene una WHERE NOT EXISTSclausola di annidamento che al suo interno WHERE INpotrebbe produrre un piano molto inefficiente, quindi prova con una dimensione realistica del set di dati e riorganizza la query se necessario.

Nota anche le dichiarazioni di transazione: assicurati che entrambe le eliminazioni siano complete o nessuna delle due funzioni. Se l'operazione sta già avvenendo all'interno di una transazione, dovrai ovviamente modificarla in modo che corrisponda alla transazione corrente e al processo di gestione degli errori.

2: utilizzare ON DELETE CASCADE

Se aggiungi l'opzione a cascata alla tua chiave esterna, SQL Server lo farà automaticamente per te, rimuovendo le righe INVENTORY_ITEMSper soddisfare il vincolo che nulla dovrebbe fare riferimento alle righe che stai eliminando. Basta aggiungere ON DELETE CASCADEalla definizione FK in questo modo:

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

Un vantaggio qui è che l'eliminazione è un'istruzione atomica che riduce (sebbene, come al solito, non la rimozione al 100%) la necessità di preoccuparsi delle impostazioni di transazione e blocco. La cascata può persino operare su più livelli genitore / figlio / nipote / ... se esiste un solo percorso tra genitore e tutti i discendenti (cercare "percorsi multipli in cascata" per esempi di dove potrebbe non funzionare).

NOTA: io, e molti altri, ritengo che le eliminazioni in cascata siano pericolose, quindi se si utilizza questa opzione fare molta attenzione a documentarla correttamente nella progettazione del database in modo che tu e altri sviluppatori non inciampiate sul pericolo in seguito . Evito le eliminazioni a cascata laddove possibile per questo motivo.

Un problema comune causato dalle eliminazioni in cascata è quando qualcuno aggiorna i dati rilasciando e ricreando le righe invece di usare UPDATEo MERGE. Questo è spesso visto dove è necessario "aggiornare le righe già esistenti, inserire quelle che non lo fanno" (a volte chiamato operazione UPSERT) e le persone che non sono consapevoli dell'istruzione MERGEtrovano più facile fare:

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

di

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

Il problema qui è che l'istruzione delete passerà in cascata alle righe figlio e l'istruzione insert non le ricrea, quindi durante l'aggiornamento della tabella padre si perderanno accidentalmente i dati dalle tabelle figlio.

Sommario

Sì, devi prima eliminare le righe figlio.

C'è un'altra opzione: ON DELETE CASCADE.

Ma ON DELETE CASCADEpuò essere pericoloso , quindi usalo con cura.

Nota a margine: usare MERGE(o UPDATE-e-- INSERTdove MERGEnon è disponibile) quando è necessaria UPSERTun'operazione, non DELETE -poi-sostituire-con- INSERTper evitare di cadere in trappole posate da altre persone che usano ON DELETE CASCADE.


2

È possibile ottenere gli ID da eliminare una sola volta, archiviarli in una tabella temporanea e utilizzare per eliminare le operazioni. Quindi avrai un miglior controllo su cosa stai eliminando.

Questa operazione non dovrebbe fallire:

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
Tuttavia, se si elimina un numero elevato di righe STOCK_ARTICLES, è probabile che ciò vada peggio delle altre opzioni a causa della creazione della tabella temporanea (per i piccoli numeri di righe è improbabile che la differenza sia significativa). Prestare inoltre attenzione a utilizzare le direttive di transazione appropriate per garantire che le tre istruzioni vengano eseguite come unità atomica se l'accesso simultaneo non è impossibile, altrimenti si potrebbero vedere errori come nuovi INVENTORY_ITEMSaggiunti tra i due DELETE.
David Spillett,

1

Ho anche riscontrato questo problema e sono stato in grado di risolverlo. Ecco la mia situazione:

Nel mio caso, ho un database utilizzato per riportare un'analisi (MYTARGET_DB), che estrae da un sistema di origine (MYSOURCE_DB). Alcune delle tabelle "MYTARGET_DB" sono uniche per quel sistema e i dati vengono creati e gestiti lì; La maggior parte delle tabelle proviene da "MYSOURCE_DB" e esiste un processo che elimina / inserisce i dati in "MYTARGET_DB" da "MYSOURCE_DB".

Una delle tabelle di ricerca [PRODOTTO] proviene da SOURCE e esiste una tabella di dati [InventoryOutsourced] memorizzata in TARGET. C'è integrità referenziale progettata nelle tabelle. Quindi, quando provo a eseguire il comando di cancellazione / inserimento, ricevo questo messaggio.

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

La soluzione alternativa che ho creato è inserire i dati nella variabile di tabella [@tempTable] da [InventoryOutsourced], eliminare i dati in [InventoryOutsourced], eseguire i lavori di sincronizzazione, inserire in [InventoryOutsourced] da [@tempTable]. In questo modo viene mantenuta l'integrità e viene conservata anche la raccolta di dati univoci. Qual è il migliore dei due mondi. Spero che sia di aiuto.

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

Non ho ancora testato, ma qualcosa del genere dovrebbe funzionare.

--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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.