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

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

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

и просто сбрасывая их в файл в perl через a:

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 installed

Ответы perl Date/DateTime-iterating также были очень хорошими, мне хотелось бы выбрать несколько ответов!

Ответы

Ответ 1

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

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

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

Ответ 2

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

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;

Что касается вывода, я бы просто использовал SELECT INTO OUTFILE вместо создания CSV вручную. Лишает нас освобождения от беспокойства о том, чтобы избежать специальных символов.

Ответ 3

не тупой, это не то, что делает 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, который будет сравнивать дату и предоставить инкремент даты.

Ответ 4

Вы можете использовать объект DateTime:

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), пока он не станет таким же, как текущая дата.

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

Ответ 5

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

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 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 );
}

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

Ответ 6

Я думаю, что самым простым общим решением проблемы было бы создание таблицы 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

Затем сделайте a LEFT JOIN from 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.

Ответ 7

Надеюсь, вы поймете остальное.

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)

Так как здесь max n3 равно 3, SELECT вернет 399 плюс 0 → 400 записей (даты в календаре).

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

Ответ 8

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

Ответ 9

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