【问题标题】:MySQL: group by consecutive days and count groupsMySQL:按连续天分组并计数组
【发布时间】:2011-08-17 13:32:16
【问题描述】:

我有一个数据库表,其中包含每个用户在城市中的签到。我需要知道用户在一个城市停留了多少天,然后,用户对一个城市进行了多少次访问(一次访问包括在一个城市的连续停留天数)。

所以,考虑我有下表(简化,仅包含 DATETIMEs - 相同的用户和城市):

      datetime
-------------------
2011-06-30 12:11:46
2011-07-01 13:16:34
2011-07-01 15:22:45
2011-07-01 22:35:00
2011-07-02 13:45:12
2011-08-01 00:11:45
2011-08-05 17:14:34
2011-08-05 18:11:46
2011-08-06 20:22:12

此用户已到该城市的天数为 630.0601.0702.0701.0805.0806.08)。

我想用SELECT COUNT(id) FROM table GROUP BY DATE(datetime)来做这个

那么,对于该用户对该城市的访问次数,查询应该返回 3 (30.06-02.07, 01.08 , 05.08-06.08).

问题是我不知道如何构建这个查询。

任何帮助将不胜感激!

【问题讨论】:

    标签: mysql datetime gaps-and-islands


    【解决方案1】:

    您可以通过查找前一天没有签到的签到来找到每次访问的第一天。

    select count(distinct date(start_of_visit.datetime))
    from checkin start_of_visit
    left join checkin previous_day
        on start_of_visit.user = previous_day.user
        and start_of_visit.city = previous_day.city
        and date(start_of_visit.datetime) - interval 1 day = date(previous_day.datetime)
    where previous_day.id is null
    

    这个查询有几个重要的部分。

    首先,每次签到都与前一天的任何签到相连。但由于它是一个外连接,如果前一天没有签入,连接的右侧将有NULL 结果。 WHERE 过滤发生在连接之后,因此它只保留那些来自左侧的签入,而右侧没有签入。 LEFT OUTER JOIN/WHERE IS NULL 非常便于查找没有的地方。

    然后它计算不同签入日期,以确保如果用户在访问的第一天多次签入,它不会重复计算。 (当我发现可能的错误时,我实际上在编辑时添加了该部分。)

    编辑:我刚刚重新阅读了您针对第一个问题提出的查询。您的查询将获得给定日期的签到次数,而不是日期计数。我想你想要这样的东西:

    select count(distinct date(datetime))
    from checkin
    where user='some user' and city='some city'
    

    【讨论】:

    • 关于第一方面...我似乎无法完全理解您的建议...是否可以提供更多详细信息?谢谢!关于第二个,我的查询是正确的,前提是您不计算用户和城市,如我的问题中所述。
    • 抱歉,我假设“用户在城市中停留的天数”的结果应该类似于 (user_id, count_of_days)。
    • 感谢您提供详细信息。经过几次调整以适应我的实际数据库表,您的查询就像一个魅力。再次感谢您!
    【解决方案2】:

    尝试将此代码应用于您的任务 -

    CREATE TABLE visits(
      user_id INT(11) NOT NULL,
      dt DATETIME DEFAULT NULL
    );
    
    INSERT INTO visits VALUES 
      (1, '2011-06-30 12:11:46'),
      (1, '2011-07-01 13:16:34'),
      (1, '2011-07-01 15:22:45'),
      (1, '2011-07-01 22:35:00'),
      (1, '2011-07-02 13:45:12'),
      (1, '2011-08-01 00:11:45'),
      (1, '2011-08-05 17:14:34'),
      (1, '2011-08-05 18:11:46'),
      (1, '2011-08-06 20:22:12'),
      (2, '2011-08-30 16:13:34'),
      (2, '2011-08-31 16:13:41');
    
    
    SET @i = 0;
    SET @last_dt = NULL;
    SET @last_user = NULL;
    
    SELECT v.user_id,
      COUNT(DISTINCT(DATE(dt))) number_of_days,
      MAX(days) number_of_visits
    FROM
      (SELECT user_id, dt
            @i := IF(@last_user IS NULL OR @last_user <> user_id, 1, IF(@last_dt IS NULL OR (DATE(dt) - INTERVAL 1 DAY) > DATE(@last_dt), @i + 1, @i)) AS days,
            @last_dt := DATE(dt),
            @last_user := user_id
       FROM
         visits
       ORDER BY
         user_id, dt
      ) v
    GROUP BY
      v.user_id;
    
    ----------------
    Output:
    
    +---------+----------------+------------------+
    | user_id | number_of_days | number_of_visits |
    +---------+----------------+------------------+
    |       1 |              6 |                3 |
    |       2 |              2 |                1 |
    +---------+----------------+------------------+
    

    说明:

    要了解它是如何工作的,让我们检查一下子查询,在这里。

    SET @i = 0;
    SET @last_dt = NULL;
    SET @last_user = NULL;
    
    
    SELECT user_id, dt,
            @i := IF(@last_user IS NULL OR @last_user <> user_id, 1, IF(@last_dt IS NULL OR (DATE(dt) - INTERVAL 1 DAY) > DATE(@last_dt), @i + 1, @i)) AS 
    
    days,
            @last_dt := DATE(dt) lt,
            @last_user := user_id lu
    FROM
      visits
    ORDER BY
      user_id, dt;
    

    如您所见,该查询返回所有行并对访问次数进行排名。这是基于变量的已知排名方法,请注意,行是按用户和日期字段排序的。此查询计算用户访问,并输出下一个数据集,其中days 列提供访问次数的排名 -

    +---------+---------------------+------+------------+----+
    | user_id | dt                  | days | lt         | lu |
    +---------+---------------------+------+------------+----+
    |       1 | 2011-06-30 12:11:46 |    1 | 2011-06-30 |  1 |
    |       1 | 2011-07-01 13:16:34 |    1 | 2011-07-01 |  1 |
    |       1 | 2011-07-01 15:22:45 |    1 | 2011-07-01 |  1 |
    |       1 | 2011-07-01 22:35:00 |    1 | 2011-07-01 |  1 |
    |       1 | 2011-07-02 13:45:12 |    1 | 2011-07-02 |  1 |
    |       1 | 2011-08-01 00:11:45 |    2 | 2011-08-01 |  1 |
    |       1 | 2011-08-05 17:14:34 |    3 | 2011-08-05 |  1 |
    |       1 | 2011-08-05 18:11:46 |    3 | 2011-08-05 |  1 |
    |       1 | 2011-08-06 20:22:12 |    3 | 2011-08-06 |  1 |
    |       2 | 2011-08-30 16:13:34 |    1 | 2011-08-30 |  2 |
    |       2 | 2011-08-31 16:13:41 |    1 | 2011-08-31 |  2 |
    +---------+---------------------+------+------------+----+
    

    然后我们按用户对这个数据集进行分组并使用聚合函数: 'COUNT(DISTINCT(DATE(dt)))' - 计算天数 'MAX(days)' - 访问次数,它是我们子查询中days 字段的最大值。

    仅此而已;)

    【讨论】:

    • 这看起来很复杂......你能提供更多关于你的代码的细节吗?将不胜感激!
    • 感谢您提供详细信息。很遗憾我不能给出两个答案。但是,我选择了另一个答案,因为查询更简单一些。真的很抱歉,再次感谢您的回答!
    【解决方案3】:

    作为 Devart 提供的数据样本,内部的“PreQuery”与 sql 变量一起工作。通过默认 @LUser 为 -1(可能不存在的用户 ID),IF() 测试检查最后一个用户和当前用户之间的任何差异。一旦有新用户,它的值就为 1... 此外,如果最后一个日期距离新签入日期超过 1 天,则它的值是 1。然后,后续列将重置@LUser 和 @LDate 为刚刚针对下一个周期测试的传入记录的值。然后,外部查询只是将它们相加并计算它们,以获得每个 Devar 数据集的最终正确结果

    User ID    Distinct Visits   Total Days
    1           3                 9
    2           1                 2
    
    select PreQuery.User_ID,
           sum( PreQuery.NextVisit ) as DistinctVisits,
           count(*) as TotalDays
       from
          (  select v.user_id,
                   if( @LUser <> v.User_ID OR @LDate < ( date( v.dt ) - Interval 1 day ), 1, 0 ) as NextVisit,
                   @LUser := v.user_id,
                   @LDate := date( v.dt )
                from 
                   Visits v,
                   ( select @LUser := -1, @LDate := date(now()) ) AtVars 
                order by
                   v.user_id,
                   v.dt  ) PreQuery
        group by 
           PreQuery.User_ID
    

    【讨论】:

    • 感谢您的回答和澄清!
    • 很高兴为您提供帮助...它是否得到了您需要的确切解决方案(因此也包含了用户 ID 信息以提供帮助)。
    【解决方案4】:

    对于第一个子任务:

    select count(*) 
    from (
    select TO_DAYS(p.d)
    from p
    group by TO_DAYS(p.d)
    ) t
    

    【讨论】:

      【解决方案5】:

      我认为您应该考虑更改数据库结构。您可以将表访问和 visit_id 添加到您的签到表中。每次您想注册新的签到时,您都会检查一天前是否有任何签到。如果是,那么您从昨天的签到中添加一个带有 visit_id 的新签到。如果没有,那么您添加新的访问访问并使用新的 visit_id 进行新的签到。

      然后你可以在一个查询中获取数据,如下所示: SELECT COUNT(id) AS number_of_days, COUNT(DISTINCT visit_id) number_of_visits FROM checkin GROUP BY user, city

      这不是非常理想,但仍然比使用当前结构做任何事情要好,而且它会起作用。此外,如果结果可以是单独的查询,它会运行得非常快。

      但当然缺点是您需要更改数据库结构、编写更多脚本并将当前数据转换为新结构(即您需要将 visit_id 添加到当前数据)。

      【讨论】:

      • 感谢您的回答,但我想坚持我目前的数据库结构,至少现在是这样。另外我在插入的时候还需要做一些进一步的操作,因为一天可能有多次签到,所以“检查一天是否有签到”并不是那么简单。这种数据操作也可以在 PHP 中使用提供的数据库结构进行,但我正在寻找一个查询来完成这项工作,因为它更干净方便。
      猜你喜欢
      • 2020-05-18
      • 1970-01-01
      • 2012-05-10
      • 2021-11-07
      • 2021-04-27
      • 2018-11-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多