为什么添加TOP 1会显着降低性能?


39

我有一个相当简单的查询

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

那给了我可怕的表现(就像从来没有想过要等待它完成一样)。查询计划如下所示:

在此处输入图片说明

但是,如果我删除,则会TOP 1得到一个看起来像这样的计划,它会在1-2秒内运行:

在此处输入图片说明

在下面更正PK和索引。

该事实TOP 1改变了查询计划并不让我感到吃惊,我只是有点惊讶,这使情况变得更糟了这么多。

注意:我已经阅读了这篇文章的结果,并了解了Row Goaletc 的概念。我很好奇的是如何去改变查询,以便它使用更好的计划。目前,我正在将数据转储到临时表中,然后从中提取第一行。我想知道是否有更好的方法。

编辑对于在事实结束后仍在阅读本文的人,这里还有一些其他信息。

  • Document_Queue-PK / CI是D_ID,它具有约5k行。
  • Correspondence_Journal-PK / CI为FILE_NUMBER,CORRESPONDENCE_ID,具有约140万行。

当我开始时,没有其他索引。我最后在Correspondence_Journal(Document_Id,File_Number)上写了一个


1
您是否有一个外键约束来强制DOCUMENT_ID两个表之间的关系(或中的每个记录在中CORRESPONDENCE_JOURNAL都有匹配的记录DOCUMENT_QUEUE)?
Daniel Hutmacher

Answers:


28

尝试强制进行哈希联接*

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

优化器可能认为循环在前1名时会更好,这是有道理的,但实际上在这里无效。在这里只是一个猜测,但是可能该假脱机的估计成本已关闭-它使用了TEMPDB-您的TEMPDB性能可能很差。


*请谨慎使用联接提示,因为它们会强制计划表访问顺序与查询中表的书面顺序相匹配(就像OPTION (FORCE ORDER)已被指定一样)。从文档链接:

BOL提取物

在示例中,这可能不会产生任何不良影响,但总的来说,这可能会产生效果。FORCE ORDER(暗示或显式)是一个非常强大的提示,它超出了执行顺序;它会阻止应用广泛的优化器技术,包括部分聚合和重新排序。

一个OPTION (HASH JOIN) 查询提示可以是在合适的情况下较少干扰,因为这并不暗示FORCE ORDER。但是,它确实适用于查询中的所有联接。其他解决方案也可用。


1
看起来像是正确的答案,它和较简单的计划之间的唯一区别是在前面有一个附加的排序。
肯尼斯·费希尔

3
不确定我喜欢这个答案。连接提示非常具有侵入性。首先应该尝试一些简单的索引更改,例如date列上的索引。
usr

@usr这是一个简单的PK连接,运行时间不到一秒钟。在这里很安全的赌注。
狗仔队

4
在强制进行哈希联接时,您将对大型表进行扫描。有更好的选择。
罗伯·法利

30

既然您使用有了正确的计划ORDER BY,也许您可​​以自己动手TOP运营商?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

在我看来,上述查询计划ROW_NUMBER()应与您使用的计划相同ORDER BY。现在,查询计划应该具有一个Segment,Sequence Project,最后还有一个Filter运算符,其余的应该看起来像您的好计划。


3
实际上,尽管它确实给了顶层运算符(以及其他一些东西(序列项目,片段和排序)),但它仍然运行了不到一秒。我要给@frisbee正确答案,因为他是第一位,而且更简单。很好的答案。
肯尼斯·费希尔

10
@KennethFisher,飞盘的答案比较简单,但是用大锤打钉子的方式比使用标准锤更简单。它还有很多风险,特别是如果长期放置在原地。除非在测试中使用,否则我将不会使用类似的提示,或者可能是一个边缘异常。
Steve Mangiameli '16

@SteveMangiameli在这种特殊情况下,只有一个连接,因此许多问题都消失了。我知道使用联接提示(或查询提示)的风险,我认为在这种情况下这是合理的。
肯尼斯·费舍尔

5
@KennethFisher Imo,查询提示的主要风险是,随着数据的增长或更改,您强制执行的查询计划可能会变得比系统自己发现的查询计划差。您已经看到了计划中的小错误如何严重影响性能。在生产中使用的提示是宣布,“我知道这个计划永远,永远是最好的,因为我这么充分了解规划师,我的数据会如何表现了这个查询生产的寿命。” 我从未对查询充满信心。
jpmc26,2016年

29

编辑:+1在这种情况下有效,因为事实证明它FILE_NUMBER是整数的零填充字符串版本。对于字符串,这里的一个更好的解决方案是附加''(空字符串),因为附加一个值会影响顺序,或者为数字添加一个常量但包含不确定函数的东西,例如sign(rand()+1)。“打破排序”的想法在这里仍然有效,只是我的方法不理想。

+1

不,我并不是说我同意任何事情,我是说这是一种解决方案。如果将查询更改为,ORDER BY cj.FILE_NUMBER + 1则的TOP 1行为将有所不同。

您会看到,在为排序查询设置了小的行目标的情况下,系统将尝试按顺序使用数据,以避免使用Sort操作符。这也将避免构建哈希表,从而避免了寻找第一行的麻烦。在您的情况下,这是错误的-从那些箭头的粗细来看,看起来它不得不消耗大量数据才能找到一个匹配项。

这些箭头的粗细表明DOCUMENT_QUEUE(DQ)表比CORRESPONDENCE_JOURNAL(CJ)表小得多。最好的计划实际上是检查DQ行,直到找到CJ行。的确,如果查询优化器(QO)中没有讨厌ORDER BY的东西,那将是它的工作方式,CJ的覆盖索引很好地支持了这一点。

因此,如果您ORDER BY完全删除了该行,我希望您会得到一个涉及嵌套循环的计划,该循环遍历DQ中的行,并寻求CJ以确保该行存在。使用TOP 1,这将在拉出单行后停止。

但是,如果您确实确实需要按FILE_NUMBER顺序排列第一行,则可以通过执行以下操作来诱使系统忽略该索引,该索引似乎(不正确)非常有帮助,ORDER BY CJ.FILE_NUMBER+1我们知道该索引将保持与以前相同的顺序,但重要的是QO没有。QO将着重于列出整个集合,以便可以满足Top N Sort运算符。此方法应产生一个计划,其中包含一个Compute Scalar运算符来计算排序值,以及一个Top N Sort运算符来获取第一行。但是在这些右边,您应该看到一个不错的嵌套循环,在CJ上进行了很多搜索。与在DQ中不匹配的大型行表中运行相比,性能更好。

哈希匹配不一定很糟糕,但是如果您从DQ返回的行集比CJ小得多(正如我期望的那样),那么哈希匹配将扫描更多的CJ超出了需要。

注意:我使用+1而不是+0,因为查询优化器可能会认识到+0并没有任何改变。当然,如果不是现在,那么将来可能会在+1上应用相同的内容。


7

我已经阅读了这篇文章的结果,并了解了“行目标”等的概念。我很好奇的是如何去改变查询,以便它使用更好的计划

添加OPTION (QUERYTRACEON 4138)只会关闭该查询的行目标的效果,而不会对最终计划有过多的规定,这可能是最简单/最直接的方法。

如果添加此提示给您带来权限错误(必需DBCC TRACEON),则可以使用计划指南来应用它:

使用QUERYTRACEON在计划指南spaghettidba

...或仅使用存储过程:

QUERYTRACEON需要什么权限?肯德拉·利特尔Kendra Little)


3

当优化器能够应用行目标优化时,较新版本的SQL Server提供了不同的(可能更好)的选项来处理性能欠佳的查询。SQL Server 2016 SP1引入了DISABLE_OPTIMIZER_ROWGOAL USE HINT与跟踪标志4138相同的作用。如果您使用的不是该版本,则还可以考虑使用OPTIMIZE FOR查询提示来获取旨在返回所有行而不是仅返回1行的查询计划。下面的查询将返回与问题中的结果相同的结果,但不会以仅获得1行为目标而创建。

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));

2

由于您正在执行TOP(1),因此建议您先ORDER BY确定一下。至少这将确保结果在功能上可预测(始终对回归测试有用)。它看起来像你需要添加DC.D_IDCJ.CORRESPONDENCE_ID为。

当查询计划看,我有时会发现它启发简化查询:可能提前选择所有相关的直流行到一个临时表,以消除与基数估计问题QUEUE_DATEPRINT_LOCATION。考虑到低行数,这应该很快。然后,您可以根据需要将索引添加到此临时表,而无需更改永久表。

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.