島とギャップに該当する時間データがある次のシナリオを使用しています。時々、既存のギャップ内にあるイベントを、イベントの時間に基づいて最も近い島に関連付ける必要があります。
実例として、期間を定義する次のデータがあるとします。
このデータは、ID 2
と7
の間に2017-07-26 00:03:00
から2017-07-26 00:07:00
までの期間に存在するギャップを除いて、連続しています。
最も近い島を特定するために、現在、次のようにギャップを2つの期間に分割しています。
このギャップに該当するイベントがある場合、GapWindowStart
/End
の時間によって、イベントを関連付ける必要のあるアイランドが決まります。たとえば、2017-07-26 00:03:20
で発生するイベントがある場合、そのイベントをID 2
に関連付けます。逆に、2017-07-26 00:05:35
でイベントが発生した場合、そのイベントを関連付けます。 ID 7
。
これまでのところ、私のアプローチをコーディングすることができた最も効率的な方法は、 SQL Server MVP Deep Divesの本からのItzik Ben-Ganの3番目のソリューション を介してギャップを組み立てることですROW_NUMBER
次に、単純なUNPIVOT
操作のように機能するCROSS APPLY
ステートメントごとにギャップを分割します。
これが、最も近いアイランドセットをアセンブルするために使用しているアプローチの db <> fiddle プランです。
最も近い島を特定したら、イベントのイベント時間を使用して、そのイベントに関連付ける最も近い島を特定します。これらの島は1日を通して変動するため、静的なマスターテーブルを作成することはできませんが、代わりに、イベントが発生したときに実行時にすべてを構築する必要があります。
これは db <>フィドルプラン であり、ランダムイベント時間に対して使用する必要があるNearestIsland値を示しています。
通常ギャップに該当する特定のイベントの最も近い島を把握するためのより良い方法はありますか?たとえば、ギャップを特定するためのより効率的な方法や、最も近い島を特定するためのより効率的な方法はありますか?私はこれについて最善の論理的な方法で進んでいますか?この質問について重要なことは何もありませんが、私は物事に「より良い」アプローチがあるかどうかを常に把握しようとしています。この問題はいくつかの創造性に役立つと思うので、他のパフォーマンスオプションを確認したいです。
私が現在取り組んでいる環境はSQL 2012ですが、まもなくSQL 2016環境に移行する予定なので、ほとんど何でもオープンです。
2番目のdb <> fiddleリンクの基礎となるコードは次のとおりです。
-- Creation of Test Data
CREATE TABLE #tmp
(
ID INT PRIMARY KEY CLUSTERED
, WindowStart DATETIME2
, WindowEnd DATETIME2
)
-- Create contiguous data set
INSERT INTO #tmp
SELECT ID
, DATEADD(HOUR, ID, CAST('0001-01-01' AS DATETIME2))
, DATEADD(HOUR, ID + 1, CAST('0001-01-01' AS DATETIME2))
FROM
(
SELECT TOP (1500000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS ID
--SELECT TOP (87591200) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS ID -- Swap line with above for larger dataset
FROM master.sys.configurations t1
CROSS JOIN master.sys.configurations t2
CROSS JOIN master.sys.configurations t3
CROSS JOIN master.sys.configurations t4
CROSS JOIN master.sys.configurations t5
) x
--DELETE 1000000 random records to create random gaps
DELETE FROM #tmp
WHERE ID IN (
SELECT TOP 1000000 ID
--SELECT TOP 77591200 ID -- Swap line with above for larger dataset
FROM #tmp
ORDER BY NEWID()
)
-- Create RandomEvent Times
CREATE TABLE #tmpEvent
(
EventTime DATETIME2
)
INSERT INTO #tmpEvent
SELECT DATEADD(SECOND, X.RandomNum, Y.minWindowEnd) AS EventDate
FROM (VALUES (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))
, (ABS(CHECKSUM(NEWID())))) AS X(RandomNum)
CROSS JOIN (SELECT MIN(WindowEnd) AS minWindowEnd FROM #tmp) AS Y
SET STATISTICS XML ON
SET STATISTICS IO ON
--Desired Output Format - Best Execution I've found so far
;WITH rankIslands AS (
SELECT ID
, WindowStart
, WindowEnd
, ROW_NUMBER() OVER (ORDER BY WindowStart) AS rnk
FROM #tmp
), rankGapsJoined AS (
SELECT t1.ID AS NearestIslandID_Lower
, t1.WindowEnd AS GapStart_Lower
, DATEADD(MINUTE, (DATEDIFF(MINUTE, t1.WindowEnd, t2.WindowStart) / 2), t1.WindowEnd) AS GapEnd_Lower
, t2.ID AS NearestIslandID_Higher
, DATEADD(MINUTE, -1 * (DATEDIFF(MINUTE, t1.WindowEnd, t2.WindowStart) / 2), t2.WindowStart) AS GapStart_Higher
, t2.WindowStart AS GapEnd_Higher
FROM rankIslands t1 INNER JOIN rankIslands t2
ON t1.rnk + 1 = t2.rnk
AND t1.WindowEnd <> t2.WindowStart
), NearestIsland AS (
SELECT xa.*
FROM rankGapsJoined t1
CROSS APPLY ( VALUES (t1.NearestIslandID_Lower, t1.GapStart_Lower, t1.GapEnd_Lower)
,(t1.NearestIslandID_Higher, t1.GapStart_Higher, t1.GapEnd_Higher) ) AS xa (NearestIslandId, GapStart, GapEnd)
)
-- Only return records that fall into the Gaps
SELECT e.EventTime, ni.*
FROM #tmpEvent e INNER JOIN NearestIsland ni
ON e.EventTime > ni.GapStart
AND e.EventTime <= ni.GapEnd
SET STATISTICS XML OFF
SET STATISTICS IO OFF
DROP TABLE #tmp
DROP TABLE #tmpEvent
質問:(@MaxVernon)
望ましい結果はギャップを含むテーブルですか?
または、最も近い隣人に着信行を割り当てようとしていますか?
それとも、あなたの例で示した正確な出力を再現したいと思っていますか?
回答:
簡単に言えば、はい、はい、いいえです。望ましい結果は、通常はギャップに収まるイベント時間に最も近い島を特定するための(その他の/より多くの)効率的な方法を特定することです。質問を拡張して、望ましい最終結果がどうなるかを示しました。
ここにはさまざまな質問がたくさんあります。完全な結果セット(IDへの時間のマッピング)を生成することになると、WindowStart
にWindowEnd
。 SQL Serverは、カバリングインデックスをスキャンし、LEAD()
を使用して次のID
およびWindowStart
値を検索できます(必要に応じて、デュアルROW_NUMBER()
アプローチ)。 、次のWindowStart
が現在のWindowEnd
と一致しない場合は、時間の中間点を使用して2つの行を追加します。
私はあなたの「大きな」データセットのためにあなたがしたのと同じデータを準備しましたが、私のマシンでより速く終了するように別の方法で:
CREATE TABLE tmp_181900
(
ID INT PRIMARY KEY CLUSTERED
, WindowStart DATETIME2
, WindowEnd DATETIME2
);
-- Create contiguous data set
INSERT INTO tmp_181900 WITH (TABLOCK)
SELECT ID
, DATEADD(HOUR, ID, CAST('0001-01-01' AS DATETIME2))
, DATEADD(HOUR, ID + 1, CAST('0001-01-01' AS DATETIME2))
FROM
(
SELECT TOP (87591200) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS ID -- Swap line with above for larger dataset
FROM master.sys.configurations t1
CROSS JOIN master.sys.configurations t2
CROSS JOIN master.sys.configurations t3
CROSS JOIN master.sys.configurations t4
CROSS JOIN master.sys.configurations t5
) x;
CREATE TABLE tmp
(
ID INT PRIMARY KEY CLUSTERED
, WindowStart DATETIME2
, WindowEnd DATETIME2
);
-- TABLESAMPLE would be faster, but I assume that you can't randomly sample at the page level
INSERT INTO tmp WITH (TABLOCK)
SELECT *
FROM tmp_181900
WHERE RIGHT(BINARY_CHECKSUM(ID, NEWID()), 3) < 115; -- keep 11.5% of rows
DROP TABLE tmp_181900;
次のコードは、私が説明したアルゴリズムを実装しています。
SELECT t2.*
FROM
(
SELECT
ID
, WindowStart
, WindowEnd
, LEAD(ID) OVER (ORDER BY WindowStart) Next_Id
, LEAD(WindowStart) OVER (ORDER BY WindowStart) Next_WindowStart
FROM tmp
) t
CROSS APPLY (
SELECT DATEADD(MINUTE, 0.5 * DATEDIFF(MINUTE, WindowEnd, Next_WindowStart), WindowEnd)
) ca (midpoint_time)
CROSS APPLY (
SELECT ID, WindowEnd, ca.midpoint_time
UNION ALL
SELECT Next_ID, ca.midpoint_time, Next_WindowStart
) t2 (NearestIslandId, GapStart, GapEnd)
WHERE t.WindowStart <> t.Next_WindowStart
AND t2.GapStart <> t2.GapEnd;
これは、あなたが持っているものと同じように機能する並べ替えのないすてきでクリーンな計画を持っています:
実際の例では、行の小さなサブセット(例では10)に最も近い島を見つけることが要件である場合は、インデックスを使用してはるかに効率的なコードを作成できます。ここでの考え方は、tmpEvent
の各行について、テーブルから前の行と次の行を見つけ、少し計算して最も近い行を見つけることです。 N
にtmpEvent
行がある場合、このコードは最大2 * N
インデックスシークを実行します。とても速いのでSTATISTICS TIME
は何も検出できません:
(10行が影響を受けました)
SQL Server実行時間:CPU時間= 0ミリ秒、経過時間= 0ミリ秒。
これが私が使用したコードです。私はあなたのロジックにかなりよくマッチすると思います。私は各部分にコメントしました:
SELECT e.EventTime
, CASE WHEN ca2.use_previous = 1 THEN previous_event.ID ELSE later_event.ID END NEAREST_ID
, CASE WHEN ca2.use_previous = 1 THEN previous_event.WindowStart ELSE later_event.WindowStart END NEAREST_WindowStart
, CASE WHEN ca2.use_previous = 1 THEN previous_event.WindowEnd ELSE later_event.WindowEnd END NEAREST_WindowEnd
FROM tmpEvent e
OUTER APPLY ( -- find the previous island, including exact matches
SELECT TOP 1 t.ID, t.WindowStart, t.WindowEnd
FROM tmp t
WHERE t.WindowStart < e.EventTime
ORDER BY t.WindowStart DESC
) previous_event
OUTER APPLY ( -- find the next island
SELECT TOP 1 t.ID, t.WindowStart, t.WindowEnd
FROM tmp t
WHERE previous_event.WindowEnd < e.EventTime -- only do this seek if not an exact match
AND t.WindowStart >= e.EventTime
ORDER BY t.WindowStart ASC
) later_event
CROSS APPLY ( -- calculate differences between times so we can reuse them
SELECT DATEDIFF_BIG(SECOND, previous_event.WindowEnd, e.EventTime) DIFF_S_TO_PREVIOUS
, DATEDIFF_BIG(SECOND, e.EventTime, later_event.WindowStart) DIFF_S_TO_NEXT
) ca
CROSS APPLY ( -- figure out if the previous event is the closest
SELECT CASE WHEN
ca.DIFF_S_TO_PREVIOUS <= 0 -- the event matches exactly
OR ca.DIFF_S_TO_NEXT IS NULL -- no ending event
OR ca.DIFF_S_TO_PREVIOUS < ca.DIFF_S_TO_NEXT -- previous is closer than later
THEN 1
ELSE 0
END
) ca2 (use_previous);
ランダムデータを生成しているため、結果セットは次のようになります。
そして、これがクエリプランです:
別のテストとして、tmpEvent
テーブルに1万行を入れて、それらをクライアントに返しました。私のシステムではこれで問題ありませんでしたが、もちろん異なるパフォーマンスが表示されました。
(10000行が影響を受けました)
テーブル「tmp」。スキャンカウント18864、論理読み取り60419、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル 'tmpEvent'。スキャンカウント1、論理読み取り22、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。
SQL Server実行時間:
CPU時間= 47 ms、経過時間= 131 ms。