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