Какой самый простой способ вставить пустые даты в результаты sql (либо в конце mysql, либо в конце perl)?

StackOverflow https://stackoverflow.com/questions/75752

  •  09-06-2019
  •  | 
  •  

Вопрос

Я быстро создаю csv-файл из таблицы mysql с запросом типа:

select DATE(date),count(date) from table group by DATE(date) order by date asc;

и просто сбрасывать их в файл на perl поверх:

while(my($date,$sum) = $sth->fetchrow) {
    print CSV "$date,$sum\n"
}

Однако в данных есть пробелы в датах:

| 2008-08-05 |           4 | 
| 2008-08-07 |          23 | 

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

| 2008-08-05 |           4 | 
| 2008-08-06 |           0 | 
| 2008-08-07 |          23 | 

Я придумал действительно неудобный (и почти наверняка глючный) обходной путь с массивом дней в месяц и некоторой математикой, но должно быть что-то более простое либо на стороне mysql, либо на perl.

Есть какие-нибудь гениальные идеи / пощечины по поводу того, почему я веду себя так глупо?


В итоге я использовал хранимую процедуру, которая сгенерировала временную таблицу для рассматриваемого диапазона дат по нескольким причинам:

  • Я знаю диапазон дат, который буду искать каждый раз
  • К сожалению, рассматриваемый сервер был не из тех, на которые я могу установить модули perl на atm, и его состояние было настолько ветхим, что на нем не было установлено ничего удаленного Date ::-y

Ответы с повторением даты / DateTime в perl также были очень хорошими, жаль, что я не могу выбрать несколько ответов!

Это было полезно?

Решение

Когда вам нужно что-то подобное на стороне сервера, вы обычно создаете таблицу, содержащую все возможные даты между двумя моментами времени, а затем соединяете эту таблицу с результатами запроса левой кнопкой мыши.Что - то вроде этого:

create procedure sp1(d1 date, d2 date)
  declare d datetime;

  create temporary table foo (d date not null);

  set d = d1
  while d <= d2 do
    insert into foo (d) values (d)
    set d = date_add(d, interval 1 day)
  end while

  select foo.d, count(date)
  from foo left join table on foo.d = table.date
  group by foo.d order by foo.d asc;

  drop temporary table foo;
end procedure

В этом конкретном случае было бы лучше поставить небольшую проверку на стороне клиента, если текущая дата не равна previos + 1, добавьте несколько дополнительных строк.

Другие советы

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

SELECT DATE(r.date),count(d.date) 
FROM dates AS r 
LEFT JOIN table AS d ON d.date = r.date 
GROUP BY DATE(r.date) 
ORDER BY r.date ASC;

Что касается вывода, я бы просто использовал ВЫБРАТЬ В ВЫХОДНОЙ ФАЙЛ вместо того, чтобы генерировать CSV вручную.Это также освобождает нас от беспокойства по поводу экранирования специальных символов.

не глупо, это не то, что делает MySQL, вставляя пустые значения даты.Я делаю это на perl с помощью двухэтапного процесса.Сначала загрузите все данные из запроса в хэш, упорядоченный по дате.Затем я создаю объект Date::EzDate и увеличиваю его на день, таким образом...

my $current_date = Date::EzDate->new();
$current_date->{'default'} = '{YEAR}-{MONTH NUMBER BASE 1}-{DAY OF MONTH}';
while ($current_date <= $final_date)
{
    print "$current_date\t|\t%hash_o_data{$current_date}";  # EzDate provides for     automatic stringification in the format specfied in 'default'
    $current_date++;
}

где конечная дата - это другой объект EzDate или строка, содержащая конец вашего диапазона дат.

EzDate сейчас нет в CPAN, но вы, вероятно, можете найти другой perl-мод, который будет сравнивать даты и предоставлять приращение даты.

Вы могли бы использовать Дата - время объект:

use DateTime;
my $dt;

while ( my ($date, $sum) = $sth->fetchrow )  {
    if (defined $dt) {
        print CSV $dt->ymd . ",0\n" while $dt->add(days => 1)->ymd lt $date;
    }
    else {
        my ($y, $m, $d) = split /-/, $date;
        $dt = DateTime->new(year => $y, month => $m, day => $d);
    }
    print CSV, "$date,$sum\n";
}

Что делает приведенный выше код, так это сохраняет последнюю напечатанную дату, сохраненную в DateTime объект $dt, и когда текущая дата превышает один день в будущем она увеличивается $dt на один день (и выводит его строкой в CSV) до тех пор, пока она не совпадет с текущей датой.

Таким образом, вам не нужны дополнительные таблицы, и вам не нужно заранее извлекать все ваши строки.

Я надеюсь, что вы разберетесь с остальным.

select  * from (
select date_add('2003-01-01 00:00:00.000', INTERVAL n5.num*10000+n4.num*1000+n3.num*100+n2.num*10+n1.num DAY ) as date from
(select 0 as num
   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) n1,
(select 0 as num
   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) n2,
(select 0 as num
   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) n3,
(select 0 as num
   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) n4,
(select 0 as num
   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) n5
) a
where date >'2011-01-02 00:00:00.000' and date < NOW()
order by date

С

select n3.num*100+n2.num*10+n1.num as date

вы получите столбец с числами от 0 до max (n3) * 100+max (n2) * 10 + max (n1)

Поскольку здесь у нас максимальное значение n3 равно 3, SELECT вернет 399 плюс 0 -> 400 записей (даты в календаре).

Вы можете настроить свой динамический календарь, ограничив его, например, с min (дата) до now().

Поскольку вы не знаете, где находятся пробелы, и все же вам нужны все значения (предположительно) от первой даты в вашем списке до последней, сделайте что-то вроде:

use DateTime;
use DateTime::Format::Strptime;
my @row = $sth->fetchrow;
my $countdate = strptime("%Y-%m-%d", $firstrow[0]);
my $thisdate = strptime("%Y-%m-%d", $firstrow[0]);

while ($countdate) {
  # keep looping countdate until it hits the next db row date
  if(DateTime->compare($countdate, $thisdate) == -1) {
    # counter not reached next date yet
    print CSV $countdate->ymd . ",0\n";
    $countdate = $countdate->add( days => 1 );
    $next;
  }

  # countdate is equal to next row's date, so print that instead
  print CSV $thisdate->ymd . ",$row[1]\n";

  # increase both
  @row = $sth->fetchrow;
  $thisdate = strptime("%Y-%m-%d", $firstrow[0]);
  $countdate = $countdate->add( days => 1 );
}

Хм, это оказалось сложнее, чем я думал..Я надеюсь, что в этом есть смысл!

Я думаю, что самым простым общим решением проблемы было бы создать Ordinal таблица с наибольшим количеством строк, которое вам нужно (в вашем случае 31*3 = 93).

CREATE TABLE IF NOT EXISTS `Ordinal` (
  `n` int(10) unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (`n`)
);
INSERT INTO `Ordinal` (`n`)
VALUES (NULL), (NULL), (NULL); #etc

Затем выполните LEFT JOIN От Ordinal на ваши данные.Вот простой случай, когда я получаю каждый день за последнюю неделю:

SELECT CURDATE() - INTERVAL `n` DAY AS `day`
FROM `Ordinal` WHERE `n` <= 7
ORDER BY `n` ASC

Две вещи, которые вам нужно было бы изменить в этом вопросе, - это начальная точка и интервал.Я использовал SET @var = 'value' синтаксис для наглядности.

SET @end = CURDATE() - INTERVAL DAY(CURDATE()) DAY;
SET @begin = @end - INTERVAL 3 MONTH;
SET @period = DATEDIFF(@end, @begin);

SELECT @begin + INTERVAL (`n` + 1) DAY AS `date`
FROM `Ordinal` WHERE `n` < @period
ORDER BY `n` ASC;

Таким образом, окончательный код выглядел бы примерно так, если бы вы присоединялись, чтобы получать количество сообщений в день за последние три месяца:

SELECT COUNT(`msg`.`id`) AS `message_count`, `ord`.`date` FROM (
    SELECT ((CURDATE() - INTERVAL DAY(CURDATE()) DAY) - INTERVAL 3 MONTH) + INTERVAL (`n` + 1) DAY AS `date`
    FROM `Ordinal`
    WHERE `n` < (DATEDIFF((CURDATE() - INTERVAL DAY(CURDATE()) DAY), ((CURDATE() - INTERVAL DAY(CURDATE()) DAY) - INTERVAL 3 MONTH)))
    ORDER BY `n` ASC
) AS `ord`
LEFT JOIN `Message` AS `msg`
  ON `ord`.`date` = `msg`.`date`
GROUP BY `ord`.`date`

Советы и комментарии:

  • Вероятно, самой сложной частью вашего запроса было определение количества дней для использования при ограничении Ordinal.Для сравнения, преобразовать эту целочисленную последовательность в даты было несложно.
  • Вы можете использовать Ordinal для всех ваших потребностей в непрерывной последовательности.Просто убедитесь, что он содержит больше строк, чем ваша самая длинная последовательность.
  • Вы можете использовать несколько запросов к Ordinal для нескольких последовательностей, например, перечисление каждого дня недели (1-5) за последние семь (1-7) недель.
  • Вы могли бы сделать это быстрее, сохранив даты в вашем Ordinal таблица, но она была бы менее гибкой.Таким образом, вам нужен только один Ordinal таблица, независимо от того, сколько раз вы ею пользуетесь.Тем не менее, если скорость того стоит, попробуйте INSERT INTO ... SELECT синтаксис.

Используйте какой-нибудь модуль Perl для вычисления даты, например рекомендуемый DateTime или Time::Piece (ядро из 5.10).Просто увеличьте дату и выведите дату и 0 до тех пор, пока дата не будет соответствовать текущей.

Я не знаю, сработает ли это, но как насчет того, если вы создадите новую таблицу, содержащую все возможные даты (это может быть проблемой с этой идеей, если диапазон дат будет меняться непредсказуемо ...), а затем выполните левое соединение для двух таблиц?Я предполагаю, что это сумасшедшее решение, если существует огромное количество возможных дат или нет способа предсказать первую и последнюю дату, но если диапазон дат либо фиксирован, либо легко поддается вычислению, то это может сработать.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top