我有一个报表(大约 10 亿行)和一个很小的维度表:
CREATE TABLE dbo.Sales_unpartitioned (
BusinessUnit int NOT NULL,
[Date] date NOT NULL,
SKU varchar(8) NOT NULL,
Quantity numeric(10, 2) NOT NULL,
Amount numeric(10, 2) NOT NULL,
CONSTRAINT PK_Sales_unpartitioned PRIMARY KEY CLUSTERED (BusinessUnit, [Date], SKU)
);
--- Demo data:
INSERT INTO dbo.Sales_unpartitioned
SELECT severity AS BusinessUnit,
DATEADD(day, message_id, '2000-01-01') AS [Date],
LEFT([text], 3) AS SKU,
1000.*RAND(CHECKSUM(NEWID())) AS Quantity,
10000.*RAND(CHECKSUM(NEWID())) AS Amount
FROM sys.messages
WHERE [language_id]=1033;
--- Artificially inflate statistics of demo data:
UPDATE STATISTICS dbo.Sales_unpartitioned WITH ROWCOUNT=1000000000;
--- Dimension table:
CREATE TABLE dbo.BusinessUnits (
BusinessUnit int NOT NULL,
SalesManager nvarchar(250) NULL,
PRIMARY KEY CLUSTERED (BusinessUnit)
);
INSERT INTO dbo.BusinessUnits (BusinessUnit)
SELECT DISTINCT BusinessUnit FROM dbo.Sales;
...我在其中添加了应用程序用于 OLTP 样式报告的报告视图。
CREATE OR ALTER VIEW dbo.SalesReport_unpartitioned
AS
SELECT bu.BusinessUnit,
s.[Date],
s.SKU,
s.Quantity,
s.Amount
FROM dbo.BusinessUnits AS bu
CROSS APPLY (
--- Regular sales
SELECT t.BusinessUnit, t.[Date], t.SKU, t.Quantity, t.Amount
FROM dbo.Sales_unpartitioned AS t
WHERE t.BusinessUnit=bu.BusinessUnit
AND t.SKU LIKE 'T%'
UNION ALL
--- This is a special reporting entry. We only
--- want to see today's row. In case of duplicates,
--- get the row with the first "SKU".
SELECT TOP (1) s.BusinessUnit, s.[Date], s.SKU, s.Quantity, s.Amount
FROM dbo.Sales_unpartitioned AS s
WHERE s.BusinessUnit=bu.BusinessUnit
AND s.[Date]=CAST(SYSDATETIME() AS date)
AND s.SKU LIKE 'S%'
ORDER BY s.BusinessUnit, s.[Date], s.SKU
) AS s
这个想法是,用户应用程序将使用 SELECT 查询来查询此视图,该查询过滤一系列日期和一个或多个业务单位。为此,我选择了一种CROSS APPLY
模式,以便查询可以“循环”每个业务单元,查找日期范围,并在 SKU 上应用残差过滤器。
示例应用查询:
DECLARE @from date='2021-01-01', @to date='2021-12-31';
SELECT *
FROM dbo.SalesReport_unpartitioned
WHERE BusinessUnit=16
AND [Date] BETWEEN @from AND @to
ORDER BY BusinessUnit, [Date], SKU;
我希望查询计划看起来像这样: Desired plan
但是,计划结果如下: 实际计划
我希望 SQL Server 在 Date 列上执行“谓词下推”,允许 Clustered Index Seek 查找单个 BusinessUnit 和日期范围,然后在 SKU 上应用剩余谓词。这适用于“s”分支中的 Seek(带有 的分支TOP
)——可能是因为它在查询中有一个硬编码的 Date 谓词——但不适用于“t”分支。
但是,在“t”分支上,SQL Server 只寻找具有 SKU 上的剩余谓词的特定 BusinessUnit,有效地检索所有日期。只有在计划结束时,它才会应用过滤日期列的过滤运算符。
在一个大表中,这会带来非常显着的性能损失——当您只需要一周的时间时,您最终可能会从磁盘读取 20 年的数据。
我尝试过的事情
解决方法:
- 将视图转换为具有过滤“s”和“t”查询的@fromDate 和@toDate 参数的内联表值函数将根据需要启用Seek on (BusinessUnit, Date),但需要重写应用程序代码。
- 将(from to )
UNION ALL
移出将启用谓词下推。它在 BusinessUnit 表上又进行了一次搜索,这是完全可以接受的。CROSS APPLY
CROSS APPLY (UNION)
CROSS APPLY() UNION CROSS APPLY()
修复了 Seek,但改变了结果:
- 令人惊讶的是,删除“s”查询的
TOP (1)
andORDER BY
会使谓词下推对“t”起作用,但会从“s”返回太多行。 UNION ALL
通过删除“s”或“t”查询来消除将启用谓词下推,但会产生不正确的结果。
无变化或不可行:
- 替换
TOP (1)
为ROW_NUMBER()
模式不会改变 Seek。 - 将 the 更改
CROSS APPLY
为强制INNER LOOP JOIN
修复了“t”上的 Seek,但实际上将“s”更改为 Scan,这甚至更糟。 - 添加跟踪标志 8780 以允许优化器在计划上工作更长时间不会改变任何事情。该计划已经完全优化,没有提前终止。
一个常见的线程似乎是更改/简化“s”查询(删除TOP
, ORDER BY
)解决了“t”查询的问题,这对我来说感觉违反直觉。
我在寻找什么
我试图了解这是否是优化器的缺点,是否是故意成本/优化机制的结果,或者我是否只是忽略了某些东西。