SQL问题 - 计算最大天数序列

时间:2012-05-04 11:33:44

标签: sql vertica window-functions

有一张包含访问数据的表格:

uid (INT) | created_at (DATETIME)

我想查找用户连续多少天访问过我们的应用。例如:

SELECT DISTINCT DATE(created_at) AS d FROM visits WHERE uid = 123

将返回:

     d      
------------
 2012-04-28
 2012-04-29
 2012-04-30
 2012-05-03
 2012-05-04

有5条记录和两个间隔 - 3天(4月28日至30日)和2天(5月3日至4日)。

我的问题是如何找到用户连续访问该应用的最大天数(示例中为3天)。试图在SQL文档中找到合适的函数,但没有成功。我错过了什么吗?


UPD: 谢谢你们的答案!实际上,我正在使用vertica分析数据库(http://vertica.com/),但这是一个非常罕见的解决方案,只有少数人有使用它的经验。虽然它支持SQL-99标准。

嗯,大多数解决方案只需稍作修改即可。最后,我创建了自己的查询版本:

-- returns starts of the vitit series 
SELECT t1.d as s FROM testing t1
LEFT JOIN testing t2 ON DATE(t2.d) = DATE(TIMESTAMPADD('day', -1, t1.d))
WHERE t2.d is null GROUP BY t1.d

          s          
---------------------
 2012-04-28 01:00:00
 2012-05-03 01:00:00

-- returns end of the vitit series 
SELECT t1.d as f FROM testing t1
LEFT JOIN testing t2 ON DATE(t2.d) = DATE(TIMESTAMPADD('day', 1, t1.d))
WHERE t2.d is null GROUP BY t1.d

          f          
---------------------
 2012-04-30 01:00:00
 2012-05-04 01:00:00

所以现在我们只需要以某种方式加入它们,例如通过行索引。

SELECT s, f, DATEDIFF(day, s, f) + 1 as seq FROM (
    SELECT t1.d as s, ROW_NUMBER() OVER () as o1 FROM testing t1
    LEFT JOIN testing t2 ON DATE(t2.d) = DATE(TIMESTAMPADD('day', -1, t1.d))
    WHERE t2.d is null GROUP BY t1.d
) tbl1 LEFT JOIN (
    SELECT t1.d as f, ROW_NUMBER() OVER () as o2 FROM testing t1
    LEFT JOIN testing t2 ON DATE(t2.d) = DATE(TIMESTAMPADD('day', 1, t1.d))
    WHERE t2.d is null GROUP BY t1.d
) tbl2 ON o1 = o2 

示例输出:

          s          |          f          | seq 
---------------------+---------------------+-----
 2012-04-28 01:00:00 | 2012-04-30 01:00:00 |   3
 2012-05-03 01:00:00 | 2012-05-04 01:00:00 |   2

10 个答案:

答案 0 :(得分:7)

另一种最短的方法是自我加入:

with grouped_result as
(
    select 
       sr.d,
       sum((fr.d is null)::int) over(order by sr.d) as group_number
    from tbl sr
    left join tbl fr on sr.d = fr.d + interval '1 day'
)
select d, group_number, count(d) over m as consecutive_days
from grouped_result
window m as (partition by group_number)

输出:

          d          | group_number | consecutive_days 
---------------------+--------------+------------------
 2012-04-28 08:00:00 |            1 |                3
 2012-04-29 08:00:00 |            1 |                3
 2012-04-30 08:00:00 |            1 |                3
 2012-05-03 08:00:00 |            2 |                2
 2012-05-04 08:00:00 |            2 |                2
(5 rows)

实时测试:http://www.sqlfiddle.com/#!1/93789/1

sr =第二行,fr =第一行(或者可能是前一行?)。基本上我们正在进行反向跟踪,这是数据库的模拟延迟,不支持LAG(Postgres支持LAG,但解决方案是very long,因为窗口不支持嵌套窗口)。所以在这个查询中,我们使用混合方法,通过join模拟LAG,然后对它使用SUM窗口,这会产生组号

<强>更新

忘了把最后的查询,上面的查询说明了组编号的基础,需要将其变形为:

with grouped_result as
(
    select 
       sr.d,
       sum((fr.d is null)::int) over(order by sr.d) as group_number
    from tbl sr
    left join tbl fr on sr.d = fr.d + interval '1 day'
)
select min(d) as starting_date, max(d) as end_date, count(d) as consecutive_days
from grouped_result
group by group_number
-- order by consecutive_days desc limit 1


STARTING_DATE                END_DATE                     CONSECUTIVE_DAYS
April, 28 2012 08:00:00-0700 April, 30 2012 08:00:00-0700 3
May, 03 2012 08:00:00-0700   May, 04 2012 08:00:00-0700   2

<强>更新

我知道为什么使用窗口函数的other solution变长了,我试图说明组编号和计数组的逻辑变得很长。如果我像MySql approach那样切入追逐,那么窗口函数可能会更短。话虽如此,这是我的旧窗口功能方法,虽然现在更好:

with headers as
(
    select 
      d,lag(d) over m is null or d - lag(d) over m  <> interval '1 day' as header
    from tbl
    window m as (order by d)
)      
,sequence_group as
(
    select d, sum(header::int) over (order by d) as group_number
    from headers  
)
select min(d) as starting_date,max(d) as ending_date,count(d) as consecutive_days
from sequence_group
group by group_number
-- order by consecutive_days desc limit 1

实时测试:http://www.sqlfiddle.com/#!1/93789/21

答案 1 :(得分:2)

在MySQL中你可以这样做:

SET @nextDate = CURRENT_DATE;
SET @RowNum = 1;

SELECT MAX(RowNumber) AS ConecutiveVisits
FROM    (   SELECT  @RowNum := IF(@NextDate = Created_At, @RowNum + 1, 1) AS RowNumber,
                    Created_At,
                    @NextDate := DATE_ADD(Created_At, INTERVAL 1 DAY) AS NextDate
            FROM    Visits
            ORDER BY Created_At
        ) Visits

此处示例:

http://sqlfiddle.com/#!2/6e035/8

然而,我并非100%确定这是最好的方法。

在Postgresql中:

 ;WITH RECURSIVE VisitsCTE AS
 (  SELECT  Created_At, 1 AS ConsecutiveDays
    FROM    Visits
    UNION ALL
    SELECT  v.Created_At, ConsecutiveDays + 1
    FROM    Visits v
            INNER JOIN VisitsCTE cte
                ON 1 + cte.Created_At = v.Created_At
)
SELECT  MAX(ConsecutiveDays) AS ConsecutiveDays
FROM    VisitsCTE

此处示例:

http://sqlfiddle.com/#!1/16c90/9

答案 2 :(得分:2)

我知道Postgresql有类似于MSSQL中可用的常用表表达式。我对Postgresql并不熟悉,但下面的代码适用于MSSQL,可以满足您的需求。

create table #tempdates (
    mydate date
)

insert into #tempdates(mydate) values('2012-04-28')
insert into #tempdates(mydate) values('2012-04-29')
insert into #tempdates(mydate) values('2012-04-30')
insert into #tempdates(mydate) values('2012-05-03')
insert into #tempdates(mydate) values('2012-05-04');

with maxdays (s, e, c)
as
(
    select mydate, mydate, 1
    from #tempdates
    union all
    select m.s, mydate, m.c + 1
    from #tempdates t
    inner join maxdays m on DATEADD(day, -1, t.mydate)=m.e
)
select MIN(o.s),o.e,max(o.c)
from (
  select m1.s,max(m1.e) e,max(m1.c) c
  from maxdays m1
  group by m1.s
) o
group by o.e

drop table #tempdates

这是SQL小提琴:http://sqlfiddle.com/#!3/42b38/2

答案 3 :(得分:2)

所有这些都是非常好的答案,但我认为我应该通过展示另一种利用Vertica特有的分析能力的方法做出贡献(毕竟它是你付出的一部分)。我保证最后的查询很简短。

首先,使用conditional_true_event()进行查询。来自Vertica的文档:

  

为每行指定一个事件窗口编号,从0开始,和   当布尔参数的结果时,将数字增加1   表达式评估为真。

示例查询如下所示:

select uid, created_at, 
       conditional_true_event( created_at - lag(created_at) > '1 day' ) 
       over (partition by uid order by created_at) as seq_id
from visits;

输出:

uid  created_at           seq_id  
---  -------------------  ------  
123  2012-04-28 00:00:00  0       
123  2012-04-29 00:00:00  0       
123  2012-04-30 00:00:00  0       
123  2012-05-03 00:00:00  1       
123  2012-05-04 00:00:00  1       
123  2012-06-04 00:00:00  2       
123  2012-06-04 00:00:00  2     

现在最后的查询变得简单了:

select uid, seq_id, count(1) num_days, min(created_at) s, max(created_at) f
from
(
    select uid, created_at, 
       conditional_true_event( created_at - lag(created_at) > '1 day' ) 
       over (partition by uid order by created_at) as seq_id
    from visits
) as seq
group by uid, seq_id;

最终输出:

uid  seq_id  num_days  s                    f                    
---  ------  --------  -------------------  -------------------  
123  0       3         2012-04-28 00:00:00  2012-04-30 00:00:00  
123  1       2         2012-05-03 00:00:00  2012-05-04 00:00:00  
123  2       2         2012-06-04 00:00:00  2012-06-04 00:00:00  

最后一点说明: num_days实际上是内部查询的行数。如果原始表格中有两次'2012-04-28'次访问(即重复),您可能需要解决此问题。

答案 4 :(得分:1)

以下内容应该是Oracle友好的,不需要递归逻辑。

;WITH
  visit_dates (
    visit_id,
    date_id,
    group_id
  )
AS
(
  SELECT
    ROW_NUMBER() OVER (ORDER BY TRUNC(created_at)),
    TRUNC(SYSDATE) - TRUNC(created_at),
    TRUNC(SYSDATE) - TRUNC(created_at) - ROW_NUMBER() OVER (ORDER BY TRUNC(created_at))
  FROM
    visits
  GROUP BY
    TRUNC(created_at)
)
,
  group_duration (
    group_id,
    duration
  )
AS
(
  SELECT
    group_id,
    MAX(date_id) - MIN(date_id) + 1  AS duration
  FROM
    visit_dates
  GROUP BY
    group_id
)
SELECT
  MAX(duration)  AS max_duration
FROM
  group_duration

答案 5 :(得分:1)

PostgreSQL的:

with headers as
(
    select 
        d,
        lag(d) over m is null or d - lag(d) over m  <> interval '1 day' as header

    from tbl
    window m as (order by d)
)      
,sequence_group as
(
    select d, sum(header::int) over m as group_number 
    from headers
    window m as (order by d)
)
,consecutive_list as
(
    select d, group_number, count(d) over m as consecutive_count
    from sequence_group 
    window m as (partition by group_number)
)
select * from consecutive_list

分治方法:3个步骤

第一步,找到标题:

with headers as
(
    select 
        d,
        lag(d) over m is null or d - lag(d) over m  <> interval '1 day' as header

    from tbl
    window m as (order by d)
)
select * from headers

输出:

          d          | header 
---------------------+--------
 2012-04-28 08:00:00 | t
 2012-04-29 08:00:00 | f
 2012-04-30 08:00:00 | f
 2012-05-03 08:00:00 | t
 2012-05-04 08:00:00 | f
(5 rows)

第二步,指定分组:

with headers as
(
    select 
        d,
        lag(d) over m is null or d - lag(d) over m  <> interval '1 day' as header

    from tbl
    window m as (order by d)
)      
,sequence_group as
(
    select d, sum(header::int) over m as group_number 
    from headers
    window m as (order by d)
)
select * from sequence_group

输出:

          d          | group_number 
---------------------+--------------
 2012-04-28 08:00:00 |            1
 2012-04-29 08:00:00 |            1
 2012-04-30 08:00:00 |            1
 2012-05-03 08:00:00 |            2
 2012-05-04 08:00:00 |            2
(5 rows)

第3步,计算最大天数:

with headers as
(
    select 
        d,
        lag(d) over m is null or d - lag(d) over m  <> interval '1 day' as header

    from tbl
    window m as (order by d)
)      
,sequence_group as
(
    select d, sum(header::int) over m as group_number 
    from headers
    window m as (order by d)
)
,consecutive_list as
(
select d, group_number, count(d) over m as consecutive_count
from sequence_group 
window m as (partition by group_number)
)
select * from consecutive_list

输出:

          d          | group_number | consecutive_count 
---------------------+--------------+-----------------
 2012-04-28 08:00:00 |            1 |               3
 2012-04-29 08:00:00 |            1 |               3
 2012-04-30 08:00:00 |            1 |               3
 2012-05-03 08:00:00 |            2 |               2
 2012-05-04 08:00:00 |            2 |               2
(5 rows)

答案 6 :(得分:1)

这是针对最短的MySQL,并使用最小变量(仅一个变量):

select 
   min(d) as starting_date, max(d) as ending_date, 
   count(d) as consecutive_days
from
(
  select 
     sr.d,
     IF(fr.d is null,@group_number := @group_number + 1,@group_number) 
        as group_number
  from tbl sr
  left join tbl fr on sr.d = adddate(fr.d,interval 1 day)
  cross join (select @group_number := 0) as grp
) as x
group by group_number

输出:

STARTING_DATE                  ENDING_DATE                  CONSECUTIVE_DAYS
April, 28 2012 08:00:00-0700   April, 30 2012 08:00:00-0700 3
May, 03 2012 08:00:00-0700     May, 04 2012 08:00:00-0700   2

实时测试:http://www.sqlfiddle.com/#!2/65169/1

答案 7 :(得分:1)

对于 PostgreSQL 8.4或更高版本,使用窗口函数并且没有JOIN ,有一种简洁的方法。
我希望这是迄今为止发布的最快的解决方案

WITH x AS (
    SELECT created_at AS d
         , lag(created_at) OVER (ORDER BY created_at) = (created_at - 1) AS nu
    FROM   visits
    WHERE  uid = 1
    )
   , y AS (
    SELECT d, count(NULLIF(nu, TRUE)) OVER (ORDER BY d) AS seq
    FROM   x
    )
SELECT count(*) AS max_days, min(d) AS seq_from,  max(d) AS seq_to
FROM   y
GROUP  BY seq
ORDER  BY 1 DESC
LIMIT  1;

返回:

max_days | seq_from   | seq_to
---------+------------+-----------
3        | 2012-04-28 | 2012-04-30

假设created_atdateunique

  1. 在CTE x中:我们的用户每天都会访问,检查他是否也在昨天。 要计算“昨天”,只需使用created_at - 1第一行是特殊情况,并在此处生成NULL。

  2. 在CTE中:每天计算“到目前为止没有昨天的天数”(seq)的运行计数。 NULL值不计算,因此count(NULLIF(nu, TRUE))是最简洁和最短的方式,也涵盖了特殊情况。

  3. 最后,每个seq的分组天数并计算天数。在此期间,我添加了序列的第一天和最后一天。 ORDER BY序列的长度,并选择最长的序列。

答案 8 :(得分:1)

这个问题已有几个答案。但是SQL语句看起来都太复杂了。这可以通过基本SQL,枚举行的方法和一些日期算法来完成。

关键的观察结果是,如果你有一堆天并且有一个平行的整数序列,那么差异就是天数在序列中的一个恒定日期。

以下查询使用此观察来回答原始问题:

select uid, min(d) as startdate, count(*) as numdaysinseq
from 
(
   select uid, d, adddate(d, interval -offset day) as groupstart
   from 
   (
     select uid, d, row_number() over (partition by uid order by date) as offset
     from 
     (
       SELECT DISTINCT uid, DATE(created_at) AS d
       FROM visits
     ) t
   ) t
) t

唉,mysql没有row_number()功能。但是,有一个变量的解决方法(大多数其他数据库都有这个功能)。

答案 9 :(得分:1)

在看到OP的Vertica数据库查询方法后,我尝试同时运行两个连接:

这些Postgresql和Sql Server查询版本都适用于Vertica

Postgresql版本:

select 
  min(gr.d) as start_date,
  max(gr.d) as end_date,
  date_part('day', max(gr.d) - min(gr.d))+1 as consecutive_days
from 
(
  select 
  cr.d, (row_number() over() - 1) / 2 as pair_number
  from tbl cr   
  left join tbl pr on pr.d = cr.d - interval '1 day'
  left join tbl nr on nr.d = cr.d + interval '1 day'
  where pr.d is null <> nr.d is null
) as gr
group by pair_number
order by start_date

关于pr.d is null <> nr.d is null。这意味着,它是前一行为空或下一行为空,但它们永远不能都为空,所以这基本上删除了非连续日期,因为非连续日期的前一个&amp;下一行是空的(这基本上给了我们所有日期,只是页眉和页脚)。这也称为XOR operation

如果我们只剩下连续日期,我们现在可以通过row_number配对它们:

(row_number() over() - 1) / 2 as pair_number

row_number()从1开始,我们需要用1减去它(我们也可以用1加1),然后我们将它除以2;这使得配对日期彼此相邻

实时测试:http://www.sqlfiddle.com/#!1/fc440/7


这是Sql Server版本:

select 
  min(gr.d) as start_date,
  max(gr.d) as end_date,
  datediff(day, min(gr.d),max(gr.d)) +1 as consecutive_days
from 
(
  select 
     cr.d, (row_number() over(order by cr.d) - 1) / 2 as pair_number
  from tbl cr   
  left join tbl pr on pr.d = dateadd(day,-1,cr.d)
  left join tbl nr on nr.d = dateadd(day,+1,cr.d)
  where         
       case when pr.d is null then 1 else 0 end
    <> case when nr.d is null then 1 else 0 end
) as gr
group by pair_number
order by start_date

与上述逻辑相同,除了日期函数的人为差异。并且sql Server在其ORDER BY上需要OVER子句,而Postgresql的OVER可以留空。

Sql Server没有第一类布尔值,这就是我们无法直接比较布尔值的原因:

pr.d is null <> nr.d is null

我们必须在Sql Server中执行此操作:

   case when pr.d is null then 1 else 0 end
<> case when nr.d is null then 1 else 0 end

实时测试:http://www.sqlfiddle.com/#!3/65df2/17