Database
 sql >> Baza danych >  >> RDS >> Database

Specjalne wyspy

Zadania luk i wysp to klasyczne wyzwania związane z zapytaniami, w których należy zidentyfikować zakresy brakujących wartości i zakresy istniejących wartości w sekwencji. Sekwencja jest często oparta na jakiejś wartości daty lub daty i godziny, które normalnie powinny pojawiać się w regularnych odstępach czasu, ale brakuje niektórych wpisów. Zadanie przerw wyszukuje brakujące okresy, a zadanie wysp wyszukuje okresy istniejące. W przeszłości omówiłem wiele rozwiązań luk i zadań wysp w moich książkach i artykułach. Niedawno mój przyjaciel Adam Machanic przedstawił mi nowe specjalne wyzwanie wyspowe, a rozwiązanie go wymagało odrobiny kreatywności. W tym artykule przedstawiam wyzwanie i rozwiązanie, które wymyśliłem.

Wyzwanie

W swojej bazie danych śledzisz usługi obsługiwane przez Twoją firmę w tabeli o nazwie CompanyServices, a każda usługa zwykle zgłasza raz na minutę, że jest online w tabeli o nazwie EventLog. Poniższy kod tworzy te tabele i wypełnia je małymi zestawami przykładowych danych:

 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;

Tabela EventLog jest obecnie wypełniona następującymi danymi:

 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

Specjalnym zadaniem wysp jest identyfikacja okresów dostępności (serwisowany, starttime, endtime). Jednym haczykiem jest to, że nie ma pewności, że usługa będzie zgłaszać, że jest online dokładnie co minutę; powinieneś tolerować odstęp do, powiedzmy, 66 sekund od poprzedniego wpisu w dzienniku i nadal uważać, że jest to część tego samego okresu dostępności (wyspa). Po upływie 66 sekund nowy wpis w dzienniku rozpoczyna nowy okres dostępności. Tak więc, dla przykładowych danych wejściowych powyżej, rozwiązanie powinno zwrócić następujący zestaw wyników (niekoniecznie w tej kolejności):

 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

Zauważ na przykład, jak wpis dziennika 5 uruchamia nową wyspę, ponieważ odstęp od poprzedniego wpisu wynosi 120 sekund (> 66), podczas gdy wpis dziennika 6 nie uruchamia nowej wyspy, ponieważ odstęp od poprzedniego wpisu wynosi 62 sekundy ( <=66). Kolejnym haczykiem jest to, że Adam chciał, aby rozwiązanie było kompatybilne ze środowiskami pre-SQL Server 2012, co sprawia, że ​​jest to znacznie trudniejsze wyzwanie, ponieważ nie można używać funkcji agregujących okien z ramką do obliczania bieżących sum i funkcji przesunięcia okien jak LGD i LEAD. Jak zwykle sugeruję samodzielne rozwiązanie problemu, zanim przyjrzę się moim rozwiązaniom. Użyj małych zestawów przykładowych danych, aby sprawdzić poprawność swoich rozwiązań. Użyj poniższego kodu, aby wypełnić tabele dużymi zestawami przykładowych danych (500 usług, ~10 mln wpisów w dzienniku w celu przetestowania wydajności rozwiązań):

  -- 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;

Dane wyjściowe, które przedstawię dla etapów moich rozwiązań, przyjmą małe zestawy przykładowych danych, a liczby wydajności, które podam, założą duże zestawy.

Wszystkie rozwiązania, które przedstawię, korzystają z następującego indeksu:

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

Powodzenia!

Rozwiązanie 1 dla SQL Server 2012+

Zanim omówię rozwiązanie, które jest kompatybilne ze środowiskami poprzedzającymi SQL Server 2012, omówię to, które wymaga minimum SQL Server 2012. Nazwę je Rozwiązaniem 1.

Pierwszym krokiem w rozwiązaniu jest obliczenie flagi o nazwie isstart, która wynosi 0, jeśli zdarzenie nie rozpoczyna nowej wyspy, a 1 w przeciwnym razie. Można to osiągnąć, korzystając z funkcji LAG, aby uzyskać czas rejestrowania poprzedniego zdarzenia i sprawdzając, czy różnica czasu w sekundach między poprzednim a bieżącym zdarzeniem jest mniejsza lub równa dozwolonej przerwie. Oto kod implementujący ten krok:

 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;

Ten kod generuje następujące dane wyjściowe:

 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

Następnie prosta suma bieżąca flagi isstart tworzy identyfikator wyspy (nazwę go grp). Oto kod implementujący ten krok:

 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;

Ten kod generuje następujące dane wyjściowe:

 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

Na koniec grupujesz wiersze według identyfikatora usługi i identyfikatora wyspy i zwracasz minimalny i maksymalny czas rejestrowania jako czas rozpoczęcia i zakończenia każdej wyspy. Oto kompletne rozwiązanie:

 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;

To rozwiązanie zajęło 41 sekund w moim systemie i stworzyło plan pokazany na rysunku 1.

Rysunek 1:Plan rozwiązania 1

Jak widać, obie funkcje okna są obliczane na podstawie kolejności indeksów, bez potrzeby jawnego sortowania.

Jeśli używasz SQL Server 2016 lub nowszego, możesz użyć sztuczki, którą tutaj omówię, aby włączyć operator Window Aggregate w trybie wsadowym, tworząc pusty filtrowany indeks magazynu kolumn, na przykład:

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

To samo rozwiązanie zajmuje teraz tylko 5 sekund w moim systemie, tworząc plan pokazany na rysunku 2.

Rysunek 2:Planowanie rozwiązania 1 w trybie wsadowym Operator agregacji okna

To wszystko świetnie, ale jak wspomniano, Adam szukał rozwiązania, które może działać w środowiskach sprzed 2012 roku.

Zanim przejdziesz dalej, upewnij się, że upuściłeś indeks magazynu kolumn w celu oczyszczenia:

 DROP INDEX idx_cs ON dbo.EventLog;

Rozwiązanie 2 dla środowisk starszych niż SQL Server 2012

Niestety, przed SQL Server 2012 nie mieliśmy obsługi funkcji przesunięcia okna, takich jak LAG, ani nie mieliśmy obsługi obliczania bieżących sum za pomocą funkcji agregujących okna z ramką. Oznacza to, że będziesz musiał pracować znacznie ciężej, aby znaleźć rozsądne rozwiązanie.

Sztuczka, której użyłem, polegała na przekształceniu każdego wpisu w dzienniku w sztuczny interwał, którego godziną rozpoczęcia jest czas rejestracji wpisu, a godziną zakończenia jest czasem dziennika wpisu plus dozwolona przerwa. Następnie możesz potraktować to zadanie jako klasyczne zadanie pakowania interwałowego.

Pierwszym krokiem w rozwiązaniu jest obliczenie sztucznych ograniczników interwałów i numerów wierszy oznaczających pozycje każdego z rodzajów zdarzeń (counteach). Oto kod implementujący ten krok:

 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;

Ten kod generuje następujące dane wyjściowe:

 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

Następnym krokiem jest rozłożenie interwałów w chronologiczną sekwencję zdarzeń początkowych i końcowych, identyfikowanych odpowiednio jako typy zdarzeń „s” i „e”. Zauważże wybór liter s i e jest ważny ('s' > 'e' ). W tym kroku obliczane są numery wierszy oznaczające prawidłową kolejność chronologiczną obu rodzajów zdarzeń, które są teraz przeplatane (policzyć oba). W przypadku, gdy jeden interwał kończy się dokładnie tam, gdzie zaczyna się inny, umieszczając wydarzenie początkowe przed wydarzeniem końcowym, spakujesz je razem. Oto kod implementujący ten krok:

 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;

Ten kod generuje następujące dane wyjściowe:

 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

Jak wspomniano, counteach oznacza pozycję zdarzenia tylko wśród zdarzeń tego samego rodzaju, a countoba oznacza pozycję zdarzenia wśród połączonych, przeplatanych zdarzeń obu rodzajów.

Magia jest następnie obsługiwana przez następny krok — obliczanie liczby aktywnych interwałów po każdym zdarzeniu w oparciu o counteach i countoba. Liczba aktywnych interwałów to liczba zdarzeń początkowych, które miały miejsce do tej pory, minus liczba zdarzeń końcowych, które miały miejsce do tej pory. W przypadku zdarzeń początkowych counteach informuje, ile zdarzeń początkowych wydarzyło się do tej pory i można obliczyć, ile zakończyło się do tej pory, odejmując counteach od countboth. Zatem pełne wyrażenie mówiące, ile interwałów jest aktywnych, to:

 counteach - (countboth - counteach)

W przypadku zdarzeń końcowych counteach mówi, ile zdarzeń końcowych wydarzyło się do tej pory, a można obliczyć, ile rozpoczęło się do tej pory, odejmując counteach od countobath. Zatem pełne wyrażenie mówiące, ile interwałów jest aktywnych, to:

 (countboth - counteach) - counteach

Używając następującego wyrażenia CASE, obliczasz kolumnę countactive na podstawie typu zdarzenia:

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

W tym samym kroku filtrujesz tylko zdarzenia reprezentujące początki i końce upakowanych interwałów. Początki upakowanych interwałów mają typ 's' i liczności 1. Końce upakowanych interwałów mają typ 'e' i liczności 0.

Po przefiltrowaniu pozostają pary zdarzeń początek-koniec w upakowanych interwałach, ale każda para jest dzielona na dwa wiersze — jeden dla zdarzenia początkowego, a drugi dla zdarzenia końcowego. Dlatego ten sam krok oblicza identyfikator pary przy użyciu numerów wierszy za pomocą formuły (rownum – 1) / 2 + 1.

Oto kod implementujący ten krok:

 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);

Ten kod generuje następujące dane wyjściowe:

 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

Ostatni krok przestawia pary zdarzeń w wiersz na interwał i odejmuje dozwoloną przerwę od czasu zakończenia, aby odtworzyć prawidłowy czas zdarzenia. Oto kompletny kod rozwiązania:

 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;

To rozwiązanie zajęło 43 sekundy w moim systemie i wygenerowało plan pokazany na rysunku 3.

Rysunek 3:Plan rozwiązania 2

Jak widać, obliczenie pierwszego numeru wiersza jest obliczane na podstawie kolejności indeksów, ale następne dwa wymagają jawnego sortowania. Mimo to wydajność nie jest taka zła, biorąc pod uwagę około 10 000 000 wierszy.

Chociaż celem tego rozwiązania jest użycie środowiska wcześniejszego niż SQL Server 2012, dla zabawy przetestowałem jego wydajność po utworzeniu filtrowanego indeksu magazynu kolumn, aby zobaczyć, jak to działa z włączonym przetwarzaniem wsadowym:

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

Po włączeniu przetwarzania wsadowego to rozwiązanie zajęło 29 sekund w moim systemie, tworząc plan pokazany na rysunku 4.

Wniosek

To naturalne, że im bardziej ograniczone jest Twoje środowisko, tym trudniejsze staje się rozwiązywanie zadań związanych z zapytaniami. Specjalne wyzwanie Adama Wyspy jest znacznie łatwiejsze do rozwiązania na nowszych wersjach SQL Server niż na starszych. Ale potem zmuszasz się do użycia bardziej kreatywnych technik. W ramach ćwiczenia, aby poprawić swoje umiejętności zadawania zapytań, możesz stawić czoła wyzwaniom, które już znasz, ale celowo nałożyć pewne ograniczenia. Nigdy nie wiesz, na jakie ciekawe pomysły możesz się natknąć!


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Halloweenowy problem – część 4

  2. Dlaczego warto uczyć się Cassandry z Hadoop?

  3. Typowe zadania Postgres w CentOS 7

  4. Prześlij dokumenty do Azure Data Lake i eksportuj dane za pomocą SSIS

  5. Raportowanie użycia opcji/pakietów bazy danych