sql >> データベース >  >> RDS >> Database

特別な島々

    ギャップとアイランドのタスクは、欠落している値の範囲と既存の値の範囲をシーケンスで特定する必要がある、古典的なクエリの課題です。シーケンスは多くの場合、特定の日付、または日付と時刻の値に基づいており、通常は定期的に表示されますが、一部のエントリが欠落しています。ギャップタスクは欠落している期間を探し、アイランドタスクは既存の期間を探します。私は過去に私の本や記事でギャップや島のタスクに対する多くの解決策を取り上げました。最近、友人のアダム・マハニックから新しい特別な島への挑戦が提示されました。それを解決するには、少しの創造性が必要でした。この記事では、私が思いついた課題と解決策を紹介します。

    課題

    データベースでは、CompanyServicesというテーブルで会社がサポートするサービスを追跡します。各サービスは通常、1分に1回程度、EventLogというテーブルでオンラインであることを報告します。次のコードは、これらのテーブルを作成し、サンプルデータの小さなセットをそれらに入力します。

     SET NOCOUNT ON;
     USE tempdb;
     IF OBJECT_ID(N'dbo.EventLog') IS NOT NULL DROP TABLE dbo.EventLog;
     IF OBJECT_ID(N'dbo.CompanyServices') IS NOT NULL DROP TABLE dbo.CompanyServices;
     
     CREATE TABLE dbo.CompanyServices
     (
       serviceid INT NOT NULL,
       CONSTRAINT PK_CompanyServices PRIMARY KEY(serviceid)
     );
     GO
     
     INSERT INTO dbo.CompanyServices(serviceid) VALUES(1), (2), (3);
     
     CREATE TABLE dbo.EventLog
     (
       logid     INT          NOT NULL IDENTITY,
       serviceid INT          NOT NULL,
       logtime   DATETIME2(0) NOT NULL,
       CONSTRAINT PK_EventLog PRIMARY KEY(logid)
     );
     GO
     
     INSERT INTO dbo.EventLog(serviceid, logtime) VALUES
       (1, '20180912 08:00:00'),
       (1, '20180912 08:01:01'),
       (1, '20180912 08:01:59'),
       (1, '20180912 08:03:00'),
       (1, '20180912 08:05:00'),
       (1, '20180912 08:06:02'),
       (2, '20180912 08:00:02'),
       (2, '20180912 08:01:03'),
       (2, '20180912 08:02:01'),
       (2, '20180912 08:03:00'),
       (2, '20180912 08:03:59'),
       (2, '20180912 08:05:01'),
       (2, '20180912 08:06:01'),
       (3, '20180912 08:00:01'),
       (3, '20180912 08:03:01'),
       (3, '20180912 08:04:02'),
       (3, '20180912 08:06:00');
     
     SELECT * FROM dbo.EventLog;

    EventLogテーブルには、現在次のデータが入力されています。

     logid       serviceid   logtime
     ----------- ----------- ---------------------------
     1           1           2018-09-12 08:00:00
     2           1           2018-09-12 08:01:01
     3           1           2018-09-12 08:01:59
     4           1           2018-09-12 08:03:00
     5           1           2018-09-12 08:05:00
     6           1           2018-09-12 08:06:02
     7           2           2018-09-12 08:00:02
     8           2           2018-09-12 08:01:03
     9           2           2018-09-12 08:02:01
     10          2           2018-09-12 08:03:00
     11          2           2018-09-12 08:03:59
     12          2           2018-09-12 08:05:01
     13          2           2018-09-12 08:06:01
     14          3           2018-09-12 08:00:01
     15          3           2018-09-12 08:03:01
     16          3           2018-09-12 08:04:02
     17          3           2018-09-12 08:06:00

    特別なアイランドタスクは、可用性期間(サービス、開始時間、終了時間)を特定することです。 1つの落とし穴は、サービスが正確に毎分オンラインであると報告するという保証がないことです。たとえば、前のログエントリから最大66秒の間隔を許容し、それでも同じ可用性期間(アイランド)の一部であると見なす必要があります。 66秒を超えると、新しいログエントリが新しい可用性期間を開始します。したがって、上記の入力サンプルデータの場合、ソリューションは次の結果セットを返すことになっています(必ずしもこの順序である必要はありません)。

     serviceid   starttime                   endtime
     ----------- --------------------------- ---------------------------
     1           2018-09-12 08:00:00         2018-09-12 08:03:00
     1           2018-09-12 08:05:00         2018-09-12 08:06:02
     2           2018-09-12 08:00:02         2018-09-12 08:06:01
     3           2018-09-12 08:00:01         2018-09-12 08:00:01
     3           2018-09-12 08:03:01         2018-09-12 08:04:02
     3           2018-09-12 08:06:00         2018-09-12 08:06:00
    >

    たとえば、前のログエントリからの間隔が120秒(> 66)であるため、ログエントリ5が新しいアイランドを開始する方法に注意してください。一方、前のエントリからの間隔が62秒(> 66)であるため、ログエントリ6は新しいアイランドを開始しません。 <=66)。もう1つの問題は、AdamがソリューションをSQL Server 2012より前の環境と互換性があることを望んでいたことです。これは、フレームでウィンドウ集計関数を使用して現在の合計とオフセットウィンドウ関数を計算できないため、非常に困難な課題です。 LAGやLEADのように、いつものように、自分の解決策を検討する前に、自分で課題を解決することをお勧めします。サンプルデータの小さなセットを使用して、ソリューションの有効性を確認します。次のコードを使用して、テーブルに大量のサンプルデータ(500サービス、ソリューションのパフォーマンスをテストするための最大1,000万のログエントリ)を入力します。

      -- Helper function dbo.GetNums
     IF OBJECT_ID(N'dbo.GetNums') IS NOT NULL DROP FUNCTION dbo.GetNums;
     GO
     CREATE FUNCTION dbo.GetNums(@low AS BIGINT, @high AS BIGINT) RETURNS TABLE
     AS
     RETURN
       WITH
         L0   AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)),
         L1   AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
         L2   AS (SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
         L3   AS (SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
         L4   AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
         L5   AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
         Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                  FROM L5)
       SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
       FROM Nums
       ORDER BY rownum;
     GO
     
     -- ~10,000,000 intervals
     DECLARE 
       @numservices      AS INT          = 500,
       @logsperservice   AS INT          = 20000,
       @enddate          AS DATETIME2(0) = '20180912',
       @validinterval    AS INT          = 60, -- seconds
       @normdifferential AS INT          = 3,  -- seconds
       @percentmissing   AS FLOAT        = 0.01;
     
     TRUNCATE TABLE dbo.EventLog;
     TRUNCATE TABLE dbo.CompanyServices;
     
     INSERT INTO dbo.CompanyServices(serviceid)
       SELECT A.n AS serviceid
       FROM dbo.GetNums(1, @numservices) AS A;
     
     WITH C AS
     (
       SELECT S.n AS serviceid,
         DATEADD(second, -L.n * @validinterval + CHECKSUM(NEWID()) % (@normdifferential + 1), @enddate) AS logtime,
         RAND(CHECKSUM(NEWID())) AS rnd
       FROM dbo.GetNums(1, @numservices) AS S
         CROSS JOIN dbo.GetNums(1, @logsperservice) AS L
     )
     INSERT INTO dbo.EventLog WITH (TABLOCK) (serviceid, logtime)
       SELECT serviceid, logtime
       FROM C
       WHERE rnd > @percentmissing;

    ソリューションのステップで提供する出力は、サンプルデータの小さなセットを想定しており、提供するパフォーマンスの数値は、大きなセットを想定しています。

    ここで紹介するすべてのソリューションは、次のインデックスの恩恵を受けています。

    CREATE INDEX idx_sid_ltm_lid ON dbo.EventLog(serviceid, logtime, logid);

    頑張ってください!

    SQLServer2012以降のソリューション1

    SQL Server 2012より前の環境と互換性のあるソリューションについて説明する前に、SQLServer2012以上を必要とするソリューションについて説明します。これをソリューション1と呼びます。

    ソリューションの最初のステップは、イベントが新しいアイランドを開始しない場合は0、それ以外の場合は1であるisstartというフラグを計算することです。これは、LAG関数を使用して前のイベントのログ時間を取得し、前のイベントと現在のイベントの間の秒単位の時間差が許容ギャップ以下であるかどうかを確認することで実現できます。この手順を実装するコードは次のとおりです。

     DECLARE @allowedgap AS INT = 66; -- in seconds
     
     SELECT *,
       CASE
         WHEN DATEDIFF(second,
                LAG(logtime) OVER(PARTITION BY serviceid ORDER BY logtime, logid),
                logtime) <= @allowedgap THEN 0
         ELSE 1
       END AS isstart
     FROM dbo.EventLog;
    >

    このコードは次の出力を生成します:

     logid       serviceid   logtime                     isstart
     ----------- ----------- --------------------------- -----------
     1           1           2018-09-12 08:00:00         1
     2           1           2018-09-12 08:01:01         0
     3           1           2018-09-12 08:01:59         0
     4           1           2018-09-12 08:03:00         0
     5           1           2018-09-12 08:05:00         1
     6           1           2018-09-12 08:06:02         0
     7           2           2018-09-12 08:00:02         1
     8           2           2018-09-12 08:01:03         0
     9           2           2018-09-12 08:02:01         0
     10          2           2018-09-12 08:03:00         0
     11          2           2018-09-12 08:03:59         0
     12          2           2018-09-12 08:05:01         0
     13          2           2018-09-12 08:06:01         0
     14          3           2018-09-12 08:00:01         1
     15          3           2018-09-12 08:03:01         1
     16          3           2018-09-12 08:04:02         0
     17          3           2018-09-12 08:06:00         1

    次に、isstartフラグの単純な現在の合計により、アイランド識別子が生成されます(これをgrpと呼びます)。この手順を実装するコードは次のとおりです。

     DECLARE @allowedgap AS INT = 66;
     
     WITH C1 AS
     (
       SELECT *,
         CASE
           WHEN DATEDIFF(second,
                  LAG(logtime) OVER(PARTITION BY serviceid ORDER BY logtime, logid),
                  logtime) <= @allowedgap THEN 0
           ELSE 1
         END AS isstart
       FROM dbo.EventLog
     )
     SELECT *,
       SUM(isstart) OVER(PARTITION BY serviceid ORDER BY logtime, logid
                         ROWS UNBOUNDED PRECEDING) AS grp
     FROM C1;

    このコードは次の出力を生成します:

     logid       serviceid   logtime                     isstart     grp
     ----------- ----------- --------------------------- ----------- -----------
     1           1           2018-09-12 08:00:00         1           1
     2           1           2018-09-12 08:01:01         0           1
     3           1           2018-09-12 08:01:59         0           1
     4           1           2018-09-12 08:03:00         0           1
     5           1           2018-09-12 08:05:00         1           2
     6           1           2018-09-12 08:06:02         0           2
     7           2           2018-09-12 08:00:02         1           1
     8           2           2018-09-12 08:01:03         0           1
     9           2           2018-09-12 08:02:01         0           1
     10          2           2018-09-12 08:03:00         0           1
     11          2           2018-09-12 08:03:59         0           1
     12          2           2018-09-12 08:05:01         0           1
     13          2           2018-09-12 08:06:01         0           1
     14          3           2018-09-12 08:00:01         1           1
     15          3           2018-09-12 08:03:01         1           2
     16          3           2018-09-12 08:04:02         0           2
     17          3           2018-09-12 08:06:00         1           3

    最後に、サービスIDとアイランド識別子で行をグループ化し、各アイランドの開始時刻と終了時刻として最小ログ時間と最大ログ時間を返します。完全なソリューションは次のとおりです。

     DECLARE @allowedgap AS INT = 66;
     WITH C1 AS
     (
       SELECT *,
         CASE
           WHEN DATEDIFF(second,
                  LAG(logtime) OVER(PARTITION BY serviceid ORDER BY logtime, logid),
                  logtime) <= @allowedgap THEN 0
           ELSE 1
         END AS isstart
       FROM dbo.EventLog
     ),
     C2 AS
     (
       SELECT *,
         SUM(isstart) OVER(PARTITION BY serviceid ORDER BY logtime, logid
                           ROWS UNBOUNDED PRECEDING) AS grp
       FROM C1
     )
     SELECT serviceid, MIN(logtime) AS starttime, MAX(logtime) AS endtime
     FROM C2
     GROUP BY serviceid, grp;

    このソリューションは、私のシステムで完了するのに41秒かかり、図1に示す計画を作成しました。

    図1:ソリューション1の計画

    ご覧のとおり、両方のウィンドウ関数は、明示的な並べ替えを必要とせずに、インデックスの順序に基づいて計算されます。

    SQL Server 2016以降を使用している場合は、ここで説明するトリックを使用して、次のように空のフィルター処理された列ストアインデックスを作成することにより、バッチモードのウィンドウ集計演算子を有効にできます。

     CREATE NONCLUSTERED COLUMNSTORE INDEX idx_cs 
      ON dbo.EventLog(logid) WHERE logid = -1 AND logid = -2;

    同じソリューションが私のシステムで完了するのに5秒しかかからず、図2に示す計画が作成されます。

    図2:バッチモードのウィンドウ集計演算子を使用したソリューション1の計画>

    これはすべて素晴らしいことですが、前述のように、Adamは2012年以前の環境で実行できるソリューションを探していました。

    続行する前に、クリーンアップのために列ストアインデックスを削除してください:

     DROP INDEX idx_cs ON dbo.EventLog;

    SQLServer2012より前の環境向けのソリューション2

    残念ながら、SQL Server 2012より前は、LAGのようなオフセットウィンドウ関数はサポートされていませんでした。また、フレームを使用したウィンドウ集計関数を使用した積算合計の計算もサポートされていませんでした。これは、合理的な解決策を考え出すために、もっと一生懸命働く必要があることを意味します。

    私が使用したトリックは、各ログエントリを、開始時刻がエントリのログ時刻であり、終了時刻がエントリのログ時刻に許容ギャップを加えたものである人工的な間隔に変換することです。その後、タスクを従来のインターバルパッキングタスクとして扱うことができます。

    ソリューションの最初のステップでは、人工的な間隔区切り文字と、各イベントの種類(カウント)の位置を示す行番号を計算します。この手順を実装するコードは次のとおりです。

     DECLARE @allowedgap AS INT = 66;
     
     SELECT logid, serviceid,
       logtime AS s, -- important, 's' > 'e', for later ordering
       DATEADD(second, @allowedgap, logtime) AS e,
       ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, logid) AS counteach
     FROM dbo.EventLog;

    このコードは次の出力を生成します:

     logid  serviceid  s                    e                    counteach
     ------ ---------- -------------------- -------------------- ----------
     1      1          2018-09-12 08:00:00  2018-09-12 08:01:06  1
     2      1          2018-09-12 08:01:01  2018-09-12 08:02:07  2
     3      1          2018-09-12 08:01:59  2018-09-12 08:03:05  3
     4      1          2018-09-12 08:03:00  2018-09-12 08:04:06  4
     5      1          2018-09-12 08:05:00  2018-09-12 08:06:06  5
     6      1          2018-09-12 08:06:02  2018-09-12 08:07:08  6
     7      2          2018-09-12 08:00:02  2018-09-12 08:01:08  1
     8      2          2018-09-12 08:01:03  2018-09-12 08:02:09  2
     9      2          2018-09-12 08:02:01  2018-09-12 08:03:07  3
     10     2          2018-09-12 08:03:00  2018-09-12 08:04:06  4
     11     2          2018-09-12 08:03:59  2018-09-12 08:05:05  5
     12     2          2018-09-12 08:05:01  2018-09-12 08:06:07  6
     13     2          2018-09-12 08:06:01  2018-09-12 08:07:07  7
     14     3          2018-09-12 08:00:01  2018-09-12 08:01:07  1
     15     3          2018-09-12 08:03:01  2018-09-12 08:04:07  2
     16     3          2018-09-12 08:04:02  2018-09-12 08:05:08  3
     17     3          2018-09-12 08:06:00  2018-09-12 08:07:06  4

    次のステップは、間隔を、それぞれイベントタイプ「s」および「e」として識別される開始イベントと終了イベントの時系列にアンピボットすることです。文字sとeの選択が重要であることに注意してください('s' > 'e' )。このステップでは、両方のイベントの種類の正しい時系列順を示す行番号を計算します。これらのイベントの種類は、現在インターリーブされています(両方ともカウントされます)。ある間隔が別の間隔の開始位置で正確に終了する場合は、開始イベントを終了イベントの前に配置することで、それらをまとめます。この手順を実装するコードは次のとおりです。

     DECLARE @allowedgap AS INT = 66;
     
     WITH C1 AS
     (
       SELECT logid, serviceid,
         logtime AS s, -- important, 's' > 'e', for later ordering
         DATEADD(second, @allowedgap, logtime) AS e,
         ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, logid) AS counteach
       FROM dbo.EventLog
     )
     SELECT logid, serviceid, logtime, eventtype, counteach,
       ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) AS countboth
     FROM C1
       UNPIVOT(logtime FOR eventtype IN (s, e)) AS U;

    このコードは次の出力を生成します:

     logid  serviceid  logtime              eventtype  counteach  countboth
     ------ ---------- -------------------- ---------- ---------- ----------
     1      1          2018-09-12 08:00:00  s          1          1
     2      1          2018-09-12 08:01:01  s          2          2
     1      1          2018-09-12 08:01:06  e          1          3
     3      1          2018-09-12 08:01:59  s          3          4
     2      1          2018-09-12 08:02:07  e          2          5
     4      1          2018-09-12 08:03:00  s          4          6
     3      1          2018-09-12 08:03:05  e          3          7
     4      1          2018-09-12 08:04:06  e          4          8
     5      1          2018-09-12 08:05:00  s          5          9
     6      1          2018-09-12 08:06:02  s          6          10
     5      1          2018-09-12 08:06:06  e          5          11
     6      1          2018-09-12 08:07:08  e          6          12
     7      2          2018-09-12 08:00:02  s          1          1
     8      2          2018-09-12 08:01:03  s          2          2
     7      2          2018-09-12 08:01:08  e          1          3
     9      2          2018-09-12 08:02:01  s          3          4
     8      2          2018-09-12 08:02:09  e          2          5
     10     2          2018-09-12 08:03:00  s          4          6
     9      2          2018-09-12 08:03:07  e          3          7
     11     2          2018-09-12 08:03:59  s          5          8
     10     2          2018-09-12 08:04:06  e          4          9
     12     2          2018-09-12 08:05:01  s          6          10
     11     2          2018-09-12 08:05:05  e          5          11
     13     2          2018-09-12 08:06:01  s          7          12
     12     2          2018-09-12 08:06:07  e          6          13
     13     2          2018-09-12 08:07:07  e          7          14
     14     3          2018-09-12 08:00:01  s          1          1
     14     3          2018-09-12 08:01:07  e          1          2
     15     3          2018-09-12 08:03:01  s          2          3
     16     3          2018-09-12 08:04:02  s          3          4
     15     3          2018-09-12 08:04:07  e          2          5
     16     3          2018-09-12 08:05:08  e          3          6
     17     3          2018-09-12 08:06:00  s          4          7
     17     3          2018-09-12 08:07:06  e          4          8

    前述のように、counteachは同じ種類のイベントのみの中でイベントの位置をマークし、countbothは両方の種類のイベントを組み合わせたインターリーブの中でイベントの位置をマークします。

    次に、魔法は次のステップで処理されます。つまり、counteachとcountbothの両方に基づいて、すべてのイベントの後にアクティブな間隔のカウントを計算します。アクティブな間隔の数は、これまでに発生した開始イベントの数から、これまでに発生した終了イベントの数を引いたものです。開始イベントの場合、counteachは、これまでに発生した開始イベントの数を示します。countbothからcounteachを引くことにより、これまでに終了したイベントの数を把握できます。したがって、アクティブな間隔の数を示す完全な式は次のようになります。

     counteach - (countboth - counteach)

    終了イベントの場合、counteachは、これまでに発生した終了イベントの数を示します。countbothからcounteachを引くことにより、これまでに開始されたイベントの数を把握できます。したがって、アクティブな間隔の数を示す完全な式は次のようになります。

     (countboth - counteach) - counteach

    次のCASE式を使用して、イベントタイプに基づいてcountactive列を計算します。

     CASE
       WHEN eventtype = 's' THEN
         counteach - (countboth - counteach)
       WHEN eventtype = 'e' THEN
         (countboth - counteach) - counteach
     END

    同じ手順で、パックされた間隔の開始と終了を表すイベントのみをフィルタリングします。パックされた間隔の開始にはタイプ「s」とカウントアクティブ1があります。パックされた間隔の終了にはタイプ「e」とカウントアクティブ0があります。

    フィルタリング後、パックされた間隔の開始-終了イベントのペアが残りますが、各ペアは2つの行に分割されます。1つは開始イベント用で、もう1つは終了イベント用です。したがって、同じ手順で、式(rownum – 1)/ 2+1を使用して行番号を使用してペア識別子を計算します。

    この手順を実装するコードは次のとおりです。

     DECLARE @allowedgap AS INT = 66;
     
     WITH C1 AS
     (
       SELECT logid, serviceid,
         logtime AS s, -- important, 's' > 'e', for later ordering
         DATEADD(second, @allowedgap, logtime) AS e,
         ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, logid) AS counteach
       FROM dbo.EventLog
     ),
     C2 AS
     (
       SELECT logid, serviceid, logtime, eventtype, counteach,
         ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) AS countboth
       FROM C1
         UNPIVOT(logtime FOR eventtype IN (s, e)) AS U
     )
     SELECT serviceid, eventtype, logtime,
       (ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) - 1) / 2 + 1 AS grp
     FROM C2
       CROSS APPLY ( VALUES( CASE
                               WHEN eventtype = 's' THEN
                                 counteach - (countboth - counteach)
                               WHEN eventtype = 'e' THEN
                                 (countboth - counteach) - counteach
                             END ) ) AS A(countactive)
     WHERE (eventtype = 's' AND countactive = 1)
        OR (eventtype = 'e' AND countactive = 0);

    このコードは次の出力を生成します:

     serviceid   eventtype  logtime              grp
     ----------- ---------- -------------------- ----
     1           s          2018-09-12 08:00:00  1
     1           e          2018-09-12 08:04:06  1
     1           s          2018-09-12 08:05:00  2
     1           e          2018-09-12 08:07:08  2
     2           s          2018-09-12 08:00:02  1
     2           e          2018-09-12 08:07:07  1
     3           s          2018-09-12 08:00:01  1
     3           e          2018-09-12 08:01:07  1
     3           s          2018-09-12 08:03:01  2
     3           e          2018-09-12 08:05:08  2
     3           s          2018-09-12 08:06:00  3
     3           e          2018-09-12 08:07:06  3

    最後のステップでは、イベントのペアを間隔ごとに1行にピボットし、終了時間から許容ギャップを差し引いて、正しいイベント時間を再生成します。完全なソリューションのコードは次のとおりです。

     DECLARE @allowedgap AS INT = 66;
     
     WITH C1 AS
     (
       SELECT logid, serviceid,
         logtime AS s, -- important, 's' > 'e', for later ordering
         DATEADD(second, @allowedgap, logtime) AS e,
         ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, logid) AS counteach
       FROM dbo.EventLog
     ),
     C2 AS
     (
       SELECT logid, serviceid, logtime, eventtype, counteach,
         ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) AS countboth
       FROM C1
         UNPIVOT(logtime FOR eventtype IN (s, e)) AS U
     ),
     C3 AS
     (
       SELECT serviceid, eventtype, logtime,
         (ROW_NUMBER() OVER(PARTITION BY serviceid ORDER BY logtime, eventtype DESC, logid) - 1) / 2 + 1 AS grp
       FROM C2
         CROSS APPLY ( VALUES( CASE
                                 WHEN eventtype = 's' THEN
                                   counteach - (countboth - counteach)
                                 WHEN eventtype = 'e' THEN
                                   (countboth - counteach) - counteach
                               END ) ) AS A(countactive)
       WHERE (eventtype = 's' AND countactive = 1)
          OR (eventtype = 'e' AND countactive = 0)
     )
     SELECT serviceid, s AS starttime, DATEADD(second, -@allowedgap, e) AS endtime
     FROM C3
       PIVOT( MAX(logtime) FOR eventtype IN (s, e) ) AS P;

    このソリューションは、私のシステムで完了するのに43秒かかり、図3に示す計画を生成しました。

    図3:ソリューション2の計画

    ご覧のとおり、最初の行番号の計算はインデックスの順序に基づいて計算されますが、次の2つには明示的な並べ替えが含まれます。それでも、約10,000,000行が含まれていることを考えると、パフォーマンスはそれほど悪くありません。

    このソリューションのポイントはSQLServer2012より前の環境を使用することですが、楽しみのために、フィルター処理された列ストアインデックスを作成した後、そのパフォーマンスをテストして、バッチ処理を有効にした場合の動作を確認しました。

     CREATE NONCLUSTERED COLUMNSTORE INDEX idx_cs 
      ON dbo.EventLog(logid) WHERE logid = -1 AND logid = -2;

    バッチ処理を有効にすると、このソリューションはシステムで完了するのに29秒かかり、図4に示す計画が作成されました。

    結論

    環境が制限されているほど、クエリタスクの解決が難しくなるのは当然です。 Adamの特別なIslandsの課題は、古いバージョンよりも新しいバージョンのSQLServerで解決する方がはるかに簡単です。しかし、その後、より創造的な手法を使用するように強制します。したがって、演習として、クエリスキルを向上させるために、すでに慣れ親しんでいる課題に取り組むことができますが、意図的に特定の制限を課すことができます。どんな面白いアイデアに出くわすかわからない!


    1. SQLのみを使用した基数36から基数10への変換

    2. 物理ファイルからのMySQLデータベースの復元

    3. 共通の列(A、B)と(A、C)を持つ2つのクエリを1つの(A、B、C)に変換するにはどうすればよいですか?

    4. PostgreSQLで配列の要素のインデックスを作成するにはどうすればよいですか?