Прогалини та острівці: клієнтське рішення та T-SQL-запит


10

Чи може рішення T-SQL для прогалин та островів працювати швидше, ніж рішення C #, що працює на клієнті?

Для конкретності надамо кілька тестових даних:

CREATE TABLE dbo.Numbers
  (
    n INT NOT NULL
          PRIMARY KEY
  ) ; 
GO 

INSERT  INTO dbo.Numbers
        ( n )
VALUES  ( 1 ) ; 
GO 
DECLARE @i INT ; 
SET @i = 0 ; 
WHILE @i < 21 
  BEGIN 
    INSERT  INTO dbo.Numbers
            ( n 
            )
            SELECT  n + POWER(2, @i)
            FROM    dbo.Numbers ; 
    SET @i = @i + 1 ; 
  END ;  
GO

CREATE TABLE dbo.Tasks
  (
    StartedAt SMALLDATETIME NOT NULL ,
    FinishedAt SMALLDATETIME NOT NULL ,
    CONSTRAINT PK_Tasks PRIMARY KEY ( StartedAt, FinishedAt ) ,
    CONSTRAINT UNQ_Tasks UNIQUE ( FinishedAt, StartedAt )
  ) ;
GO

INSERT  INTO dbo.Tasks
        ( StartedAt ,
          FinishedAt
        )
        SELECT  DATEADD(MINUTE, n, '20100101') AS StartedAt ,
                DATEADD(MINUTE, n + 2, '20100101') AS FinishedAt
        FROM    dbo.Numbers
        WHERE   ( n < 500000
                  OR n > 500005
                )
GO

Цей перший набір тестових даних має рівно один пробіл:

SELECT  StartedAt ,
        FinishedAt
FROM    dbo.Tasks
WHERE   StartedAt BETWEEN DATEADD(MINUTE, 499999, '20100101')
                  AND     DATEADD(MINUTE, 500006, '20100101')

Другий набір тестових даних має прогалини 2М -1, зазор між кожними двома сусідніми інтервалами:

TRUNCATE TABLE dbo.Tasks;
GO

INSERT  INTO dbo.Tasks
        ( StartedAt ,
          FinishedAt
        )
        SELECT  DATEADD(MINUTE, 3*n, '20100101') AS StartedAt ,
                DATEADD(MINUTE, 3*n + 2, '20100101') AS FinishedAt
        FROM    dbo.Numbers
        WHERE   ( n < 500000
                  OR n > 500005
                )
GO

На даний момент я працюю 2008 R2, але рішення 2012 року дуже вітаються. Я опублікував своє рішення C # як відповідь.

Відповіді:


4

І рішення за 1 секунду ...

;WITH cteSource(StartedAt, FinishedAt)
AS (
    SELECT      s.StartedAt,
            e.FinishedAt
    FROM        (
                SELECT  StartedAt,
                    ROW_NUMBER() OVER (ORDER BY StartedAt) AS rn
                FROM    dbo.Tasks
            ) AS s
    INNER JOIN  (
                SELECT  FinishedAt,
                    ROW_NUMBER() OVER (ORDER BY FinishedAt) + 1 AS rn
                FROM    dbo.Tasks
            ) AS e ON e.rn = s.rn
    WHERE       s.StartedAt > e.FinishedAt

    UNION ALL

    SELECT  MIN(StartedAt),
        MAX(FinishedAt)
    FROM    dbo.Tasks
), cteGrouped(theTime, grp)
AS (
    SELECT  u.theTime,
        (ROW_NUMBER() OVER (ORDER BY u.theTime) - 1) / 2
    FROM    cteSource AS s
    UNPIVOT (
            theTime
            FOR theColumn IN (s.StartedAt, s.FinishedAt)
        ) AS u
)
SELECT      MIN(theTime),
        MAX(theTime)
FROM        cteGrouped
GROUP BY    grp
ORDER BY    grp

Це приблизно на 30% швидше, ніж ваше інше рішення. 1 розрив: (00: 00: 12.1355011 00: 00: 11.6406581), пробіли 2M-1 (00: 00: 12.4526817 00: 00: 11.7442217). Однак це приблизно на 25% повільніше, ніж рішення клієнтської служби в гіршому випадку, саме так, як це передбачив Адам Маханік на Twitter.
АК

4

Наступний код C # вирішує проблему:

    var connString =
        "Initial Catalog=MyDb;Data Source=MyServer;Integrated Security=SSPI;Application Name=Benchmarks;";

    var stopWatch = new Stopwatch();
    stopWatch.Start();

    using (var conn = new SqlConnection(connString))
    {
        conn.Open();
        var command = conn.CreateCommand();
        command.CommandText = "dbo.GetAllTaskEvents";
        command.CommandType = CommandType.StoredProcedure;
        var gaps = new List<string>();
        using (var dr = command.ExecuteReader())
        {
            var currentEvents = 0;
            var gapStart = new DateTime();
            var gapStarted = false;
            while (dr.Read())
            {
                var change = dr.GetInt32(1);
                if (change == -1 && currentEvents == 1)
                {
                    gapStart = dr.GetDateTime(0);
                    gapStarted = true;
                }
                else if (change == 1 && currentEvents == 0 && gapStarted)
                {
                    gaps.Add(string.Format("({0},{1})", gapStart, dr.GetDateTime(0)));
                    gapStarted = false;
                }
                currentEvents += change;
            }
        }
        File.WriteAllLines(@"C:\Temp\Gaps.txt", gaps);
    }

    stopWatch.Stop();
    System.Console.WriteLine("Elapsed: " + stopWatch.Elapsed);

Цей код викликає цю збережену процедуру:

CREATE PROCEDURE dbo.GetAllTaskEvents
AS 
  BEGIN ;
    SELECT  EventTime ,
            Change
    FROM    ( SELECT  StartedAt AS EventTime ,
                      1 AS Change
              FROM    dbo.Tasks
              UNION ALL
              SELECT  FinishedAt AS EventTime ,
                      -1 AS Change
              FROM    dbo.Tasks
            ) AS TaskEvents
    ORDER BY EventTime, Change DESC ;
  END ;
GO

Він знаходить і друкує один проміжок через 2М інтервали в наступний час, теплий кеш:

1 gap: Elapsed: 00:00:01.4852029 00:00:01.4444307 00:00:01.4644152

Він знаходить і друкує пробіли 2М-1 через інтервали 2М у наступні рази, прогріваючи кеш:

2M-1 gaps Elapsed: 00:00:08.8576637 00:00:08.9123053 00:00:09.0372344 00:00:08.8545477

Це дуже просте рішення - на розробку знадобилося 10 хвилин. З цим може придумати недавній випускник коледжу. З боку бази даних, план виконання - це тривіальне об'єднання об'єднань, яке використовує дуже мало процесора та пам'яті.

Редагувати: щоб бути реалістичним, я запускаю клієнт і сервер на окремих полях.


Так, але що робити, якщо я хочу повернути результат як набір даних, а не як файл?
Пітер Ларссон

Більшість програм хочуть використовувати IEnumerable <SomeClassOrStruct> - у цьому випадку ми просто отримуємо повернення, а не додаємо рядок до списку. Щоб цей приклад короткий, я видалив багато речей, не важливих для вимірювання продуктивності.
АК

І це безкоштовно без процесора? Або це додає часу на ваше рішення?
Пітер Ларссон

@PeterLarsson Ви можете запропонувати кращий спосіб орієнтування? Запис у файл імітує досить повільне споживання даних клієнтом.
АК

3

Я думаю, що я вичерпав межі своїх знань на SQL сервері на цьому ...

Для пошуку розриву в SQL-сервері (що робить код C #), і вам не байдуже починати чи закінчувати прогалини (ті, які були перед першим запуском або після останнього завершення), то наступний запит (або варіанти) - це найшвидший, який я міг знайти:

SELECT e.FinishedAt as GapStart, s.StartedAt as GapEnd
FROM 
(
    SELECT StartedAt, ROW_NUMBER() OVER (ORDER BY StartedAt) AS rn
    FROM dbo.Tasks
) AS s
INNER JOIN  
(
    SELECT  FinishedAt, ROW_NUMBER() OVER (ORDER BY FinishedAt) + 1 AS rn
    FROM    dbo.Tasks
) AS e ON e.rn = s.rn and s.StartedAt > e.FinishedAt

Що працює, хоч і незначно, що для кожного набору стартових фінішів ви можете ставитись до старту та фінішу як до окремих послідовностей, зсув фінішу на один і пропуски показані.

наприклад, взяти (S1, F1), (S2, F2), (S3, F3) і замовити так: {S1, S2, S3, null} і {null, F1, F2, F3} Потім порівняти рядок n з рядком n у кожному наборі, і прогалини там, де задане значення F менше, ніж встановлене значення S ... проблема, я думаю, що на SQL-сервері немає способу з'єднати або порівняти два окремих набори суто за порядком значень у набір ... отже, використання функції row_number дозволяє дозволити нас об'єднати лише на основі рядка номер ... але немає можливості сказати SQL-серверу, що ці значення унікальні (без вставлення їх у таблицю var з індексом на це - що займає більше часу - я спробував це), тому я думаю, що об'єднання об'єднань є менш оптимальним? (хоча важко довести, коли це швидше за все, що я міг би зробити)

Мені вдалося отримати рішення за допомогою функцій LAG / LEAD:

select * from
(
    SELECT top (100) percent StartedAt, FinishedAt, LEAD(StartedAt, 1, null) OVER (Order by FinishedAt) as NextStart
    FROM dbo.Tasks
) as x
where NextStart > FinishedAt

(що, до речі, результати не гарантую - це, здається, працює, але я думаю, що покладається на те, щоб StartedAt був у порядку в таблиці завдань ... і це було повільніше)

Використання зміни суми:

select * from
(
    SELECT EventTime, Change, SUM(Change) OVER (ORDER BY EventTime, Change desc ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as RunTotal --, x.*
    FROM    
    ( 
        SELECT StartedAt AS EventTime, 1 AS Change
        FROM dbo.Tasks
    UNION ALL
        SELECT  FinishedAt AS EventTime, -1 AS Change
        FROM dbo.Tasks
    ) AS TaskEvents
) as x
where x.RunTotal = 0 or (x.RunTotal = 1 and x.Change = 1)
ORDER BY EventTime, Change DESC

(не дивно, також повільніше)

Я навіть спробував функцію сукупності CLR (щоб замінити суму - це було повільніше, ніж сума, і покладався на row_number () для збереження порядку даних), і CLR - табличну функцію (щоб відкрити два набори результатів і порівняти значення на основі чисто по послідовності) ... і це теж було повільніше. Я стільки разів стукав головою про обмеження SQL та CLR, намагаючись багато інших методів ...

А для чого?

Працюючи на одній машині та плюючи і дані C #, і SQL відфільтровані дані у файл (відповідно до оригінального коду C #), часи практично однакові .... приблизно 2 секунди для даних 1 проміжку (C # зазвичай швидше ), 8-10 секунд для множинного набору даних (SQL зазвичай швидше).

ПРИМІТКА . Не використовуйте середовище розробки SQL Server для порівняння часу, оскільки його відображення до сітки вимагає часу. Як перевірено на SQL 2012, VS2010, .net 4.0 Профіль клієнта

Я зазначу, що обидва рішення виконують майже однакове сортування даних на SQL-сервері, тому завантаження сервера для вибору сортування буде подібним, яке б рішення ви не використовували, різницею є лише обробка на клієнті (а не на сервері) , і передачу по мережі.

Я не знаю, яка різниця може бути при розподілі на різних співробітників, або коли вам можуть знадобитися додаткові дані з інформацією про розрив (хоча я не можу придумати більше іншого, крім ідентифікатора персоналу), або, звичайно, якщо існує повільне з'єднання даних між сервером SQL та клієнтською машиною (або повільним клієнтом) ... Я також не порівнював час блокування, або проблеми з суперечками, або проблеми з процесором / мережею для декількох користувачів ... Тож я не знаю, який з них, швидше за все, буде вузьким місцем у цьому випадку.

Що я знаю, так, так, SQL-сервер не дуже хороший у подібних порівняннях, і якщо ви не пишете запит правильно, ви заплатите за нього дорого.

Це легше чи складніше, ніж писати версію C #? Я не зовсім впевнений, що зміна +/- 1, що працює з тотальним рішенням, теж не зовсім інтуїтивно зрозуміла, і я, але це не перше рішення, до якого прийшов би середній випускник ... як тільки це зробити, це досить просто скопіювати, але писати потрібно в першу чергу ... те ж саме можна сказати і для версії SQL. Що складніше? Що є більш надійним для неправдивих даних? Який має більше потенціалу для паралельних операцій? Чи справді важливо, коли різниця настільки мала в порівнянні з програмуванням?

Остання остання нота; є нестабільне обмеження в даних - StartedAt повинен бути меншим, ніж FinishedAt, інакше ви отримаєте погані результати.


3

Ось рішення, яке працює за 4 секунди.

WITH cteRaw(ts, type, e, s)
AS (
    SELECT  StartedAt,
        1 AS type,
        NULL,
        ROW_NUMBER() OVER (ORDER BY StartedAt)
    FROM    dbo.Tasks

    UNION ALL

    SELECT  FinishedAt,
        -1 AS type, 
        ROW_NUMBER() OVER (ORDER BY FinishedAt),
        NULL
    FROM    dbo.Tasks
), cteCombined(ts, e, s, se)
AS (
    SELECT  ts,
        e,
        s,
        ROW_NUMBER() OVER (ORDER BY ts, type DESC)
    FROM    cteRaw
), cteFiltered(ts, grpnum)
AS (
    SELECT  ts, 
        (ROW_NUMBER() OVER (ORDER BY ts) - 1) / 2 AS grpnum
    FROM    cteCombined
    WHERE   COALESCE(s + s - se - 1, se - e - e) = 0
)
SELECT      MIN(ts) AS starttime,
        MAX(ts) AS endtime
FROM        cteFiltered
GROUP BY    grpnum;

Пітер, у наборі даних з одним проміжком це більш ніж у 10 разів повільніше: (00: 00: 18.1016745 - 00: 00: 17.8190959) На даних із пробілами 2M-1 це в 2 рази повільніше: (00:00 : 17.2409640 00: 00: 17.6068879)
АК
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.