web-dev-qa-db-ja.com

ギャップと島-最寄りの島を見つける

島とギャップに該当する時間データがある次のシナリオを使用しています。時々、既存のギャップ内にあるイベントを、イベントの時間に基づいて最も近い島に関連付ける必要があります。

実例として、期間を定義する次のデータがあるとします。

enter image description here

このデータは、ID 27の間に2017-07-26 00:03:00から2017-07-26 00:07:00までの期間に存在するギャップを除いて、連続しています。

最も近い島を特定するために、現在、次のようにギャップを2つの期間に分割しています。

enter image description here

このギャップに該当するイベントがある場合、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)

  • 望ましい結果はギャップを含むテーブルですか?

  • または、最も近い隣人に着信行を割り当てようとしていますか?

  • それとも、あなたの例で示した正確な出力を再現したいと思っていますか?

回答:

簡単に言えば、はい、はい、いいえです。望ましい結果は、通常はギャップに収まるイベント時間に最も近い島を特定するための(その他の/より多くの)効率的な方法を特定することです。質問を拡張して、望ましい最終結果がどうなるかを示しました。

5
John Eisbrener

ここにはさまざまな質問がたくさんあります。完全な結果セット(IDへの時間のマッピング)を生成することになると、WindowStartWindowEnd。 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;

これは、あなたが持っているものと同じように機能する並べ替えのないすてきでクリーンな計画を持っています:

DOP 1 plan for all rows

実際の例では、行の小さなサブセット(例では10)に最も近い島を見つけることが要件である場合は、インデックスを使用してはるかに効率的なコードを作成できます。ここでの考え方は、tmpEventの各行について、テーブルから前の行と次の行を見つけ、少し計算して最も近い行を見つけることです。 NtmpEvent行がある場合、このコードは最大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);

ランダムデータを生成しているため、結果セットは次のようになります。

enter image description here

そして、これがクエリプランです:

enter image description here

別のテストとして、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。

6
Joe Obbish