MySQL, как заполнить отсутствующие даты в диапазоне?
У меня есть таблица с двумя столбцами, датой и счетом. Он имеет не более 30 записей за каждый из последних 30 дней.
date score
-----------------
1.8.2010 19
2.8.2010 21
4.8.2010 14
7.8.2010 10
10.8.2010 14
Моя проблема в том, что некоторые даты отсутствуют - я хочу видеть:
date score
-----------------
1.8.2010 19
2.8.2010 21
3.8.2010 0
4.8.2010 14
5.8.2010 0
6.8.2010 0
7.8.2010 10
...
Что мне нужно от одного запроса, так это получить: 19,21,9,14,0,0,10,0,0,14... Это означает, что недостающие даты заполняются 0.
Я знаю, как получить все значения и на серверном языке, итерации по датам и отсутствию пробелов. Но можно ли это сделать в mysql, чтобы сортировать результат по дате и получать недостающие фрагменты.
EDIT: в этой таблице есть еще один столбец с именем UserID, поэтому у меня есть 30 000 пользователей, а некоторые из них имеют счет в этой таблице. Я удаляю даты каждый день, если дата < 30 дней назад, потому что мне нужен последний 30-дневный балл для каждого пользователя. Причина в том, что я делаю график активности пользователя за последние 30 дней и для построения графика мне нужны 30 значений, разделенных запятой. Поэтому я могу сказать, что в запросе введите мне USERID = 10203, и запрос получит мне 30 баллов, по одному за каждый из последних 30 дней. Надеюсь, теперь я более ясен.
Ответы
Ответ 1
MySQL не имеет рекурсивной функциональности, поэтому вам остается использовать трюк таблицы NUMBERS -
-
Создайте таблицу, содержащую только инкрементные числа - легко сделать с помощью auto_increment:
DROP TABLE IF EXISTS `example`.`numbers`;
CREATE TABLE `example`.`numbers` (
`id` int(10) unsigned NOT NULL auto_increment,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-
Заполните таблицу, используя:
INSERT INTO `example`.`numbers`
( `id` )
VALUES
( NULL )
... для количества значений, которые вам нужны.
-
Используйте DATE_ADD для создания списка дат, увеличивая дни на основе значения NUMBERS.id. Замените "2010-06-06" и "2010-06-14" на соответствующие даты начала и окончания (но используйте тот же формат, YYYY-MM-DD) -
SELECT `x`.*
FROM (SELECT DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY)
FROM `numbers` `n`
WHERE DATE_ADD('2010-06-06', INTERVAL `n`.`id` -1 DAY) <= '2010-06-14' ) x
-
LEFT JOIN на таблицу данных на основе временной части:
SELECT `x`.`ts` AS `timestamp`,
COALESCE(`y`.`score`, 0) AS `cnt`
FROM (SELECT DATE_FORMAT(DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY), '%m/%d/%Y') AS `ts`
FROM `numbers` `n`
WHERE DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY) <= '2010-06-14') x
LEFT JOIN TABLE `y` ON STR_TO_DATE(`y`.`date`, '%d.%m.%Y') = `x`.`ts`
Если вы хотите сохранить формат даты, используйте функцию DATE_FORMAT:
DATE_FORMAT(`x`.`ts`, '%d.%m.%Y') AS `timestamp`
Ответ 2
Вы можете выполнить это, используя Таблица календаря. Таблицу, которую вы создаете один раз и заполняете диапазоном дат (например, один набор данных для каждого дня 2000-2050, который зависит от ваших данных). Затем вы можете сделать внешнее соединение таблицы против таблицы календаря. Если в вашей таблице отсутствует дата, вы получите 0 для оценки.
Ответ 3
Я не поклонник других ответов, требующих создания таблиц и т.д. Этот запрос делает это эффективно без вспомогательных таблиц.
SELECT
IF(score IS NULL, 0, score) AS score,
b.Days AS date
FROM
(SELECT a.Days
FROM (
SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS Days
FROM (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS a
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS b
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS c
) a
WHERE a.Days >= curdate() - INTERVAL 30 DAY) b
LEFT JOIN your_table
ON date = b.Days
ORDER BY b.Days;
Так что давайте проанализируем это.
SELECT
IF(score IS NULL, 0, score) AS score,
b.Days AS date
Если будут обнаружены дни, у которых не было баллов, и установите их на 0. b.Days - это настроенное количество дней, которое вы выбрали для получения с текущей даты, до 1000.
(SELECT a.Days
FROM (
SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS Days
FROM (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS a
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS b
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS c
) a
WHERE a.Days >= curdate() - INTERVAL 30 DAY) b
Этот подзапрос - это то, что я видел в stackoverflow. Он эффективно генерирует список последних 1000 дней с текущей даты. Интервал (в настоящее время 30) в предложении WHERE в конце определяет, какие дни возвращаются; максимум 1000. Этот запрос может быть легко изменен, чтобы возвращать 100-летние даты, но 1000 - это хорошо для большинства вещей.
LEFT JOIN your_table
ON date = b.Days
ORDER BY b.Days;
Это та часть, которая приносит вашу таблицу, которая содержит оценку. Вы сравниваете с выбранным диапазоном дат от запроса генератора дат, чтобы иметь возможность заполнить 0s, где это необходимо (сначала будет установлен NULL
, потому что это LEFT JOIN
, это зафиксировано в инструкции select). Я также заказываю его по датам, просто потому, что. Это предпочтение, вы также можете заказать по счету.
Перед ORDER BY
вы можете легко присоединиться к своей таблице о пользовательской информации, которую вы упомянули в своем правлении, чтобы добавить это последнее требование.
Надеюсь, эта версия запроса поможет кому-то. Спасибо за прочтение.
Ответ 4
Ответ Майкла Конарда великолепен, но мне нужны были 15-минутные интервалы, когда время должно начинаться с начала каждой 15-й минуты:
SELECT a.Days
FROM (
SELECT FROM_UNIXTIME( FLOOR( UNIX_TIMESTAMP() / (15 * 60) ) * (15 * 60)) - INTERVAL 15 * (a.a + (10 * b.a) + (100 * c.a)) MINUTE AS Days
FROM (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS a
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS b
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS c
) a
WHERE a.Days >= curdate() - INTERVAL 30 DAY
Это установит текущее время к предыдущему раунду 15-й минуты:
FROM_UNIXTIME( FLOOR( UNIX_TIMESTAMP() / (15 * 60) ) * (15 * 60))
И это уберет время с 15-минутным шагом:
- INTERVAL 15 * (a.a + (10 * b.a) + (100 * c.a)) MINUTE
Если есть более простой способ сделать это, пожалуйста, дайте мне знать.