Instrução DELETE em conflito com a restrição REFERENCE

10

Minha situação é assim:

Tabela STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

Tabela LOCALIZAÇÃO:

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

Os 3 FKs em INVENTORY_ITEMS referenciam as colunas "ID" nas respectivas tabelas, obviamente.

As tabelas relevantes aqui são STOCK_ARTICLE e INVENTORY_ITEMS.

Agora, há um trabalho SQL que consiste em várias etapas (scripts SQL) que "sincronizam" o banco de dados mencionado acima com outro banco de dados (OTHER_DB). Uma das etapas deste trabalho é para "limpeza". Exclui todos os registros de STOCK_ITEMS onde não há registro correspondente no outro banco de dados com o mesmo ID. Se parece com isso:

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)

Mas esta etapa sempre falha com:

A instrução DELETE entra em conflito com a restrição REFERENCE "FK_INVENTORY_ITEMS_STOCK_ARTICLES". O conflito ocorreu no banco de dados "FIRST_DB", tabela "dbo.INVENTORY_ITEMS", coluna 'STOCK_ARTICLES'. [SQLSTATE 23000] (Erro 547) A instrução foi finalizada. [SQLSTATE 01000] (Erro 3621). A etapa falhou.

Portanto, o problema é que ele não pode excluir registros de STOCK_ARTICLES quando eles são referenciados por INVENTORY_ITEMS. Mas essa limpeza precisa funcionar. O que significa que eu provavelmente tenho que estender o script de limpeza para que ele primeiro identifique os registros que devem ser excluídos de STOCK_ITEMS, mas não porque o ID correspondente é mencionado de dentro de INVENTORY_ITEMS. Em seguida, ele deve primeiro excluir esses registros dentro de INVENTORY_ITEMS e depois excluir os registros dentro de STOCK_ARTICLES. Estou certo? Como seria o código SQL então?

Obrigado.

derwodaso
fonte

Respostas:

13

Esse é o ponto principal das restrições de chave estrangeira: elas impedem a exclusão de dados que são referidos em outros lugares, a fim de manter a integridade referencial.

Existem duas opções:

  1. Exclua as linhas do INVENTORY_ITEMSprimeiro e, em seguida , as linhas de STOCK_ARTICLES.
  2. Use ON DELETE CASCADEpara na definição de chave.

1: Exclusão na ordem correta

A maneira mais eficiente de fazer isso varia dependendo da complexidade da consulta que decide quais linhas excluir. Um padrão geral pode ser:

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

Isso é bom para consultas simples ou para excluir um único item de estoque, mas, como a instrução delete contém uma WHERE NOT EXISTScláusula de aninhamento que WHERE INpode produzir um plano muito ineficiente, teste com um tamanho realista do conjunto de dados e reorganize a consulta, se necessário.

Observe também as instruções da transação: você deseja garantir que as exclusões sejam concluídas ou nenhuma delas. Se a operação já estiver ocorrendo dentro de uma transação, você obviamente precisará alterar isso para corresponder ao seu processo atual de transação e tratamento de erros.

2: Use ON DELETE CASCADE

Se você adicionar a opção em cascata à sua chave estrangeira, o SQL Server fará isso automaticamente, removendo as linhas de INVENTORY_ITEMSpara satisfazer a restrição de que nada deve se referir às linhas que você está excluindo. Basta adicionar ON DELETE CASCADEà definição FK da seguinte forma:

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

Uma vantagem aqui é que a exclusão é uma instrução atômica que reduz (embora, como sempre, não remova 100%) a necessidade de se preocupar com as configurações de transação e bloqueio. A cascata pode até operar em vários níveis de pai / filho / neto / ... se houver apenas um caminho entre pai e todos os descendentes (procure "vários caminhos em cascata" para exemplos de onde isso pode não funcionar).

NOTA: Eu e muitos outros consideramos as exclusões em cascata perigosas; portanto, se você usar esta opção, tenha muito cuidado para documentá-la adequadamente no design do banco de dados, para que você e outros desenvolvedores não troquem o perigo posteriormente . Evito exclusões em cascata sempre que possível por esse motivo.

Um problema comum causado com exclusões em cascata é quando alguém atualiza dados descartando e recriando linhas em vez de usar UPDATEor MERGE. Isso geralmente é visto onde "atualizar as linhas que já existem, inserir aquelas que não existem" (às vezes chamada de operação UPSERT) é necessário e as pessoas que desconhecem a MERGEinstrução acham mais fácil:

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

do que

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

O problema aqui é que a instrução de exclusão cascateará em linhas filhas e a instrução de inserção não as recriará. Portanto, ao atualizar a tabela pai, você acidentalmente perde dados das tabelas filho.

Sumário

Sim, você deve excluir as linhas filho primeiro.

Não há outra opção: ON DELETE CASCADE.

Mas ON DELETE CASCADEpode ser perigoso , portanto, use com cuidado.

Nota lateral: use MERGE(ou UPDATE- e - INSERTonde MERGEnão estiver disponível) quando precisar de uma UPSERToperação, não DELETE substitua por - INSERTpara evitar cair nas armadilhas colocadas por outras pessoas ON DELETE CASCADE.

David Spillett
fonte
2

Você pode obter IDs para excluir apenas uma vez, armazená-los em tabela temporária e usar para excluir operações. Então você tem melhor controle do que está excluindo.

Esta operação não deve falhar:

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
fonte
2
Embora, ao excluir um grande número de linhas STOCK_ARTICLES, é provável que ele tenha um desempenho pior do que as outras opções devido à criação da tabela temporária (para pequenos números de linhas, é improvável que a diferença seja significativa). Também tome cuidado ao usar diretivas de transação apropriadas para garantir que as três instruções sejam executadas como uma unidade atômica se o acesso simultâneo não for impossível; caso contrário, você poderá ver erros como novos INVENTORY_ITEMSsendo adicionados entre os dois DELETEs.
precisa
1

Também encontrei este problema e resolvi-o. Aqui está a minha situação:

No meu caso, tenho um banco de dados usado para relatar uma análise (MYTARGET_DB), que é extraída de um sistema de origem (MYSOURCE_DB). Algumas das tabelas 'MYTARGET_DB' são exclusivas para esse sistema e os dados são criados e gerenciados lá; A maioria das tabelas é de 'MYSOURCE_DB' e existe um trabalho que exclui / insere os dados em 'MYTARGET_DB' de 'MYSOURCE_DB'.

Uma das tabelas de pesquisa [PRODUCT] é da SOURCE e existe uma tabela de dados [InventoryOutsourced] armazenada no TARGET. Há integridade referencial projetada nas tabelas. Então, quando tento executar a exclusão / inserção, recebo esta mensagem.

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

A solução alternativa que criei é inserir dados na variável de tabela [@tempTable] de [InventoryOutsourced], excluir dados em [InventoryOutsourced], executar os trabalhos de sincronização, inserir em [InventoryOutsourced] em [@tempTable]. Isso mantém a integridade no lugar e a coleta de dados exclusiva também é mantida. Qual é o melhor dos dois mundos. Espero que isto ajude.

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

Ainda não testei completamente, mas algo assim deve funcionar.

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