DELETE 语句与 REFERENCE 约束冲突

der*_*aso 10 sql-server delete referential-integrity

我的情况是这样的:

表 STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME
Run Code Online (Sandbox Code Playgroud)

表位置:

ID *[PK]*
LOCATION_NAME
Run Code Online (Sandbox Code Playgroud)

表 WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME
Run Code Online (Sandbox Code Playgroud)

表 INVENTORY_ITEMS:

ID *[PK]*
ITEM_NAME
STOCK_ARTICLE *[FK]*
LOCATION *[FK]*
WORK_PLACE *[FK]*
Run Code Online (Sandbox Code Playgroud)

显然,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)
Run Code Online (Sandbox Code Playgroud)

但是这一步总是失败:

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 代码会是什么样子呢?

谢谢你。

Dav*_*ett 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
Run Code Online (Sandbox Code Playgroud)

这适用于简单查询或删除单个库存项目,但鉴于您的 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
Run Code Online (Sandbox Code Playgroud)

这里的一个优点是删除是一个原子语句,减少了(尽管,像往常一样,不是 100% 删除)担心事务和锁定设置的需要。如果父级和所有后代之间只有一条路径,级联甚至可以在多个父级/子级/孙子级/...级别上运行(搜索“多个级联路径”以获取可能不起作用的示例)。

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

级联删除导致的一个常见问题是当有人通过删除和重新创建行而不是使用UPDATE或来更新数据时MERGE。这在需要“更新已经存在的行,插入那些不存在的行”(有时称为 UPSERT 操作)的情况下经常出现,并且人们不知道该MERGE语句会发现它更容易做到:

DELETE <all rows that match IDs in the new data>
INSERT <all rows from the new data>
Run Code Online (Sandbox Code Playgroud)

-- 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
Run Code Online (Sandbox Code Playgroud)

这里的问题是 delete 语句将级联到子行,而 insert 语句不会重新创建它们,因此在更新父表时,您不小心丢失了子表中的数据。

概括

是的,您必须先删除子行。

还有一个选择:ON DELETE CASCADE

ON DELETE CASCADE可能是危险的,所以要小心使用。

旁注:在需要操作时使用MERGE(或UPDATE-and- INSERTwhereMERGE不可用)UPSERT而不是 DELETE-then-replace-with-INSERT以避免落入其他人使用ON DELETE CASCADE.


Sco*_*red 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);
Run Code Online (Sandbox Code Playgroud)