使用游標改進查詢到基於 SET 的方法
我還是查詢優化的新手,我有一個儲存過程,它使用游標遍歷表中的每一行,並執行以下操作:
- 計算每行之間的時間差
- 計算每行之間的距離
- 如果距離 < 5 AND TimeDifference > 3 分鐘,則添加到臨時表
我嘗試將此游標轉換為 WHILE 循環,但性能下降。所以我需要幫助將其轉換為一種
SET BASED
方法而不是一種Procedural Based
方法所以游標執行這個邏輯:
-- READ Current Row into Cursor Variables FETCH NEXT FROM crAssetIgnitionOnOff INTO @current_iVehicleMonitoringID , @current_iAssetID , @current_dtUTCDateTime , @current_sptGeoLocationPoint , @current_fLatitude , @current_fLongitude , @current_fAngle , @current_fSpeedKPH , @current_sIgnitionStatus , @current_eEventCode , @current_sEventCode IF(@current_iAssetID = @prev_iAssetID) BEGIN ---- Calculate Time Difference from previous Point DECLARE @diffInSeconds INT SET @diffInSeconds = DATEDIFF(SECOND, @prev_dtUTCDateTime, @current_dtUTCDateTime) DECLARE @diffInMinutes INT SET @diffInMinutes = @diffInSeconds / 60 -- Calcualte the Distance from previous position DECLARE @tempDistance FLOAT; SELECT @tempDistance = @current_sptGeoLocaitonPoint.STDistance(@prev_sptGeoLocaitonPoint); -- Check if distance travelled less than 5, AND Time difference between points greater than user selected Idle Minutes (@iIdleMinutes) AND prev ignition status = On IF(@diffInSeconds > @iIdleMinutes AND @tempDistance < 5 AND @prev_sIgnitionStatus = 'On') BEGIN DECLARE @sTime VARCHAR(30) SELECT @sTime = dbo.xPT_ConvertTimeToDDHHMMSS(@diffInSeconds,'s') INSERT INTO @tblExcessiveIdleTime( AssetID, PreviousDate, CurrentDate, TimeString, TimeInSeconds ) VALUES ( @current_iAssetId, @prev_dtUTCDateTime, @current_dtUTCDateTime, @sTime, @diffInSeconds ) END END -- Set Previous Values End of Loop SET @prev_iVehicleMonitoringID = @current_iVehicleMonitoringID SET @prev_iAssetID = @current_iAssetID SET @prev_dtUTCDateTime = @current_dtUTCDateTime SET @prev_sptGeoLocationPoint = @current_sptGeoLocationPoint SET @prev_fLatitude = @current_fLatitude SET @prev_fLongitude = @current_fLongitude SET @prev_fAngle = @current_fAngle SET @prev_fSpeedKPH = @current_fSpeedKPH SET @prev_sIgnitionStatus = @current_sIgnitionStatus SET @prev_eEventCode = @current_eEventCode SET @prev_sEventCode = @current_sEventCode END
現在這在某些情況下需要 17 分鐘才能執行,所以我嘗試將其轉換為 WHILE 循環 - ( http://www.sqlbook.com/SQL/Avoiding-using-SQL-Cursors-20.aspx )
這不是一個好主意 - 因為邏輯讀取次數的性能是游標的 4 倍。而且處理時間更長:
WHILE @RowCount <= @NumberRecords BEGIN -- Check for First Row IF @RowCount = 1 BEGIN -- Set First Row as Previous SELECT @previous_iAssetID = iAssetID, @previous_sptGeoLocaitonPoint = sptGeoLocaitonPoint, @previous_dtUTCDateTime = dtUTCDateTime, @previous_sIgnitionStatus = sIgnitionStatus FROM #tblVehicleMonitoringLog WHERE RowID = @RowCount END ELSE BEGIN /* Select current Row */ SELECT @current_iAssetID = iAssetID, @current_sptGeoLocaitonPoint = sptGeoLocaitonPoint, @current_dtUTCDateTime = dtUTCDateTime, @current_sIgnitionStatus = sIgnitionStatus FROM #tblVehicleMonitoringLog WHERE RowID = @RowCount /******** IMPLEMENT REPORT LOGIC **********/ IF(@current_iAssetID = @previous_iAssetID) BEGIN ---- Calculate Time Difference from previous Point DECLARE @diffInSeconds INT SET @diffInSeconds = DATEDIFF(SECOND, @previous_dtUTCDateTime, @current_dtUTCDateTime) DECLARE @diffInMinutes INT SET @diffInMinutes = @diffInSeconds / 60 -- Calcualte the Distance from previous position DECLARE @tempDistance FLOAT; SELECT @tempDistance = @current_sptGeoLocaitonPoint.STDistance(@previous_sptGeoLocaitonPoint); -- Check if distance travelled less than 5, AND Time difference between points greater than user selected Idle Minutes (@iIdleMinutes) AND prev ignition status = On IF(@diffInSeconds > @iIdleMinutes AND @tempDistance < 5 AND @previous_sIgnitionStatus = 'On') BEGIN DECLARE @sTime VARCHAR(30) SELECT @sTime = dbo.xPT_ConvertTimeToDDHHMMSS(@diffInSeconds,'s') INSERT INTO @tblExcessiveIdleTime( iAssetID, dtIgnitionOn, dtNextPeriodic, sTime, iTimeDurationInSeconds ) VALUES ( @current_iAssetId, DATEADD(hour, @fGmtOffSet, @previous_dtUTCDateTime), DATEADD(hour, @fGmtOffSet, @current_dtUTCDateTime), @sTime, @diffInSeconds ) END END -- Set Previous Values End of Loop SET @previous_iAssetID = @current_iAssetID; SET @previous_sptGeoLocaitonPoint = @current_sptGeoLocaitonPoint; SET @previous_dtUTCDateTime = @current_dtUTCDateTime; SET @previous_sIgnitionStatus = @current_sIgnitionStatus; END -- increment Row Number SET @RowCount = @RowCount + 1 END -- END OF WHILE LOOP
因此,在再次線上查看後-我發現我們可以計算兩行之間的時間差。(https://stackoverflow.com/questions/2357515/calculate-time-difference-between-two-rows)
這就是原始數據的樣子。
我需要計算時差和每行之間的距離 WHERE
- 目前 iAssetID = 以前的 iAssetID
- 上一個 sDigitalInputValue = ‘10000000’
這是我提出的查詢:
WITH rows AS ( SELECT *, ROW_NUMBER() OVER (ORDER BY dtUTCDateTime) AS rn FROM VehicleMonitoringLog Where dtUTCDateTime > GetDate() - 1 --Order by iAssetId, dtUTCDateTime ) SELECT mc.iVehicleMonitoringId as CurrentID, mp.iVehicleMonitoringId as PreviousID, mc.iAssetId as CurrentAsset, mp.iAssetId As PreviousAsset, mc.dtUTCDateTime as CurrentTime, mp.dtUTCDateTime as PreviousTime, DATEDIFF(second, mc.dtUTCDateTime, mp.dtUTCDateTime) AS DateDiffSeconds FROM rows mc JOIN rows mp ON mc.rn = mp.rn - 1
編輯
我的查詢現在正在執行 - 如果您發現任何性能問題,請告訴我:
SELECT dt.CurrentAsset, dt.Distance, dt.DateDiffSeconds, dt.CurrentIgnition, dt.PreviousIgnition, ta.sReference, ta.sCategoryName, ta.sSiteName, dbo.xPT_ConvertTimeToDDHHMMSS(DateDiffSeconds,'s') FROM ( SELECT iVehicleMonitoringId AS CurrentID, LEAD(iVehicleMonitoringId, 1) OVER (PARTITION BY iAssetID ORDER BY dtUTCDateTime) AS PreviousID, iAssetId AS CurrentAsset, LEAD(iAssetId, 1) OVER (PARTITION BY iAssetID ORDER BY dtUTCDateTime) AS PreviousAsset, sDigitalInputValue AS CurrentIgnition, LEAD(sDigitalInputValue, 1) OVER (PARTITION BY iAssetID ORDER BY dtUTCDateTime) AS PreviousIgnition, dtUTCDateTime AS CurrentTime, LEAD(dtUTCDateTime, 1) OVER (PARTITION BY iAssetID ORDER BY dtUTCDateTime) AS PreviousTime, DATEDIFF(second, dtUTCDateTime, LEAD(dtUTCDateTime, 1) OVER (PARTITION BY iAssetID ORDER BY dtUTCDateTime)) AS DateDiffSeconds, sptGeoLocaitonPoint.STDistance(LEAD(sptGeoLocaitonPoint, 1) OVER (PARTITION BY iAssetID ORDER BY dtUTCDateTime)) AS Distance FROM VehicleMonitoringLog WHERE dtUTCDateTime > @utcStartDate AND dtUTCDateTime < @utcEndDate ) AS dt Inner join #tblAssets ta on ta.iAssetID = dt.CurrentAsset WHERE CurrentIgnition = '10000000' AND Distance < 5 AND DateDiffSeconds > @iIdleMinutes
您使用視窗函式的基於 CTE 的方法是一個很好的開始。您可以使用另一個更合適的視窗函式:LAG()。
這是如何做:
SELECT iVehicleMonitoringId AS CurrentID, LAG(iVehicleMonitoringId, 1) OVER (ORDER BY dtUTCDateTime) AS PreviousID, iAssetId AS CurrentAsset, LAG(iAssetId, 1) OVER (ORDER BY dtUTCDateTime) AS PreviousAsset, dtUTCDateTime AS CurrentTime, LAG(dtUTCDateTime, 1) OVER (ORDER BY dtUTCDateTime) AS PreviousTime, DATEDIFF(second, dtUTCDateTime, LAG(dtUTCDateTime, 1) OVER (ORDER BY dtUTCDateTime) ) AS DateDiffSeconds FROM VehicleMonitoringLog WHERE dtUTCDateTime > DATEADD(day, -1, SYSDATETIME());
基本上,
LAG(column, n) OVER (ORDER BY x)
返回column
,n
row(s) 的值(因此 n=1 返回前一行),按x
.您的 CTE 解決方案將掃描
VehicleMonitoringLog
兩次,然後加入兩個流。此查詢將僅執行一次掃描,這要高效得多。該LAG()
函式(及其表親LEAD()
)自 SQL Server 2012 起可用。分區
看起來您忘記了某種類型的分區術語(iAssetID,也許?)。分區項用於分隔不同車輛之間的數據點,以防兩輛車同時出差。通過將 OVER() 子句從 更改為 ,將此分區項添加到查詢
OVER (ORDER BY dtUTCDateTime)
中OVER (PARTITION BY iAssetID ORDER BY dtUTCDateTime)
。索引
為了使該解決方案真正發揮作用,我將在以下位置創建以下索引
VehicleMonitoringLog
:CREATE INDEX... (iAssetID, dtUTCDateTime); --- if you're using PARTITION BY
.. 要麼
CREATE INDEX... (dtUTCDateTime); --- without PARTITION BY