主/明细表之间的散列连接产生过低的基数估计

Jus*_*ant 9 sql-server execution-plan sql-server-2014 cardinality-estimates

将主表连接到详细表时,如何鼓励 SQL Server 2014 使用较大(详细)表的基数估计作为连接输出的基数估计?

例如,当将 10K 主行连接到 100K 详细行时,我希望 SQL Server 估计连接为 100K 行——与估计的详细行数相同。我应该如何构建我的查询和/或表和/或索引以帮助 SQL Server 的估算器利用每个详细信息行始终具有相应的主行这一事实?(这意味着它们之间的连接永远不应该减少基数估计。)

这里有更多细节。我们的数据库有一对主/明细表:VisitTarget每个销售交易占一行,每个交易VisitSale中的每个产品占一行。这是一个一对多的关系:一个 VisitTarget 行平均有 10 个 VisitSale 行。

表格如下所示:(我正在简化为仅针对此问题的相关列)

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;
Run Code Online (Sandbox Code Playgroud)

出于性能原因,我们通过将最常见的过滤列(例如SaleDate)从主表复制到每个明细表行来部分非规范化,然后我们在两个表上添加覆盖索引以更好地支持日期过滤查询。这对于减少运行日期过滤查询时的 I/O 非常有效,但我认为这种方法在将主表和详细表连接在一起时会导致基数估计问题。

当我们连接这两个表时,查询如下所示:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 
Run Code Online (Sandbox Code Playgroud)

明细表 ( VisitSale)上的日期过滤器是多余的。它可以在详细信息表上为按日期范围过滤的查询启用顺序 I/O(又名索引查找运算符)。

此类查询的计划如下所示:

在此处输入图片说明

可以在此处找到具有相同问题的查询的实际计划。

如您所见,连接的基数估计(图片左下角的工具提示)太低了 4 倍:实际为 2.1M,估计为 0.5M。这会导致性能问题(例如溢出到 tempdb),尤其是当此查询是在更复杂的查询中使用的子查询时。

但是连接的每个分支的行数估计值接近实际的行数。连接的上半部分是 100K 实际与 164K 估计。连接的下半部分实际为 210 万行,而估计为 370 万行。哈希桶分布看起来也不错。这些观察结果告诉我,每个表的统计数据都可以,问题是连接基数的估计。

起初我认为问题是 SQL Server 期望每个表中的 SaleDate 列是独立的,而实际上它们是相同的。所以我尝试将销售日期的相等比较添加到连接条件或 WHERE 子句中,例如

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate
Run Code Online (Sandbox Code Playgroud)

或者

WHERE vt.SaleDate = vs.SaleDate
Run Code Online (Sandbox Code Playgroud)

这没有用。它甚至使基数估计变得更糟!因此,要么 SQL Server 没有使用该等式提示,要么其他原因是问题的根本原因。

对如何排除故障并希望解决此基数估计问题有任何想法吗?我的目标是估计主/从联接的基数与联接的较大(“详细信息表”)输入的估计值相同。

如果重要的话,我们正在 Windows Server 上运行 SQL Server 2014 Enterprise SP2 CU8 build 12.0.5557.0。没有启用跟踪标志。数据库兼容级别为 SQL Server 2014。我们在多个不同的 SQL Server 上看到相同的行为,因此这似乎不太可能是特定于服务器的问题。

SQL Server 2014 Cardinality Estimator中有一个优化,这正是我正在寻找的行为:

但是,新的 CE 使用更简单的算法,该算法假定大表和小表之间存在一对多连接关联。这假设大表中的每一行都与小表中的一行完全匹配。该算法返回较大输入的估计大小作为连接基数。

理想情况下,我可以得到这种行为,其中连接的基数估计将与大表的估计相同,即使我的“小”表仍将返回超过 100K 行!

Joe*_*ish 6

假设通过对统计数​​据做一些事情或使用旧版 CE 无法获得任何改进,那么解决您的问题最直接的方法是将您的更改INNER JOINLEFT OUTER JOIN

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'
Run Code Online (Sandbox Code Playgroud)

如果表之间有外键,则始终SaleDate对两个表的相同范围进行过滤,并且表SaleDate之间始终匹配,则查询结果不应更改。使用这样的外连接可能看起来很奇怪,但可以将其视为通知查询优化器连接到VisitTarget表永远不会减少查询返回的行数。不幸的是,外键不会改变基数估计,所以有时你需要求助于这样的技巧。(Microsoft Connect 建议:使用元数据使优化器估计更准确)。

根据连接后查询中发生的其他情况,以这种形式编写查询可能无法正常工作。您可以尝试使用临时表来保存具有最重要基数估计的结果集的中间结果。