MySQL как заполнить недостающие даты в диапазоне?

У меня есть таблица с 2 столбцами, датой и оценкой. Он содержит не более 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, чтобы я отсортировал результат по дате и получил недостающие части.

РЕДАКТИРОВАТЬ: В этой таблице есть еще один столбец с именем UserID, поэтому у меня 30 000 пользователей, и у некоторых из них есть оценка в этой таблице. Я удаляю даты каждый день, если date ‹30 дней назад, потому что мне нужен счет за последние 30 дней для каждого пользователя. Причина в том, что я составляю график активности пользователей за последние 30 дней, и для построения диаграммы мне нужны 30 значений, разделенных запятой. Таким образом, я могу сказать, что в запросе мне дадут активность USERID = 10203, и запрос даст мне 30 баллов, по одной за каждый из последних 30 дней. Надеюсь, теперь я стал более ясным.


person Jerry2    schedule 21.08.2010    source источник
comment
Да, это возможно, но зачем вы это сделали?   -  person NullUserException    schedule 22.08.2010
comment
Я все еще не понимаю. Не извлекайте ненужные данные из базы данных, если вы можете заполнить эти пробелы тем, что строит график, и сэкономите на накладных расходах.   -  person NullUserException    schedule 22.08.2010
comment
но затем мне нужно ВЫБРАТЬ данные для USERID, я получаю, например, 20 строк с датой и набираю очки, а затем мне нужно зацикливаться на моем серверном языке (ASP), чтобы проверить, есть ли дата 30 дней назад, если это не так сделать 0 иначе сделать значение базы данных ... Разве это не более трудоемко, чем выборка из базы данных 30 значений и просто создание строки?   -  person Jerry2    schedule 22.08.2010
comment
возможный дубликат MySQL: выбрать все даты В диапазоне, даже если нет записей   -  person Jonathan Leffler    schedule 06.04.2013


Ответы (6)


MySQL не имеет рекурсивной функции, поэтому вам остается использовать трюк с таблицей NUMBERS -

  1. Создайте таблицу, в которой хранятся только увеличивающиеся числа - это легко сделать с помощью 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;
    
  2. Заполните таблицу, используя:

    INSERT INTO `example`.`numbers`
      ( `id` )
    VALUES
      ( NULL )
    

    ... для любого количества значений.

  3. Используйте DATE_ADD, чтобы создать список дат, увеличивая дни на основе значения NUMBERS.id. Замените «2010-06-06» и «2010-06-14» соответствующими датами начала и окончания (но используйте тот же формат, ГГГГ-ММ-ДД) -

    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
    
  4. 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`
person OMG Ponies    schedule 21.08.2010
comment
Спасибо. Является ли это быстрой операцией, не посоветуете ли вы использовать такой подход и проводить вычисления на стороне сервера? - person Jerry2; 22.08.2010
comment
@ Jerry2: Я предпочитаю обрабатывать как можно больше данных в базе данных, за исключением действительно сложных презентационных материалов. Я не завидую делать это в коде приложения, лишь бы это было одно обращение к базе данных ... - person OMG Ponies; 22.08.2010
comment
Чтобы использовать индексы, условия (предложения WHERE и ON) можно переписать на WHERE n.id < DATEDIFF('2010-06-14', '2010-06-06') и LEFT JOIN TABLE y ON y.date = DATE_FORMAT(x.ts, '%d.%m.%Y') - person Paul Spiegel; 15.10.2017
comment
Как только я добавляю предложение WHERE, например WHERE 'y'.'score' = 2, все заполненные даты больше не отображаются - person Seba M; 29.04.2019
comment
@SebaM: Это потому, что предложения WHERE применяются после присоединений. Итак, вы начинаете с полного диапазона, левое соединение для ваших данных, а затем вырезаете кучу строк. На самом деле вам нужно отфильтровать y таблицу как часть соединения ... SELECT * FROM x LEFT JOIN y ON y.foo = x.bah AND y.score = 2 - person MatBailie; 06.02.2021

Я не поклонник других ответов, требующих создания таблиц и т.д. Этот запрос делает это эффективно без вспомогательных таблиц.

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

If обнаружит дни, в которых не было оценок, и установит для них значение 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;

Это та часть, которая вводит вашу таблицу со счетом. Вы сравниваете с выбранным диапазоном дат из запроса генератора дат, чтобы иметь возможность заполнять нули там, где это необходимо (оценка будет изначально установлена ​​на NULL, потому что это LEFT JOIN; это зафиксировано в операторе выбора). Я тоже заказываю по датам, просто потому что. Это предпочтение, вы также можете заказать его по очкам.

До ORDER BY вы могли легко присоединиться к своей таблице с информацией о пользователе, которую вы упомянули в своем редактировании, чтобы добавить это последнее требование.

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

person Michael Conard    schedule 18.06.2018
comment
Недостатком является то, что для этого требуется, чтобы даты генерировались каждый раз, когда вы запускаете запрос, но также вам необходимо скопировать и вставить этот код во все запросы, где это необходимо. Я вижу аргумент в пользу того, что создание последовательности в памяти происходит быстрее, чем чтение ее с диска (хотя, скорее всего, она все равно будет кэширована в памяти), но я все равно хотел бы создать функцию или view, чтобы создать последовательность, чтобы я мог повторно использовать код. (это означает, что аргумент против создания объекта в базе данных становится немым.) - person MatBailie; 06.02.2021

Это можно сделать с помощью календарного стола. Это таблица, которую вы создаете один раз и заполняете диапазоном дат (например, один набор данных на каждый день 2000-2050; это зависит от ваших данных). Затем вы можете сделать внешнее соединение своей таблицы с таблицей календаря. Если дата отсутствует в вашей таблице, вы возвращаете 0 для оценки.

person Soundlink    schedule 21.08.2010
comment
Верно, но таблица чисел более гибкая - см. Мой ответ. IE: что, если теперь вам нужны и порядковые номера? Вам нужна таблица для каждого типа данных? - person OMG Ponies; 22.08.2010
comment
Другой вариант использования - последовательные числа ;-) Если вам нужно настроить таргетинг на другую СУБД (например, Oracle, MySQL, SQL-Server), ваш подход потребует немного измененного оператора, и я подозреваю, что подход DATE_ADD медленнее, чем календарная таблица (но я думаю, что здесь это не актуально) - person Soundlink; 22.08.2010
comment
Существует приятная и простая процедура создания календарной таблицы по адресу http://www.media-division.com/using-mysql-generate-daily-sales-reports-filled-gaps/. И хотя, как упоминалось выше @ omg-ponies, трюк с числами почти так же быстр, как и таблица календаря, иногда использование забавных уловок может вводить в заблуждение. Особенно, если вы ожидаете, что другие разработчики будут поддерживать ваш код в будущем. - person obaranovsky; 10.01.2016
comment
По сравнению с решением-таблицей чисел календарная таблица позволит вам писать простые запросы, такие как SELECT c.date, COALESCE(y.score, 0) AS cnt FROM calendar c LEFT JOIN y ON y.date = c.date WHERE c.date BETWEEN '2010-06-06' AND '2010-06-14' - person Paul Spiegel; 15.10.2017

Прошло время с тех пор, как был задан этот вопрос. MySQL 8.0 был выпущен в 2018 году и добавил поддержку рекурсивные общие табличные выражения, которые предоставляют элегантный современный способ решения этого вопроса.

Следующий запрос можно использовать для создания списка дат, скажем, за первые 15 дней августа 2010 года:

with recursive all_dates(dt) as (
    -- anchor
    select '2010-08-01' dt
        union all 
    -- recursion with stop condition
    select dt + interval 1 day from all_dates where dt + interval 1 day <= '2010-08-15'
)
select * from all_dates

Затем вы можете left join этот набор результатов со своей таблицей, чтобы сгенерировать ожидаемый результат:

with recursive all_dates(dt) as (
    -- anchor
    select '2010-08-01' dt
        union all 
    -- recursion with stop condition
    select dt + interval 1 day from all_dates where dt + interval 1 day <= '2010-08-15'
)
select d.dt date, coalesce(t.score, 0) score
from all_dates d
left join mytable t on t.date = d.dt
order by d.dt

Демонстрация DB Fiddle:

date       | score
:--------- | ----:
2010-08-01 |    19
2010-08-02 |    21
2010-08-03 |     0
2010-08-04 |    14
2010-08-05 |     0
2010-08-06 |     0
2010-08-07 |    10
2010-08-08 |     0
2010-08-09 |     0
2010-08-10 |    14
2010-08-11 |     0
2010-08-12 |     0
2010-08-13 |     0
2010-08-14 |     0
2010-08-15 |     0
person GMB    schedule 20.12.2019
comment
БЛАГОДАРЮ ВАС! Можно было легко изменить это, чтобы работать с минутами! - person Jonathan S. Fisher; 10.02.2020

Ответ Майкла Конарда великолепен, но мне нужны интервалы в 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

Если есть более простой способ сделать это, дайте мне знать.

person phoenix    schedule 28.05.2019

вы можете использовать прямо с даты начала до сегодняшнего дня со вставкой

        with recursive all_dates(dt) as (
        -- anchor
        select '2021-01-01' dt
            union all 
        -- recursion with stop condition
        INSERT IGNORE  INTO mytable (date,score) VALUES (dt + interval 1 day ,0 )  where dt + interval 1 day <= curdate()
    )
    select * from all_dates
person Akram Elhaddad    schedule 06.02.2021