Database
 sql >> база данни >  >> RDS >> Database

Специални острови

Задачите за пропуски и острови са класически предизвикателства за заявки, при които трябва да идентифицирате диапазони от липсващи стойности и диапазони от съществуващи стойности в последователност. Последователността често се основава на някаква дата или стойности за дата и час, които обикновено трябва да се появяват на редовни интервали, но някои записи липсват. Задачата за пропуски търси липсващите периоди, а задачата за острови търси съществуващите периоди. Покрих много решения на задачи за пропуски и острови в моите книги и статии в миналото. Наскоро ми беше представено ново специално предизвикателство за острови от моя приятел Адам Мачаник и решаването му изискваше малко креативност. В тази статия представям предизвикателството и решението, което измислих.

Предизвикателството

Във вашата база данни вие следите услугите, които вашата компания поддържа в таблица, наречена CompanyServices, и всяка услуга обикновено отчита около веднъж в минута, че е онлайн в таблица, наречена 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

Специалната задача на островите е да идентифицира периодите на наличност (обслужен, начален, краен час). Една уловка е, че няма гаранция, че дадена услуга ще докладва, че е онлайн точно всяка минута; трябва да толерирате интервал до, да речем, 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

Забележете, например, как запис в дневника 5 започва нов остров, тъй като интервалът от предишния запис в дневника е 120 секунди (> 66), докато запис в дневника 6 не започва нов остров, тъй като интервалът от предишния запис е 62 секунди ( <=66). Друга уловка е, че Адам искаше решението да бъде съвместимо с пред-SQL Server 2012 среди, което го прави много по-трудно предизвикателство, тъй като не можете да използвате агрегатни функции на прозореца с рамка за изчисляване на текущи суми и функции на прозореца за изместване като LAG и LEAD. Както обикновено, предлагам да опитате сами да решите предизвикателството, преди да разгледате моите решения. Използвайте малките набори от примерни данни, за да проверите валидността на вашите решения. Използвайте следния код, за да попълните таблиците си с големи набори от примерни данни (500 услуги, ~10 милиона записи в регистрационния файл, за да тествате производителността на вашите решения):

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

Успех!

Решение 1 за SQL Server 2012+

Преди да разгледам решение, което е съвместимо със среди преди SQL Server 2012, ще разгледам едно, което изисква минимум SQL Server 2012. Ще го нарека Решение 1.

Първата стъпка в решението е да се изчисли флаг, наречен isstart, който е 0, ако събитието не стартира нов остров, и 1 в противен случай. Това може да се постигне чрез използване на функцията 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

И накрая, вие групирате редовете по идентификатор на услугата и идентификатора на острова и връщате минималните и максималните регистрационни времена като начален и краен час на всеки остров. Ето пълното решение:

 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 или по-нова версия, можете да използвате трика, който разглеждам тук, за да активирате оператора Window Aggregate в пакетен режим, като създадете празен филтриран индекс на columnstore, както следва:

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

Завършването на същото решение сега отнема само 5 секунди в моята система, като се получава планът, показан на фигура 2.

Фигура 2:Планиране на решение 1 с помощта на оператора Window Aggregate в пакетния режим

Всичко това е страхотно, но както споменахме, Адам търсеше решение, което може да работи в среди преди 2012 г.

Преди да продължите, уверете се, че сте изхвърлили индекса на columnstore за почистване:

 DROP INDEX idx_cs ON dbo.EventLog;

Решение 2 за среди преди SQL Server 2012

За съжаление, преди SQL Server 2012, нямахме поддръжка за функции на офсетни прозорци като LAG, нито пък имахме поддръжка за изчисляване на текущи суми с агрегатни функции на прозореца с рамка. Това означава, че ще трябва да работите много повече, за да намерите разумно решение.

Трикът, който използвах, е да превърна всеки запис в дневника в изкуствен интервал, чийто начален час е времето на записа и чийто крайен час е времето на записа плюс разрешената празнина. След това можете да третирате задачата като класическа задача за пакетиране на интервали.

Първата стъпка в решението изчислява изкуствените ограничители на интервали и номерата на редове, маркиращи позициите на всеки от видовете събития (counteach). Ето кода, който изпълнява тази стъпка:

 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 ви казва колко начални събития са се случили досега и можете да разберете колко са приключили досега, като извадите counteach от countboth. И така, пълният израз, който ви казва колко интервала са активни, тогава е:

 counteach - (countboth - counteach)

За крайни събития counteach ви казва колко крайни събития са се случили досега и можете да разберете колко са започнали досега, като извадите counteach от countboth. И така, пълният израз, който ви казва колко интервала са активни, тогава е:

 (countboth - counteach) - counteach

Използвайки следния CASE израз, изчислявате колоната с активен брой на базата на типа събитие:

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

В същата стъпка филтрирате само събития, представляващи началото и края на пакетирани интервали. Началите на пакетирани интервали имат тип „s“ и countactive 1. Краищата на пакетирани интервали имат тип „e“ и countactive 0.

След филтриране оставате с двойки събития в началото и края на пакетирани интервали, но всяка двойка е разделена на два реда – един за началното събитие и друг за крайното събитие. Следователно същата стъпка изчислява идентификатор на двойка, като използва номера на редове с формулата (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

Последната стъпка завърта двойките събития в ред за интервал и изважда позволената празнина от крайното време, за да възстанови правилното време за събитие. Ето пълния код на решението:

 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

Както можете да видите, изчисляването на номера на първия ред се изчислява въз основа на реда на индекса, но следващите два включват изрично сортиране. И все пак производителността не е толкова лоша, като се има предвид, че има около 10 000 000 замесени реда.

Въпреки че целта на това решение е да се използва среда преди SQL Server 2012, само за забавление, тествах неговата производителност, след като създадох филтриран индекс на columnstore, за да видя как се справя с активирана пакетна обработка:

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

С активирана пакетна обработка това решение отне 29 секунди, за да завърши в моята система, създавайки плана, показан на фигура 4.

Заключение

Естествено е, че колкото по-ограничена е вашата среда, толкова по-предизвикателно става решаването на задачи за запитване. Специалното предизвикателство на Адам е много по-лесно за решаване на по-нови версии на SQL Server, отколкото на по-стари. Но след това се принуждавате да използвате по-креативни техники. Така че като упражнение, за да подобрите уменията си за запитване, можете да се справите с предизвикателства, с които вече сте запознати, но умишлено да наложите определени ограничения. Никога не знаеш в какви интересни идеи може да се натъкнеш!


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Правилата на Код в SQL

  2. Как стартират паралелните планове – част 3

  3. Как да конкатенираме низове в SQL

  4. Какво представляват потребителските права на база данни?

  5. Основи на табличните изрази, част 6 – Рекурсивни CTE