web-dev-qa-db-ja.com

DELETEステートメントがREFERENCE制約と競合しています

私の状況は次のようになります:

テーブル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です。

これで、上記のデータベースをanotherデータベース(OTHER_DB)と「同期」するいくつかの手順(SQLスクリプト)で構成されるSQLジョブがあります。このジョブ内のステップの1つは「クリーンアップ」です。同じ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コードはそのときどのように見えますか?

ありがとうございました。

10
derwodaso

それが外部キー制約の要点です。参照整合性を維持するために、他の場所で参照されているデータを削除することを止めます。

2つのオプションがあります。

  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

ここでの利点は、削除が1つのアトミックステートメントであり、トランザクションとロックの設定について心配する必要が(通常は100%削除されるわけではありませんが)削減されることです。カスケードは、複数の親/子/孫/ ...レベルifでも機能し、親とすべての子孫の間にはパスが1つしかありません(これが機能しない場合の例については、「複数のカスケードパス」を検索してください。

注:私や他の多くの人は、カスケード削除を危険だと考えているので、このオプションを使用する場合は、データベース設計に適切に文書化して、あなたや他の開発者がつまずかないように十分注意してください後で危険。このため、可能な限りカスケード削除を避けています。

カスケード削除で発生する一般的な問題は、UPDATEまたはMERGEを使用する代わりに、誰かが行を削除して再作成することによってデータを更新する場合です。これは、「既存の行を更新し、存在しない行を挿入する」(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- and -MERGEUPSERTを使用できない場合)を使用してください。 notDELETE- then-replace-with -INSERTを使用して、他の人がON DELETE CASCADE

13
David Spillett

IDを取得して削除するのは1回だけで、一時テーブルに保存して、操作の削除に使用できます。次に、何を削除するかをより適切に制御できます。

この操作は失敗しないはずです。

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
Paweł Tajs

私もこの問題に遭遇し、解決することができました。これが私の状況です:

私の場合、分析のレポートに使用するデータベース(MYTARGET_DB)があり、ソースシステム(MYSOURCE_DB)から取得しています。一部の「MYTARGET_DB」テーブルはそのシステムに固有であり、データはそこで作成および管理されます。ほとんどのテーブルは「MYSOURCE_DB」からのものであり、「MYSOURCE_DB」から「MYTARGET_DB」にデータを削除/挿入するジョブがあります。

ルックアップテーブル[PRODUCT]の1つがSOURCEからのものであり、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]に挿入することです。これにより、整合性が維持され、固有のデータ収集も保持されます。これは両方の長所の1つです。お役に立てれば。

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

私は完全にはテストしていませんが、このようなものが動作するはずです。

--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);
0
Scott Hodgin