差距和島嶼 - 尋找最近的島嶼
我正在處理以下場景,其中我的時間數據屬於孤島和間隙。每隔一段時間,我需要根據事件發生的時間將一個位於現有間隙內的事件與其最近的島嶼相關聯。
為了展示,假設我有以下數據定義我的時間段:
2
該數據是連續的,除了 ID和之間存在的間隙外,在的7
時間段內。2017-07-26 00:03:00``2017-07-26 00:07:00
為了確定最近的島嶼,我目前將差距分為兩個時期,如下所示:
如果我有一個事件落在此間隙內,
GapWindowStart
/End
times 將確定我需要將事件與哪個島關聯。因此,例如,如果我有一個發生在的事件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
視窗函式來組裝間隙,然後根據執行的CROSS APPLY
語句拆分間隙像一個簡單的UNPIVOT
操作。這是我用來組裝最近的島集的方法的db<>fiddle計劃。
確定最近的島嶼後,我使用事件的事件時間來確定與所述事件相關聯的最近島嶼。因為這些島嶼在一天中都是不穩定的,所以我無法製作靜態主表,而是必須依賴於在遇到事件時在執行時建構所有內容。
這是一個db<>fiddle 計劃,顯示了應針對隨機事件時間使用的 NearestIsland 值。
對於通常會陷入空白的給定事件,有沒有更好的方法來找出最近的島嶼?例如,是否有更有效的方法來辨識差距或更有效的方法來辨識最近的島嶼?我什至會以最合乎邏輯的方式解決這個問題嗎?這個問題沒有什麼重要的,但我總是試圖弄清楚是否有一種“更好”的方法來處理事情,我認為這個問題本身就有一些創造力,所以我很樂意看到其他性能選擇。
我正在使用的目前環境是 SQL 2012,但我們很快就會遷移到 SQL 2016 環境,所以我幾乎可以接受任何事情。
第二個 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 可以掃描覆蓋索引,查找下一個ID
和WindowStart
值使用LEAD()
(或者如果您願意,可以使用雙重方法),如果下一個與目前不匹配,ROW_NUMBER()
則使用時間之間的中間點添加兩行。WindowStart``WindowEnd
我準備了與您為“大”數據集所做的數據相同的數據,但採用了不同的方式,因此它可以在我的機器上更快地完成:
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;
這有一個不錯的、乾淨的計劃,沒有排序,其性能與您所擁有的類似:
如果要求實際上是為一小部分行(例如您的範例中的十行)找到最近的島,那麼可以使用索引編寫更高效的程式碼。這裡的想法是從表中為每一行找到上一行和下一行,
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);
這是結果集,對您來說會有所不同,因為我們正在生成隨機數據:
這是查詢計劃:
作為另一項測試,我將 10k 行放入
tmpEvent
表中,甚至將它們返回給客戶端。在我的系統上這很好,但當然你可以看到不同的性能:(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 毫秒,經過時間 = 131 毫秒。