web-dev-qa-db-ja.com

カーソルからSETベースのアプローチを使用してクエリを改善する

私はまだクエリの最適化に慣れていないので、カーソルを使用してテーブル内の各行を移動し、次の操作を実行するストアドプロシージャを持っています。

  1. 各行間の時間差を計算する
  2. 各行間の距離を計算する
  3. 距離<5かつTimeDifference> 3分の場合は、TEMP TABLEに追加

このカーソルを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

もう一度オンラインで調べたところ、2つの行の時間差を計算できることがわかりました。 ( https://stackoverflow.com/questions/2357515/calculate-time-difference-between-two-rows

これがRAWデータの外観です。

This is what the Raw data looks like

各行間の時間差と距離を計算する必要がありますWHERE

  1. 現在のiAssetID =以前のiAssetID
  2. 以前の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
1
Dawood Awan

ウィンドウ関数を使用した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行を戻して(n = 1は前の行を返します)、xの順に並べます。

CTEソリューションはVehicleMonitoringLogを2回スキャンしてから、2つのストリームを結合します。このクエリは単一のスキャンのみを実行するため、はるかに効率的です。 LAG()関数(およびその従属関数LEAD())は、SQL Server 2012以降で使用できます。

パーティショニング

何らかのタイプのパーティション用語(iAssetID、たぶん?)を忘れたようです。分割条件は、2台の車両が同時に往路と復路の場合に、異なる車両間でデータポイントを分離するために使用されます。 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
_
6

SQL Server 2012では、LEADLAGなど、欠落しているウィンドウ化された集計関数がいくつか導入されました。 SQL Serverのウィンドウ関数:パート2-フレーム または Microsoft SQL Server 2012のウィンドウ関数の使用方法

LAGを使用すると、カーソルを使用せずに前の行のデータにアクセスできます。

SELECT ...
FROM
 (
   SELECT ...

      DATEDIFF(second, dtUTCDateTime,
                       LAG(dtUTCDateTime) 
                           OVER (ORDER BY dtUTCDateTime) AS DateDiffSeconds
   FROM VehicleMonitoringLog
 ) AS dt
WHERE DateDiffSeconds > 180 
4
dnoeth

Dnoethが言及しているように、ラグを使用して以前の値を確認できるはずです。

SELECT ...
 , DATEDIFF(second, dtUTCDateTime, prev_dtUTCDateTime) AS DateDiffSeconds
FROM (
    SELECT  ...
         , LAG(dtUTCDateTime) OVER (ORDER BY dtUTCDateTime) 
              AS prev_dtUTCDateTime
    FROM    VehicleMonitoringLog
    Where dtUTCDateTime > GetDate() - 1
) AS x;
2
Lennart