2つの日付列のSARGable WHERE句


24

私には、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行すべてが読み取られます。推定行が悪臭を放ちます。これを本番環境に配置することはありません。

いいえ、私はそれが好きではありませんでした。

CTEを具体化できればいいと思います。それは、技術的に言えば、これをもっとSARGできるようにするのに役立つからです。しかし、いいえ、トップと同じ実行計画を取得します。

/*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内のクエリ、、および計算列の述語を含む最終クエリはすべて、より優れた推定値を備えた優れたプランを提供します。WHERECTE

私はこれで生きることができました。

それは私に質問をもたらします:単一のクエリで、この検索を実行するためのSARGableな方法はありますか?

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

自己結合、CTE、サブクエリ、またはデータの複数パスで問題ありません。SQL Serverの任意のバージョンで動作できます。

計算列を避けることは人為的な制限です。なぜなら、私は他の何よりもクエリソリューションに関心があるからです。

回答:


16

これをすぐに追加して、答えとして存在するようにします(ただし、それはあなたが望む答えではないことは承知しています)。

インデックス付き計算列は、通常、この種の問題に最適なソリューションです。

それ:

  • 述部をインデックス可能な式にします
  • より良いカーディナリティ推定のために自動統計を作成できます
  • 実表にスペースをとる必要ありません

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

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

...次の簡単な計画を作成します。

実行計画

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

アーロンが彼の答えで述べているようにこれは本当に解決すべき実際の問題がある場合にのみ本当に重要です(コードチャレンジは別として)。

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


12

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表には、それらのそれぞれのためのインデックス(範囲スキャン)をシーク実行DateCol1DateCol2、前方である少なくとも48日間。

IOが増え、実行時間がわずかに長くなり、行の見積もりはまだ十分ではありません。再帰のために並列化の可能性はゼロです。DateCol1(シークの数を抑える)。

クレイジーな再帰CTEクエリプラン


9

私はたくさんの奇抜なバリエーションを試しましたが、あなたのバージョンよりも優れたバージョンは見つかりませんでした。主な問題は、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に対する計算と比較して、潜在的にCPUを削るのを回避し、また、いくつかの厄介な暗黙的な変換を回避するためです(なぜ存在するのかを聞かないでください)ここでバージョンは:datetimeoffset(7)DATEDIFF

<述語>
<ScalarOperator ScalarString = "datediff(day、CONVERT_IMPLICIT(datetimeoffset(7)、[splunge]。[dbo]。[sargme]。[DateCol1] as 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]) ">

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

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

そして、それはどれほど重要なのでしょうか?知りません。テーブルを1,000万行作成し、上記のクエリのバリエーションはすべて1秒未満で完了しました。そして、これはラップトップ上のVMにあります(SSDを使用して提供されます)。


3

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

「テーブル構造を変更しない」とは、追加のインデックスがないことを意味するのかどうかわかりませんでした。これは、インデックススキャンを完全に回避するソリューションですが、結果として、個別のインデックスシーク大量に発生します。つまり、テーブル内の日付値の最小/最大範囲にある DateCol1の日付ごとに1つです。(ダニエルの場合とは異なり、テーブルに実際に表示される個々の日付ごとに1つのシークが行われます)。理論的には、再帰を回避する並列処理の候補です。しかし、正直なところ、このことはDATEDIFFをスキャンして実行するよりも高速なデータ配布を見ることは困難です。(たぶん、本当に高いDOPですか?)そして...コードはcodeいです。この努力は「精神運動」としてカウントされると思います。

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

質問の編集者として質問の編集者が元々追加したコミュニティ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;

よし!not-even-sargableクエリを再度実行します。

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が文書化されていないトレースフラグを使用してクエリを実行し、各クエリが使用した統計情報が非常に優れていることを確認します。計算列が作成され、インデックスが作成されるまで、どのプランも統計オブジェクトに影響を与えないことがわかりました。

血栓

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

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

回答してくれたすべての人に感謝します。あなたはすべて素晴らしいです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.