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

时间:2010-03-26 17:10:04

标签: sql-server performance sql-server-2008 cascade

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

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

架构看起来像这样:

Widgets
   |
   +--- Anvils [1:1]
   |    |
   |    +--- AnvilTestData [1:N]
   |
   +--- WidgetHistory (1:N)
        |
        +--- WidgetHistoryDetails (1:N)

列定义如下所示:

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, ...)

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

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

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

关系都已编入索引,统计信息是最新的。普通查询很快。除了删除之外,系统往往会非常流畅地哼唱。

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

DELETE FROM Widgets
WHERE WidgetID = @WidgetID

非常简单,无害的删除... 需要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

这两种“优化”都带来了显着的加速,每次加速都会在执行时间内缩短近一分钟,因此最初的2分钟删除现在大约需要5-10秒 - 至少对于 new < / em>小部件,没有太多历史或测试数据。

为了绝对清楚,从CASCADEWidgetHistory仍有WidgetHistoryDetails,其中扇出最高,我只删除了来自Widgets的那个。

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

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

现在我是“SELECT is not broken”理念的热心支持者,但除了{{1}的压抑,令人难以置信的低效率之外,似乎没有任何合理的解释。关系。

所以,我的问题是:

  • 这是SQL Server中DRI的已知问题吗?(我似乎无法在Google或SO中找到任何此类内容的引用;我怀疑答案不是。)

  • 如果没有,是否有其他解释我正在看到的行为?

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

1 个答案:

答案 0 :(得分: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)

,这个查询:

DELETE
FROM    t_g
WHERE   id > 50000

及其计划:

  |--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)

首先,SQL Server删除t_g的记录,然后加入用t_p删除的记录,并从后者删除,最后,将t_p中删除的记录与{{ 1}}并从t_c删除。

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

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