web-dev-qa-db-ja.com

キャストまたは範囲を使用して日付時刻を日付に結合する必要がありますか?

この質問は、ここで提示された優れた質問からの離陸です。

これまでのキャストは検索可能ですが、それは良いアイデアですか?

私の場合、WHERE句は関係ありませんが、タイプDATEの列を持つイベントテーブルに参加することに関心があります。

1つのテーブルには_DATETIME2_があり、もう1つのテーブルにはDATE...があるため、CAST( AS DATE)を使用してJOINを効果的に実行するか、「従来の」範囲クエリを使用できます(> =日付AND <日付+1)。

私の質問はどちらが望ましいですか? DATETIME値は、述語DATE値とほとんど一致しません

私はDATETIMEを持つ2M行のオーダーにとどまり、DATEを持つ5k行未満にとどまると予想します(この考慮事項が違いを生む場合)

JOIN句を使用する場合と同じ動作をWHEREでも期待できますか?スケーリングでパフォーマンスを維持したいのはどちらですか。 MSSQL 2012で答えは変わりますか?

私の一般的な使用例は、イベントテーブルをカレンダーテーブルのように扱うことです

_SELECT
    events.columns
    ,SOME_AGGREGATIONS(tasks.column)
FROM
    events
LEFT OUTER JOIN
    tasks
        --This appropriately states my intent clearer
        ON CAST(tasks.datetimecolumn AS DATE) = events.datecolumn 
        --But is this more effective/scalable?
        --ON tasks.datetimecolumn >= events.datecolumn 
        --AND tasks.datetimecolumn < DATEADD(day,1,events.datecolumn)
GROUP BY
    events.columns
_
3
Matthew

"場合によります"。

_=_述語とこれまでのcastの利点の1つは、結合をハッシュまたはマージできることです。範囲バージョンは、ネストされたループ計画を強制します。

datetimecolumntasksにシークするのに役立つインデックスがない場合、これは大きな違いをもたらします。

質問で言及された5K/200万行のテストデータの設定

_CREATE TABLE events
  (
     eventId    INT IDENTITY PRIMARY KEY,
     datecolumn DATE NOT NULL,
     details    CHAR(1000) DEFAULT 'D'
  )

INSERT INTO events
            (datecolumn)
SELECT TOP 5000 DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY @@SPID), GETDATE())
FROM   spt_values v1,
       spt_values v2

CREATE TABLE tasks
  (
     taskId         INT IDENTITY PRIMARY KEY,
     datetimecolumn DATETIME2 NOT NULL,
     details        CHAR(1000) DEFAULT 'D'
  );

WITH N
     AS (SELECT number
         FROM   spt_values
         WHERE  number BETWEEN 1 AND 40
                AND type = 'P')
INSERT INTO tasks
            (datetimecolumn)
SELECT DATEADD(MINUTE, number, CAST(datecolumn AS DATETIME2))
FROM   events,
       N
_

次に、電源を入れます

_SET STATISTICS IO ON;
SET STATISTICS TIME ON;
_

CASTバージョンを試す

_SELECT events.eventId,
       MAX(tasks.details)
FROM   events
       LEFT OUTER JOIN tasks
         ON CAST(tasks.datetimecolumn AS DATE) = events.datecolumn
GROUP  BY events.eventId
_

7.4秒で完了

_Table 'Worktable'. Scan count 0, logical reads 0
Table 'tasks'. Scan count 1, logical reads 28679
Table 'events'. Scan count 1, logical reads 719

   CPU time = 3042 ms,  elapsed time = 7434 ms.
_

結合から_GROUP BY_に入る行の推定数は小さすぎ(5006.27と実際の2,000,000)、ハッシュ集計はtempdbに溢れました

Plan 1

範囲述語を試す

_SELECT events.eventId,
       MAX(tasks.details)
FROM   events
       LEFT OUTER JOIN tasks
         ON tasks.datetimecolumn >= events.datecolumn
            AND tasks.datetimecolumn < DATEADD(day, 1, events.datecolumn)
GROUP  BY events.eventId 
_

等価述語の欠如は、ネストされたループ計画を強制します。このクエリをサポートするのに役立つインデックスはないため、200万行のテーブルを5,000回スキャンする以外に選択肢はありません。

私のマシンでは、最終的に1分40秒後に完了した並列プランを提供しました。

_Table 'tasks'. Scan count 4, logical reads 143390000
Table 'events'. Scan count 5, logical reads 788
Table 'Worktable'. Scan count 0, logical reads 0
  CPU time = 368193 ms,  elapsed time = 100528 ms.
_

今回は、結合からアグリゲートに入る行の数が大幅にover推定されました(推定124,939,000対実際の2,000,000)

Plan 2

テーブルを変更してからそれぞれの日付/時刻列をクラスター化された主キーにした後、実験を繰り返して結果を変更しました。

どちらのクエリでも、ネストされたループプランが選択されました。 CAST as DATEバージョンは、4.5秒で完了したシリアルバージョンと、CPU時間3.2秒で経過時間1.1秒で完了したパラレルプランを範囲バージョンに提供しました。

2番目のクエリに_MAXDOP 1_を適用して図をより簡単に比較できるようにすると、次の結果が返されます。

クエリ1

_Table 'Worktable'. Scan count 0, logical reads 0
Table 'tasks'. Scan count 5000, logical reads 78137
Table 'events'. Scan count 1, logical reads 719
   CPU time = 3167 ms,  elapsed time = 4497 ms.
_

クエリ2

_Table 'tasks'. Scan count 5000, logical reads 49440
Table 'events'. Scan count 1, logical reads 719
   CPU time = 3042 ms,  elapsed time = 3147 ms.
_

クエリ1では、結合から推定された5006.73行があり、ハッシュ集計がtempdbに再度あふれました。

クエリ2にも大きな過大評価があります(今回は120,927,000)。

2つの結果のもう1つの明らかな違いは、範囲クエリが何らかの方法でtasksをより効率的にシークできるように見えることです。 _49,440_ページと_78,137_の読み取りのみ。

日付バージョンとしてのキャストが求める範囲は、内部関数GetRangeThroughConvertから導出されます。計画はCONVERT(date,[dbo].[tasks].[datetimecolumn],0)= [dbo].[events].[datecolumn]の残余述語を示しています。

クエリ2が次のように変更された場合

_   LEFT OUTER JOIN tasks
     ON tasks.datetimecolumn > DATEADD(day, -1, events.datecolumn)
        AND tasks.datetimecolumn < DATEADD(day, 1, events.datecolumn)
_

その後、読み取りの数は同じになります。 _CAST AS DATE_バージョンで使用される動的シークは、不要な行(1日ではなく2日)を読み取り、残りの述語でそれらを破棄します。

もう1つの可能性は、dateおよびtimeコンポーネントを異なる列に格納するようにテーブルを再構成することです。

_CREATE TABLE [dbo].[tasks](
    [taskId] [int] IDENTITY(1,1) NOT NULL,
    [datecolumn] date NOT NULL,
    [timecolumn] time NOT NULL,
    [datetimecolumn]  AS DATEADD(day, DATEDIFF(DAY,0,[datecolumn]), CAST([timecolumn] AS DATETIME2(7))),
    [details] [char](1000) NULL,
PRIMARY KEY CLUSTERED 
(
    [datecolumn] ASC,
    [timecolumn] ASC
))
_

datetimecolumnコンポーネントパーツから派生 にすることができ、これは行サイズに影響を与えません(date + time(n)の幅は同じであるため) datetime2(n))の幅として。 (例外として、追加の列が_NULL_BITMAP_のサイズを増やす場合)

クエリは単純な_=_述語になります

_SELECT events.eventId,
       MAX(tasks.details)
FROM   events
       LEFT OUTER JOIN tasks
         ON tasks.datecolumn = events.datecolumn
GROUP  BY events.eventId
_

これにより、並べ替える必要なく、テーブル間のマージ結合が可能になります。これらのテーブルサイズでは、ネストされたループ結合がとにかく以下のように統計で選択されています。

_Table 'tasks'. Scan count 5000, logical reads 44285
Table 'events'. Scan count 1, logical reads 717
   CPU time = 2980 ms,  elapsed time = 3012 ms.
_

また、先頭のインデックス列としてdateを個別に格納するさまざまな論理結合タイプを許可することも、日付によるグループ化など、tasksに対する他のクエリに利益をもたらす可能性があります。

なぜ_=_述語は、同じネストされたループプラン(_> <=_ vs _44,285_)の_49,440_バージョンよりもtasksでの論理読み取りが少ないことを示します先読みメカニズムに関連します。

トレースフラグ_652_をオンにすると、範囲バージョンの論理読み取りが等価バージョンの論理読み取りと同じになります。

6
Martin Smith

カーディナリティの見積もりは、このアプローチと日付範囲アプローチのどちらを使用する場合でも潜在的に影響を受ける可能性があるというマーティンに同意します。また、_CONVERT(DATE_を使用し、それでもsargabilityを取得すると、他の人々がコードを読んだり、コードから学習したりする可能性があるため、特に列がインデックス付けされている場合は、列に対して関数を使用することをお勧めします。これはonly例外であり、他のすべてのケースでは、シークが可能だった可能性があるときに実際にスキャンを強制するため、例外を使用することはお勧めしませんコードの作成者以外に実際の利点はありません。それは短命です。より簡潔な式を書くために数秒節約でき、それは一度行うことです。私は質問に答えるときいつも同じ反対に直面します-長さなしでvarcharを宣言するなど、悪い習慣を含む回答を投稿している他の人を見かけ、よくコメントします。私が聞いた言い訳は、それがthisの場合にうまく機能するということですが、それはポイントではありません-人々はこのケースから学び、他のケースにそれを適用します。また、同じケースで壊れる可能性もあります。たとえば、後で数週間または半日などで参加したい場合、別のデータ型を使用する必要があり、得ていたと思っていた利点を失う可能性があることを想像してください。

INNER JOINの場合、WHERE句とON句を使用しても違いはありません。ただし、同様に、条件をON句で結合し、条件をWHERE句でフィルタリングし続けます。もちろん、OUTER JOINについて話している場合は、特定の基準の配置がセマンティクスを変更する可能性があるため、これは変更されます。

私はこのようにクエリを記述します(そして、ほとんどの人から面白い視線を得る可能性があるため、作り上げた「Word」パフォーマンスを使用する場所には注意します)。

_LEFT OUTER JOIN
  dbo.tasks -- always use schema prefix!
  ON tasks.datetimecolumn >= events.datecolumn
    AND tasks.datetimecolumn < DATEADD(DAY, 1, events.datecolumn)
_

もちろん、これをテストして、DATEADD()がイベントテーブルに与える影響を確認する必要があります。それはテーブルが大きく、マージンが小さいように聞こえるので、効果が大きくなることはないと思いますが、チェックしても問題はありません。

4
Aaron Bertrand