2009-08-22 161 views
5

我有一個包含2個字段的表:唯一ID,用戶ID(外鍵)和日期時間。這是一個服務的訪問日誌。我在SQL Server中工作,但我會感謝不可知論的答案。SQL:查找最長日期間隔

我想使用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不一定是順序的。

謝謝

+0

您能否澄清一下:您是否在按ID排序並按用戶過濾時,或者是同一用戶的任意兩個*記錄之間的最大差距,查找*相鄰*記錄之間的最大差距?對於任何一個,你的測試用例的答案是2。 – richardtallent 2009-08-23 15:07:11

+0

@richardtellent:我正在尋找「相鄰」用戶條目之間最長的間隔,其中「相鄰」表示它們之間沒有日期時間條目(而不是基於ID)。我希望澄清。我不確定我是否理解你的第二個解釋,因爲任何兩個記錄之間的最大差距在第一個(1)和最後一個(4)之間。 – 2009-08-23 15:21:17

回答

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!如Dems指出的那樣,如果你可能有兩次相同的時間(一個'間隔'爲0),那麼你需要一種斷開關係的方式。如果您可以使用ID作爲打破平局,然後更改例如

and (starttime.time < intermediatetime.time) 

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

假設 '身份證' 就是要打破關係的有效途徑。

事實上,如果你知道該ID將是單調遞增的(我知道你說「不連續」 - 尚不清楚,如果這意味着它們不會每行增加,或只是的的ID兩個相關的條目可能不是連續的,因爲例如另一個用戶具有在其間的條目),您可以使用ID而不是時間在的所有的比較以使其更簡單。

+1

+1:使用有意義的表別名。 (謝謝!)而且我喜歡外部連接,以找到將日期對縮減到只有那些沒有任何內容的日期。從來沒有見過,但很有道理。並指出datediff是SQL Server specific.It會很高興看到這一切通過篩選結果只顯示信息的最大(差距) – 2009-08-22 22:46:40

+0

尼斯。 Upvoting,我喜歡使用LEFT OUTER連接,而不是使用相關子查詢的兩倍。 – richardtallent 2009-08-23 15:03:56

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. 如果用戶在其最大差距的開始或結束邊界上有重複記錄,將返回多個記錄。

如果需要,可以通過改變「t1.time < t2.time」到「t1.time < = t2.time」修復#2的上方,這將給你的0的間隙如果只有一個用戶記錄。

+0

這是_is_基本數據庫不可知的,所以+1 :) – MatBailie 2009-08-22 12:35:17

+0

EXISTS(SELECT * FROM x)已經顯示出比SQL Server中的SELECT NULL更快。本質上,SQL Server已被調整爲此目的。 – MatBailie 2009-08-22 12:46:22

+0

-1,你沒有查看連續時間點之間的差距,但得到: - 從t組中選擇「user-id」,min(time),max(time),diff(..)「user-id_該ID與用戶ID匹配的最小值(時間) – 2009-08-22 22:39:54

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 w/oa數據庫寫入測試。我有更多時間來處理這個問題,並且實際測試並測量它在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不同用戶的結果:這是在我的測試puny實例(熱緩存)1ms內返回,48頁讀取。

如果刪除了UserId = 42過濾器,每個用戶的最大間隔和時間(包含多個最大間隔的重複數據)需要6379139個讀取,而且很重,並且在我的測試機器上需要14s。

的時間,如果只有用戶名和最大的差距是需要(無信息發生的最大間隙)被切成兩半:

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函數,該函數現在被刪除。我感覺有些讀者將'不可知論'理解爲'我最喜歡的供應商也理解的最不常見的分母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表單的方式更具可讀性。

爲了完整起見,這裏是適用於我的一個簡化查詢(僅限最大的差距,沒有差距結束時間和訪問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 
+0

公用表表達式,分區等不是數據庫不可知的... – MatBailie 2009-08-22 12:34:13

+0

但是,如果您在SQL Server上實現,帶窗口功能的CTE可能會快得多。提供不可知論的和具體的答案是很好的,我認爲,有時當你看到性能上的差異時,用不可知論的方法去解決問題的願望會消失。 – 2009-08-22 22:24:55

+0

雖然這不是一個完整的答案。應該將生成gap_lengh的select包裝在另一個命名的CTE中,然後按用戶排序,最後選擇rank = 1。 – 2009-08-22 22:27:29

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 


最後是用相同的時間標記多個條目的方法可行。當發生這種情況時,我們需要額外的信息來決定訂單,以便我們確定哪個記錄是「下一個」。

如果有多個條目具有相同的時間戳,所有的酒吧一個將有一個0 GapTime:
- '12:00' (1峽,直到下一個條目)
- '12:01' (0間隙直到下一個條目)
- '12:01' (0間隙直到下一個條目)
- '12:01' (0間隙直到下一個條目)
- '12:01' (間隙1到下一個條目)

- '12:02'(NULL直到下一個條目的空位)

Only 「最後一個」將具有非零時間戳。雖然這個問題表明「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 
+0

用你數據庫實現中存在的任何函數替換DATEDIFF,其餘的應該是相當通用的 – MatBailie 2009-08-22 12:44:16

+0

不壞...我進入了開始和結束記錄之間的連接路線,而不是相關的子查詢,因爲如果OP希望稍後從任一側選擇附加信息,則它更加靈活。兩者應該有類似的表現。 – richardtallent 2009-08-23 14:57:28