使用WHERE IN进行删除操作期间发生意外扫描


40

我有一个类似以下的查询:

DELETE FROM tblFEStatsBrowsers WHERE BrowserID NOT IN (
    SELECT DISTINCT BrowserID FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID IS NOT NULL
)

tblFEStatsBrowsers有553行。
tblFEStatsPaperHits已获得47.974.301行。

tblFEStatsBrowsers:

CREATE TABLE [dbo].[tblFEStatsBrowsers](
    [BrowserID] [smallint] IDENTITY(1,1) NOT NULL,
    [Browser] [varchar](50) NOT NULL,
    [Name] [varchar](40) NOT NULL,
    [Version] [varchar](10) NOT NULL,
    CONSTRAINT [PK_tblFEStatsBrowsers] PRIMARY KEY CLUSTERED ([BrowserID] ASC)
)

tblFEStatsPaperHits:

CREATE TABLE [dbo].[tblFEStatsPaperHits](
    [PaperID] [int] NOT NULL,
    [Created] [smalldatetime] NOT NULL,
    [IP] [binary](4) NULL,
    [PlatformID] [tinyint] NULL,
    [BrowserID] [smallint] NULL,
    [ReferrerID] [int] NULL,
    [UserLanguage] [char](2) NULL
)

tblFEStatsPaperHits上有一个聚集索引,其中不包含BrowserID。因此,执行内部查询将需要对tblFEStatsPaperHits进行全表扫描-完全可以。

当前,对tblFEStatsBrowsers中的每一行都执行了完整扫描,这意味着我已经对tblFEStatsPaperHits进行了553次全表扫描。

仅重写为“ WHERE EXISTS”不会改变计划:

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
)

但是,正如Adam Machanic所建议的那样,添加HASH JOIN选项确实可以实现最佳的执行计划(只需对tblFEStatsPaperHits进行一次扫描):

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
) OPTION (HASH JOIN)

现在,这不再是如何解决此问题的问题-我可以使用OPTION(哈希联接)或手动创建临时表。我更想知道为什么查询优化器会使用其当前执行的计划。

由于QO在BrowserID列上没有任何统计信息,因此我猜测它是假设最坏的值-5000万个不同的值,因此需要相当大的内存/ tempdb工作表。因此,最安全的方法是对tblFEStatsBrowsers中的每一行执行扫描。两个表中的BrowserID列之间没有外键关系,因此QO无法从tblFEStatsBrowsers中扣除任何信息。

听起来很简单,这是原因吗?

更新1
要提供一些统计信息:选项(
哈希联接):208.711逻辑读取(12次扫描)

选项(LOOP JOIN,HASH GROUP):
11.008.698逻辑读取(〜按浏览器ID扫描(339))

无选项:
11.008.775逻辑读取(〜按浏览器ID(339)进行扫描)

更新2
,所有人,很好的答案-谢谢!很难选择一个。尽管马丁是第一位,雷木思提供了一个出色的解决方案,但我不得不将其交给新西兰人,以便于在细节上投入精力:)


5
您是否可以按照将统计信息从一台服务器复制到另一台服务器的方式编写脚本,以便我们进行复制?
Mark Storey-Smith

2
@ MarkStorey史密斯当然- pastebin.com/9HHRPFgK 假设你在一个空数据库运行该脚本,这使我能够重现问题的查询,包括显示执行计划的时候。这两个查询都包含在脚本的末尾。
Mark S. Rasmussen 2012年

Answers:


61

“我更想知道为什么查询优化器会使用当前使用的计划。”

换句话说,问题是为什么与备选方案(其中有很多)相比,以下计划对优化程序而言看起来最便宜。

原计划

联接的内侧实际上对以下每个相关值运行以下形式的查询BrowserID

DECLARE @BrowserID smallint;

SELECT 
    tfsph.BrowserID 
FROM dbo.tblFEStatsPaperHits AS tfsph 
WHERE 
    tfsph.BrowserID = @BrowserID 
OPTION (MAXDOP 1);

纸命中扫描

请注意,估计的行数为185,220(而不是289,013),因为相等比较隐式排除NULL(除非ANSI_NULLSOFF)。上述计划的估计费用为206.8单位。

现在让我们添加一个TOP (1)子句:

DECLARE @BrowserID smallint;

SELECT TOP (1)
    tfsph.BrowserID 
FROM dbo.tblFEStatsPaperHits AS tfsph 
WHERE 
    tfsph.BrowserID = @BrowserID 
OPTION (MAXDOP 1);

有顶(1)

估计成本现在为0.00452单位。“顶部”物理运算符的添加将“顶部”运算符的行目标设置为1行。然后,问题就变成了如何为聚簇索引扫描得出“行目标”。也就是说,在一行与BrowserID谓词匹配之前,扫描应处理多少行?

可用的统计信息显示166个不同的BrowserID值(1 / [全部密度] = 1 / 0.006024096 = 166)。成本核算假设不同的值在物理行上均匀分布,因此“聚簇索引扫描”上的行目标设置为166.302(考虑到由于收集了抽样统计信息而导致的表基数变化)。

扫描预期的166行的估计成本不是很大(即使执行339次,每次更改一次BrowserID)-聚集索引扫描显示的估计成本为1.3219单位,显示了行目标的缩放效果。I / O和CPU的未缩放操作员成本分别显示为153.93152.8698

行目标按比例估算的费用

在实践中,这是非常不可能的从索引扫描的第166行(可以以任意顺序发生,他们将返回)将包含每个可能的一个BrowserID值。尽管如此,该DELETE计划的总成本为1.40921单位,因此由优化程序选择。巴特邓肯(Bart Duncan)在最近发表的题为“ 行进的目标去了盗贼 ”中展示了这种类型的另一个例子。

还有趣的是,执行计划中的Top运算符与Anti Semi Join(特别是Martin提到的“短路”)没有关联。通过首先禁用名为GbAggToConstScanOrTop的探索规则,我们可以开始查看Top的来源:

DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
GO
DBCC RULEON ('GbAggToConstScanOrTop');

GbAggToConstScanOrTop已禁用

该计划的估计成本为364.912,并且表明“顶部”替换了“按汇总分组”(按相关列分组BrowserID)。聚合不是由于DISTINCT查询文本中的冗余引起的:它是可以由两个探索规则LASJNtoLASJNonDistLASJOnLclDist引入的优化。同时禁用这两个将产生此计划:

DBCC RULEOFF ('LASJNtoLASJNonDist');
DBCC RULEOFF ('LASJOnLclDist');
DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
GO
DBCC RULEON ('LASJNtoLASJNonDist');
DBCC RULEON ('LASJOnLclDist');
DBCC RULEON ('GbAggToConstScanOrTop');

假脱机计划

该计划的估计成本为40729.3单位。

没有从分组依据到顶部的转换,优化器自然会BrowserID在反半联接之前选择具有聚合的哈希联接计划:

DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, RECOMPILE);
GO
DBCC RULEON ('GbAggToConstScanOrTop');

无最高DOP 1计划

在没有MAXDOP 1限制的情况下,一个并行计划是:

无顶级平行计划

“修复”原始查询的另一种方法BrowserID是在执行计划报告中创建丢失的索引。当对内部进行索引时,嵌套循环最适用。在最佳时机,估计半联接的基数具有挑战性。没有适当的索引(大表甚至没有唯一的键!)根本没有帮助。

我在“ 行目标”(第4部分:反联接反模式)中写了更多有关此的内容。


3
我向您鞠躬,您刚刚向我介绍了一些以前从未遇到过的新概念。只是当您感觉到自己知道某事时,外面有人会让您失望的-一种好方法:)添加索引肯定会有所帮助。但是,除了此一次性操作之外,BrowserID列永远不会访问/聚合该字段,因此我宁愿保存这些字节,因为表很大(这只是许多相同数据库中的一个)。桌子上没有唯一的键,因为它没有自然的唯一性。所有选择都是通过PaperID进行的,还可以选择一个句点。
Mark S. Rasmussen 2012年

22

当我运行您的脚本以创建仅统计信息的数据库和问题中的查询时,我得到以下计划。

计划

计划中显示的表基数为

  • tblFEStatsPaperHits48063400
  • tblFEStatsBrowsers339

因此,它估计将需要执行tblFEStatsPaperHits339次扫描。每次扫描都有关联的谓词tblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULL,该谓词被下推到扫描运算符中。

该计划并不意味着将进行339次完整扫描。由于它位于反半联接运算符的控制之下,因此一旦在每次扫描中找到第一条匹配行,它就会使其余部分短路。此节点的估计子树成本为1.32603,整个计划的成本为1.41337

对于哈希联接,它给出了以下计划

哈希加入

整个计划的成本为418.415(比嵌套循环计划高300倍左右),而单个完整的聚集索引扫描仅按tblFEStatsPaperHits成本计算206.8。将其与1.32603之前给出的339次局部扫描的估计值进行比较(平均局部扫描的估计成本= 0.003911592)。

因此,这表明每次部分扫描的成本比完全扫描的成本低53,000倍。如果成本计算与行数成线性比例,则这意味着假设平均而言,在找到匹配行并短路之前,每次迭代仅需要处理900行。

但是,我认为成本核算并不会以线性方式扩展。我认为它们还包含固定启动成本的某些要素。TOP在以下查询中尝试各种值

SELECT TOP 147 BrowserID 
FROM [dbo].[tblFEStatsPaperHits] 

147给出最接近的估计子树成本为0.003911592at 0.0039113。不管哪种方式,很明显,这都是基于每次扫描仅需处理表的一小部分(大约几百行而不是几百万行)的成本估算。

我不确定确切地基于此假设的数学原理,而且还不能真正与计划其余部分中的行计数估算值相加(嵌套循环中的236个估算的行联接将暗示有236个行)如果根本找不到匹配的行,则需要进行全面扫描)。我认为这只是一种情况,其中建模假设有所下降,并使嵌套循环计划的成本大大降低。


20

在我的书甚至一个 50M的行扫描是不可接受的......我惯用的伎俩就是将这种独特价值,并保持它最新的委托发动机:

create view [dbo].[vwFEStatsPaperHitsBrowserID]
with schemabinding
as
select BrowserID, COUNT_BIG(*) as big_count
from [dbo].[tblFEStatsPaperHits]
group by [BrowserID];
go

create unique clustered index [cdxVwFEStatsPaperHitsBrowserID] 
  on [vwFEStatsPaperHitsBrowserID]([BrowserID]);
go

这样,每个浏览器ID即可为您提供一个物化索引,从而无需扫描5000万行。引擎将为您维护它,并且QO将在您发布的语句中“按原样”使用它(不带任何提示或查询重写)。

不利之处当然是争论。中的任何插入或删除操作tblFEStatsPaperHits(我想是带有大量插入的日志记录表)都必须序列化对给定BrowserID的访问。如果您愿意购买,可以通过多种方法使它可行(延迟更新,两阶段日志记录等)。


我听说您,任何大尺寸的扫描都绝对不能接受。在这种情况下,它是用于一些一次性数据清理操作的,因此我选择不创建其他索引(并且由于会中断系统,因此不能临时创建其他索引)。我没有EE,但考虑到这是一次性的,提示会没事的。我主要的好奇心是QO如何制定计划:)该表是一个日志表,并且有大量插入内容。虽然有一个单独的异步日志记录表,但稍后会更新tblFEStatsPaperHits中的行,以便如有必要,我可以自己管理它。
Mark S. Rasmussen 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.