我一直在分析一个与特别慢的删除操作相关的系统中反复出现的“错误报告”(性能问题)。长话短说:似乎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分钟才能运行,对于无数据的小部件!
在完成执行计划后,我终于能够选择AnvilTestData
和WidgetHistoryDetails
删除作为成本最高的子操作。所以我尝试关闭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>小部件,没有太多历史或测试数据。
为了绝对清楚,从CASCADE
到WidgetHistory
仍有WidgetHistoryDetails
,其中扇出最高,我只删除了来自Widgets
的那个。
级联关系的进一步“扁平化”导致逐渐减少戏剧性但仍然明显的加速,一旦删除所有级联删除较大的表,删除新小部件几乎是瞬间的并替换为显式删除。
我在每次测试前都使用DBCC DROPCLEANBUFFERS
和DBCC FREEPROCCACHE
。我已经禁用了可能导致进一步减速的所有触发器(尽管这些触发器会出现在执行计划中)。我也正在测试较旧的小部件,并注意到那里的显着加速;过去需要花费5分钟的删除现在需要20-40秒。
现在我是“SELECT is not broken”理念的热心支持者,但除了{{1}的压抑,令人难以置信的低效率之外,似乎没有任何合理的解释。关系。
所以,我的问题是:
这是SQL Server中DRI的已知问题吗?(我似乎无法在Google或SO中找到任何此类内容的引用;我怀疑答案不是。)
如果没有,是否有其他解释我正在看到的行为?
如果这是一个已知问题,为什么会出现问题?我可以使用更好的解决方法吗?
答案 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
如果您忘记在引用列上创建索引,上帝会帮助您。