10

我一直在分析我们的一个系统中与特别慢的删除操作相关的重复出现的“错误报告”(性能问题)。长话短说:似乎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, ...)

没什么太可怕的,真的。AWidget可以是不同的类型,anAnvil是特殊类型,因此关系是 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 秒 - 至少对于的小部件来说,没有太多历史记录或测试数据。

为了绝对清楚,仍然有一个CASCADEfrom WidgetHistoryto WidgetHistoryDetails,扇出最高的地方,我只删除了一个 originating from Widgets

级联关系的进一步“扁平化”导致了逐渐不那么显着但仍然明显的加速,以至于一旦删除了对较大表的所有级联删除并替换为显式删除,删除新小部件几乎是瞬时的。

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

现在我是“SELECT ain't broken”哲学的热心支持者,但对于这种行为似乎没有任何合乎逻辑的解释,除了CASCADE DELETE关系的粉碎、令人难以置信的低效率。

所以,我的问题是:

  • 这是 SQL Server 中 DRI 的已知问题吗?(我似乎在 Google 或 SO 中找不到任何关于此类事情的参考资料;我怀疑答案是否定的。)

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

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

4

1 回答 1

9

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与从t_c删除的记录连接t_c

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

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

于 2010-03-26T17:25:43.630 回答