两个日期列的 SARGable WHERE 子句

Eri*_*ing 25 performance index sql-server query-performance

对我来说,我有一个关于 SARGability 的有趣问题。在这种情况下,它是关于对两个日期列之间的差异使用谓词。这是设置:

USE [tempdb]
SET NOCOUNT ON  

IF OBJECT_ID('tempdb..#sargme') IS NOT NULL
BEGIN
DROP TABLE #sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO #sargme
FROM sys.[messages] AS [m]

ALTER TABLE [#sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [#sargme] ([DateCol1], [DateCol2])
Run Code Online (Sandbox Code Playgroud)

我会经常看到的是这样的:

/*definitely not sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48;
Run Code Online (Sandbox Code Playgroud)

...这绝对不是SARGable。它导致索引扫描,读取所有 1000 行,不好。估计行很臭。你永远不会把它投入生产。

不,先生,我不喜欢它。

如果我们能够实现 CTE,那就太好了,因为从技术上讲,这将帮助我们使这个,好吧,更 SARGable-er。但是不,我们得到了与 top 相同的执行计划。

/*would be nice if it were sargable*/
WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [#sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;
Run Code Online (Sandbox Code Playgroud)

当然,因为我们没有使用常量,所以这段代码没有任何改变,甚至不是半个 SARGable。没有乐趣。相同的执行计划。

/*not even half sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])
Run Code Online (Sandbox Code Playgroud)

如果您感到幸运,并且您遵守了连接字符串中的所有 ANSI SET 选项,您可以添加一个计算列,并对其进行搜索...

ALTER TABLE [#sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [#sargme] AS [s]
WHERE [ddiff] >= 48
Run Code Online (Sandbox Code Playgroud)

这将为您提供三个查询的索引查找。奇怪的人是我们将 48 天添加到 DateCol1 的地方。与查询DATEDIFFWHERE子句,CTE以计算列上的谓词,并最终查询全部给你好得多估计更加美好的计划,以及所有。

我可以忍受这个。

这让我想到了一个问题:在单个查询中,是否有一种 SARGable 方式来执行此搜索?

没有临时表,没有表变量,没有改变表结构,也没有视图。

我可以使用自联接、CTE、子查询或多次传递数据。可以使用任何版本的 SQL Server。

避免计算列是一个人为的限制,因为我对查询解决方案比其他任何事情都更感兴趣。

Pau*_*ite 17

只需快速添加它,使其作为答案存在(尽管我知道这不是您想要的答案)。

一个索引计算列通常是这种类型的问题的解决方案。

它:

  • 使谓词成为可索引的表达式
  • 允许创建自动统计以更好地估计基数
  • 并不需要采取任何空间,在基表

要清楚最后一点,在这种情况下不需要保留计算列:

-- Note: not PERSISTED, metadata change only
ALTER TABLE #sargme
ADD DayDiff AS DATEDIFF(DAY, DateCol1, DateCol2);

-- Index the expression
CREATE NONCLUSTERED INDEX index_name
ON #sargme (DayDiff)
INCLUDE (DateCol1, DateCol2);
Run Code Online (Sandbox Code Playgroud)

现在查询:

SELECT
    S.ID,
    S.DateCol1,
    S.DateCol2,
    DATEDIFF(DAY, S.DateCol1, S.DateCol2)
FROM
    #sargme AS S
WHERE
    DATEDIFF(DAY, S.DateCol1, S.DateCol2) >= 48;
Run Code Online (Sandbox Code Playgroud)

...给出了以下微不足道的计划:

执行计划

正如 Martin Smith 所说,如果您有使用错误设置选项的连接,您可以创建一个常规列并使用触发器维护计算值。

当然,正如 Aaron 在他的回答中所说,所有这些只有在有真正的问题需要解决时才真正重要(代码挑战除外)。

考虑到这一点很有趣,但是鉴于问题中的限制,我不知道有什么方法可以合理地实现您想要的目标。似乎任何最佳解决方案都需要某种类型的新数据结构;我们最接近的是由上述非持久计算列上的索引提供的“函数索引”近似值。


Dan*_*her 13

冒着受到 SQL Server 社区中一些知名人士的嘲笑的风险,我要伸出脖子说,不。

为了使您的查询成为 SARGable,您必须基本上构建一个查询,该查询可以在索引的一系列连续行中查明起始行。使用 index ix_dates,行不会按DateCol1和之间的日期差异排序DateCol2,因此您的目标行可以分布在索引中的任何位置。

自联接、多次传递等都有一个共同点,即它们至少包括一次索引扫描,尽管(嵌套循环)联接很可能使用索引查找。但我看不出如何消除扫描。

至于获得更准确的行估计,没有关于日期差异的统计。

以下相当丑陋的递归 CTE 构造在技术上确实消除了对整个表的扫描,尽管它引入了嵌套循环连接和(可能非常大)数量的索引查找。

DECLARE @from date, @count int;
SELECT TOP 1 @from=DateCol1 FROM #sargme ORDER BY DateCol1;
SELECT TOP 1 @count=DATEDIFF(day, @from, DateCol1) FROM #sargme WHERE DateCol1<=DATEADD(day, -48, {d '9999-12-31'}) ORDER BY DateCol1 DESC;

WITH cte AS (
    SELECT 0 AS i UNION ALL
    SELECT i+1 FROM cte WHERE i<@count)

SELECT b.*
FROM cte AS a
INNER JOIN #sargme AS b ON
    b.DateCol1=DATEADD(day, a.i, @from) AND
    b.DateCol2>=DATEADD(day, 48+a.i, @from)
OPTION (MAXRECURSION 0);
Run Code Online (Sandbox Code Playgroud)

它创建一个包含每次一个索引后台DateCol1在表中,然后执行索引查找(范围扫描)对每个那些的DateCol1DateCol2是至少48天前进。

更多的 IO,稍长的执行时间,行估计仍然遥遥无期,并且由于递归而并行化的机会为零:我猜这个查询可能有用,如果您在相对较少的不同的连续值中有非常多的值DateCol1(保持搜索次数减少)。

疯狂的递归 CTE 查询计划


Aar*_*and 9

我尝试了一堆古怪的变体,但没有找到比你更好的版本。主要问题是您的索引在 date1 和 date2 如何排序方面看起来像这样。第一列将在一个很好的搁置线上,而它们之间的间隙将非常参差不齐。您希望它看起来更像一个漏斗,而不是它真正的样子:

Date1    Date2
-----    -------
*             *
*             *
*              *
 *       * 
 *        *
 *         *
  *      *
  *           *
Run Code Online (Sandbox Code Playgroud)

我真的没有任何办法可以使两点之间的某个增量(或增量范围)可查找。我的意思是执行一次 + 范围扫描的单个搜索,而不是为每一行执行的搜索。这将在某个时候涉及扫描和/或排序,而这些显然是您想要避免的。太糟糕了,您不能在过滤索引中使用DATEADD/ 之类的表达式,也不能DATEDIFF执行任何可能的架构修改,以允许对日期差异的乘积进行排序(例如在插入/更新时计算增量)。照原样,这似乎是扫描实际上是最佳检索方法的情况之一。

你说这个查询不好玩,但如果你仔细看,这是迄今为止最好的查询(如果你忽略计算标量输出会更好):

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])
Run Code Online (Sandbox Code Playgroud)

原因是,DATEDIFF针对索引中的非前导键列进行计算相比,避免潜在地减少了一些 CPU ,并且还避免了一些令人讨厌的隐式转换datetimeoffset(7)(不要问我为什么会有这些,但它们确实存在)。这是DATEDIFF版本:

<Predicate>
<ScalarOperator ScalarString="datediff(day,CONVERT_IMPLICIT(datetimeoffset(7),[splunge].[dbo].[sargme].[DateCol1] as [s].[DateCol1],0),CONVERT_IMPLICIT(datetimeoffset( 7),[splunge].[dbo].[sargme].[DateCol2] 为 [s].[DateCol2],0))>=(48)">

这是没有的DATEDIFF

<Predicate>
<ScalarOperator ScalarString="[splunge].[dbo].[sargme].[DateCol2] as [s].[DateCol2]>=dateadd(day,(48),[splunge].[dbo].[ sargme].[DateCol1] 作为 [s].[DateCol1])">

此外,当我将索引更改为仅包含 时,我发现在持续时间方面的结果稍好一些DateCol2(并且当两个索引都存在时,SQL Server 总是选择一个键和一个包含列与多键的索引)。对于这个查询,因为无论如何我们都必须扫描所有行才能找到范围,所以将第二个日期列作为键的一部分并以任何方式排序都没有好处。虽然我知道我们在这里无法进行搜索,但通过强制针对前导键列进行计算,并且仅针对次要或包含的列执行计算,不会妨碍获得搜索的能力,这本身就是一种良好的感觉。

如果是我,并且我放弃了寻找可解决的解决方案,我知道我会选择哪一个 - 使 SQL Server 完成最少工作量的那个(即使增量几乎不存在)。或者更好的是,我会放宽我对架构更改等的限制。

所有这些有多重要?我不知道。我在表中创建了 1000 万行,并且所有上述查询变化仍然在不到一秒钟的时间内完成。这是在笔记本电脑上的虚拟机上(当然,带有 SSD)。