SQL:找到最长的日期差距

时间:2009-08-22 05:56:59

标签: sql

我有一个包含2个字段的表:唯一ID,用户ID(外键)和日期时间。这是服务的访问日志。我在SQL Server工作但我很欣赏不可知的答案。

我想使用SQL为某个用户查找最长间隙开始的ID。

例如,假设我的值如下(对一个用户的简化):

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搜索最长的间隙,我会得到ID 2(在那里得到间隙的长度也很好,然后,但不那么重要)。

在SQL中实现此目的的最有效方法是什么?

注意:ID不一定是顺序的。

谢谢

4 个答案:

答案 0 :(得分:10)

数据库无关,属于richardtallent的变体,但没有限制。

从此设置开始:

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

然后,您可以按顺序对间隙表达式进行排序,然后选择最佳结果。

一些解释:就像richardtallent的回答一样,你加入表格以找到一个'后来'的记录 - 这基本上将所有记录与他们后来的记录中的任何一对(所以对1 + 2,1 + 3,1 + 4) ,2 + 3,2 + 4,3 + 4)。然后是另一个自连接,这次是左连接,找到之前选择的两个之间的行(1 + 2 + null,1 + 3 + 2,1 + 4 + 2,1 + 4 + 3,2 + 3 + null,2 + 4 + 3,3 + 4 + null)。但是,WHERE子句将这些过滤掉(仅保留没有中间行的行),因此只保留1 + 2 + null,2 + 3 + null和3 + 4 + null。 TAA-DAA!

如果有可能,可能会在那里有两次相同的时间(“差距”为0)那么你需要一种方法来打破关系,正如Dems指出的那样。如果您可以使用ID作为决胜局,那么请更改,例如

and (starttime.time < intermediatetime.time) 

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

假设'id'是打破关系的有效方式。

事实上,如果你知道该ID会单调增加(我知道你说'不是顺序' - 不清楚这是否意味着它们不会随着每一行而增加,或者只是那个两个相关条目的ID可能不是顺序的,因为例如另一个用户之间有条目),您可以在所有比较中使用ID而不是时间来使这更简单。

答案 1 :(得分:3)

将排名时间加入一次性排名以获得差距:

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;

从1M记录,~47k个不同的用户,在我的测试微不足道的实例(暖缓存)上读取的结果为1ms,48页读取。

如果移除UserId = 42过滤器,则每个用户的最大间隙和时间(多个最大间隙重复)需要6379139次读取,非常重,并且在我的测试机器上需要14秒。

如果只需要UserId和max gap,则时间可减少一半(当发生最大间隙时没有信息):

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个读取,只有一半与之前相比,并且在1M记录中在6秒内完成。之所以出现这种差异是因为之前的版本需要一次评估每个间隙以找到最大间隙,然后再次评估它们以找到与最大值相等的间隔。请注意,对于此性能结果,我建议使用索引(UserId,Time)的表的结构是严重

关于CTE和'分区'(更好地称为排名函数)的使用:这是所有ANSI SQL-99,并且得到大多数供应商的支持。唯一的SQL Server特定构造是datediff函数的使用,现在已将其删除。我有一种感觉,一些读者将“不可知”理解为“我最喜欢的供应商也能理解的最不常见的分母”。另请注意,使用公用表表达式和交叉应用运算符仅用于提高查询的可读性。两者都可以使用简单的机械替换来替换派生表。这是非常相同的查询,其中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表单更方便,更具可读性。

为了完整性,以下是应用于我的简化查询的相同转换(仅限最大间隙,无间隙结束时间和访问ID):

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

答案 2 :(得分:1)

首先,将表连接到自身,以便给定用户的每条记录与该用户的任何记录配对。

然后,只选择第一个在最后一个之前的那些对,在第一个之前没有记录,在最后一个之后没有记录。

 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. 如果用户在其最大间隙的起点或终点边界上有重复记录,则会为用户返回多条记录。
  4. 如果需要,您可以通过将“t1.time&lt; t2.time”更改为“t1.time&lt; = t2.time”来修复上面的#2,如果只有一个,则会给出0的差距为用户记录。

答案 3 :(得分:1)

与RichardTallent的答案非常相似......

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'(直到下次输入时为空的差距)

只有'last'的那个才会有非零时间戳。虽然问题表明“id”可能不是有序的,但是当时间戳相同时,它是我们确定哪个reocrd是'last'的唯一信息。

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