Наиболее активное время суток в зависимости от времени начала и окончания

Я регистрирую статистику геймеров в своем сообществе. Для обоих онлайновых и внутриигровых состояний я регистрируюсь, когда они "начинают" и когда они "заканчиваются". Чтобы показать самый активный день и час дня, я хотел бы использовать инструкцию SQL, которая измеряет наиболее активные моменты, основанные на значениях "начало" и "конец" даты и времени.

Глядя на SQL - выберите наиболее "активное" время из db. Я вижу сходства, но мне нужно также включить моменты между началом и временем окончания.

Возможно, самый простой способ - написать cron, который выполняет вычисления, но я надеюсь, что этот вопрос может научить меня, как решить эту проблему в SQL вместо этого.

Я искал инструкцию SQL, которая позволяет создать период времени datetime и использовать это, чтобы вычитать один час и несколько дней. Но безрезультатно.

--- обновить

Как я уже думаю об этом, мне интересно, может ли быть разумным запускать 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. Столбцы также являются, например, целями, независимо от того, что вы считаете легким для использования в объяснении, которое можно идентифицировать, поскольку начало и конец в порядке со мной )

Дополнительная информация; PHP 5.5+, PDO, MySQL 5+ Таблица layout для ingame будет: gameactivity: activityid, userid, gameid, запущен, закончился

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;

Ответ 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). Обратите внимание, что это не означает, что 02:00 12/4 был самым активным часом, как вы можете видеть в этом примере. Если это не то, что вы хотите, проясните конкретные примеры ввода и желаемого результата.

Сделаем пару предположений:

  • Запись активности может начинаться с одной даты и заканчиваться на следующей. Например: онлайн 2013-12-02 23:35, offline 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;

Ответ 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 человек в режиме онлайн!

Ответ 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 |
+------+----------+

Ответ 4

Этот запрос предназначен для оракула, но вы можете получить от него идею:

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

Fiddle: 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

Fiddle: http://sqlfiddle.com/#!2/de01c/1

Ответ 5

Я сам задумывался над этим вопросом и основывался на всех ответах, которые, как мне кажется, очевидно заключаются в следующем:

В целом, вероятно, легко реализовать какую-то отдельную таблицу, в которой есть часы дня, и делать внутренние выделения из этой отдельной таблицы. В других примерах без отдельной таблицы есть много подборов, даже с четырьмя уровнями, что заставляет меня поверить, что они, вероятно, не будут масштабироваться. Решения Cron пришли мне на ум, но вопрос был задан - из любопытства - сосредоточиться на SQL-запросах, а не на других решениях.

В моем собственном случае и полностью вне сферы моего собственного вопроса я считаю, что лучшим решением является создание отдельной таблицы с двумя полями (час [Ymd H], onlinecount, playcount), который подсчитывает количество пользователей в Интернете по адресу определенный час и люди, играющие в определенный час. Когда игрок перестает играть или выходит в автономный режим, мы обновляем счет (+1) в зависимости от времени начала и окончания. Таким образом, я легко могу вывести таблицы и графики из этой отдельной таблицы.

Пожалуйста, дайте мне знать, приходите ли вы к такому же выводу. Я благодарю @lolo, @rsanchez и @abasterfield. Хотел бы я разбить щедрость:)

Ответ 6

sqlFiddle, этот запрос даст вам период с наибольшим количеством пользователей, период может быть в любое время, он просто дает вам время начала и времени окончания, которое имеет наибольшее значение 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:[email protected] + 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

просто удалите LIMIT 1, если вы хотите видеть userCount в течение других часов.

Ответ 7

Самое простое решение - запустить cron в начале каждого часа, у которого есть время начала, но нет конечного времени (нулевое конечное время? если вы reset при входе в систему) и регистрируете этот счет. Это даст вам количество зарегистрированных в настоящее время в каждый час без необходимости делать смешные изменения схемы или дикие запросы.

Теперь, когда вы проверите следующий час, и они вышли из системы, они выпадут из ваших результатов. Этот запрос будет работать, если вы закончите время reset при входе в систему.

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. Если вы обнаружите, что у вас новый максимальный счетчик, вы делаете обновление, если запись не существует в течение дня, вставьте в журнал. В противном случае нет, вы не били пик_хауром ранее в тот же день, не делайте обновления. Это означает, что каждый день даст вам только 1 строку в вашем столе. Тогда вам не нужно делать какие-либо агрегации, вам все равно, что вы увидите дату и час в течение недели или месяца или что-то еще.