Dan*_*her 5 optimization sql-server-2019 query-performance
我有一个报告表(大约 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;
Run Code Online (Sandbox Code Playgroud)
...我添加了一个应用程序使用的报告视图,用于 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
Run Code Online (Sandbox Code Playgroud)
这个想法是用户应用程序将使用 SELECT 查询来查询这个视图,该查询过滤了一系列日期和一个或多个 BusinessUnit。为此,我选择了一种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;
Run Code Online (Sandbox Code Playgroud)
我希望查询计划如下所示:
想要的计划
然而,计划结果是这样的:
实际计划
我希望 SQL Server 对日期列执行“谓词下推”,允许聚集索引查找查找单个业务单位和日期范围,然后在 SKU 上应用剩余谓词。这适用于“s”分支(带有TOP
)的 Seek - 可能是因为它在查询中具有硬编码的 Date 谓词 - 但不适用于“t”分支。
但是,在“t”分支上,SQL Server 仅通过 SKU 上的剩余谓词寻找特定的 BusinessUnit,从而有效地检索所有日期。只有在计划结束时,它才会应用过滤器运算符来过滤日期列。
在一个大表中,这会带来非常显着的性能损失 - 当您只需要一周时,您最终可能会从磁盘读取 20 年的数据。
我尝试过的事情
解决方法:
UNION ALL
出来的CROSS APPLY
(从CROSS APPLY (UNION)
到CROSS APPLY() UNION CROSS APPLY()
)将使谓词下推。它在 BusinessUnit 表上再进行一次查找,这是完全可以接受的。修复了 Seek,但更改了结果:
TOP (1)
andORDER BY
会使谓词下推对 "t" 起作用,但可能会从 "s" 返回太多行。UNION ALL
通过删除“s”或“t”查询来消除将启用谓词下推,但会生成不正确的结果。没有改变或不可行:
TOP (1)
用ROW_NUMBER()
模式替换不会改变 Seek。CROSS APPLY
为强制INNER LOOP JOIN
修复了“t”上的 Seek,但实际上将“s”更改为 Scan,这甚至更糟。一个常见的思路似乎是更改/简化“s”查询(删除TOP
, ORDER BY
)修复了“t”查询上的问题,这对我来说是违反直觉的。
我在找什么
我试图了解这是否是优化器的缺点,是否是故意成本计算/优化机制的结果,或者我是否只是忽略了某些东西。
我试图了解这是否是优化器的缺点,是否是故意成本计算/优化机制的结果,或者我是否只是忽略了某些东西。
这是所有这些的一点点。
所提出的查询中发生了很多事情——真的太多了——所以为了避免写半本书,我将把它归结为导致你没有得到你所追求的计划的主要因素:
优化器不会将谓词下推到应用的内侧。
上的关系的选择(过滤器,谓词)操作一个以上的规则应用被调用时,很自然地,SELonApply
。它执行以下逻辑替代:
Sel (A 申请 B) -> Sel (Sel A 申请 B)
它接受涉及 A 和 B 的潜在复杂选择的一部分,并将那些它可以的部分推送到驱动表 A。没有选择的部分被推送到 B。选择的部分不能被推倒留在后面。
这听起来像是一个令人震惊的疏忽,与经验背道而驰。那是因为它不是完整的故事。
优化器尝试在编译过程的早期(在简化期间,在琐碎计划和基于成本的优化之前)将应用转换为等效的连接。它能够在安全的地方将选择推到join 的任一侧。在基于成本的优化过程中,该连接又可以转化为物理应用。
所有这些的效果是让优化器看起来像是将谓词推到了应用的内侧:
让我给你看一个例子:
DECLARE @T1 table (pk integer PRIMARY KEY, c1 integer NOT NULL INDEX ic1);
DECLARE @T2 table (fk integer NOT NULL, c2 integer NOT NULL, PRIMARY KEY (fk, c2));
SELECT
T1.*,
T2.*
FROM @T1 AS T1
CROSS APPLY
(
SELECT T2.*
FROM @T2 AS T2
WHERE T2.fk = T1.pk
) AS T2
WHERE
1 = 1
AND T1.c1 = 1
AND T2.c2 = 2;
Run Code Online (Sandbox Code Playgroud)
如果仔细查看计划,您会看到 T2 上的谓词被推到了内部查找,并且嵌套循环连接是一个应用(它有外部引用)。这是唯一可能的,因为优化器最初能够将应用重写为连接,推送谓词,然后稍后转换回应用。
我们可以使用未记录的跟踪标志 9114 禁用 apply-to-join 重写:
DECLARE @T1 table (pk integer PRIMARY KEY, c1 integer NOT NULL INDEX ic1);
DECLARE @T2 table (fk integer NOT NULL, c2 integer NOT NULL, PRIMARY KEY (fk, c2));
SELECT
T1.*,
T2.*
FROM @T1 AS T1
CROSS APPLY
(
SELECT T2.*
FROM @T2 AS T2
WHERE T2.fk = T1.pk
) AS T2
WHERE
1 = 1
AND T1.c1 = 1
AND T2.c2 = 2
OPTION (QUERYTRACEON 9114);
Run Code Online (Sandbox Code Playgroud)
这意味着only SELonApply
can be used,只推送到驱动表A:
请注意,T2.c2 上的选择部分“卡在”过滤器中的应用上方。(内侧查找仅在 apply 中指定的 fk/pk 相等性上。)
优化器建立在关系原则之上。它欣赏关系模式设计和使用关系构造的查询。Apply(横向连接)是一个相对较新的扩展。优化器在 join 方面知道更多的技巧,它在 apply 方面做了很多,因此早期努力重写。
当您使用诸如 apply 或非关系 Top 之类的东西时,您隐含地对最终计划形状承担了更多责任。换句话说,您将更多地需要以不同的方式表达您的查询(如在您的解决方法中)以获得良好的结果。
作为记录,我更喜欢使用带有显式谓词放置的内联表值函数。如果我要重写视图,我可能会选择:
CREATE OR ALTER VIEW dbo.SalesReport_unpartitioned
AS
--- Regular sales
SELECT
BU.BusinessUnit,
RS.[Date],
RS.SKU,
RS.Quantity,
RS.Amount
FROM dbo.BusinessUnits AS BU
JOIN dbo.Sales_unpartitioned AS RS
ON RS.BusinessUnit = BU.BusinessUnit
WHERE
RS.SKU LIKE 'T%'
UNION ALL
--- This is a special reporting entry.
SELECT
BU.BusinessUnit,
SR.[Date],
SR.SKU,
SR.Quantity,
SR.Amount
FROM dbo.BusinessUnits AS BU
JOIN dbo.Sales_unpartitioned AS SR
ON SR.BusinessUnit = BU.BusinessUnit
WHERE
1 = 1
AND SR.SKU LIKE 'S%'
--- We only want to see today's row.
AND SR.[Date] = CONVERT(date, SYSDATETIME())
--- In case of duplicates, get the row with the first "SKU".
AND SR.SKU =
(
SELECT
MIN(SR2.SKU)
FROM dbo.Sales_unpartitioned AS SR2
WHERE
SR2.BusinessUnit = SR.BusinessUnit
AND SR2.[Date] = SR.[Date]
AND SR2.SKU LIKE 'S%'
);
GO
Run Code Online (Sandbox Code Playgroud)
对于提供的测试查询:
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;
Run Code Online (Sandbox Code Playgroud)
执行计划是:
橙色部分为常规销售。黄色部分用于特殊报告条目。
归档时间: |
|
查看次数: |
228 次 |
最近记录: |