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

INVENTORY_ITEMS의 3 개의 FK는 각각 다른 테이블의 "ID"열을 참조합니다.

여기에 관련된 테이블은 STOCK_ARTICLE 및 INVENTORY_ITEMS입니다.

이제 위에서 언급 한 데이터베이스 를 다른 데이터베이스 (OTHER_DB) 와 "동기화"하는 여러 단계 (SQL 스크립트)로 구성된 SQL 작업이 있습니다 . 이 작업의 단계 중 하나는 "정리"입니다. 동일한 데이터베이스 ID를 가진 다른 데이터베이스에 해당 레코드가없는 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). 단계가 실패했습니다.

따라서 문제는 INVENTORY_ITEMS에서 참조 할 때 STOCK_ARTICLES에서 레코드를 삭제할 수 없다는 것입니다. 그러나이 정리가 작동해야합니다. 즉, STOCK_ITEMS에서 삭제해야 할 레코드를 먼저 식별하기 위해 정리 스크립트를 확장해야하지만 해당 ID가 INVENTORY_ITEMS 내부에서 참조되기 때문에 정리 스크립트를 확장해야합니다. 그런 다음 먼저 INVENTORY_ITEMS 내의 레코드를 삭제 한 후 STOCK_ARTICLES 내의 레코드를 삭제해야합니다. 내가 맞아? 그러면 SQL 코드는 어떻게 생겼습니까?

감사합니다.

답변:


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

이는 간단한 쿼리 나 단일 재고 항목을 삭제하는 데는 좋지만 delete 문 에 매우 비효율적 인 계획을 생성 할 수 있는 WHERE NOT EXISTS절 중첩이 포함되어 WHERE IN있으므로 현실적인 데이터 세트 크기로 테스트하고 필요한 경우 쿼리를 다시 정렬하십시오.

또한 트랜잭션 명령문을 참고하십시오. 두 삭제가 모두 완료되었는지 또는 두 가지 모두 완료되지 않도록하십시오. 트랜잭션 내에서 작업이 이미 진행중인 경우 현재 트랜잭션 및 오류 처리 프로세스와 일치하도록이를 변경해야합니다.

2 : 사용 ON DELETE 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 % 제거는 아님)입니다. 캐스케이드는 부모와 모든 자손 사이에 하나의 경로 만있는 경우 여러 부모 / 자식 / 할아버지 / ... 레벨 에서 작동 할 수도 있습니다 (이것이 작동하지 않을 수있는 예는 "다중 캐스케이드 경로"검색).

참고 : 저와 다른 많은 사람들은 계단식 삭제가 위험하다고 생각하므로이 옵션을 사용하는 경우 데이터베이스 디자인에 문서를 올바르게 문서화하는 것이 매우 중요하므로 나중에 다른 개발자가 위험을 넘어 가지 않도록하십시오 . 이러한 이유로 가능한 경우 계단식 삭제를 피합니다.

누군가가 삭제하고 다시 행을 대신 사용하여 데이터를 업데이트 할 때 계단식 삭제로 인해 발생하는 일반적인 문제는 UPDATEMERGE. 이것은 종종 "이미 존재하는 행을 갱신하고, 존재하지 않는 행을 삽입합니다"(때때로 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 문이 자식 행으로 계단식으로 연결되고 insert 문이 행을 다시 만들지 않으므로 부모 테이블을 업데이트하는 동안 실수로 자식 테이블의 데이터가 손실된다는 것입니다.

요약

예, 먼저 하위 행을 삭제해야합니다.

다른 옵션이 있습니다 : ON DELETE CASCADE.

그러나 ON DELETE CASCADE위험 할 수 있으므로 주의해서 사용하십시오.

사이드 참고 : 사용 MERGE(또는 UPDATE- 및 - INSERT어디에서 MERGE사용할 수 없습니다) 당신이 필요로 할 때 UPSERT동작을 하지 DELETE - 일 - 교체 - 뿐인데 INSERT사용하는 다른 사용자에 의해 규정 함정에 빠지지 않도록하기 위해 ON DELETE CASCADE.


2

ID를 한 번만 삭제하고 임시 테이블에 저장 한 후 조작을 삭제하는 데 사용할 수 있습니다. 그런 다음 삭제중인 대상을 더 잘 제어 할 수 있습니다.

이 작업은 실패하지 않아야합니다.

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_ITEMSDELETEs 사이에 오류가 새로 추가 된 것으로 볼 수 있습니다 .
David Spillett

1

또한이 문제가 발생하여 해결할 수있었습니다. 내 상황은 다음과 같습니다.

필자의 경우 소스 시스템 (MYSOURCE_DB)에서 가져온 분석 (MYTARGET_DB)을보고하는 데 사용되는 데이터베이스가 있습니다. 'MYTARGET_DB'테이블 중 일부는 해당 시스템에 고유하며 데이터가 작성 및 관리됩니다. 대부분의 테이블은 'MYSOURCE_DB'에 있으며 'MYSOURCE_DB'에서 'MYTARGET_DB'에 데이터를 삭제 / 삽입하는 작업이 있습니다.

조회 테이블 [PRODUCT] 중 하나가 소스에서 가져온 것이며 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'.

내가 만든 해결 방법은 [InventoryOutsourced]에서 [@tempTable] 테이블 변수에 데이터를 삽입하고 [InventoryOutsourced]에서 데이터를 삭제하고 동기화 작업을 실행하고 [@tempTable]에서 [InventoryOutsourced]에 삽입하는 것입니다. 이것은 무결성을 유지하고 고유 한 데이터 수집도 유지됩니다. 어느 쪽이 세상에서 가장 좋은가. 도움이 되었기를 바랍니다.

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

나는 완전히 테스트하지는 않았지만 이와 같은 것이 작동해야합니다.

--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);
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.