SQL*_*DBA 5 sql-server query-performance sql-server-2022
我有一个查询,其中以下where子句需要花费大量时间来执行,因为基础表中有大量数据:
环境:SQL Server 2022
WHERE条款:
DATEFROMPARTS(dyear, dmonth, dday)
BETWEEN DATEADD(m, -2, DATEADD(mm, DATEDIFF(m, 0, GETDATE()), 0))
AND CAST(CAST(DATEADD(d, -2, GETDATE()) AS DATE) AS DATETIME)
Run Code Online (Sandbox Code Playgroud)
dyear, dmonth, dday由于使用了函数,它当前不使用索引(所有三个 int 列)。
如何重写它以便可以使用底层索引来使查询运行得更快?
抱歉,由于安全限制,我无法共享执行计划。
数据存储格式由第三方定义,不受我们控制。例如,不能选择将计算列添加到随后可以建立索引的表中。
您可以扩大日期范围,然后对该范围内的每个日期执行相关索引搜索。
上的相等性dyear, dmonth, dday是可控制的。每次查找都可以准确读取当天的行,因此总的来说,查找读取的行数可以恰好是所需的数量,而不会读取过多的行。
由于您只显示了查询的一部分,我不知道列表SELECT需要是什么。不要使用*- 理想情况下它将被这三列上的复合索引覆盖。
样本数据
CREATE TABLE #yourtable (dyear int not null, dmonth int not null, dday int not null, index ix (dyear, dmonth, dday))
INSERT #yourtable
SELECT TOP 1000000 CRYPT_GEN_RANDOM(1) + 1970,
CRYPT_GEN_RANDOM(1)%12 + 1,
CRYPT_GEN_RANDOM(1)%28 + 1
FROM sys.all_objects o1,
sys.all_objects o2
Run Code Online (Sandbox Code Playgroud)
询问
SELECT yt.dyear,
yt.dmonth,
yt.dday
FROM generate_series(0, DATEDIFF(DAY, DATETRUNC(MONTH, DATEADD(MONTH, -2, GETDATE())), GETDATE()) - 2)
CROSS APPLY (VALUES(DATEADD(DAY, value, DATETRUNC(MONTH, DATEADD(MONTH, -2, GETDATE()))))) D(calc_date)
CROSS APPLY (SELECT *
FROM #yourtable yt
WHERE yt.dday = DAY(calc_date)
AND yt.dmonth = month(calc_date)
AND yt.dyear = YEAR(calc_date)) yt
Run Code Online (Sandbox Code Playgroud)
注意:我确实尝试使用 Nenad 的答案中的方法获得索引查找计划,但我无法获得一个既不读取许多额外行也不具有删除重复项的步骤的计划。
当谓词正好是时,查找读取的行数正是所需的
WHERE ( dyear > @StartYear
OR ( dyear = @StartYear
AND dmonth > @StartMonth )
OR ( dyear = @StartYear
AND dmonth = @StartMonth
AND dday >= @StartDay ) )
Run Code Online (Sandbox Code Playgroud)
但它似乎并没有非常有效地结合开始和结束条件。
我也尝试扩大条件。
select *
from #yourtable with (forceseek)
WHERE
((dyear > @StartYear) AND (dyear < @EndYear)) OR
((dyear > @StartYear) AND (dyear = @EndYear AND dmonth < @EndMonth)) OR
((dyear > @StartYear) AND (dyear = @EndYear AND dmonth = @EndMonth AND dday <= @EndDay)) OR
((dyear = @StartYear AND dmonth > @StartMonth) AND (dyear < @EndYear)) OR
((dyear = @StartYear AND dmonth > @StartMonth) AND (dyear = @EndYear AND dmonth < @EndMonth)) OR
((dyear = @StartYear AND dmonth > @StartMonth) AND (dyear = @EndYear AND dmonth = @EndMonth AND dday <= @EndDay)) OR
((dyear = @StartYear AND dmonth = @StartMonth AND dday >= @StartDay) AND (dyear < @EndYear)) OR
((dyear = @StartYear AND dmonth = @StartMonth AND dday >= @StartDay) AND (dyear = @EndYear AND dmonth < @EndMonth)) OR
((dyear = @StartYear AND dmonth = @StartMonth AND dday >= @StartDay) AND (dyear = @EndYear AND dmonth = @EndMonth AND dday <= @EndDay))
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)
这确实寻求正确的范围(可以简化为RECOMPILE以下内容)
但该计划有一个令人讨厌的独特类型,尽管这些是脱节的
另一种方法是使用动态 SQL 生成析取范围:
DECLARE
@StartDate date = DATETRUNC(MONTH, DATEADD(MONTH, -2, GETDATE())),
@EndDate date = DATEADD(DAY, -2, GETDATE());
DECLARE
@SQL nvarchar(max) = N'SELECT * FROM #yourtable WHERE 0 = 1';
WHILE @StartDate < @EndDate
BEGIN
SET @SQL =
CONCAT
(
@SQL,
NCHAR(13), NCHAR(10),
N' OR (dyear = ', YEAR(@StartDate),
N' AND dmonth = ', MONTH(@StartDate),
-- First month only if short
IIF
(
DAY(@StartDate) > 1,
CONCAT(N' AND dday >= ', DAY(@StartDate)),
N''
),
-- Last month only if short
IIF
(
DATETRUNC(MONTH, @StartDate) = DATETRUNC(MONTH, @EndDate),
CONCAT(N' AND dday <= ', DAY(@EndDate)),
N''
),
N')'
);
-- Next month
SET @StartDate = DATETRUNC(MONTH, DATEADD(MONTH, 1, @StartDate));
END;
PRINT @SQL;
EXECUTE (@SQL);
Run Code Online (Sandbox Code Playgroud)
今天,该脚本生成:
SELECT * FROM #yourtable WHERE 0 = 1
OR (dyear = 2023 AND dmonth = 7)
OR (dyear = 2023 AND dmonth = 8)
OR (dyear = 2023 AND dmonth = 9 AND dday <= 12)
Run Code Online (Sandbox Code Playgroud)
执行计划是执行三个范围搜索的单个搜索运算符:
该脚本适用于任何开始和结束日期。