Quel est le moyen le plus simple de remplir les dates vides dans les résultats SQL (sur mysql ou perl end)?

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

  •  09-06-2019
  •  | 
  •  

Question

Je construis un csv rapide à partir d'une table mysql avec une requête du type:

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

et simplement les déposer dans un fichier Perl sur un:

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

Il existe des intervalles de date dans les données, cependant:

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

Je souhaite compléter les données pour renseigner les jours manquants avec des entrées de comptage nul et aboutir à:

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

J'ai mis en place une solution de contournement vraiment gênante (et certainement certainement boguée) avec un éventail de jours par mois et quelques calculs, mais il doit y avoir quelque chose de plus simple, du côté de mysql ou de perl.

Des idées de génie / des gifles au visage pour lesquelles je suis si bête?

J'ai fini par utiliser une procédure stockée qui a généré une table temporaire pour la plage de dates en question pour plusieurs raisons:

  • Je connais la plage de dates que je rechercherai chaque fois
  • Malheureusement, le serveur en question n’a pas pu installer des modules Perl sous atm. Il était en état de décrépitude et il n’avait rien installé à distance. Date :: - y est installé

Les réponses itératives perl Date / DateTime étaient également très bonnes, j'aimerais pouvoir sélectionner plusieurs réponses!

Était-ce utile?

La solution

Lorsque vous avez besoin de quelque chose comme ça côté serveur, vous créez généralement une table contenant toutes les dates possibles entre deux instants, puis vous rejoignez cette table avec les résultats de la requête. Quelque chose comme ça:

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

Dans ce cas particulier, il serait préférable de mettre un peu de contrôle côté client, si la date du jour n’est pas précédente + 1, insérez quelques chaînes d’ajout.

Autres conseils

Lorsque j'ai dû traiter ce problème, pour compléter les dates manquantes, j'ai en fait créé une table de référence contenant uniquement toutes les dates qui m'intéressaient et rejoint la table de données dans le champ de date. C'est brutal, mais ça marche.

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 ce qui concerne la sortie, je voudrais simplement utiliser SELECT INTO OUTFILE au lieu de générer le CSV à la main. Nous ne craignons pas d'échapper à des caractères spéciaux.

pas stupide, ce n’est pas quelque chose que MySQL fait, en insérant les valeurs de date vides. Je le fais en Perl avec un processus en deux étapes. Commencez par charger toutes les données de la requête dans un hachage organisé par date. Ensuite, je crée un objet Date :: EzDate et l’incrémente par jour, donc ...

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

où date finale est un autre objet EzDate ou une chaîne contenant la fin de votre plage de dates.

EzDate n’est pas sur CPAN pour le moment, mais vous pouvez probablement trouver un autre mod Perl capable de comparer les dates et de fournir un incrément de date.

Vous pouvez utiliser un objet 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";
}

Le code ci-dessus conserve la dernière date imprimée stockée dans un fichier. DateTime objet $ dt , et lorsque la date actuelle est supérieure à un jour à l'avenir, il incrémente $ dt d'un jour (et affiche une ligne sur CSV ) jusqu'à ce qu'il soit identique à la date du jour.

De cette façon, vous n’avez pas besoin de tables supplémentaires ni de chercher tous vos fichiers. lignes à l'avance.

J'espère que vous saurez tout le reste.

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

Avec

select n3.num*100+n2.num*10+n1.num as date

vous obtiendrez une colonne avec des nombres de 0 à max (n3) * 100 + max (n2) * 10 + max (n1)

Etant donné que nous avons n3 max en tant que 3, SELECT renverra 399, plus 0 - > 400 enregistrements (dates dans le calendrier).

Vous pouvez ajuster votre calendrier dynamique en le limitant, par exemple, à min (date), vous devez maintenant ().

Puisque vous ne savez pas où se trouvent les lacunes et que vous souhaitez néanmoins conserver toutes les valeurs (vraisemblablement) de la première à la dernière date de votre liste, procédez comme suit:

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, cela s’est avéré être plus compliqué que je ne le pensais .. j’espère que cela a un sens!

Je pense que la solution générale la plus simple au problème serait de créer une table Ordinal avec le plus grand nombre de lignes dont vous avez besoin (dans votre cas, 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

Ensuite, effectuez un JOINDRE GAUCHE à partir de Ordinal sur vos données. Voici un cas simple: tous les jours de la semaine dernière:

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

Les deux choses à changer à ce sujet sont le point de départ et l’intervalle. J'ai utilisé la syntaxe SET @var = 'valeur' ?? pour plus de clarté.

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;

Ainsi, le code final ressemblerait à quelque chose comme ceci, si vous vous joigniez pour obtenir le nombre de messages par jour au cours des trois derniers mois:

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`

Conseils et commentaires:

  • La partie la plus difficile de votre requête consistait probablement à déterminer le nombre de jours à utiliser pour limiter Ordinal . Par comparaison, il était facile de transformer cette séquence entière en dates.
  • Vous pouvez utiliser Ordinal pour tous vos besoins en séquences ininterrompues. Assurez-vous simplement qu'il contient plus de lignes que votre plus longue séquence.
  • Vous pouvez utiliser plusieurs requêtes sur Ordinal pour plusieurs séquences, par exemple, répertorier tous les jours de la semaine (1-5) les sept (1-7) dernières semaines.
  • Vous pourriez accélérer les choses en stockant les dates dans votre table Ordinal , mais ce serait moins flexible. Ainsi, vous n’avez besoin que d’une seule table Ordinal , quel que soit le nombre de fois où vous l’utilisez. Néanmoins, si la vitesse en vaut la peine, essayez la syntaxe INSERT INTO ... SELECT .

Utilisez un module Perl pour faire des calculs de date, comme recommandé DateTime ou Time :: Piece (base de 5.10). Il suffit d’incrémenter la date et la date d’impression, et 0 jusqu’à ce que la date corresponde à la date actuelle.

Je ne sais pas si cela fonctionnerait, mais que diriez-vous de créer un nouveau tableau contenant toutes les dates possibles (cela pourrait poser problème avec cette idée, si la plage de dates va changer de manière imprévisible .. .) et ensuite faire une jointure gauche sur les deux tables? Je suppose que c'est une solution folle s'il y a un grand nombre de dates possibles, ou aucun moyen de prédire la première et la dernière date, mais si la plage de dates est soit fixe, soit facile à définir, cela pourrait fonctionner.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top