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

Rad*_*hiu 14 sql-server sql-server-2008-r2

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

我试图改进的操作是计划右侧的“索引搜索”,来自节点 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
Run Code Online (Sandbox Code Playgroud)

答案:

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

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

  2. 为什么OPTION RECOMPILE

    :因为我可以负担得起重新编译以避免参数嗅探(数据是/可能有偏差)。我已经测试过并且我对优化器在使用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选项。我真的不认为即使更换也会有区别,无论哪种方式,这都没有问题。

Pau*_*ite 12

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

参数嵌入优化

与当前问题相关,SQL Server 2008 R2 RTM 不支持OPTION (RECOMPILE). 现在,您正在支付重新编译的成本,而没有意识到主要好处之一。

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

散列、排序和交换溢出

只有在 SQL Server 2012 或更高版本上编译查询时,这些才会显示在执行计划中。在早期版本中,我们必须在使用 Profiler 或扩展事件执行查询时监视溢出。溢出总是会导致到(和来自)支持tempdb的持久存储的物理 I/O ,这可能会产生重要的性能后果,特别是如果溢出很大,或者 I/O 路径处于压力之下。

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

计划中还有一个 Hash Match (Inner Join) 操作符。为散列表保留的内存基于对探测端输入行估计。探针输入估计有 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;
Run Code Online (Sandbox Code Playgroud)

临时表统计缓存

尽管使用了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);
Run Code Online (Sandbox Code Playgroud)

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

如果实例有足够的可用内存,则这种实现很可能发生在内存中(避免tempdb I/O)。升级到 SQL Server 2012(SP1 CU10/SP2 CU1 或更高版本)后,这种情况更有可能发生,它改进了 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);
Run Code Online (Sandbox Code Playgroud)

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

最终计划

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

无论如何,关于最终计划的重要事情是估计应该好得多,并且分组操作的复杂序列已减少到单个流聚合(不需要内存,因此不会溢出到磁盘)。

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


Joe*_*ish 9

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

当查询优化器选择错误的计划或没有为计划授予足够的内存时,基数估计不佳通常是一个问题。我没有看到这个计划对 tempdb 的任何溢出,所以内存看起来没问题。对于您调用的嵌套循环连接,您有一个小的外部表和一个索引的内部表。那有什么问题?准确地说,您希望查询优化器在这里做些什么不同的事情?

在提高性能方面,对我来说突出的事情是 SQL Server 使用散列算法来分布并行行,这导致所有行都在同一线程上:

线程不平衡

因此,一个线程使用索引查找完成所有工作:

线程不平衡查找

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

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

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