¿Cuál es la forma más sencilla de rellenar fechas vacías en los resultados de SQL (ya sea en MySQL o Perl)?

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

  •  09-06-2019
  •  | 
  •  

Pregunta

Estoy creando un csv rápido a partir de una tabla MySQL con una consulta como:

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

y simplemente volcarlos en un archivo en Perl a través de:

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

Sin embargo, hay lagunas de fechas en los datos:

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

Me gustaría rellenar los datos para completar los días que faltan con entradas de conteo cero para terminar con:

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

Preparé una solución alternativa realmente incómoda (y casi con seguridad con errores) con una serie de días por mes y algo de matemáticas, pero tiene que haber algo más sencillo ya sea en el lado de mysql o perl.

¿Alguna idea genial o una bofetada que explique por qué estoy siendo tan tonto?


Terminé usando un procedimiento almacenado que generaba una tabla temporal para el rango de fechas en cuestión por un par de razones:

  • Sé el rango de fechas que buscaré cada vez
  • Desafortunadamente, el servidor en cuestión no era uno en el que pudiera instalar módulos Perl en un cajero automático, y su estado era lo suficientemente decrépito como para que no tuviera nada instalado de forma remota. Fecha::-y

Las respuestas de iteración de fecha/fecha y hora de Perl también fueron muy buenas. ¡Ojalá pudiera seleccionar varias respuestas!

¿Fue útil?

Solución

Cuando necesita algo así en el lado del servidor, generalmente crea una tabla que contiene todas las fechas posibles entre dos puntos en el tiempo y luego se une a esta tabla con los resultados de la consulta.Algo como esto:

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

En este caso particular, sería mejor poner una pequeña verificación en el lado del cliente, si la fecha actual no es anterior+1, agregar algunas cadenas.

Otros consejos

Cuando tuve que lidiar con este problema, para completar las fechas faltantes, creé una tabla de referencia que solo contenía todas las fechas que me interesan y me uní a la tabla de datos en el campo de fecha.Es tosco, pero funciona.

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;

En cuanto a la salida, solo usaría SELECCIONAR EN ARCHIVO en lugar de generar el CSV a mano.También nos deja libres de preocuparnos por escapar de personajes especiales.

No es tonto, esto no es algo que haga MySQL, insertando los valores de fecha vacíos.Hago esto en Perl con un proceso de dos pasos.Primero, cargue todos los datos de la consulta en un hash organizado por fecha.Luego, creo un objeto Date::EzDate y lo incremento por día, así que...

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

donde la fecha final es otro objeto EzDate o una cadena que contiene el final de su rango de fechas.

EzDate no está en CPAN en este momento, pero probablemente puedas encontrar otro mod de Perl que haga comparaciones de fechas y proporcione un incrementador de fechas.

Podrías usar un Fecha y hora objeto:

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

Lo que hace el código anterior es mantener la última fecha impresa almacenada en unDateTime objeto $dt, y cuando la fecha actual es más de un día en el futuro, se incrementa $dt por un día (e imprime una línea paraCSV) hasta que sea la misma que la fecha actual.

De esta manera, no necesita tablas adicionales, y no necesita obtener todas sus filas con anticipación.

Espero que descubras el 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

obtendrá una columna con números del 0 al max(n3)*100+max(n2)*10+max(n1)

Como aquí tenemos un máximo de n3 como 3, SELECT devolverá 399, más 0 -> 400 registros (fechas en el calendario).

Puede ajustar su calendario dinámico limitándolo, por ejemplo, desde min(fecha) hasta ahora().

Como no sabe dónde están los espacios y, sin embargo, desea todos los valores (presumiblemente) desde la primera fecha de su lista hasta la última, haga algo como:

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

Hmm, eso resultó ser más complicado de lo que pensé...¡Espero que tenga sentido!

Creo que la solución general más simple al problema sería crear un Ordinal tabla con el mayor número de filas que necesita (en su 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

A continuación, haz un LEFT JOIN de Ordinal en sus datos.A continuación se muestra un caso sencillo, que se obtiene todos los días de la última semana:

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

Las dos cosas que necesitarías cambiar son el punto de partida y el intervalo.He usado SET @var = 'value' sintaxis para mayor claridad.

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;

Entonces, el código final se vería así, si te unieras para obtener la cantidad de mensajes por día durante los últimos tres meses:

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`

Consejos y comentarios:

  • Probablemente la parte más difícil de su consulta fue determinar la cantidad de días a usar al limitar Ordinal.En comparación, transformar esa secuencia de números enteros en fechas fue fácil.
  • Puedes usar Ordinal para todas sus necesidades de secuencia ininterrumpida.Solo asegúrate de que contenga más filas que tu secuencia más larga.
  • Puede utilizar varias consultas en Ordinal para secuencias múltiples, por ejemplo, enumerando todos los días de la semana (1-5) durante las últimas siete (1-7) semanas.
  • Podrías hacerlo más rápido almacenando fechas en tu Ordinal mesa, pero sería menos flexible.De esta manera solo necesitas uno Ordinal mesa, no importa cuántas veces la uses.Aún así, si la velocidad vale la pena, prueba el INSERT INTO ... SELECT sintaxis.

Utilice algún módulo de Perl para realizar cálculos de fechas, como el recomendado DateTime o Time::Pieza (núcleo de 5.10).Simplemente incremente la fecha e imprima la fecha y 0 hasta que la fecha coincida con la actual.

No sé si esto funcionaría, pero ¿qué tal si crearas una nueva tabla que contuviera todas las fechas posibles (ese podría ser el problema con esta idea, si el rango de fechas va a cambiar de forma impredecible...) y ¿Entonces hacer una unión izquierda en las dos mesas?Supongo que es una solución loca si hay una gran cantidad de fechas posibles, o no hay forma de predecir la primera y la última fecha, pero si el rango de fechas es fijo o fácil de calcular, entonces esto podría funcionar.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top