将主表连接到明细表时,如何鼓励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万行,联接的基数估算值也将与大表的基数估算值相同!