主表/明细表之间的哈希联接产生的基数估计值太低


9

将主表连接到明细表时,如何鼓励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;

出于性能原因,我们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. 

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

这些查询的计划如下所示:

在此处输入图片说明

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

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

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

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

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

要么

WHERE vt.SaleDate = vs.SaleDate

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

对如何解决并希望解决此基数估计问题有任何想法?我的目标是要估计主/明细连接的基数与该连接的较大(“明细表”)输入的估计相同。

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

SQL Server 2014基数估计器中有一个优化,正是我要寻找的行为:

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

理想情况下,我可以得到这种行为,即使我的“小”表仍返回超过10万行,联接的基数估算值也将与大表的基数估算值相同!

Answers:


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'

如果表之间具有外键,则始终在SaleDate两个表的相同范围内进行过滤,并且SaleDate在表之间始终匹配,则查询结果不应更改。像这样使用外部联接似乎很奇怪,但是可以认为它是告知查询优化器该VisitTarget表的联接将永远不会减少查询返回的行数。不幸的是,外键不会更改基数估计,因此有时您需要诉诸此类技巧。(Microsoft Connect建议:通过使用元数据来使优化器估计更加准确)。

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

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.