Пробелы и острова: клиентское решение против запроса 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.
AK
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

Он находит и печатает один разрыв с интервалами 2M в следующие моменты времени, горячий кэш:

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> - в этом случае мы просто возвращаем return вместо добавления строки в список. Чтобы этот пример был коротким, я удалил много вещей, не необходимых для измерения необработанной производительности.
AK
И это бесплатно процессора? Или это добавляет время к вашему решению?
Питер Ларссон
@PeterLarsson Можете ли вы предложить лучший способ для сравнения? Запись в файл имитирует довольно медленное потребление данных клиентом.
AK
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 и клиентской машиной (или медленным клиентом) ... Также я сделал сравнение запираемых раз, или проблемы раздора, или CPU / вопросы сети для нескольких пользователей ... Так что я не знаю, какой из них, скорее всего, будет узким местом в этом случае.

Что я действительно знаю, так это то, что SQL-сервер не подходит для такого рода сравнений множеств, и если вы не напишете запрос правильно, вы заплатите за него дорого.

Это проще или сложнее, чем писать версию на C #? Я не совсем уверен, что решение Change +/- 1, работающее в целом, также не совсем интуитивно понятно, и я, но это не первое решение, к которому придет среднестатистический выпускник ... после этого его достаточно легко скопировать, но Прежде всего, нужно разобраться, чтобы написать ... То же самое можно сказать и о версии SQL. Что сложнее? Что является более надежным для мошеннических данных? Который имеет больший потенциал для параллельных операций? Действительно ли имеет значение, когда разница настолько мала по сравнению с усилиями по программированию?

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

puzsol
источник
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)
АК