在 SQL Server 中应用基数估计问题

Пав*_*лёв 10 sql-server optimization cross-apply cardinality-estimates query-performance

现在,我面临着基数估计的逻辑问题,这个问题在一个看似相当简单的情况下对我来说不是很清楚。我在工作中遇到过这种情况,因此出于隐私考虑,下面仅对问题进行一般性描述,但为了更详细的分析,我在AdventureWorksDW培训基地中模拟了此问题。

有以下形式的查询:

SELECT <some columns>
FROM <some dates table>
CROSS APPLY(

    SELECT
        <some p columns>
    FROM <some table> p
    WHERE p.StartDate <= Dates.d
      AND p.EndDate >= Dates.d
) t
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

从上面给出的执行计划中可以看出,基数估计器估计索引查找操作中的估计行数为 17,884,200(对应于 NL 外部的每行 2,980,700),这与实际数量非常接近.

现在我将修改查询并添加到 CROSS APPLY LEFT OUTER JOIN:

SELECT <some columns t>
FROM <some dates table>
CROSS APPLY(

    SELECT
        <some p columns>
    <some columns f>
    FROM <some table> p
    LEFT JOIN <some table> f ON p.key = f.key
        AND f.date = Dates.d
    WHERE p.StartDate <= Dates.d
      AND p.EndDate >= Dates.d
) t

Run Code Online (Sandbox Code Playgroud)

此查询给出了以下计划:

在此处输入图片说明

看到查询的逻辑形式,可以合理地假设Index Seek操作的预期行数将保持不变,虽然我理解寻找计划的路线不同,但是,似乎部分红色突出显示没有变化,相同的谓词等,但Index Seek的估计现在是664,506(对应于NL外部部分的每行110,751),这是一个严重的错误并且在生产环境中会导致严重的tempdb溢出数据。

上述查询是在 Sql Server 2012 (SP4) (KB4018073) - 11.0.7001.0 (x64) 的实例上执行的。

为了获得更多详细信息并简化分析,我在 SQL Server 2019 (RTM) - 15.0.2000.5 (X64) 实例上的 AdventureWorksDW2017 数据库中模拟了此问题,但我在打开 9481 跟踪标志的情况下执行查询以模拟系统使用基数估计器版本 70。

下面是一个带有左外连接的查询。

DECLARE @db DATE = '20130720'
DECLARE @de DATE = '20130802'

;WITH Dates AS(

    SELECT [FullDateAlternateKey] AS d
    FROM [AdventureWorksDW2017].[dbo].[DimDate]
    WHERE [FullDateAlternateKey] BETWEEN @db AND @de
)
SELECT *
FROM Dates
CROSS APPLY(

    SELECT
        p.[ProductAlternateKey]
       ,f.[OrderQuantity]
    FROM [AdventureWorksDW2017].[dbo].[DimProduct] p
    LEFT JOIN [AdventureWorksDW2017].[dbo].[FactInternetSales] f ON f.ProductKey = p.ProductKey
       AND f.[OrderDate] = Dates.d
    WHERE p.StartDate <= Dates.d
      AND ISNULL(p.EndDate, '99991231') >= Dates.d

) t
OPTION(QUERYTRACEON 9481 /*force legacy CE*/)

Run Code Online (Sandbox Code Playgroud)

还值得注意的是,在 DimProduct 表上创建了以下索引:

CREATE NONCLUSTERED INDEX [Date_Indx] ON [dbo].[DimProduct]
(
    [StartDate] ASC,
    [EndDate] ASC
)
INCLUDE([ProductAlternateKey])
Run Code Online (Sandbox Code Playgroud)

该查询给出了以下查询计划:(1)

在此处输入图片说明

如您所见,以红色突出显示的查询部分估计为 59,754(每行约 182 个)。现在我将演示一个没有左外连接的查询计划。(2)

在此处输入图片说明

如您所见,以红色突出显示的查询部分的得分为 97 565(每行 ~ 297),但差异不是很大,但过滤器 (3) 运算符的基数得分明显不同 ~ 每行 244与具有左外连接的查询中的 ~ 54 相比。

(3) – 过滤谓词:

isnull([AdventureWorksDW2017].[dbo].[DimProduct].[EndDate] as [p].[EndDate],'9999-12-31 00:00:00.000')>=[AdventureWorksDW2017].[dbo].[DimDate].[FullDateAlternateKey]
Run Code Online (Sandbox Code Playgroud)

为了更深入地研究,我查看了上述计划中的物理操作员树。

以下是未记录标志 8607 和 8612 跟踪的最重要部分。

对于计划 (2):

PhyOp_Apply lookup TBL: AdventureWorksDW2017.dbo.DimProduct
…
PhyOp_Range TBL: AdventureWorksDW2017.dbo.DimProduct(alias TBL: p)(6) ASC  Bmk ( QCOL: [p].ProductKey) IsRow: COL: IsBaseRow1002  [ Card=296.839 Cost(RowGoal 0,ReW 0,ReB 327.68,Dist 328.68,Total 328.68)= 0.174387 ](Distance = 2)
              ScaOp_Comp x_cmpLe
                 ScaOp_Identifier QCOL: [p].StartDate
                 ScaOp_Identifier QCOL: [AdventureWorksDW2017].[dbo].[DimDate].FullDateAlternateKey
Run Code Online (Sandbox Code Playgroud)

对于计划 (1):

PhyOp_Apply (x_jtInner)
…
PhyOp_Range TBL: AdventureWorksDW2017.dbo.DimProduct(alias TBL: p)(6) ASC  Bmk ( QCOL: [p].ProductKey) IsRow: COL: IsBaseRow1002  [ Card=181.8 Cost(RowGoal 0,ReW 0,ReB 327.68,Dist 328.68,Total 328.68)= 0.132795 ](Distance = 2)


                 ScaOp_Comp x_cmpLe

                    ScaOp_Identifier QCOL: [p].StartDate

                    ScaOp_Identifier QCOL: [AdventureWorksDW2017].[dbo].[DimDate].FullDateAlternateKey
Run Code Online (Sandbox Code Playgroud)

如您所见,优化器选择了 Apply 运算符的各种实现,(2) 中的 PhyOp_Apply 查找和 (1) 中的 PhyOp_Apply (x_jtInner),但我仍然不明白我可以从中提取什么。

通过重写没有左外连接的原始查询,我可以获得与计划 (1) 中相同的估计,如下所示:

DECLARE @db DATE = '20130720'
DECLARE @de DATE = '20130802'

;WITH Dates AS(

    SELECT [FullDateAlternateKey] AS d
    FROM [AdventureWorksDW2017].[dbo].[DimDate]
    WHERE [FullDateAlternateKey] BETWEEN @db AND @de
)
SELECT *
FROM Dates
CROSS APPLY(

    SELECT TOP(1000000000)
        p.[ProductAlternateKey]
    FROM [AdventureWorksDW2017].[dbo].[DimProduct] p
    WHERE p.StartDate <= Dates.d
      AND ISNULL(p.EndDate, '99991231') >= Dates.d

) t
OPTION(QUERYTRACEON 9481 /*force legacy CE*/)
Run Code Online (Sandbox Code Playgroud)

其中给出了以下计划:(4)

在此处输入图片说明

如您所见,以红色突出显示的区域的估计与物理运算符树中的计划 (1) 和 PhyOp_Apply (x_jtInner) 运算符一致。

请帮我回答这个问题,有没有办法影响这样的基数估计,可能是通过提示或更改查询形式等,并帮助理解为什么优化器在这种情况下会给出这样的估计。

Pau*_*ite 11

通常有多种方法可以得出基数估计值,每种方法都会给出不同(但同样有效)的答案。这就是统计和估计的本质。

您基本上会问为什么一种方法会产生296.839行的估计值,而另一种方法会产生181.8行。


让我们看一下问题中给出的相同 AdventureWorksDW2017 连接的更简单示例:

示例 1 - 加入

DECLARE @db date = '20130720';
DECLARE @de date = '20130802';

SELECT DD.FullDateAlternateKey, DP.ProductAlternateKey
FROM dbo.DimDate AS DD
JOIN dbo.DimProduct AS DP
    ON DP.StartDate <= CONVERT(datetime, DD.FullDateAlternateKey)
WHERE
    DD.FullDateAlternateKey BETWEEN @db AND @de
OPTION (FORCE ORDER, USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'));
Run Code Online (Sandbox Code Playgroud)

这是以下之间的连接:

  • DimDate(过滤FullDateAlternateKey BETWEEN @db AND @de);和
  • DimProduct

连接谓词是:

  • DP.StartDate <= CONVERT(datetime, DD.FullDateAlternateKey)

计算连接选择性的一种方法是使用直方图信息考虑FullDateAlternateKey值将如何与StartDate值重叠。

的直方图步骤FullDateAlternateKey将针对 的选择性进行缩放BETWEEN @db AND @de,然后与 进行比较DP.StartDate以查看它们如何连接。

使用原始 CE,连接估计将在“连接”之前使用线性插值逐步对齐两个直方图。

一旦我们使用此方法计算了连接的选择性,连接是散列、合并、嵌套循环还是应用都无关紧要(除了显示目的)。

基于直方图的计算步骤并不是特别困难,但它们太冗长,无法在此处显示。所以我将切入正题,简单地展示一下结果:

在此处输入图片说明

请注意在搜索中估计的296.839DimProduct

这是连接基数估计计算为 97,565.2 行(使用直方图)的结果。过滤器DimDate通过 328.68 行,因此内侧必须平均每次迭代产生 296.839 行才能使数学计算出来。

如果此查询可以使用散列或合并连接(由于不等式,它不是),DimProduct将扫描该表,生成其所有 606 行。连接的结果仍然是 97,565.2 行。

此估计是作为连接进行估计的结果。

示例 2 - 应用

我们还可以将此查询估计为apply。用 T-SQL 编写的逻辑等价形式是:

DECLARE @db date = '20130720';
DECLARE @de date = '20130802';

SELECT DD.FullDateAlternateKey, DP.ProductAlternateKey
FROM dbo.DimDate AS DD
CROSS APPLY
(
    SELECT DP.ProductAlternateKey
    FROM dbo.DimProduct AS DP
    WHERE
        DP.StartDate <= CONVERT(datetime, DD.FullDateAlternateKey)
) AS DP
WHERE
    DD.FullDateAlternateKey BETWEEN @db AND @de
OPTION (FORCE ORDER, USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'), QUERYTRACEON 9114);
Run Code Online (Sandbox Code Playgroud)

(跟踪标志 9114 阻止优化器将应用重写为连接)

这次的估计方法是DimProductDimDate(每次迭代)评估每行将匹配多少行:

在此处输入图片说明

我们有328.68行从DimDate以前一样,但现在每这些行的预期相符181.8DimProduct

这只是对 选择性的猜测StartDate <= FullDateAlternateKey

猜测是 606 行的 30% DimProduct:0.3 * 606 = 181.8行。

该估计是作为应用进行估计的结果。

最后的笔记

您的示例引入了外部联接作为使查询过于复杂的一种方式,优化器无法从应用形式转换为联接形式。使用TOP里面的应用是另一种方式来说服优化不转换的申请加入(即使它可以)。