Qual è il modo più semplice per riempire le date vuote nei risultati SQL (su MySQL o Perl)?
Domanda
Sto costruendo un rapido CSV da una tabella mysql con una query come:
select DATE(date),count(date) from table group by DATE(date) order by date asc;
e semplicemente scaricandoli in un file in perl su a:
while(my($date,$sum) = $sth->fetchrow) {
print CSV "$date,$sum\n"
}
Ci sono però delle lacune date nei dati:
| 2008-08-05 | 4 |
| 2008-08-07 | 23 |
Vorrei riempire i dati per riempire i giorni mancanti con voci a conteggio zero per finire con:
| 2008-08-05 | 4 |
| 2008-08-06 | 0 |
| 2008-08-07 | 23 |
Ho messo insieme una soluzione davvero imbarazzante (e quasi certamente piena di bug) con una serie di giorni al mese e un po' di matematica, ma deve esserci qualcosa di più semplice sia sul lato mysql che sul perl.
Qualche idea geniale/schiaffo in faccia sul perché sono così stupido?
Alla fine ho utilizzato una procedura memorizzata che generava una tabella temporanea per l'intervallo di date in questione per un paio di motivi:
- Conosco l'intervallo di date che cercherò ogni volta
- Sfortunatamente il server in questione non era uno di quelli su cui potevo installare moduli Perl su ATM, e il suo stato era abbastanza decrepito da non avere nulla in remoto Date::-y installato
Anche le risposte perl Date/DateTime erano molto buone, vorrei poter selezionare più risposte!
Soluzione
Quando hai bisogno di qualcosa del genere sul lato server, di solito crei una tabella che contiene tutte le date possibili tra due punti nel tempo, quindi unisciti a questa tabella con i risultati della query.Qualcosa come questo:
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
In questo caso particolare sarebbe meglio mettere un piccolo check lato client, se la data corrente non è precedente+1, inserire delle stringhe aggiuntive.
Altri suggerimenti
Quando ho dovuto affrontare questo problema, per inserire le date mancanti ho creato una tabella di riferimento che conteneva solo tutte le date che mi interessavano e ho unito la tabella dati nel campo data.È rozzo, ma funziona.
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;
Per quanto riguarda l'output, userei semplicemente SELEZIONA NELL'OUTFILE invece di generare il CSV manualmente.Ci lascia liberi di preoccuparci anche di sfuggire a personaggi speciali.
non stupido, questo non è qualcosa che fa MySQL, inserendo i valori di data vuoti.Lo faccio in Perl con un processo in due passaggi.Innanzitutto, carica tutti i dati dalla query in un hash organizzato per data.Quindi, creo un oggetto Date::EzDate e lo incremento di giorno, quindi...
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++;
}
dove la data finale è un altro oggetto EzDate o una stringa contenente la fine dell'intervallo di date.
EzDate non è su CPAN in questo momento, ma probabilmente puoi trovare un altro mod Perl che effettuerà confronti di date e fornirà un incremento di data.
Potresti usare a Appuntamento oggetto:
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";
}
Ciò che fa il codice sopra è mantenere l'ultima data stampata memorizzata in un fileDateTime
oggetto $dt
, e quando la data corrente è più di un giorno in futuro, aumenta $dt
entro un giorno (e stampa una riga suCSV
) finché non corrisponde alla data corrente.
In questo modo non hai bisogno di tavoli extra e non è necessario recuperare tutte le righe in anticipo.
Spero che capirai il resto.
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
Con
select n3.num*100+n2.num*10+n1.num as date
otterrai una colonna con numeri da 0 a max(n3)*100+max(n2)*10+max(n1)
Poiché qui abbiamo un massimo di n3 pari a 3, SELECT restituirà 399, più 0 -> 400 record (date nel calendario).
Puoi ottimizzare il tuo calendario dinamico limitandolo, ad esempio, da min(data) a adesso().
Dato che non sai dove sono gli spazi vuoti, e tuttavia desideri tutti i valori (presumibilmente) dalla prima data all'ultima nell'elenco, fai qualcosa del tipo:
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 );
}
Uhm, la cosa si è rivelata più complicata di quanto pensassi..Spero che abbia senso!
Penso che la soluzione generale più semplice al problema sarebbe creare un file Ordinal
tabella con il maggior numero di righe necessarie (nel tuo caso 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
Quindi, fai a LEFT JOIN
da Ordinal
sui tuoi datiEcco un caso semplice, ottenuto tutti i giorni dell'ultima settimana:
SELECT CURDATE() - INTERVAL `n` DAY AS `day`
FROM `Ordinal` WHERE `n` <= 7
ORDER BY `n` ASC
Le due cose che dovresti cambiare a riguardo sono il punto di partenza e l'intervallo.ho usato SET @var = 'value'
sintassi per chiarezza.
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;
Quindi il codice finale sarebbe simile a questo, se ti iscrivessi per ottenere il numero di messaggi al giorno negli ultimi tre mesi:
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`
Suggerimenti e commenti:
- Probabilmente la parte più difficile della tua query è stata determinare il numero di giorni da utilizzare durante la limitazione
Ordinal
.In confronto, trasformare quella sequenza di numeri interi in date era facile. - Puoi usare
Ordinal
per tutte le tue esigenze di sequenza ininterrotta.Assicurati solo che contenga più righe della sequenza più lunga. - Puoi utilizzare più query su
Ordinal
per sequenze multiple, ad esempio elencando tutti i giorni feriali (1-5) nelle ultime sette (1-7) settimane. - Potresti renderlo più veloce memorizzando le date nel tuo file
Ordinal
tabella, ma sarebbe meno flessibile.In questo modo te ne servirà solo unoOrdinal
tavolo, non importa quante volte lo usi.Tuttavia, se ne vale la pena, prova ilINSERT INTO ... SELECT
sintassi.
Utilizza alcuni moduli Perl per eseguire calcoli sulla data, come DateTime o Time::Piece consigliato (core dalla 5.10).Basta incrementare la data e stampare la data e 0 finché la data non corrisponderà a quella corrente.
Non so se funzionerebbe, ma che ne dici di creare una nuova tabella che contenga tutte le date possibili (questo potrebbe essere il problema con questa idea, se l'intervallo di date cambierà in modo imprevedibile...) e quindi eseguire un'unione a sinistra sui due tavoli?Immagino che sia una soluzione pazzesca se esiste un vasto numero di date possibili o non c'è modo di prevedere la prima e l'ultima data, ma se l'intervallo di date è fisso o facile da calcolare, allora potrebbe funzionare.