键之间基数差异较大的表的基数估计

Yan*_*ton 10 sql-server statistics azure-sql-database cardinality-estimates

语境

我偶然发现了 SQL Azure 中的一个问题,其中Sort-operator 由于行估计不佳而溢出到 TempDB 中。

  • 我们正在查询与多个明细表连接的主表
  • 该表使用TenantId-column 对每个租户的表进行分区
  • 有些租户有 10,000 行,有些只有 100 行。
  • 有一个行级安全策略,可以为FILTER PREDICATE上述所有查询添加一个TenantId
  • 查询由 .NET 应用程序中的实体框架生成
  • 所有索引统计数据都是最新的
  • 所有明细表行都通过Index Seeks检索

问题

由于租户之间的行数差异很大,基数估计器产生的估计值非常低。这与两个内部连接相结合,进一步减少了估计,使得实际产生 3600 行的查询预计只产生 3。这是 3 个数量级的下降。

我尝试了什么?

  1. Filtered Statistics为那些产生大量行的键值定义,作为对 CE 的额外提示。
  2. 在处理参数化查询时遇到了限制。这OPTION ( RECOMPILE )适用于某些谓词,但不适用于TenantId通过上述安全策略注入的谓词。
  3. 内联过滤器谓词,因此我们在同一列上有效过滤两次有效,但似乎......至少可以说是多余的
  4. INNER JOINs更改为s 可以LEFT OUTER JOIN改善错误连接估计,但由于我们使用实体框架,我更喜欢不需要更改查询的解决方案。注意:显然,如果唯一的方法是重写查询,那么这就是我们将要走的路线。

其他想法

  • 我曾考虑过添加一个包含 10 万条记录的虚拟租户来抵消估计值的想法,这样行估计值至少对于最大的真实租户来说足够大,但这会使我们对小租户的估计过高。

我在寻找什么?

  • 我做错了什么 - 我把自己画到了一个角落里吗?
  • 有没有我可以考虑的替代方案?

我欢迎您的任何想法,谢谢!


排序运算符用于分页。我实际上不想检索所有行。所以简而言之,排序需要发生在数据库中(而不是在应用程序中)。

另外,要明确的是,这里的问题不是 EF 生成的查询。这是一个简单的查询,带有许多INNER/LEFT OUTER连接和一些过滤谓词。它不是典型的 5000 行 SQL 语句,没有人能看懂。

似乎细节查找是通过hash match它丢失索引的排序顺序完成的。此外,订购的列是用户定义的,并根据用例进行更改,因此为了避免排序,我需要每列的索引,它们可能会排序。

附加信息

行级安全策略

CREATE FUNCTION Security.myAccessPredicate(@TenantId nvarchar(128))
    RETURNS TABLE WITH SCHEMABINDING
    AS
    RETURN SELECT 1 AS accessResult
    WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS nvarchar(128))
    OR CAST(SESSION_CONTEXT(N'TenantId') AS nvarchar(128)) LIKE 'ReservedTenantIdForCrossTenantOperations'");

CREATE SECURITY POLICY Security.mySecurityPolicy
    ADD FILTER PREDICATE Security.myAccessPredicate(TenantId) ON Schema1.Object10,
    ADD BLOCK PREDICATE Security.myAccessPredicate(TenantId) ON Schema1.Object10,
    ... And all the other tables
Run Code Online (Sandbox Code Playgroud)

主表索引 DDL

CREATE NONCLUSTERED INDEX [IX_Object10] ON Schema1.Object10
(
    [TenantId] ASC,
    [Discriminator] ASC,
    [IsDeleted] ASC,
    [CreatedAt] ASC
)
INCLUDE (
    [Id],
    [Column3],
    ...
    [Column37]) 
WITH (
    PAD_INDEX = OFF
    , STATISTICS_NORECOMPUTE = OFF
    , SORT_IN_TEMPDB = OFF
    , DROP_EXISTING = OFF
    , ONLINE = OFF
    , ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON
)
Run Code Online (Sandbox Code Playgroud)

查询(匿名)

EXEC sp_set_session_context @key=N'TenantId', @value=N'TenantId123'

SELECT *
FROM ( SELECT 
    Object2.Column2 AS Column2, 
    Object2.Column3 AS CreatedAt, 
    -- Removed some extra columns from Object2
    Object3.Column4 AS Column3, 
    Object4.Column4 AS Column5, 
    Object5.Column4 AS Column6, 
    Object6.Column7 AS Column7, 
    Object7.Column10 AS Column10, 
    Object8.Column10 AS Column11, 
    Object9.Column4 AS Column20, 
    CASE WHEN (Variable1 = ?) THEN Object2.Column26 ELSE cast(? as decimal(18)) END AS Column21, 
    CASE WHEN (Variable2 = ?) THEN Object2.Column27 ELSE ? END AS Column22, 
    CASE WHEN (Variable3 = ?) THEN Object2.Column28 ELSE ? END AS Column23, 
    CASE WHEN (Variable4 = ?) THEN Object2.Column29 END AS Column24
    FROM Schema1.Object10 AS Object2
    INNER JOIN Schema1.Object11 AS Object3 ON Object2.Column30 = Object3.Column2
    INNER JOIN Schema1.Object12 AS Object4 ON Object2.Column31 = Object4.Column2
    LEFT OUTER JOIN Schema1.Object10 AS Object5 ON (Object2.Column32 = Object5.Column2) AND (Object5.Column33 = ?)
    LEFT OUTER JOIN Schema1.Object10 AS Object6 ON (Object2.Column34 = Object6.Column2) AND (Object6.Column33 = ?)
    LEFT OUTER JOIN Schema1.Object13 AS Object7 ON Object2.Column35 = Object7.Column2
    LEFT OUTER JOIN Schema1.Object13 AS Object8 ON Object2.Column36 = Object8.Column2
    LEFT OUTER JOIN Schema1.Object14 AS Object9 ON Object2.Column37 = Object9.Column2
    WHERE (Object2.Discriminator = N'SomeCategory') AND (0 = Object2.IsDeleted)
)  AS Object1
ORDER BY Object1.CreatedAt DESC
OFFSET ? ROWS FETCH NEXT ? ROWS ONLY
Run Code Online (Sandbox Code Playgroud)

查询计划(匿名):https : //pastebin.com/msjPQ6Vs

估计的

在此处输入图片说明

实际情况

在此处输入图片说明

主表 (Schema1.Object10 AS Object2) 从索引查找返回 3600 多行实际行,但 CE 估计为 381 行。然后到 Schema1.Object11 和 Schema1.Object12 的两个内部联接进一步将估计值减少到 2.9 个估计行数。这对我来说毫无意义。内连接发生在具有外键约束的非空 id 列上,因此应该不可能找不到匹配项。

排序运算符工具提示:

在此处输入图片说明

实际时间,而排序运算符之前的累积时间为 16 毫秒

在此处输入图片说明

小智 1

抱歉,如果我在快速阅读中错过了这一点......但是您是否尝试过在数据库上启用旧基数估计?

我是一个应用程序的 ManyHatAdmin,该应用程序能够生成针对大型数据库动态运行的 SQL 查询,通常使用 INNER JOINS。我们发现,在 SQL 2016 之后(当 CE 经历了许多更改时),由于新的 CE,某些查询的运行速度会慢得多。我们找到的解决方案是在数据库级别将旧基数估计设置为“ON”。

我相信它也可以仅使用('FORCE_LEGACY_CARDINALITY_ESTIMATION')查询提示来激活特定查询。