基于开始和结束时间的最活跃时间

时间:2013-12-11 09:44:12

标签: mysql

我正在记录社区中游戏玩家的统计数据。对于他们在线和游戏中的状态,我在他们“开始”和“结束”时注册。为了显示当天最活跃的日期和小时,我想使用一个SQL语句,根据“开始”和“结束”日期时间值来衡量最活跃的时刻。

SQL - select most 'active' time from db我可以看到相似之处,但我还需要包括开始和结束时间之间的时刻。

也许最简单的方法是编写一个执行计算的cron,但我希望这个问题可以教会我如何在SQL中解决这个问题。

我一直在搜索一条允许创建日期时间段并使用它来减去单个小时和天数的SQL语句。但无济于事。

---更新

由于我正在考虑这个问题,我想知道根据一天中的每个小时(最活跃的小时)运行24个查询以及最活跃的一天的几个查询是否明智。但这似乎是对性能的浪费。但是这个解决方案可能会使查询成为可能:

SELECT COUNT(`userID`), DATE_FORMAT("%H",started) AS starthour, 
       DATE_FORMAT("%H",ended) AS endhour 
       FROM gameactivity 
       WHERE starthour >= $hour 
             AND endhour <= $hour GROUP BY `userID`

(为了示例目的,添加了$小时,当然我正在使用PDO。列也只是用于示例目的,无论您认为哪些都很容易用于解释可以识别为开始和结束对我来说是好的)

其他信息; PHP 5.5 +,PDO,MySQL 5+ ingame的表格布局是:gameactivity:activityid,userid,gameid,started,ended

DDL:

CREATE TABLE IF NOT EXISTS `steamonlineactivity` (
  `activityID` int(13) NOT NULL AUTO_INCREMENT,
  `userID` varchar(255) NOT NULL,
  `online` datetime DEFAULT NULL,
  `offline` datetime DEFAULT NULL,
  PRIMARY KEY (`activityID`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;

7 个答案:

答案 0 :(得分:4)

如果我正确理解了您的要求,那么此图表代表用户活动:

       Day 
       12/1 12/2 12/3 12/4 ...
Hour 0  xx    x    x   xx
     1   x   xx        xx
     2 xxx    x    x   xx
     3   x              x
     4        x         x
     5   x              x
     6                  x
   ...

您想知道02:00是平均活动最高的一天的时间(7 x行),12/4是最活跃的一天(10列{{1 }})。请注意,这并不意味着12/4的02:00是有史以来最活跃的小时,正如您在示例中所看到的那样。如果这不是你想要的,请用输入和期望结果的具体例子来澄清。

我们做了几个假设:

  • 活动记录可以在一个日期开始,在下一个日期结束。例如:在线x,离线2013-12-02 23:35
  • 没有活动记录的持续时间超过23小时,或者此类记录的数量可以忽略不计。

我们需要定义'活动'的含义。我选择了在每种情况下更容易计算的标准。如果需要,两者都可以更准确,代价是更复杂的查询。

  • 一天中最活跃的时间将是更多活动记录重叠的小时。请注意,如果用户在一小时内启动和停止多次,则会被计算多次。
  • 最活跃的一天将是在一天中的任何时间都有更多独特用户的活动日。

对于一天中最活跃的时间,我们将使用一个小的辅助桌来保持24小时。它也可以使用其他答案中描述的技术动态生成和连接。

2013-12-03 00:13

然后以下查询给出了所需的结果:

CREATE TABLE hour ( hour tinyint not null, primary key(hour) );
INSERT hour (hour)
VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)
     , (11), (12), (13), (14), (15), (16), (17), (18), (19), (20)
     , (21), (22), (23);

答案 1 :(得分:1)

您需要一个序列来获取没有活动的小时值(例如,没有人开始或完成的小时数,但是有人在线时已经开始但在那段时间没有完成)。不幸的是,没有很好的方法可以在MySQL中创建序列,因此您必须手动创建序列;

CREATE TABLE `hour_sequence` (
  `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `hour` datetime NOT NULL,
  KEY (`hour`),
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

# this is not great
INSERT INTO `hour_sequence` (`hour`) VALUES
("2013-12-01 00:00:00"),
("2013-12-01 01:00:00"),
("2013-12-01 02:00:00"),
("2013-12-01 03:00:00"),
("2013-12-01 04:00:00"),
("2013-12-01 05:00:00"),
("2013-12-01 06:00:00"),
("2013-12-01 07:00:00"),
("2013-12-01 08:00:00"),
("2013-12-01 09:00:00"),
("2013-12-01 10:00:00"),
("2013-12-01 11:00:00"),
("2013-12-01 12:00:00");

现在创建一些测试数据

CREATE TABLE `log_table` (
  `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `userID` bigint(20) unsigned NOT NULL,
  `started` datetime NOT NULL,
  `finished` datetime NOT NULL,
  KEY (`started`),
  KEY (`finished`),
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET latin1;

INSERT INTO `log_table` (`userID`,`started`,`finished`) VALUES
(1, "2013-12-01 00:00:12", "2013-12-01 02:25:00"),
(2, "2013-12-01 07:25:00", "2013-12-01 08:23:00"),
(1, "2013-12-01 04:25:00", "2013-12-01 07:23:00");

现在查询 - 我们每小时都会记录一个小时(每小时开始一次会议的累积/运行总数/积分等)

  SELECT
   HS.hour as period_starting,
   COUNT(LT.userID) AS starts
  FROM `hour_sequence` HS
   LEFT JOIN `log_table` LT ON HS.hour > LT.started
  GROUP BY
   HS.hour

还有多少人同样离线

  SELECT
   HS.hour as period_starting,
   COUNT(LT.userID) AS finishes
  FROM `hour_sequence` HS
   LEFT JOIN `log_table` LT ON HS.hour > LT.finished
  GROUP BY
   HS.hour

通过从那个时间点上线的人的积累中减去在某个时间点离线的人的积累,我们得到那个时候在线的人数及时(假设数据开始时在线人数为零)。

SELECT
 starts.period_starting,
 starts.starts as users_started,
 finishes.finishes as users_finished,
 starts.starts - finishes.finishes as users_online

FROM
 (
  SELECT
   HS.hour as period_starting,
   COUNT(LT.userID) AS starts
  FROM `hour_sequence` HS
   LEFT JOIN `log_table` LT ON HS.hour > LT.started
  GROUP BY
   HS.hour
 ) starts

 LEFT JOIN (
  SELECT
   HS.hour as period_starting,
   COUNT(LT.userID) AS finishes
  FROM `hour_sequence` HS
   LEFT JOIN `log_table` LT ON HS.hour > LT.finished
  GROUP BY
   HS.hour
 ) finishes ON starts.period_starting = finishes.period_starting;

现在有几点需要注意。首先,随着时间的推移,您将需要一个进程来保持序列表中的每小时时间戳。此外,由于连接过多,累加器无法很好地扩展大量日志数据 - 在开始和完成子查询以及序列表时,通过时间戳约束对日志表的访问是明智的。

  SELECT
   HS.hour as period_starting,
   COUNT(LT.userID) AS finishes
  FROM `hour_sequence` HS
   LEFT JOIN `log_table` LT ON HS.hour > LT.finished
  WHERE
   LT.finished BETWEEN ? AND ? AND HS.hour BETWEEN ? AND ?
  GROUP BY
   HS.hour

如果您开始将log_table数据约束到特定的时间范围,请记住,如果在您开始查看日志数据时,已经有人在线,那么您将遇到偏移问题。如果您开始查看日志数据时有1000人在线,那么您从查询中将它们全部从服务器上扔掉,看起来我们从0人上线到-1000人在线!

答案 2 :(得分:1)

@rsanchez有一个惊人的答案,但是当处理在同一小时开始和结束的会话时间(短会话)时,对于大多数活动时间的查询都有一种奇怪的行为。该查询似乎计算它们持续24小时。

通过反复试验,我将该部分的查询更正为

SELECT hour, count(*) AS activity
FROM steamonlineactivity, hour
WHERE ( hour >= HOUR(online) AND hour <= HOUR(offline)
  OR HOUR(online) > HOUR(offline) AND HOUR(online) <= hour
  OR HOUR(offline) >= hour AND HOUR(offline) < HOUR(online) )
GROUP BY hour
ORDER BY activity DESC;

所以使用以下结构:

CREATE TABLE hour ( hour tinyint not null, primary key(hour) );
INSERT hour (hour)
VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)
 , (11), (12), (13), (14), (15), (16), (17), (18), (19), (20)
 , (21), (22), (23);

CREATE TABLE `steamonlineactivity` (
  `activityID` int(13) NOT NULL AUTO_INCREMENT,
  `userID` varchar(255) NOT NULL,
  `online` datetime DEFAULT NULL,
  `offline` datetime DEFAULT NULL,
  PRIMARY KEY (`activityID`)
);

INSERT INTO `steamonlineactivity` (`activityID`, `userID`, `online`, `offline`) VALUES
(1, '1',    '2014-01-01 16:01:00',  '2014-01-01 19:01:00'),
(2, '2',    '2014-01-02 16:01:00',  '2014-01-02 19:01:00'),
(3, '3',    '2014-01-01 22:01:00',  '2014-01-02 02:01:00'),
(4, '4',    '2014-01-01 16:01:00',  '2014-01-01 16:05:00');

获取最活跃时间输出的最高查询:

+------+----------+
| hour | activity |
+------+----------+
|   16 |        3 |
|   17 |        2 |
|   18 |        2 |
|   19 |        2 |
|   22 |        1 |
|   23 |        1 |
|    0 |        1 |
|    1 |        1 |
|    2 |        1 |
+------+----------+

而不是给出以下错误结果的原始查询:

+------+----------+
| hour | activity |
+------+----------+
|   16 |        3 |
|   17 |        3 |
|   18 |        3 |
|   19 |        3 |
|    0 |        2 |
|    1 |        2 |
|    2 |        2 |
|   22 |        2 |
|   23 |        2 |
|   11 |        1 |
|   12 |        1 |
|   13 |        1 |
|   14 |        1 |
|   15 |        1 |
|    3 |        1 |
|    4 |        1 |
|   20 |        1 |
|    5 |        1 |
|   21 |        1 |
|    6 |        1 |
|    7 |        1 |
|    8 |        1 |
|    9 |        1 |
|   10 |        1 |
+------+----------+

答案 3 :(得分:0)

此查询适用于oracle,但您可以从中获知:

SELECT
    H, M, 
    COUNT(BEGIN)
FROM
    -- temporary table that should return numbers from 0 to 1439
    -- each number represents minute of the day, for example 0 represents 0:00, 100 represents 1:40, etc.
    -- in oracle you can use CONNECT BY clause which is designated to do recursive queries
    (SELECT LEVEL - 1 DAYMIN, FLOOR((LEVEL - 1) / 60) H, MOD((LEVEL - 1), 60) M FROM dual CONNECT BY LEVEL <= 1440) T LEFT JOIN

    -- join stats to each row from T by converting discarding date and converting time to minute of a day
    STATS S ON 60 * TO_NUMBER(TO_CHAR(S.BEGIN, 'HH24')) + TO_NUMBER(TO_CHAR(S.BEGIN, 'MI')) <= T.DAYMIN AND
               60 * TO_NUMBER(TO_CHAR(S.END, 'HH24'))   + TO_NUMBER(TO_CHAR(S.END, 'MI'))   >  T.DAYMIN

GROUP BY H, M
HAVING COUNT(BEGIN) > 0
ORDER BY H, M

GROUP BY H, M
HAVING COUNT(BEGIN) > 0
ORDER BY H, M

小提琴:http://sqlfiddle.com/#!4/e5e31/9

我们的想法是为时间点设置一个临时表或视图,并保持连接。在我的例子中,每天每分钟都有一行。在mysql中,您可以使用变量即时创建此类视图。

MySQL版本:

SELECT
    FLOOR(T.DAYMIN / 60), -- hour
    MOD(T.DAYMIN, 60), -- minute
    -- T.DAYMIN, -- minute of the day
    COUNT(S.BEGIN) -- count not null stats
FROM
    -- temporary table that should return numbers from 0 to 1439
    -- each number represents minute of the day, for example 0 represents 0:00, 100 represents 1:40, etc.
    -- in mysql you must have some table which has at least 1440 rows; 
    -- I use (INFORMATION_SCHEMA.COLLATIONSxINFORMATION_SCHEMA.COLLATIONS) for that purpose - it should be
    -- in every database
    (
        SELECT 
            @counter := @counter + 1 AS DAYMIN
        FROM
            INFORMATION_SCHEMA.COLLATIONS A CROSS JOIN
            INFORMATION_SCHEMA.COLLATIONS B CROSS JOIN
            (SELECT @counter := -1) C
        LIMIT 1440
    ) T LEFT JOIN

    -- join stats to each row from T by converting discarding date and converting time to minute of a day
    STATS S ON (
        (60 * DATE_FORMAT(S.BEGIN, '%H')) + (1 * DATE_FORMAT(S.BEGIN, '%i')) <= T.DAYMIN AND
        (60 * DATE_FORMAT(S.END, '%H'))   + (1 * DATE_FORMAT(S.END, '%i'))   >  T.DAYMIN
    )

GROUP BY T.DAYMIN
HAVING COUNT(S.BEGIN) > 0 -- filter empty counters
ORDER BY T.DAYMIN

小提琴:http://sqlfiddle.com/#!2/de01c/1

答案 4 :(得分:0)

我自己一直在思考这个问题并根据每个人的答案,我认为以下结论很明显;

通常,可能很容易实现某种具有一天中的小时数的单独表,并从该单独的表中进行内部选择。没有单独表的其他示例有许多子选择,即使有四个层,这使我相信它们可能无法扩展。我也想到了Cron解决方案,但问题是出于好奇心 - 专注于SQL查询而不是其他解决方案。

就我自己的情况而言,完全超出了我自己的问题范围,我认为最好的解决方案是创建一个单独的表,其中包含两个字段(小时[Ymd H],onlinecount,playcount),用于统计在线人数一个小时,人们在某个小时玩耍。当玩家停止播放或离线时,我们会根据开始和结束时间更新计数(+1)。因此,我可以从这个单独的表中轻松推断出表格和图表。

请告诉我你是否得出同样的结论。感谢@lolo,@ rsanchez和@abasterfield。我希望我能分开赏金:)

答案 5 :(得分:-1)

sqlFiddle,此查询将为您提供具有最多userCount的时段,该时段可以在任何时间之间,它只是为您提供具有最多userCount的开始时间和结束时间

SELECT StartTime,EndTime,COUNT(*)as UserCount FROM
(
   SELECT T3.StartTime,T3.EndTime,GA.Started,GA.Ended FROM
       (SELECT starttime,(SELECT MIN(endtime) FROM
                         (SELECT DISTINCT started as endtime FROM gameactivity WHERE started BETWEEN  '1970-01-01 00:00:00' AND '1970-01-01 23:59:59'
                          UNION
                          SELECT DISTINCT ended as endtime  FROM gameactivity WHERE ended BETWEEN '1970-01-01 00:00:00' AND '1970-01-01 23:59:59'
                         )T1
                      WHERE T1.endtime > T2.starttime
                     )as endtime
        FROM
        (SELECT DISTINCT started as starttime FROM gameactivity WHERE started BETWEEN '1970-01-01 00:00:00' AND '1970-01-01 23:59:59'
         UNION
         SELECT DISTINCT ended as starttime  FROM gameactivity WHERE ended BETWEEN '1970-01-01 00:00:00' AND '1970-01-01 23:59:59'
        )T2
    )T3,
    GameActivity GA
    WHERE T3.StartTime BETWEEN GA.Started AND GA.Ended
    AND   T3.EndTime BETWEEN GA.Started AND GA.Ended
)FinalTable
GROUP BY StartTime,EndTime
ORDER BY UserCount DESC
LIMIT 1

只需将“1970-01-01”发生的日期更改为您尝试从中获取数据的日期。

查询的作用是选择内部查询中的所有时间,然后从中创建间隔,然后与GameActivity连接并计算这些间隔内用户的出现次数,并返回具有最多userCount(最活动)的时间间隔。 / p>

这是一个sqlFiddle,少了一层

SELECT StartTime,EndTime,COUNT(*)as UserCount FROM
(
SELECT T3.StartTime,T3.EndTime,GA.Started,GA.Ended FROM
(SELECT DISTINCT started as starttime,(SELECT MIN(ended)as endtime FROM
                   gameactivity T1 WHERE ended BETWEEN '1970-01-01 00:00:00' AND '1970-01-01 23:59:59'
                   AND T1.ended > T2.started
                  )as endtime
FROM
 gameactivity T2
 WHERE started BETWEEN '1970-01-01 00:00:00' AND '1970-01-01 23:59:59'
 )T3,
GameActivity GA
WHERE T3.StartTime BETWEEN GA.Started AND GA.Ended
AND   T3.EndTime BETWEEN GA.Started AND GA.Ended
)FinalTable
GROUP BY StartTime,EndTime
ORDER BY UserCount DESC
LIMIT 1

或根据您在上述问题中的查询,您似乎并不关心日期,但只关注所有日期的小时统计信息,然后下面的查询可能会这样做(您的查询只会查看{{1}的小时}和started并忽略播放时间超过1小时的用户。 以下查询可能会为您sqlFiddle

执行此操作
ended

如果您想在其他时间内查看userCount,也可以删除SELECT COUNT(*) as UserCount, HOURSTABLE.StartHour, HOURSTABLE.EndHour FROM (SELECT @hour as StartHour, @hour:=@hour + 1 as EndHour FROM gameActivity as OrAnyTableWith24RowsOrMore, (SELECT @hour:=0)as InitialValue LIMIT 24) as HOURSTABLE, gameActivity GA WHERE HOUR(GA.started) >= HOURSTABLE.StartHour AND HOUR(GA.ended) <= HOURSTABLE.EndHour GROUP BY HOURSTABLE.StartHour,HOURSTABLE.EndHour ORDER BY UserCount DESC LIMIT 1

答案 6 :(得分:-1)

最简单的解决方案是在每个小时的顶部运行一个cron,其中有一个开始时间但没有结束时间(空结束时间?如果你在登录时重置它)并记录该计数。这将为您提供每小时当前登录的计数,而无需进行时髦的架构更改或疯狂查询。

现在,当您检查下一个小时并且他们已经退出时,他们将失去您的结果。如果您在登录时重置结束时间,则此查询将起作用。

SELECT CONCAT(CURDATE(), ' ', HOUR(NOW()), ' ', COUNT(*)) FROM activity WHERE DATE(start) = CURDATE() AND end IS NULL;

然后,您可以将此内容记录到文件或另一个表中(当然,您可能需要根据日志表调整选择)。例如,您可以拥有一个每天获得一个条目的表,并且只会更新一次。

假设一个日志表,如:

current_date | peak_hour | peak_count

SELECT IF(peak_count< $peak_count, true, false) FROM log where DATE(current_date) = NOW();

其中$ peak_count是来自你的cron的变量。如果您发现有更大的峰值计数,则进行更新,如果当天不存在记录,请插入日志。否则,没有你没有从当天早些时候击败peak_hour,不要做更新。这意味着每天只会在您的表中为您提供一行。然后你不需要进行任何聚合,你可以在一周或一个月或其他任何时间看到日期和小时。