Qual è il modo più semplice per riempire le date vuote nei risultati SQL (su MySQL o Perl)?

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

  •  09-06-2019
  •  | 
  •  

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!

È stato utile?

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 uno Ordinal tavolo, non importa quante volte lo usi.Tuttavia, se ne vale la pena, prova il INSERT 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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top