включение отсутствующих (нулевых) строк при использовании GROUP BY

#mysql #count #group-by #aggregate-functions

#mysql #количество #группа по #агрегатные функции

Вопрос:

У меня есть приложение, которое получает sms-сообщения. Что я хочу сделать, так это составить статистику с помощью mysql, которая будет подсчитывать сообщения через час. Например, в 7 утра я получил 10 sms-сообщений, в 8 утра я получил 20 и т.д. В моей таблице есть следующие столбцы ID, smsText, smsDate … (другие не важны). Когда я запускаю этот скрипт:

 SELECT HOUR(smsDate), COUNT(ID) FROM SMS_MESSAGES GROUP BY HOUR(smsDate)
 

он показывает, сколько сообщений я получаю каждый час. Проблема в том, что когда я не получаю никакого сообщения, например, в 5 вечера, этот оператор не возвращает строку 17 со счетом 0, и у меня есть такой результат:

 Hour Count
...
15 10
16 5
18 2
...
 

и то, что я хочу получить, это

 Hour Count
...
15 10
16 5
17 0
18 2
...
 

Я искал решение в Интернете, что-то с UNION, но я не понимаю, как реализовать это в моем. Надеюсь, кто-нибудь сможет мне помочь.

Ответ №1:

Вы можете создать таблицу со всеми часами и объединить таблицы:

 CREATE TABLE IF NOT EXISTS `hours` (
  `hour` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `hours` (`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 hours.hour, count( SMS_MESSAGES.ID ) 
FROM hours
LEFT JOIN SMS_MESSAGES ON ( hours.hour = HOUR( SMS_MESSAGES.smsDate ) ) 
GROUP BY 1 
 

Ответ №2:

Поскольку hellocode ответил, что создание новой таблицы, содержащей значения часов, является хорошим подходом, вот еще один способ добиться этого с помощью объединения

 select t.`hour`,count(s.ID) from (
select 0 as `hour`
union
select 1 as `hour`
union
select 2 as `hour`
union
.
.
.
select 23 as `hour`
) t
left join SMS_MESSAGES s on(t.`hour` = hour(s.smsDate))
group by t.`hour`
 

Ответ №3:

Наблюдение: HOUR() просто извлекает час из метки времени. Возможно, вам нужны дата и час в вашем запросе. В этом ответе указаны дата и час.

Вам нужен способ получить виртуальную таблицу, содержащую все часовые временные метки в соответствующем диапазоне. Затем вам нужно присоединить эту таблицу к вашему агрегированному запросу.

Перво-наперво: вот запрос, который получит временные метки в диапазоне.

 SELECT mintime   INTERVAL seq.seq HOUR AS msghour
  FROM (
        SELECT MIN(DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR) AS mintime,
               MAX(DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR) AS maxtime
          FROM SMS_MESSAGES
       ) AS minmax
  JOIN seq_0_to_999999 AS seq ON seq.seq < TIMESTAMPDIFF(HOUR,mintime,maxtime)
 

Что здесь происходит? Три вещи.

Первое: DATE(smsDate) INTERVAL HOUR(smsDate) HOUR преобразует любую произвольную временную метку в временную метку в начале часа. Это позволяет нам извлекать первую и последнюю часовую временную метку в вашей таблице.

Во-вторых, у нас есть подзапрос, который определяет первый и последний час (минимальный и максимальный smsDate), о котором мы заботимся в отчетах.

Во-вторых, у нас есть таблица с именем seq_0_to_999999. Он содержит последовательность кардинальных чисел: целых чисел, начинающихся с нуля. Подробнее об этом чуть позже.

Объединение этих двух таблиц вместе, затем с помощью выражения

 mintime   INTERVAL seq.seq HOUR AS msghour
 

мы можем получить таблицу, которая имеет непрерывный цикл часовых временных меток.

Затем мы присоединяем это к вашему запросу. Вот где это начинает выглядеть более сложным, чем оно есть. Мы делаем это в общих чертах:

  SELECT DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR, COUNT(ID)
   FROM SMS_MESSAGES 
   JOIN ( /*the query above wit the sequence of timestamps*/) AS sq 
     ON DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR = msghour
  GROUP BY DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR
  ORDER BY DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR
 

Собрав все это вместе, это выглядит так:

  SELECT DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR, COUNT(ID)
   FROM SMS_MESSAGES 
   JOIN ( 
        SELECT mintime   INTERVAL seq.seq HOUR AS msghour
          FROM (
                SELECT MIN(DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR) AS mintime,
                       MAX(DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR) AS maxtime
                  FROM SMS_MESSAGES
               ) AS minmax
          JOIN seq_0_to_999999 AS seq ON seq.seq < TIMESTAMPDIFF(HOUR,mintime,maxtime)
       ) AS sq 
     ON DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR = msghour
  GROUP BY DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR
  ORDER BY DATE(smsDate)   INTERVAL HOUR(smsDate) HOUR
 

Это даст вам результирующий набор с меткой времени и количеством для каждого часа в диапазоне.

Наконец, что насчет этой seq_0_to_999999 таблицы последовательностей? Где мы получаем эти целые числа, начинающиеся с нуля? Ответ таков: мы должны договориться об этом; эти числа не встроены в MySQL (они есть в MariaDB v10 ).

Простой способ — создать таблицу с множеством целых чисел в ней. Однако это займет место в памяти, поэтому мы это пропустим.

Другой способ — создать короткую таблицу с целыми числами от 0 до 9 в ней, например:

 DROP TABLE IF EXISTS seq_0_to_9;
CREATE TABLE seq_0_to_9 AS
   SELECT 0 AS seq UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
    UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9;
 

Затем мы можем создать представление, которое объединяет эту таблицу с самим собой, чтобы сгенерировать 1000 таких комбинаций:

 DROP VIEW IF EXISTS seq_0_to_999;
CREATE VIEW seq_0_to_999 AS (
SELECT (a.seq   10 * (b.seq   10 * c.seq)) AS seq
  FROM seq_0_to_9 a
  JOIN seq_0_to_9 b
  JOIN seq_0_to_9 c
);
 

Наконец, мы можем объединить эту таблицу из 1000 чисел с самой собой, чтобы создать представление, которое будет генерировать миллион подобных комбинаций:

 DROP VIEW IF EXISTS seq_0_to_999999;
CREATE VIEW seq_0_to_999999 AS (
SELECT (a.seq   (1000 * b.seq)) AS seq
  FROM seq_0_to_999 a
  JOIN seq_0_to_999 b
);
 

Вот запись, предоставляющая дополнительную информацию обо всем этом. http://www.plumislandmedia.net/mysql/filling-missing-data-sequences-cardinal-integers /