【问题标题】:most active time of day based on start and end time基于开始和结束时间的一天中最活跃的时间
【发布时间】:2017-06-27 02:36:13
【问题描述】:

我正在记录社区中游戏玩家的统计数据。对于他们的在线和游戏状态,我在他们“开始”和“结束”时进行注册。为了显示一天中最活跃的日期和时间,我想使用一个 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`

($hour 是出于示例目的而添加的,当然我使用的是 PDO。列也只是为了示例目的,无论您认为容易使用什么来解释可识别为 start 和 end 的我都可以)

附加信息; 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;

【问题讨论】:

  • 您使用什么 RDBMS?
  • @lolo,添加了标签 MySQL。 Ps,我正在更新帖子以匹配一些额外的研究想法。
  • 能否提供DDL和DML?
  • @gvee 将表格布局添加到附加信息中
  • 请向我们展示所需的输出。您想知道 13:00 通常是最活跃的时间,还是特别是“2013-12-15 13:00”是最活跃的时间?

标签: mysql


【解决方案1】:

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

       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 个x)。请注意,这并不意味着 12/4 的 02:00 是有史以来最活跃的时间,正如您在示例中看到的那样。如果这不是您想要的,请通过具体的输入示例和所需结果进行说明。

我们做了几个假设:

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

我们需要定义“活动”是什么意思。我选择了在每种情况下更容易计算的标准。如果需要,两者都可以变得更准确,但代价是查询更复杂。

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

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

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);

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

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

SELECT date, count(DISTINCT userID) AS activity
  FROM ( 
       SELECT userID, date(online) AS date
         FROM steamonlineactivity
        UNION
       SELECT userID, date(offline) AS date
         FROM steamonlineactivity
   ) AS x
 GROUP BY date
 ORDER BY activity DESC;

【讨论】:

  • 谢谢@rsanchez,这是一个了不起的例子。真的很喜欢阅读它,它似乎也简单有效!
【解决方案2】:

您需要一个序列来获取没有活动的小时的值(例如,没有人开始或完成的小时,但有在线的人已经开始但在那段时间尚未完成)。不幸的是,在 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 人在线!

【讨论】:

  • 非常感谢您的澄清。所以底线是:这是可能的,但不是很可行,也不是性能友好。
  • 可能,但在实践中有点痛苦。您需要手动创建时间序列,并且必须密切关注您拥有多少数据,因为这种连接永远不会扩展到非平凡数据LEFT JOIN log_table LT ON HS.hour &gt; LT.finished
【解决方案3】:

@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 |
+------+----------+

【讨论】:

  • 感谢提出另一个解决方案;我实际上从未对生产案例应用任何答案,所以如果你能明确解释你的方法与桑切斯方法的区别和好处;我可能会将你的作为答案。
  • 实际上没有@rsanzes 有其他非常好的和全面的答案,但在第一个选择语句中只有一个错误=)我对贡献 StackOverflow 还是很陌生,但如果你能够更改第一个在 rsanzes 对我在这里的回答中选择语句,答案是完整的,我们都很高兴 :D 不同之处仅在于在接受的答案中,有人在同一小时开始和结束会话(在同一天) 被错误地计算为 24 小时会话,因此也错误地增加了所有小时的会话计数,而不仅仅是一个小时,
  • 所以简短地说:这实际上不是您问题的完整答案。这个答案中的第一个查询实际上是对 rsanzes 查询的修复,因为那里存在缺陷。我无法评论正确答案,因为我没有足够的声望点:(
【解决方案4】:

这个查询是针对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

【讨论】:

  • 由于LEVEL 和额外的东西,很难看到你到底做了什么。您能否解释一下查询或格式化它以匹配我的表设置(开始、结束、用户 ID、ID)。谢谢,我确实明白你在做什么,但我真的不得不睁大眼睛:P
  • @Luceos 我有一段时间没来了,但我已经制作了我的查询的 mysql 版本。我还添加了一些 cmets
【解决方案5】:

我自己一直在想这个问题,根据大家的回答,我认为很明显可以得出以下结论;

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

在我自己的情况下并且完全超出了我自己的问题的范围,我认为最好的解决方案是创建一个单独的表,其中包含两个字段(小时 [Y-m-d H]、onlinecount、playcount)来计算在线人数一个特定的时间,人们在特定的时间玩耍。当玩家停止游戏或离线时,我们会根据开始和结束时间更新计数 (+1)。因此,我可以很容易地从这个单独的表格中推导出表格和图表。

请告诉我您是否得出相同的结论。感谢@lolo、@rsanchez 和@abasterfield。我希望我能平分赏金:)

【讨论】:

    【解决方案6】:

    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 并计算这些间隔内用户的出现次数,并返回用户计数最多(活动最多)的间隔。

    这是一个少一层的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
    

    或者根据您在上述问题中的查询,您似乎并不关心日期,但只有所有日期的小时统计数据,然后下面的查询可能会这样做(您的查询只查看 started 的小时数和ended 并忽略玩超过 1 小时的用户。 下面的查询可能会为你做sqlFiddle

    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
    

    如果您还想查看其他时间的 userCount,只需删除 LIMIT 1

    【讨论】:

    • 据我所知,你有一个第四层的子选择,这个规模和性能效果一定是怪物!不过,我很重视您对保持在一个查询范围内的贡献;)
    • 检查我的 sqlFiddle 底部少一层。
    • 我在最底部添加了另一个查询,以防万一您只是在寻找一天中最繁忙的时间而不关心日期
    【解决方案7】:

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

    现在,当您检查下一个小时并且他们已注销时,他们会从您的结果中消失。如果您在他们登录时重置结束时间,则此查询将起作用。

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

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

    假设一个日志表如下:

    当前日期 | peak_hour | peak_count

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

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

    【讨论】:

    • 请注意我特别要求使用查询。 cron 似乎是最简单的解决方案,但正如我在问题中所说:“但我希望这个问题可以让我学会如何在 SQL 中解决这个问题”
    • 其实我明白你说的,但你也说过“但这似乎是在浪费性能”,并且作为 cron 运行可能更容易。因此,提出您的问题是因为我“更愿意”将其作为单个查询运行,但会选择最好的方法,并且我正在提供它,所以我不确定为什么我在这里投了反对票。老实说,现在您有一个随机小时表,可以避免利用 mysql 日期函数。不管怎样,很高兴你解决了它。下次更清楚的是,仅接受查询的答案。我会给你一个更有效的:-(
    • 我很抱歉默默无闻。请编辑您的问题,以便我至少可以删除反对票(似乎是删除反对票的唯一方法)。
    • 不得不戳你一个旧帖子,你批准了这个stackoverflow.com/documentation/review/changes/123007作为文档,请在批准文档之前阅读一下!这显然是一个问题,而不是在任何类型的文档中......将此评论标记为过于健谈,之后将其删除!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-31
    • 1970-01-01
    • 2016-02-03
    • 2021-04-09
    • 2021-07-23
    相关资源
    最近更新 更多