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。

现在有一个SQL作业,包含几个步骤(SQL脚本),这些步骤将上述数据库与另一个数据库(OTHER_DB)“同步” 。此作业中的步骤之一是“清理”。它会从STOCK_ITEMS中删除所有记录,而另一个数据库中没有对应的具有相同ID的记录。看起来像这样:

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代码将如何?

谢谢。

Answers:


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%删除)担心事务和锁定设置的需要。如果父级和所有子级之间只有一条路径,级联甚至可以在多个父级/子级/ 孙子级/ ...级上运行(搜索“多个级联路径”以获取可能不起作用的示例)。

注意:我和其他许多人都认为级联删除是危险的,因此,如果您使用此选项,请非常小心地在数据库设计中正确记录它,以免您和其他开发人员以后再碰到危险。由于这个原因,我尽量避免级联删除。

级联删除引起的常见问题是有人通过删除并重新创建行而不是使用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-and- INSERTMERGE不可用的地方)UPSERT而不是 DELETE -then-place-with-,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_ITEMS这两个语句之间添加了新的语句DELETE
David Spillett

1

我也遇到了这个问题,并且能够解决它。这是我的情况:

就我而言,我有一个数据库用于报告分析数据(MYTARGET_DB),该数据库是从源系统(MYSOURCE_DB)提取的。一些“ MYTARGET_DB”表对于该系统是唯一的,并且数据是在该系统中创建和管理的。大多数表来自“ MYSOURCE_DB”,并且有一个作业可以将数据从“ MYSOURCE_DB”中删除/插入到“ MYTARGET_DB”中。

查找表之一[PRODUCT]来自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]。这样可以保持完整性,并保留唯一的数据收集。两全其美。希望这可以帮助。

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);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.