在不返回任何行的查询中包含 ORDER BY 会严重影响性能

Haf*_*hor 16 performance sql-server

给定一个简单的三表连接,当包含 ORDER BY 时,即使没有返回行,查询性能也会发生巨大变化。实际问题场景需要 30 秒才能返回零行,但在不包括 ORDER BY 时是即时的。为什么?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */
Run Code Online (Sandbox Code Playgroud)

我知道我可以在 bigtable.smallGuidId 上建立索引,但是,我相信在这种情况下这实际上会使情况变得更糟。

这是创建/填充表以进行测试的脚本。奇怪的是,smalltable 有一个 nvarchar(max) 字段似乎很重要。我使用 guid 加入 bigtable 似乎也很重要(我猜这使它想要使用哈希匹配)。

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 
Run Code Online (Sandbox Code Playgroud)

我已经在 SQL 2005、2008 和 2008R2 上测试过,结果相同。

Pau*_*ite 35

我同意 Martin Smith 的回答,但问题不仅仅是统计数据之一。foreignId 列的统计信息(假设启用了自动统计)准确地显示不存在值为 3 的行(只有一个,值为 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM
Run Code Online (Sandbox Code Playgroud)

统计输出

SQL Server 知道自从捕获统计信息以来事情可能已经发生了变化,因此在执行计划时可能会有值 3 的行。此外,计划编译和执行之间可能会花费任何时间(毕竟计划被缓存以供重用)。正如 Martin 所说,SQL Server 包含检测何时进行了足够的修改以证明出于优化原因重新编译任何缓存计划的逻辑。

然而,这一切最终都不重要。除了一个边缘情况例外,优化器永远不会估计表操作产生的行数为零。如果它可以静态确定输出必须始终为零行,则该操作是多余的,将完全删除。

优化器的模型改为估计最少一行。使用这种启发式方法往往会产生更好的计划,而不是在可能的情况下进行较低的估计。一个在某个阶段产生零行估计的计划从处理流中的那个点开始就没有用了,因为没有做出基于成本的决策的基础(无论如何,零行就是零行)。如果估计结果是错误的,则高于零行估计的计划形状几乎没有可能是合理的。

第二个因素是另一个建模假设,称为遏制假设。这实质上是说,如果查询将一个值范围与另一个值范围连接起来,那是因为范围重叠。另一种表达方式是说正在指定连接是因为期望返回行。如果没有这种推理,成本通常会被低估,从而导致针对广泛的常见查询的计划不佳。

本质上,您在这里拥有的是一个不符合优化器模型的查询。我们无法使用多列或过滤索引来“改进”估计;这里没有办法得到低于 1 行的估计值。一个真实的数据库可能有外键来确保不会出现这种情况,但假设这在此处不适用,我们只能使用提示来纠正模型外的情况。任何数量的不同提示方法都适用于此查询。 OPTION (FORCE ORDER)是一种恰好适用于所写查询的方法。


Mar*_*ith 22

这里的基本问题是统计问题之一。

对于这两个查询,估计的行数表明它相信最终SELECT将返回 1,048,580 行(估计存在于 中的行数相同bigtable)而不是实际发生的 0。

您的两个JOIN条件都匹配并且会保留所有行。它们最终被淘汰,因为其中的单行tinytablet.foreignId=3谓词不匹配。

如果你跑

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 
Run Code Online (Sandbox Code Playgroud)

并查看它的估计行数,1而不是0这个错误在整个计划中传播。tinytable当前包含 1 行。在发生500 行修改之前,不会为此表重新编译统计信息,因此可以添加匹配的行并且不会触发重新编译。

当您添加ORDER BY子句并且其中有一varchar(max)列时,Join Order 发生变化的原因smalltable是因为它估计varchar(max)列将平均增加 4,000 字节的行大小。将其乘以 1048580 行,这意味着排序操作估计需要 4GB,因此明智地决定SORTJOIN.

您可以使用如下提示强制ORDER BY查询采用非ORDER BY连接策略。

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 
Run Code Online (Sandbox Code Playgroud)

该计划显示了一个排序运算符,它具有几乎12,000错误的估计行数和估计数据大小的估计子树成本。

计划

顺便说一句,UNIQUEIDENTIFIER在我的测试中,我没有发现用整数来替换列。