cro*_*sek 9 sql-server optimization view sql-server-2012
使用 Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64)。
给定一个表和索引:
create table [User].[Session]
(
SessionId int identity(1, 1) not null primary key
CreatedUtc datetime2(7) not null default sysutcdatetime())
)
create nonclustered index [IX_User_Session_CreatedUtc]
on [User].[Session]([CreatedUtc]) include (SessionId)
Run Code Online (Sandbox Code Playgroud)
以下每个查询的实际行数为 310 万,估计行数显示为注释。
当这些查询在 View 中提供另一个查询时,由于 1 行估计,优化器选择循环连接。 如何在此基础级别改进估计以避免覆盖父查询连接提示或求助于 SP?
使用硬编码日期效果很好:
select distinct SessionId from [User].Session -- 2.9M (great)
where CreatedUtc > '04/08/2015' -- but hardcoded
Run Code Online (Sandbox Code Playgroud)
这些等效查询与视图兼容,但都估计为 1 行:
select distinct SessionId from [User].Session -- 1
where CreatedUtc > dateadd(day, -365, sysutcdatetime())
select distinct SessionId from [User].Session -- 1
where dateadd(day, 365, CreatedUtc) > sysutcdatetime();
select distinct SessionId from [User].Session s -- 1
inner loop join (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
on d.MinCreatedUtc < s.CreatedUtc
-- (also tried reversing join order, not shown, no change)
select distinct SessionId from [User].Session s -- 1
cross apply (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
where d.MinCreatedUtc < s.CreatedUtc
-- (also tried reversing join order, not shown, no change)
Run Code Online (Sandbox Code Playgroud)
尝试一些提示(但 N/A 无法查看):
select distinct SessionId from [User].Session -- 1
where CreatedUtc > dateadd(day, -365, sysutcdatetime())
option (recompile);
select distinct SessionId from [User].Session -- 1
where CreatedUtc > (select dateadd(day, -365, sysutcdatetime()))
option (recompile, optimize for unknown);
select distinct SessionId -- 1
from (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
inner loop join [User].Session s
on s.CreatedUtc > d.MinCreatedUtc
option (recompile);
Run Code Online (Sandbox Code Playgroud)
尝试使用参数/提示(但不适用):
declare
@minDate datetime2(7) = dateadd(day, -365, sysutcdatetime());
select distinct SessionId from [User].Session -- 1.2M (adequate)
where CreatedUtc > @minDate;
select distinct SessionId from [User].Session -- 2.96M (great)
where CreatedUtc > @minDate
option (recompile);
select distinct SessionId from [User].Session -- 1.2M (adequate)
where CreatedUtc > @minDate
option (optimize for unknown);
Run Code Online (Sandbox Code Playgroud)
统计数据是最新的。
DBCC SHOW_STATISTICS('user.Session', 'IX_User_Session_CreatedUtc') with histogram;
Run Code Online (Sandbox Code Playgroud)
直方图的最后几行(共 189 行)如下所示:
比 Aaron 的答案更不全面,但核心问题是DATEADD使用datetime2类型时的基数估计错误:
连接:当 sysdatetime 出现在 dateadd() 表达式中时估计不正确
一种解决方法是使用GETUTCDATE(返回日期时间):
WHERE CreatedUtc > CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()))
Run Code Online (Sandbox Code Playgroud)
请注意,转换为datetime2必须在 之外DATEADD以避免错误。
当使用 70 模型基数估计器时,错误的基数估计会在 SQL Server 的所有版本中重现,直到并包括 2019 CU8 GDR(内部版本 15.0.4083)。
Aaron Bertrand为 SQLPerformance.com 写了一篇关于此的文章:
在某些情况下,SQL Server 可以对DATEADD/进行非常疯狂的估计DATEDIFF,具体取决于参数是什么以及您的实际数据是什么样的。我DATEDIFF在处理月初的时候写过这个,还有一些解决方法,在这里:
但是,我的典型建议是停止在 where/join 子句中使用DATEADD/ DATEDIFF。
以下方法虽然在过滤范围内的闰年不是非常准确(在这种情况下它将包括额外的一天),并且在四舍五入到当天时,会得到更好的(但仍然不是很好!)估计,就像您DATEDIFF对列方法不可进行讨论,并且仍然允许使用搜索:
DECLARE @start date = DATEFROMPARTS
(
YEAR(GETUTCDATE())-1,
MONTH(GETUTCDATE()),
DAY(GETUTCDATE())
);
SELECT ... WHERE CreatedUtc >= @start;
Run Code Online (Sandbox Code Playgroud)
您可以操纵输入以DATEFROMPARTS避免闰日出现问题,用于DATETIMEFROMPARTS获得更高的精度而不是四舍五入到当天等。这只是为了证明您可以使用过去的日期填充变量而不使用DATEADD(它只是一个多做一点工作),从而避免估计错误中更严重的部分(在 2014+ 中修复)。
为避免闰日出错,您可以改为执行此操作,从去年的 2 月 28 日而不是 29 日开始:
DECLARE @start date = DATEFROMPARTS
(
YEAR(GETUTCDATE())-1,
MONTH(GETUTCDATE()),
CASE WHEN DAY(GETUTCDATE()) = 29 AND MONTH(GETUTCDATE()) = 2
THEN 28 ELSE DAY(GETUTCDATE()) END
);
Run Code Online (Sandbox Code Playgroud)
您也可以通过检查我们今年是否过了闰日来说添加一天,如果是,则在开头添加一天(有趣的是,使用DATEADD 此处仍然可以进行准确估计):
DECLARE @base date = GETUTCDATE();
IF GETUTCDATE() >= DATEFROMPARTS(YEAR(GETUTCDATE()),3,1) AND
TRY_CONVERT(datetime, DATEFROMPARTS(YEAR(GETUTCDATE()),2,29)) IS NOT NULL
BEGIN
SET @base = DATEADD(DAY, 1, GETUTCDATE());
END
DECLARE @start date = DATEFROMPARTS
(
YEAR(@base)-1,
MONTH(@base),
CASE WHEN DAY(@base) = 29 AND MONTH(@base) = 2
THEN 28 ELSE DAY(@base) END
);
SELECT ... WHERE CreatedUtc >= @start;
Run Code Online (Sandbox Code Playgroud)
如果您需要比午夜更准确,那么您可以在选择之前添加更多操作:
DECLARE @accurate_start datetime2(7) = DATETIME2FROMPARTS
(
YEAR(@start), MONTH(@start), DAY(@start),
DATEPART(HOUR, SYSUTCDATETIME()),
DATEPART(MINUTE,SYSUTCDATETIME()),
DATEPART(SECOND,SYSUTCDATETIME()),
0,0
);
SELECT ... WHERE CreatedUtc >= @accurate_start;
Run Code Online (Sandbox Code Playgroud)
现在,您可以将所有这些都塞进一个视图中,它仍然会使用搜索和 30% 的估计,而不需要任何提示或跟踪标志,但它并不漂亮。嵌套 CTE 只是为了让我不必键入SYSUTCDATETIME()一百次或重复重复使用的表达式——它们仍然可以被评估多次。
CREATE VIEW dbo.v5
AS
WITH d(d) AS ( SELECT SYSUTCDATETIME() ),
base(d) AS
(
SELECT DATEADD(DAY,CASE WHEN d >= DATEFROMPARTS(YEAR(d),3,1)
AND TRY_CONVERT(datetime,RTRIM(YEAR(d))+RIGHT('0'+RTRIM(MONTH(d)),2)
+RIGHT('0'+RTRIM(DAY(d)),2)) IS NOT NULL THEN 1 ELSE 0 END, d)
FROM d
),
src(d) AS
(
SELECT DATETIME2FROMPARTS
(
YEAR(d)-1,
MONTH(d),
CASE WHEN MONTH(d) = 2 AND DAY(d) = 29
THEN 28 ELSE DAY(d) END,
DATEPART(HOUR,d),
DATEPART(MINUTE,d),
DATEPART(SECOND,d),
10*DATEPART(MICROSECOND,d),
7
) FROM base
)
SELECT DISTINCT SessionId FROM [User].[Session]
WHERE CreatedUtc >= (SELECT d FROM src);
Run Code Online (Sandbox Code Playgroud)
这比您DATEDIFF针对该列的内容要冗长得多,但正如我在评论中提到的,这种方法是不可行的,并且在无论如何必须阅读大部分表格的情况下可能会具有竞争力,但我怀疑它会成为一种负担因为“去年”在表格中所占的百分比较低。
另外,仅供参考,以下是我尝试重现时获得的一些指标:
我无法得到 1 行的估计值,我非常努力地匹配您的分布(313 万行,去年为 289 万行)。但是你可以看到:
不要从持续时间数字中得出太多 - 他们现在很接近,但随着表的增长可能不会保持接近(再次,我相信,因为即使是搜索仍然必须阅读大部分表)。
以下是 v4(您的日期与列的差异)和 v5(我的版本)的计划:
| 归档时间: |
|
| 查看次数: |
372 次 |
| 最近记录: |