【问题标题】:SQL issue - calculate max days sequenceSQL 问题 - 计算最大天数序列
【发布时间】:2012-05-13 22:48:50
【问题描述】:

有一张表有访问数据:

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 文档中找到合适的函数,但没有成功。我错过了什么吗?


统一更新: 谢谢你们的回答!实际上,我正在使用 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

【问题讨论】:

  • 用两种不同的 sql 实现标记? MySQLPostGreSQL 都有不同的能力...
  • +1 好方法。在WHERE t2.d is null GROUP BY t1.d,你可以删除GROUP BY t1.d 但是:-) 你的WHERE t2.d is null 已经返回唯一的t1.d 反正
  • 谢谢你的建议,迈克尔!

标签: sql vertica window-functions


【解决方案1】:

在 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

【讨论】:

  • 你可以使用简单的 + 代替 DATE_ADD()
【解决方案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】:

    以下应该是 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
    

    【讨论】:

      【解决方案4】:

      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)
      

      第三步,计算最大天数:

      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)
      

      【讨论】:

        【解决方案5】:

        另一种最短的方法是进行自联接:

        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,因为窗口不支持嵌套窗口)。所以在这个查询中,我们使用混合方法,通过连接模拟 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 :非常好。不确定我是否有关于运行总类型的答案(生成组 ID)。哦,如果有一个客户端具有这样一个最新的 sql 实现,那该多好。有兴趣将其与基于 ROW_NUMBER() 的分组机制(避免连接)进行比较。
        • 我认为有一种更短(更快)的窗口函数方式,并且没有 JOIN 比您链接到的方式。我发布了一个答案。
        • @Dems 不错,拥有最新的 sql 实现真是太好了。更奇怪的是开源/免费数据库胜过付费数据库
        • @ErwinBrandstetter 我现在缩短了我的查询(并且可以更快,指标在于查询分析(检查执行计划),或者在生产数据库中测试它) .我只是在此处的答案中缩短代码。在这里查看我的编辑
        【解决方案6】:

        这适用于 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】:

          对于 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 y 中:计算每天的“到目前为止没有昨天的天数”(seq) 的运行计数。 NULL 值不计算在内,因此count(NULLIF(nu, TRUE)) 是最快和最短的方法,也涵盖了特殊情况。

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

          【讨论】:

            【解决方案8】:

            这个问题已经有好几个答案了。然而,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】:

              在看到 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 &lt;&gt; nr.d is null。这意味着,前一行为空或下一行为空,但它们永远不能同时为空,所以这基本上删除了非连续日期,因为非连续日期的上一行和下一行为空(这基本上给我们所有只是页眉和页脚的日期)。这也称为XOR operation

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

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

              row_number() 以 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 需要在其OVER 上添加一个ORDER BY 子句,而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

              【讨论】:

                【解决方案10】:

                所有答案都是非常好的答案,但我认为我应该通过展示另一种利用 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' 访问(即重复),您可能需要解决这个问题。

                【讨论】:

                  猜你喜欢
                  • 2012-02-15
                  • 1970-01-01
                  • 2021-04-21
                  • 1970-01-01
                  • 1970-01-01
                  • 2019-02-08
                  • 2018-11-03
                  • 2021-06-22
                  • 2017-06-24
                  相关资源
                  最近更新 更多