更改查询以改善操作员估计


14

我有一个可以在可接受的时间内运行的查询,但我想从中获得最大的性能。

我要改进的操作是计划的右侧节点17处的“索引搜索”。

在此处输入图片说明

我已经添加了适当的索引,但是对于该操作,我得到的估计值是它们应有的一半。

我一直在寻找更改索引,添加临时表并重新编写查询的方法,但是为了获得正确的估算值,我无法对其进行简化。

有人对我可以尝试的方法有任何建议吗?

完整计划及其详细信息可以在这里找到

可以在此处找到非匿名计划。

更新:

我觉得问题的最初版本引起了很多混乱,因此我将在原始代码中添加一些解释。

create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
    set nocount on;

    declare @dist_ca_id int;

    select *
    into #temp
    from @customAttrValIds
        where id is not null;

    select @dist_ca_id = count(distinct CustomAttrID) 
    from CustomAttributeValues c
        inner join #temp a on c.Id = a.id;

    select a.Id
        , a.AssortmentId 
    from Assortments a
        inner join AssortmentCustomAttributeValues acav
            on a.Id = acav.Assortment_Id
        inner join CustomAttributeValues cav 
            on cav.Id = acav.CustomAttributeValue_Id
    where a.AssortmentType = @asType
        and acav.CustomAttributeValue_Id in (select id from #temp)
    group by a.AssortmentId
        , a.Id
    having count(distinct cav.CustomAttrID) = @dist_ca_id
    option(recompile);

end

答案:

  1. 为什么在pasteThePlan链接中使用奇怪的初始命名?

    :因为我使用了SQL Sentry Plan Explorer中的匿名计划。

  2. 为什么OPTION RECOMPILE

    :因为我可以负担得起重新编译以避免参数嗅探的作用(数据可能是/可能是歪斜的)。我已经测试过,并且对Optimizer在使用时生成的计划感到满意OPTION RECOMPILE

  3. WITH SCHEMABINDING

    :我真的想避免这种情况,只有在拥有索引视图时才使用它。无论如何,这是一个系统函数(COUNT()),因此SCHEMABINDING此处无用。

回答更多可能的问题:

  1. 我为什么用 INSERT INTO #temp FROM @customAttrributeValues

    答案:因为我注意到并且现在知道使用查询中插入的变量时,使用变量进行的任何估算始终为1。我测试了将数据放入临时表中,然后估算实际行相等。

  2. 为什么要使用and acav.CustomAttributeValue_Id in (select id from #temp)

    :我可以用#temp上的JOIN替换它,但是开发人员非常困惑,因此IN选择了该选项。我真的认为即使更换也不会有任何区别,无论哪种方式,这都没有问题。


我猜想,#temp创建和使用将是性能的问题,而不是收益。您将保存到未索引表中,仅可使用一次。尝试将其完全删除(并可能将其更改in (select id from #temp)exists子查询。
ypercubeᵀᴹ17年

@ypercubeᵀᴹ是的,使用变量而不是临时表读取的页面少了几页。
Radu Gheorghiu

顺便说一句,表变量将提供正确的行数估计与期权(重新编译)使用时-但仍然没有颗粒状统计,基数等
TH

@TH好了,我确实在实际执行计划中查看了估算值,当使用select id from @customAttrValIds而不是时select id from #temp,估算的行数是1针对变量和3#temp的(与实际的行数匹配)。这就是为什么我换成@#。我确实记得有一次演讲(来自Brent O或Aaron Bertrand),他们说,当使用tbl变量时,该变量的估计值始终为1。并且为了获得更好的估计值,他们将使用临时表。
Radu Gheorghiu

@RaduGheorghiu是的,但是在那些家伙的世界中,选项(重新编译)很少是一个选项,并且出于其他有效的原因,他们也更喜欢临时表。也许估算值总是总是错误地显示为1,因为它确实改变了计划,如下所示:theboreddba.com/Categories/FunWithFlags/…–
TH

Answers:


12

该计划是在SQL Server 2008 R2 RTM实例(内部版本10.50.1600)上编译的。你应该安装 Service Pack 3(内部版本10.50.6000),然后安装最新的补丁程序,以使其达到(当前)最新的内部版本10.50.6542。出于多种原因,这很重要,包括安全性,错误修复和新功能。

参数嵌入优化

与当前问题相关,SQL Server 2008 R2 RTM不支持用于以下方面的参数嵌入优化(PEO): OPTION (RECOMPILE)。目前,您在没有意识到主要好处之一的情况下付出了重新编译的费用。

当PEO可用时,SQL Server可以直接在查询计划中使用存储在局部变量和参数中的文字值。这可以导致极大的简化并提高性能。在我的文章Parameter Sniffing,Embedding和RECOMPILE Options中有关于此的更多信息。

散列,分类和交换溢出

这些仅在查询是在SQL Server 2012或更高版本上编译时才显示在执行计划中。在早期版本中,我们必须使用Profiler或Extended Events执行查询时监视溢出情况。溢出总会导致与持久性存储后备tempdb之间的物理I / O(或来自持久性存储后备tempdb的物理I / O),这可能会产生重要的性能后果,尤其是在溢出量很大或I / O路径承受压力的情况下。

在您的执行计划中,有两个哈希匹配(聚合)运算符。为哈希表保留的内存基于对输出行估计(换句话说,它与运行时找到的组数成正比)。授予的内存在执行开始之前就已固定,并且无论执行实例有多少空闲内存,在执行期间都无法增长。在提供的计划中,两个哈希匹配(聚合)运算符产生的行均多于优化程序预期的行,因此可能会导致tempdb溢出在运行时。

计划中还有一个哈希匹配(内部联接)运算符。为哈希表保留的内存基于对探针侧输入行估计。探针输入估计为847,399行,但在运行时遇到1,223,636。这种过量也可能导致散列溢出。

冗余骨料

节点8上的哈希匹配(聚合)在上执行分组操作(Assortment_Id, CustomAttrID),但输入行等于输出行:

节点8哈希匹配(聚合)

这表明列组合是关键(因此,分组在语义上是不必要的)。由于需要跨哈希分区交换两次传递140万行(两侧的并行运算符),因此增加了执行冗余聚合的成本。

由于涉及的列来自不同的表,因此比平时更难将唯一性信息传达给优化器,因此可以避免多余的分组操作和不必要的交换。

线程分配效率低下

Joe Obbish的回答所述,节点14处的交换使用哈希分区在线程之间分配行。不幸的是,行数少和可用的调度程序意味着所有三行最终都在一个线程中。显然并行的计划以串行方式运行(具有并行开销),直到节点9处的交换为止。

您可以通过消除节点13上的Distinct Sort来解决此问题(以获取循环分区或广播分区)。最简单的方法是在#temp表上创建集群主键,并在加载表时执行不同的操作:

CREATE TABLE #Temp
(
    id integer NOT NULL PRIMARY KEY CLUSTERED
);

INSERT #Temp
(
    id
)
SELECT DISTINCT
    CAV.id
FROM @customAttrValIds AS CAV
WHERE
    CAV.id IS NOT NULL;

临时表统计信息缓存

尽管使用OPTION (RECOMPILE),SQL Server仍可以在过程调用之间缓存临时表对象及其关联的统计信息。通常,这是一个可喜的性能优化,但是如果在临时表中在相邻过程调用中填充了相似数量的数据,则重新编译的计划可能基于错误的统计信息(从先前的执行中缓存)。这在我的文章《存储过程中的临时表解释的临时表缓存》中有详细介绍。

为避免这种情况,请在填充临时表之后以及在查询中引用它之前OPTION (RECOMPILE)与显式命令一起使用UPDATE STATISTICS #TempTable

查询重写

这部分假设#Temp已经对表的创建进行了更改。

考虑到可能的散列溢出和冗余聚合(以及周围的交换)的成本,可能需要在节点10上实现集:

CREATE TABLE #Temp2
(
    CustomAttrID integer NOT NULL,
    Assortment_Id integer NOT NULL,
);

INSERT #Temp2
(
    Assortment_Id,
    CustomAttrID
)
SELECT
    ACAV.Assortment_Id,
    CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
    ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
    ON T.id = ACAV.CustomAttributeValue_Id;

ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);

PRIMARY KEY在单独的步骤中添加,以保证指数的构建有准确的基数信息,避免临时表的统计数据缓存的问题。

如果实例具有足够的可用内存,则这种实现很可能会在内存中发生(避免使用tempdb I / O)。一旦升级到SQL Server 2012(SP1 CU10 / SP2 CU1或更高版本),这种可能性就更大了,SQL Server 2012 改善了Eager Write行为

此操作为优化器提供了有关中间集的准确基数信息,允许其创建统计信息,并允许我们声明(Assortment_Id, CustomAttrID)为键。

的填充计划#Temp2应如下所示(请注意的聚集索引扫描#Temp,没有明显的排序,并且交换现在使用循环行分区):

#Temp2人口

有了该设置,最终查询将变为:

SELECT
    A.Id,
    A.AssortmentId
FROM
(
    SELECT
        T.Assortment_Id
    FROM #Temp2 AS T
    GROUP BY
        T.Assortment_Id
    HAVING
        COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
    ON A.Id = DT.Assortment_Id
WHERE
    A.AssortmentType = @asType
OPTION (RECOMPILE);

我们可以手动将重写COUNT_BIG(DISTINCT...为一个简单的COUNT_BIG(*),但是有了新的关键信息,优化器就会为我们完成:

最终计划

最终计划可能会使用循环/哈希/合并联接,具体取决于有关我无权访问的数据的统计信息。另一个小注意事项:我假设CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);存在类似的索引。

无论如何,关于最终计划的重要一点是,估算应该更好,并且将分组操作的复杂序列减少为单个流聚合(不需要内存,因此不能溢出到磁盘)。

很难说在这种情况下,使用额外的临时表实际上会更好,但是估计和计划选择将对数据量和分布随时间的变化具有更大的弹性。从长远来看,这可能比今天的少量性能提升更有价值。无论如何,您现在将获得更多信息以最终决定为依据。


9

您查询的基数估计实际上非常好。很难获得估计的行数来准确匹配实际的行数,尤其是当您有这么多的联接时。对于优化器来说,加入基数估计很困难。需要注意的重要一件事是,嵌套循环内部的估计行数是该循环的每次执行。因此,当SQL Server说将使用索引查找来获取463869行时,在这种情况下,实际的估计数是执行次数(2)* 463869 = 927738,与实际的行数1391608相差不大。嵌套循环在节点ID 10处连接后,估计的行数接近完美。

当查询优化器选择了错误的计划或没有为计划分配足够的内存时,基数估计不佳通常是一个问题。我认为该计划不会对tempdb造成任何影响,因此内存看起来还可以。对于您调用的嵌套循环联接,您有一个小的外部表和一个索引内部表。怎么了 确切地说,您希望查询优化器在这里有何不同?

在提高性能方面,对我而言突出的是,SQL Server使用哈希算法来分发并行行,这导致所有并行行都在同一线程上:

线程不平衡

结果,一个线程完成了索引查找的所有工作:

线程不平衡寻求

这意味着,直到节点ID为9的重新分区流运算符,您的查询才有效地并行运行。您可能想要的是循环分区,以使每一行以其自己的线程结尾。这将允许两个线程对节点ID 17进行索引查找。添加多余的TOP运算符可能会使您进行循环分区。如果您愿意,我可以在此处添加详细信息。

如果您确实要关注基数估计,则可以将第一次连接后的行放入临时表中。如果您在临时表上收集统计信息,从而为优化器提供有关调用的嵌套循环联接的外部表的更多信息。它还可能导致循环分区。

如果您不使用跟踪标志4199或2301,则可以考虑使用它们。跟踪标志4199提供了各种优化程序修复程序,但它们可能会降低某些工作负载。跟踪标志2301更改了查询优化器的一些连接基数假设,并使之更加努力。在这两种情况下,在启用它们之前都要仔细测试。


-2

我相信,对该连接进行更好的估计不会改变计划,除非1.4 mill是表的足够部分,以使优化程序选择具有哈希或合并连接的索引(而非群集)扫描。我怀疑不会在这里是如此,人也不是真正有用的,但你可以测试通过更换影响内部联接针对与CustomAttributeValues 内哈希联接内合并联接

我也从更广泛的角度看了代码,看不到任何改进的方法-当然,我有兴趣被证明是错误的。而且,如果您想发布自己想要完成的全部逻辑,那么我会对另一种外观感兴趣。


3
该查询的计划空间很大,有许多用于连接顺序和嵌套,并行性,本地/全局聚合等的选项,等等。其中大多数会受到派生统计信息(分布以及原始基数)变化的影响。在计划节点10处。还应注意,通常应避免使用OPTION(FORCE ORDER)联接提示,因为它们带有silent(无声),这会阻止优化程序根据文本顺序对联接进行重新排序,此外还会阻止许多其他优化。
保罗·怀特9

-12

您不会从[非聚集]索引搜索中得到改善。比非聚集索引查找更好的唯一事情是聚集索引查找。

此外,我在过去的十年中一直是SQL DBA,在那之前的五年中一直是SQL开发人员,根据我的经验,通过研究您无法执行的执行计划来发现SQL Query的改进非常罕见。无法通过其他方式找到。生成执行计划的主要原因是,它通常会向您建议缺少的索引,您可以添加这些索引以提高性能。

如果效率低下,主要的性能提升将在于调整SQL查询本身。例如,几个月前,我通过重写SELECT UNION SELECT样式数据透视表以使用标准SQL PIVOT运算符,使SQL函数的运行速度提高了160倍。

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

因此,让我们看一看,SELECT * INTO它通常不如standard高效INSERT Object1 (column list) SELECT column list。所以我会重写它。接下来,如果Function1的定义不带WITH SCHEMABINDING,则添加WITH SCHEMABINDING子句应使其运行更快。

您已经选择了许多没有意义的别名,例如将Object2别名为Object3。您应该选择不会混淆代码的更好的别名。您具有“ Object7.Column5 in(从Object1中选择Column1)”。

IN这种性质的子句总是更有效地写成EXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5)。也许我应该用另一种方式写。EXISTS永远至少和一样好IN。它并不总是更好,但通常是。

另外,我怀疑这option(recompile)是否在提高查询性能。我会测试将其删除。


6
如果非聚集索引查找覆盖了查询,那么它几乎总是比聚集索引查找更好,因为根据定义,聚集索引中包含所有列,并且非聚集索引具有较少的列,因此需要较少的页面查找(并且进入b树的步骤数较少)来检索数据。因此,说聚集索引查找总是更好会是不准确的。
ErikE
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.