web-dev-qa-db-ja.com

2つの日付列のSARGable WHERE句

SARGabilityについて興味深い質問があります。この場合は、2つの日付列の違いに述語を使用することです。これが設定です:

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])

私が頻繁に目にするのは、次のようなものです。

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

...これは間違いなくSARG可能ではありません。その結果、インデックススキャンが行われ、1000行すべてが読み取られます。推定行は悪臭を放ちます。これを本番環境で使用することはありません。

No sir, I didn't like it.

技術的に言えば、CTEを具体化できればいいのです。しかし、いいえ、私たちはトップと同じ実行計画を取得します。

/*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;

そしてもちろん、定数を使用していないため、このコードは何も変更せず、SARGの半分も実行できません。楽しくない。同じ実行計画。

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

運が良ければ、接続文字列のすべての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

これにより、3つのクエリでインデックスシークが行われます。奇妙なことに、DateCol1に48日を追加します。 DATEDIFF句にWHEREを含むクエリ、CTE、および計算列に述語を含む最後のクエリはすべて、はるかに優れた見積もりを備えた、より優れたプランを提供します。そしてそのすべて。

I could live with this.

これにより、1つのクエリで、この検索を実行するSARGableな方法はありますか。

一時テーブル、テーブル変数、テーブル構造の変更、ビューはありません。

データに対する自己結合、CTE、サブクエリ、または複数のパスで問題ありません。 SQL Serverのどのバージョンでも使用できます。

何よりもクエリソリューションに関心があるため、計算列を回避することは人為的な制限です。

24
Erik Darling

これをすばやく追加して、それが答えとして存在するようにします(ただし、あなたが望む答えではないことはわかっていますが)。

インデックス付きの計算された列は通常、このタイプの問題に対する適切な解決策です。

それ:

  • 述語を索引付け可能な式にします
  • カーディナリティの推定を改善するために自動統計を作成できます
  • ベーステーブルのスペースをとる必要はありません

最後の点を明確にするために、この場合、計算列は永続化する必要はありません

-- 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);

次にクエリ:

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;

...以下の簡単なプランを提供します:

Execution plan

マーティン・スミスが言ったように、間違った設定オプションを使用して接続している場合は、通常の列を作成し、トリガーを使用して計算値を維持できます。

もちろん、アーロンが 彼の回答 で述べているように、解決すべき実際の問題がある場合にのみ、これは本当に問題になります(コードの問題は別として)。

これは考えるのは楽しいですが、質問の制約を考慮して、あなたが合理的に望むことを達成する方法はわかりません。最適なソリューションには、何らかのタイプの新しいデータ構造が必要になるようです。上記のように非永続計算列のインデックスによって提供される「関数インデックス」の近似値が最も近いものです。

16
Paul White 9

SQL Serverコミュニティで最も有名な人からのばかげたことを恐れて、首をかしげて言ってみます。

クエリをSARG可能にするためには、基本的に、連続する行の範囲の開始行を特定できるクエリを作成する必要があります。インデックス。インデックスix_datesを使用すると、行はDateCol1DateCol2の日付の違いで並べ替えられないため、ターゲット行がインデックス内のどこにでも広がる可能性があります。

自己結合、複数パスなどはすべて、少なくとも1つのインデックススキャンが含まれているという共通点がありますが、(ネストされたループ)結合ではインデックスシークを使用できます。しかし、私はスキャンを排除することがどのようにして可能になるのか分かりません。

より正確な行の見積もりを取得することに関しては、日付の違いに関する統計はありません。

次のかなり醜い再帰的な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);

テーブル内のすべてのDateCol1を含むインデックススプールを作成し、少なくとも48日先のDateCol1およびDateCol2のそれぞれに対してインデックスシーク(範囲スキャン)を実行します。

より多くのIO、わずかに長い実行時間、行の見積もりはまだかなりずれており、再帰による並列化の可能性はありません。このクエリは、比較的少数の連続した値内に非常に多数の値がある場合に役立つ可能性があると思いますDateCol1(シークの数を抑える)。

Crazy recursive CTE query plan

12

風変わりなバリエーションをたくさん試しましたが、あなたのバージョンよりも優れたバージョンは見つかりませんでした。主な問題は、date1とdate2がどのように並べ替えられるかに関して、インデックスがこのように見えることです。最初の列は、すてきな棚のラインになり、それらの間のギャップは非常にギザギザになります。あなたはこれを実際の方法よりもじょうごのように見せたいです:

_Date1    Date2
-----    -------
*             *
*             *
*              *
 *       * 
 *        *
 *         *
  *      *
  *           *
_

2つのポイント間の特定のデルタ(またはデルタの範囲)でシークできるようにする方法は、実際にはありません。また、1回のシーク+範囲スキャンであり、すべての行に対して実行されるシークではありません。これには、ある時点でのスキャンやソートが含まれますが、これらは明らかに避けたいものです。フィルター処理されたインデックスでDATEADD/DATEDIFFのような式を使用できないか、日付の差分の積でソートを可能にする可能性のあるスキーマ変更を実行できないのは残念です(デルタの計算など)挿入/更新時)。現状では、これはスキャンが実際に最適な検索方法であるケースの1つであるようです。

このクエリは面白くないと言いましたが、よく見ると、これは断然最良のクエリです(計算スカラー出力を省略した場合はさらに良いでしょう)。

_SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])
_

その理由は、DATEDIFFを回避すると、インデックスのonly非先頭キー列に対する計算と比較して、一部のCPUが潜在的に削られ、datetimeoffset(7)(なぜそれらが存在するのか尋ねないでください。これはDATEDIFFバージョンです:

<述語>
<ScalarOperator ScalarString = "datediff(day、CONVERT_IMPLICIT(datetimeoffset(7)、[splunge]。[dbo]。[sargme]。[DateCol1] as [s]。[DateCol1]、0)、CONVERT_IMPLICIT( datetimeoffset(7)、[splunge]。[dbo]。[sargme]。[DateCol2] as [s]。[DateCol2]、0))> =(48) ">

そして、これがDATEDIFFのないものです:

<述語>
<ScalarOperator ScalarString = "[splunge]。[dbo]。[sargme]。[DateCol2] as [s]。[DateCol2]> = dateadd(day、(48)、[splunge]。[dbo] 。[sargme]。[DateCol1] as [s]。[DateCol1]) ">

また、インデックスをinclude _DateCol2_のみに変更した場合、期間の観点で少し優れた結果が見つかりました(両方のインデックスが存在する場合、SQL Serverは常に1つのキーと1つのインクルードを持つインデックスを選択しました列とマルチキー)。このクエリでは、とにかく範囲を見つけるためにすべての行をスキャンする必要があるため、2番目の日付列をキーの一部として使用し、何らかの方法で並べ替えても意味がありません。そして、私はここでシークを取得できないことを知っていますが、本質的に良い感じがありますnot主要なキー列に対して計算を強制し、セカンダリに対してのみ実行することによって取得する能力を妨げますまたは含まれている列。

それが私であり、検索可能な解決策を見つけることをあきらめた場合、私はどちらを選択するかを知っています-デルタがほとんど存在しない場合でも、SQL Serverに最小限の作業を行わせるものです。あるいは、スキーマの変更などに関する制限を緩和する方がよいでしょう。

そして、どれだけ重要なのでしょうか?知りません。テーブルを1000万行作成しましたが、上記のクエリのバリエーションはすべて1秒未満で完了しました。そして、これはラップトップ上のVM(認可済み、SSD付き))にあります。

9
Aaron Bertrand

質問の編集者として質問の作成者によって最初に追加されたコミュニティWiki回答

これを少し置いて、いくつかの本当に賢い人が口を揃えた後、これに関する私の最初の考えは正しいようです:列を追加せずにこのクエリを書くための正気でSARGableな方法はありません。トリガー。

私は他のいくつかのことを試しました、そして私は他のいくつかの観察結果を読んでいます。

まず、一時テーブルではなく通常のテーブルを使用してセットアップを再実行します

  • 彼らの評判は知っていますが、複数列の統計情報を試してみたかったのです。彼らは役に立たなかった。
  • 使用されている統計を確認したかった

これが新しいセットアップです。

USE [tempdb]
SET NOCOUNT ON  

DBCC FREEPROCCACHE

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])

CREATE STATISTICS [s_sargme] ON [sargme] ([DateCol1], [DateCol2])

次に、最初のクエリを実行すると、以前と同じようにix_datesインデックスとスキャンが使用されます。ここで変更はありません。これは冗長に思えますが、私に固執してください。

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48

CTEクエリをもう一度実行しますが、同じです...

WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

よし!引数が半分にならないクエリをもう一度実行します。

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

次に、計算列を追加し、計算列にヒットするクエリと共に、3つすべてを再実行します。

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

ここにこだわったなら、ありがとう。これは、投稿の興味深い部分です。

Fabiano Amorim でドキュメント化されていないトレースフラグを使用してクエリを実行すると、各クエリで使用されている統計がわかります。計算された列が作成されてインデックスが作成されるまで、プランが統計オブジェクトに触れないことがわかりました。

What the bloodclot

計算された列にのみヒットするクエリでさえ、数回実行して単純なパラメーター化を行うまで統計オブジェクトに触れませんでした。したがって、最初はすべてix_datesインデックスをスキャンしていましたが、利用可能な統計オブジェクトではなく、ハードコードされたカーディナリティ推定(テーブルの30%)を使用していました。

ここで眉をひそめたもう1つの点は、非クラスター化インデックスのみを追加した場合、クエリプランは両方の日付列で非クラスター化インデックスを使用するのではなく、すべてHEAPをスキャンしたことです。

回答してくれた皆さんありがとう。あなたはすべて素晴らしいです。

3
Paul White 9

WHERE句を引数可能にするために私が考えた方法はすべて複雑であり、手段ではなく最終目標としてインデックスシークに向けて取り組むように感じます。だから、いや、それは(語法的に)可能だとは思いません。

「テーブル構造を変更しない」が追加のインデックスを意味しないかどうかはわかりませんでした。インデックススキャンを完全に回避するソリューションですが、個別のインデックスシークのLOTが発生します。つまり、テーブル内の日付値の最小/最大範囲内の可能性があるDateCol1の日付ごとに1つになります。テーブルに実際に表示される個別の日付を探します。理論的には並列処理の候補であり、再帰を回避します。しかし、正直なところ、これをスキャンしてDATEDIFFを実行するよりも高速なデータ分布を確認することは困難です(たぶん、本当に高いDOPでしょうか?)そして...コードは醜いです。

--Add this index to avoid the scan when determining the @MaxDate value
--CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([DateCol2]);
DECLARE @MinDate DATE, @MaxDate DATE;
SELECT @MinDate=DateCol1 FROM (SELECT TOP 1 DateCol1 FROM #sargme ORDER BY DateCol1 ASC) ss;
SELECT @MaxDate=DateCol2 FROM (SELECT TOP 1 DateCol2 FROM #sargme ORDER BY DateCol2 DESC) ss;

--Used 44 just to get a few more rows to test my logic
DECLARE @DateDiffSearchValue INT = 44, 
    @MinMaxDifference INT = DATEDIFF(DAY, @MinDate, @MaxDate);

--basic data profile in the table
SELECT [MinDate] = @MinDate, 
        [MaxDate] = @MaxDate, 
        [MinMaxDifference] = @MinMaxDifference, 
        [LastDate1SearchValue] = DATEADD(DAY, 0-@DateDiffSearchValue, @MaxDate);

;WITH rn_base AS (
SELECT [col1] = 0
        UNION ALL SELECT 0
        UNION ALL SELECT 0
        UNION ALL SELECT 0
),
rn_1 AS (
    SELECT t0.col1 FROM rn_base t0
        CROSS JOIN rn_base t1
        CROSS JOIN rn_base t2
        CROSS JOIN rn_base t3
),
rn_2 AS (
    SELECT rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM rn_1 t0
        CROSS JOIN rn_1 t1
),
candidate_searches AS (
    SELECT 
        [Date1_EqualitySearch] = DATEADD(DAY, t.rn-1, @MinDate),
        [Date2_RangeSearch] = DATEADD(DAY, t.rn-1+@DateDiffSearchValue, @MinDate)
    FROM rn_2 t
    WHERE DATEADD(DAY, t.rn-1, @MinDate) <= DATEADD(DAY, 0-@DateDiffSearchValue, @MaxDate)
    /* Of course, ignore row-number values that would result in a
       Date1_EqualitySearch value that is < @DateDiffSearchValue days before @MaxDate */
)
--select * from candidate_searches

SELECT c.*, xapp.*, dd_rows = DATEDIFF(DAY, xapp.DateCol1, xapp.DateCol2)
FROM candidate_searches c
    cross apply (
        SELECT t.*
        FROM #sargme t
        WHERE t.DateCol1 = c.date1_equalitysearch
        AND t.DateCol2 >= c.date2_rangesearch
    ) xapp
ORDER BY xapp.ID asc --xapp.DateCol1, xapp.DateCol2 
3
Aaron Morelli