SQL: найти самый длинный промежуток даты - PullRequest
5 голосов
/ 22 августа 2009

У меня есть таблица с 2 полями: уникальный идентификатор, идентификатор пользователя (внешний ключ) и дата-время. Это лог доступа к сервису. Я работаю в SQL Server, но буду признателен за независимые ответы.

Я хотел бы использовать SQL, чтобы найти для определенного пользователя идентификатор, с которого начинается самый длинный разрыв.

Так, например, скажем, мои значения следующие (упрощение для одного пользователя):

ID |  User-ID |  Time
----------------------------------
1  |  1       |  11-MAR-09, 8:00am
2  |  1       |  11-MAR-09, 6:00pm
3  |  1       |  13-MAR-09, 7:00pm
4  |  1       |  14-MAR-09, 6:00pm

Если я найду самый длинный промежуток для пользователя 1, я получу идентификатор 2 (было бы неплохо получить длину промежутка тут же, но гораздо менее критично).

Какой самый эффективный способ добиться этого в SQL?

Примечание. Идентификатор не обязательно является последовательным.

Спасибо

Ответы [ 4 ]

10 голосов
/ 23 августа 2009

База данных не зависит от варианта Ричардталента, но без ограничений.

Начиная с этой настройки:

create table test(id int, userid int, time datetime)
insert into test values (1, 1, '2009-03-11 08:00')
insert into test values (2, 1, '2009-03-11 18:00')
insert into test values (3, 1, '2009-03-13 19:00')
insert into test values (4, 1, '2009-03-14 18:00')

(я SQL Server 2008 здесь, но это не должно иметь значения)

Выполнение этого запроса:

select 
  starttime.id as gapid, starttime.time as starttime, endtime.time as endtime, 
  /* Replace next line with your DB's way of calculating the gap */
  DATEDIFF(second, starttime.time, endtime.time) as gap
from 
  test as starttime
inner join test as endtime on 
  (starttime.userid = endtime.userid) 
  and (starttime.time < endtime.time) 
left join test as intermediatetime on 
  (starttime.userid = intermediatetime.userid) 
  and (starttime.time < intermediatetime.time) 
  and (intermediatetime.time < endtime.time) 
where 
  (intermediatetime.id is null)

Дает следующее:

gapid  starttime                endtime                  gap
1      2009-03-11 08:00:00.000  2009-03-11 18:00:00.000  36000
2      2009-03-11 18:00:00.000  2009-03-13 19:00:00.000  176400
3      2009-03-13 19:00:00.000  2009-03-14 18:00:00.000  82800

Затем вы можете просто ORDER BY выражения пробела по убыванию и выбрать лучший результат.

Некоторое объяснение: как и ответ Ричардталента, вы присоединяете таблицу к себе, чтобы найти «более позднюю» запись - это в основном объединяет все записи с ЛЮБЫМИ из их более поздних записей (поэтому пары 1 + 2, 1 + 3, 1 + 4 , 2 + 3, 2 + 4, 3 + 4). Затем есть другое самостоятельное соединение, на этот раз левое соединение, чтобы найти строки между двумя ранее выбранными так (1 + 2 + ноль, 1 + 3 + 2, 1 + 4 + 2, 1 + 4 + 3, 2+ 3 + ноль, 2 + 4 + 3, 3 + 4 + ноль). Предложение WHERE, однако, отфильтровывает их (сохраняет только строки без промежуточной строки), следовательно, сохраняет только 1 + 2 + ноль, 2 + 3 + ноль и 3 + 4 + ноль. Таа-даа!

Если вы, возможно, могли бы иметь одно и то же время там дважды («разрыв» 0), тогда вам понадобится способ разорвать связи, как указывает Демс. Если вы можете использовать ID в качестве тай-брейка, измените, например,

and (starttime.time < intermediatetime.time) 

до

and ((starttime.time < intermediatetime.time) 
  or ((starttime.time = intermediatetime.time) and (starttime.id < intermediatetime.id)))

при условии, что id является допустимым способом разрыва связей.

На самом деле, если вы знаете , что ID будет монотонно увеличиваться (я знаю, что вы сказали «не последовательно» - неясно, означает ли это, что они не увеличиваются с каждой строкой, или что Идентификаторы двух соответствующих записей могут быть не последовательными, потому что, например, у другого пользователя есть записи между ними), вы можете использовать идентификатор вместо времени в всех сравнениях, чтобы сделать это еще проще.

3 голосов
/ 22 августа 2009

Присоединяйтесь к рангу Время на одноразовом ранге, чтобы получить разрыв:

with cte_ranked as (
select *, row_number() over (partition by UserId order by Time) as rn
from table)
select l.*, datediff(minute, r.Time, l.Time) as gap_length
from cte_ranked l join cte_ranked r on l.UserId = r.UserId and l.rn = r.rn-1

Затем вы можете использовать множество методов, чтобы определить максимальный разрыв, когда он начался и т. Д.

Обновление

Мой оригинальный ответ был написан с Mac без базы данных для тестирования. У меня было еще немного времени, чтобы поиграть с этой проблемой и на самом деле протестировать и измерить, как она работает с таблицей записей 1M. Моя тестовая таблица определена так:

create table access (id int identity(1,1)
    , UserId int not null
    , Time datetime not null);
create clustered index cdx_access on access(UserID, Time);
go

Для выбора записи для любой информации мой предпочтительный ответ до сих пор таков:

with cte_gap as (
    select Id, UserId, a.Time, (a.Time - prev.Time) as gap
    from access a
    cross apply (
        select top(1) Time 
        from access b
        where a.UserId = b.UserId
            and a.Time > b.Time
        order by Time desc) as prev)
, cte_max_gap as (
    select UserId, max(gap) as max_gap
    from cte_gap
    group by UserId)
select g.* 
    from cte_gap g
    join cte_max_gap m on m.UserId = g.UserId and m.max_gap = g.gap
where g.UserId = 42;

Из 1М записи, ~ 47k разных пользователей, результат для этого возвращается в 1мс на моем тестовом маленьком экземпляре (теплый кеш), чтение 48 страниц.

Если фильтр UserId = 42 удаляется, максимальный разрыв и время, за которое он произошел для каждого пользователя (с дубликатами для нескольких максимальных разрывов), требуют 6379139 операций чтения, довольно тяжелых и занимающих 14 с на моей тестовой машине.

Время можно сократить вдвое, если требуется только идентификатор пользователя и максимальный разрыв (нет информации , когда произошел максимальный разрыв):

select UserId, max(a.Time-prev.Time) as gap
    from access a
    cross apply (
        select top(1) Time 
        from access b
        where a.UserId = b.UserId
            and a.Time > b.Time
        order by Time desc
    ) as prev
group by UserId

Для этого требуется только 3193448 операций чтения, только половина по сравнению с предыдущими, и завершение за 6 секунд на записях 1M. Разница возникает из-за того, что предыдущей версии нужно было оценить каждый пробел один раз, чтобы найти максимальный, а затем снова оценить их, чтобы найти те, которые равны максимальному. Обратите внимание, что для этих результатов производительности структура таблицы, которую я предложил с индексом (UserId, Time), составляет критических .

Что касается использования CTE и «разделов» (более известных как функции ранжирования): это все ANSI SQL-99 и поддерживается большинством поставщиков. Единственной конструкцией, специфичной для SQL Server, было использование функции datediff, которая теперь удалена. У меня есть чувство, что некоторые читатели понимают «независимость» как «наименее распространенный знаменатель SQL, понимаемый также моим любимым поставщиком». Также обратите внимание, что использование общих табличных выражений и оператора перекрестного применения используются исключительно для улучшения читаемости запроса. Оба могут быть заменены производной таблицей с помощью простой механической замены. Вот тот же самый запрос , где CTE были заменены производными таблицами. Я позволю вам судить о его удобочитаемости по сравнению с CTE:

select g.*
    from (    
        select Id, UserId, a.Time, (a.Time - (
            select top(1) Time 
            from access b
            where a.UserId = b.UserId
                and a.Time > b.Time
            order by Time desc
        )) as gap
        from access a) as g
    join (
        select UserId, max(gap) as max_gap
            from (
                select Id, UserId, a.Time, (a.Time - (
                   select top(1) Time 
                   from access b
                   where a.UserId = b.UserId
                     and a.Time > b.Time
                   order by Time desc
                   )) as gap
            from access a) as cte_gap
        group by UserId) as m on m.UserId = g.UserId and m.max_gap = g.gap
    where g.UserId = 42

Черт, я прыгал, в итоге получится более запутанным, лол. Это вполне читабельно, потому что у него было только два CTE. Тем не менее, при запросах с 5-6 производными таблицами форма CTE более удобна для чтения.

Для полноты, вот то же преобразование, примененное к моему упрощенному запросу (только максимальные промежутки, без времени окончания промежутка и идентификатора доступа):

select UserId, max(gap)
    from (
        select UserId, a.Time-(
            select top(1) Time 
            from access b
            where a.UserId = b.UserId
                and a.Time > b.Time
            order by Time desc) as gap
    from access a) as gaps
group by UserId
1 голос
/ 22 августа 2009

Очень похоже на ответ Ричарда Таллента ...

SELECT
   t1.id,
   t1.[user-id],
   t1.time,
   DATEDIFF(s, t1.time, t2.time) AS GapTime
FROM
   t AS t1
INNER JOIN
   t AS t2
      ON  t2.[user-id] = t1.[user-id]
      AND t2.time = (
         SELECT
            MIN(time)
         FROM
            t
         WHERE
            [user-id] = t1.[user-id]
            AND time > t1.time
      )


Поскольку вы фактически используете значение времени из t2, вы можете реорганизовать его следующим образом, чтобы иметь дело только с одной записью для пользователей ...

SELECT
   t1.id,
   t1.[user-id],
   t1.time,
   DATEDIFF(
      s,
      t1.time,
      (
         SELECT
            MIN(time)
         FROM
            t
         WHERE
            [user-id] = t1.[user-id]
            AND time > t1.time
      )
   ) AS GapTime
FROM
   t1


Наконец, существует возможность нескольких записей с одной и той же отметкой времени. Когда это происходит, нам нужна дополнительная информация для определения порядка, позволяющая нам определить, какая запись «следующая».

Если имеется несколько записей с одной и той же отметкой времени, все такты одного будут иметь GapTime 0:
- '12: 00 '(разрыв от 1 до следующей записи)
- '12: 01' (разрыв от 0 до следующей записи)
- «12: 01» (разрыв от 0 до следующей записи)
- «12: 01» (разрыв от 0 до следующей записи)
- «12: 01» (разрыв от 1 до следующей записи)

- «12: 02» (разрыв до нуля до следующей записи)

Только тот, который является «последним», будет иметь ненулевую отметку времени. Хотя в вопросе говорится, что «id» может быть не по порядку, это единственная информация, которую мы имеем, чтобы определить, какой reocrd является «последним», когда метки времени совпадают.

SELECT
   t1.id,
   t1.[user-id],
   t1.time,
   DATEDIFF(
      s,
      t1.time,
      (
         SELECT
            MIN(time)
         FROM
            t
         WHERE
            [user-id] = t1.[user-id]
            AND
            (
               (time > t1.time)
               OR
               (time = t1.time AND id > t1.id)
            )
      )
   ) AS GapTime
FROM
   t1
1 голос
/ 22 августа 2009

Сначала присоедините таблицу к себе, чтобы каждая запись для данного пользователя была связана с любой записью для того же пользователя.

Затем выберите только те пары, где первая находится перед последней, нет записи перед первой и нет записи после последней.

 SELECT t1.id, t1.[user-id], t1.time, (t2.time - t1.time) AS GapTime
 FROM
     t AS t1
     INNER JOIN t AS t2 ON t1.[user-id] = t2.[user-id]
 WHERE
     t1.time < t2.time
     AND NOT EXISTS (SELECT NULL FROM t AS t3 WHERE t3.[user-id] = t1.[user-id]
         AND t3.time > t2.time)
     AND NOT EXISTS (SELECT NULL FROM t AS t4 WHERE t4.[user-id] = t1.[user-id]
         AND t4.time < t1.time)

Предостережения:

  1. Не возвращает пользователей с 0 или 1 записями.
  2. Не возвращает пользователей, у которых все записи имеют одинаковую дату / время.
  3. Возвращает несколько записей для пользователя, если у пользователя есть дубликаты записей на начальной или конечной границе их наибольшего разрыва.

При желании вы можете исправить № 2 выше, изменив «t1.time

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...