重写T-SQL where子句导致性能问题

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 列)。

如何重写它以便可以使用底层索引来使查询运行得更快?

抱歉,由于安全限制,我无法共享执行计划。

数据存储格式由第三方定义,不受我们控制。例如,不能选择将计算列添加到随后可以建立索引的表中。

Mar*_*ith 9

您可以扩大日期范围,然后对该范围内的每个日期执行相关索引搜索。

上的相等性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以下内容)

  • dyear = 2023 且 dmonth = 7 且 dday >= 1
  • dyear = 2023 且 dmonth > 7 且 dmonth < 9
  • dyear = 2023 且 dmonth = 9 且 dday <= 11

但该计划有一个令人讨厌的独特类型,尽管这些是脱节的

在此输入图像描述


Pau*_*ite 5

另一种方法是使用动态 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)

执行计划是执行三个范围搜索的单个搜索运算符:

执行计划

数据库<>小提琴

该脚本适用于任何开始和结束日期。