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ジョブがあります。このジョブ内のステップの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コードはそのときどのように見えますか?

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

回答:


13

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

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

これは単純なクエリや単一の在庫アイテムの削除にはWHERE NOT EXISTS問題ありWHERE INませんが、deleteステートメントにネストされた句が含まれていると非常に非効率的な計画になる可能性があるため、現実的なデータセットサイズでテストし、必要に応じてクエリを再配置します。

また、トランザクションステートメントにも注意してください。削除が両方とも完了したか、どちらも完了していないことを確認したい場合。操作がトランザクション内で既に発生している場合は、現在のトランザクションとエラー処理プロセスに一致するように、これを変更する必要があることは明らかです。

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%削除されるわけではありませんが)削減されることです。親とすべての子孫の間にパスが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-と- INSERTMERGE利用できない場合)を使用します。-then-replace-with- UPSERTではなく 、を使用して他の人が仕掛けた罠に陥らないようにします。DELETEINSERTON DELETE CASCADE


2

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
ただし、STOCK_ARTICLES行を大量に削除すると、一時テーブルが作成されるため、他のオプションよりもパフォーマンスが低下する可能性があります(行数が少ない場合、違いはそれほど大きくありません)。また、適切なトランザクションディレクティブを使用して、同時アクセスが不可能でない場合に3つのステートメントがアトミックユニットとして実行されるように注意してください。そうしないとINVENTORY_ITEMS、2つのDELETEの間に新しいものが追加されるため、エラーが発生する可能性があります。
David Spillett、2016年

1

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

私の場合、分析のレポートに使用するデータベース(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

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.