SQL Server DRI(ON DELETE CASCADE)是否缓慢?

Aar*_*ght 7 sql-server performance cascade sql-server-2008

我一直在分析一个与特别慢的删除操作相关的系统中反复发生的"错误报告"(性能问题).长话短说:CASCADE DELETE关键似乎主要是负责任的,我想知道(a)这是否有意义,以及(b)为什么会这样.

我们有一个架构,比如说,小部件,那些位于相关表和相关相关表的大图的根部,等等.要非常清楚,主动不鼓励从此表中删除; 这是"核选择",用户并没有相反的幻想.然而,有时候必须这样做.

架构看起来像这样:

Widgets
   |
   +--- Anvils [1:1]
   |    |
   |    +--- AnvilTestData [1:N]
   |
   +--- WidgetHistory (1:N)
        |
        +--- WidgetHistoryDetails (1:N)
Run Code Online (Sandbox Code Playgroud)

列定义如下所示:

Widgets (WidgetID int PK, WidgetName varchar(50))
Anvils (AnvilID int PK, WidgetID int FK/IX/UNIQUE, ...)
AnvilTestData (AnvilID int FK/IX, TestID int, ...Test Data...)
WidgetHistory (HistoryID int PK, WidgetID int FK/IX, HistoryDate datetime, ...)
WidgetHistoryDetails (HistoryID int FK/IX, DetailType smallint, ...)
Run Code Online (Sandbox Code Playgroud)

真的,没什么可怕的.A Widget可以是不同的类型,a Anvil是特殊类型,因此关系是1:1(或更准确地说是1:0..1).然后是大量的数据 - 可能随着时间的推移AnvilTestData每次Anvil收集数千行,处理硬度,腐蚀,精确重量,锤子兼容性,可用性问题以及卡通头的冲击测试.

然后每个人Widget都有各种类型交易的漫长而枯燥的历史 - 生产,库存移动,销售,缺陷调查,RMA,维修,客户投诉等.单个小部件可能有10-20k的详细信息,或者根本没有,取决于它的年龄.

所以,不出所料,这里的CASCADE DELETE各个层面都存在关系.如果Widget需要删除,则意味着某些内容出现了严重错误,我们需要删除现有的小部件的任何记录,包括其历史记录,测试数据等.再次,核选项.

关系都是索引,统计数据是最新的.普通查询很快.除了删除之外,系统往往会非常流畅地哼唱.

最后,由于各种原因,我们只允许一次删除一个小部件,因此删除语句如下所示:

DELETE FROM Widgets
WHERE WidgetID = @WidgetID
Run Code Online (Sandbox Code Playgroud)

非常简单,无害的看起来删除... 运行需要2分钟,对于没有数据的小部件!

在完成执行计划之后,我终于能够选择AnvilTestDataWidgetHistoryDetails删除作为具有最高成本的子操作.所以我尝试关闭CASCADE(但保留实际的FK,只是设置它NO ACTION)并将脚本重写为非常类似于以下内容:

DECLARE @AnvilID int
SELECT @AnvilID = AnvilID FROM Anvils WHERE WidgetID = @WidgetID

DELETE FROM AnvilTestData
WHERE AnvilID = @AnvilID

DELETE FROM WidgetHistory
WHERE HistoryID IN (
    SELECT HistoryID
    FROM WidgetHistory
    WHERE WidgetID = @WidgetID)

DELETE FROM Widgets WHERE WidgetID = @WidgetID
Run Code Online (Sandbox Code Playgroud)

这两个"优化"都带来了显着的加速,每个加速都会在执行时间内缩短近一分钟,因此最初的2分钟删除现在需要大约5-10秒 - 至少对于新的小部件而言,没有太多的历史或测试数据.

只是要绝对清晰,还有一个是CASCADEWidgetHistoryWidgetHistoryDetails,其中扇出是最高的,我只能从删除的一个起源Widgets.

级联关系的进一步"扁平化"导致逐渐减少的戏剧性但仍然明显的加速,一旦删除所有级联删除较大的表并删除显式删除,删除新的小部件几乎是瞬时的.

我使用DBCC DROPCLEANBUFFERSDBCC FREEPROCCACHE每次测试之前.我已经禁用了可能导致进一步减速的所有触发器(尽管这些触发器会出现在执行计划中).我也正在测试较旧的小部件,并注意到那里的显着加速; 过去需要花费5分钟的删除现在需要20-40秒.

现在,我是"SELECT is not broken"哲学的热心支持者,但除了压榨,令人难以置信的CASCADE DELETE关系低效之外,似乎没有任何合理的解释.

所以,我的问题是:

  • 这是SQL Server中DRI的已知问题吗?(我似乎无法在谷歌或此处找到任何关于此类事物的引用;我怀疑答案是否定的.)

  • 如果没有,是否有另一种解释我所看到的行为?

  • 如果这是一个已知问题,为什么这是一个问题,我可以使用更好的解决方法吗?

Qua*_*noi 8

SQL Server最好是基于集合的操作,而CASCADE删除本质上是基于记录的.

SQL Server与其他服务器不同,它尝试优化基于集合的直接操作,但是,它只能在一个级别上运行.它需要在上层表中删除记录以删除较低级别表中的记录.

换句话说,级联操作可以向上运行,而您的解决方案可以向下运行,这更加基于集合和高效.

这是一个示例模式:

CREATE TABLE t_g (id INT NOT NULL PRIMARY KEY)

CREATE TABLE t_p (id INT NOT NULL PRIMARY KEY, g INT NOT NULL, CONSTRAINT fk_p_g FOREIGN KEY (g) REFERENCES t_g ON DELETE CASCADE)

CREATE TABLE t_c (id INT NOT NULL PRIMARY KEY, p INT NOT NULL, CONSTRAINT fk_c_p FOREIGN KEY (p) REFERENCES t_p ON DELETE CASCADE)

CREATE INDEX ix_p_g ON t_p (g)

CREATE INDEX ix_c_p ON t_c (p)
Run Code Online (Sandbox Code Playgroud)

,这个查询:

DELETE
FROM    t_g
WHERE   id > 50000
Run Code Online (Sandbox Code Playgroud)

及其计划:

  |--Sequence
       |--Table Spool
       |    |--Clustered Index Delete(OBJECT:([test].[dbo].[t_g].[PK__t_g__176E4C6B]), WHERE:([test].[dbo].[t_g].[id] > (50000)))
       |--Index Delete(OBJECT:([test].[dbo].[t_p].[ix_p_g]) WITH ORDERED PREFETCH)
       |    |--Sort(ORDER BY:([test].[dbo].[t_p].[g] ASC, [test].[dbo].[t_p].[id] ASC))
       |         |--Table Spool
       |              |--Clustered Index Delete(OBJECT:([test].[dbo].[t_p].[PK__t_p__195694DD]) WITH ORDERED PREFETCH)
       |                   |--Sort(ORDER BY:([test].[dbo].[t_p].[id] ASC))
       |                        |--Merge Join(Inner Join, MERGE:([test].[dbo].[t_g].[id])=([test].[dbo].[t_p].[g]), RESIDUAL:([test].[dbo].[t_p].[g]=[test].[dbo].[t_g].[id]))
       |                             |--Table Spool
       |                             |--Index Scan(OBJECT:([test].[dbo].[t_p].[ix_p_g]), ORDERED FORWARD)
       |--Index Delete(OBJECT:([test].[dbo].[t_c].[ix_c_p]) WITH ORDERED PREFETCH)
            |--Sort(ORDER BY:([test].[dbo].[t_c].[p] ASC, [test].[dbo].[t_c].[id] ASC))
                 |--Clustered Index Delete(OBJECT:([test].[dbo].[t_c].[PK__t_c__1C330188]) WITH ORDERED PREFETCH)
                      |--Table Spool
                           |--Sort(ORDER BY:([test].[dbo].[t_c].[id] ASC))
                                |--Hash Match(Inner Join, HASH:([test].[dbo].[t_p].[id])=([test].[dbo].[t_c].[p]))
                                     |--Table Spool
                                     |--Index Scan(OBJECT:([test].[dbo].[t_c].[ix_c_p]), ORDERED FORWARD)
Run Code Online (Sandbox Code Playgroud)

首先,SQL Server删除记录的t_g,然后加入与删除的记录t_p,并删除从后者,最后加入已删除的记录t_pt_c和删减t_c.

在这种情况下,单个三表连接会更有效,这就是您使用解决方法所做的事情.

如果它让你感觉更好,Oracle不会以任何方式优化级联操作:NESTED LOOPS如果你忘记在引用列上创建索引,它们总是如此,上帝会帮助你.