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


15

给定一个简单的三表联接,即使没有返回任何行,当包含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 */

我知道我可以在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 

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

Answers:


32

我同意马丁·史密斯(Martin Smith)的回答,但实际上,问题不仅仅是简单的统计数据之一。foreignId列的统计信息(假设启用了自动统计信息)准确地表明不存在值为3的行(只有1个行,值为7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

统计输出

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

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

相反,优化程序的模型估计至少需要一行。与可能使用较低的估算值相比,采用这种启发式方法通常会产生更好的计划。从某个时候开始在某个阶段产生零行估计的计划在处理流中将是无用的,因为将没有依据做出基于成本的决策的依据(零行等于零行)。如果估计结果是错误的,则零行估计上方的计划形状几乎没有合理的机会。

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

本质上,这里的查询不适合优化器的模型。我们无法采取任何措施来通过多列或过滤索引来“改善”估算值;这里没有办法使估算值低于1行。真实的数据库可能具有外键,以确保不会发生这种情况,但是假设此处不适用,我们将使用提示来纠正模型外条件。此查询可以使用多种不同的提示方法。 OPTION (FORCE ORDER)是一种恰好与编写的查询配合使用的方法。


21

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

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

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

如果你跑

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

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

添加ORDER BY子句并在其中加入一varchar(max)列时,连接顺序发生变化的原因smalltable是,它估计各varchar(max)列将使行大小平均增加4,000个字节。将其乘以1048580行,这意味着排序操作将需要大约4GB的空间,因此它明智地决定在SORT之前执行该操作JOIN

您可以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) 

该计划显示了一个排序运算符,其估计的子树代价接近12,000错误的估计行数和估计数据大小。

计划

顺便说一句,我没有发现用UNIQUEIDENTIFIER整数替换列会改变我的测试。


2

打开“显示执行计划”按钮,您可以看到正在发生的事情。这是“慢”查询的计划: 在此处输入图片说明

这是“快速”查询: 在此处输入图片说明

看一下-一起运行,第一个查询的“昂贵”价格大约高出33倍(比率为97:3)。SQL正在优化第一个按日期时间排序BigTable的查询,然后在SmallTable和TinyTable上运行一个小的“搜索”循环,分别执行100万次(您可以将鼠标悬停在“ Clustered Index Seek”图标上以获取更多统计信息)。因此,排序(27%)和在小型表上进行的2 x 1百万“搜索”(23%和46%)是昂贵查询中的绝大部分。相比之下,非ORDER BY查询总共执行3次扫描。

基本上,您已经在特定情况下的SQL优化器逻辑中发现了一个漏洞。但是,正如TysHTTP所说,如果添加索引(这会减慢插入/更新的速度),则扫描很快就会变得疯狂。


2

发生的情况是SQL决定在限制之前执行命令。

尝试这个:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

这样可以提高性能(在这种情况下,返回的结果计数非常小),而实际上不会因添加另一个索引而降低性能。尽管SQL优化器决定在连接之前按顺序执行是很奇怪的,但这很可能是因为,如果您实际上有返回数据,那么在连接之后对数据进行排序会比不进行排序需要更长的时间。

最后,尝试运行以下脚本,然后查看更新的统计信息和索引是否可以解决您遇到的问题:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

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.