Qual é a maneira mais simples para preencher um vazio datas no sql resultados (no mysql ou final de perl)?

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

  •  09-06-2019
  •  | 
  •  

Pergunta

Estou construindo uma rápida csv a partir de uma tabela mysql com uma consulta como:

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

e apenas dumping-los para um arquivo em perl mais de uma:

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

Não há data de lacunas nos dados, que:

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

Eu gostaria de acrescentar os dados para preencher os dias ausentes com zero-contagem de entradas para acabar com:

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

Me deu um tapa em conjunto uma muito estranho (e quase certamente buggy) solução com uma matriz de dias por mês e um pouco de matemática, mas tem que ser algo mais simples no mysql ou perl lado.

Qualquer gênio ideias/dá um tapa na cara para por que eu estou sendo tão idiota?


Acabei indo com um procedimento armazenado, o que gerou uma tabela temporária para o intervalo de datas em questão por duas razões:

  • Eu sei que o intervalo de data que eu vou estar olhando para cada vez que
  • O servidor em questão, infelizmente, não foi um que eu possa instalar os módulos perl em atm, e o estado era decrépito o suficiente para que ele não tivesse nada remotamente Data::-y instalado

O perl de Data/Hora-iteração respostas foram muito boas também, eu desejo que eu poderia selecionar várias respostas!

Foi útil?

Solução

Quando você precisa de algo como que no lado do servidor, você normalmente cria uma tabela que contém todas as datas possíveis entre dois pontos no tempo, e, em seguida, left join esta tabela com os resultados da consulta.Algo como isto:

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

Neste caso particular, seria melhor colocar um pouco de verificação no lado do cliente, se a data atual não é anterior+1, coloque um pouco além de cadeias de caracteres.

Outras dicas

Quando eu tinha de lidar com este problema, para o preenchimento falta de datas na verdade, eu criei uma tabela de referência que apenas continha todas as datas em que estou interessado e juntou-se a tabela de dados no campo de data.É bruto, mas 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;

Como para saída, eu tinha acabado de usar SELECT INTO OUTFILE em vez de gerar o CSV com a mão.Deixa-nos livres da preocupação de escapar caracteres especiais como bem.

não mudo, isso não é algo que o MySQL ainda, inserir a data vazia de valores.Eu faço isso em perl com um processo de duas etapas.Primeiro, carregar todos os dados da consulta em um hash organizados por data.Então, vou criar uma Data::EzDate objeto e incrementá-lo, por dia, então...

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

onde a data final é outro EzDate objeto ou uma seqüência de caracteres contendo o fim do seu intervalo de datas.

EzDate não está no CPAN agora, mas provavelmente você pode encontrar um outro perl mod que fará data compara e fornecer uma data incrementor.

Você pode usar um DateTime 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";
}

O que o código acima faz é ele mantém a última data impressa armazenados em um DateTime objeto $dt, e quando a data atual é mais do que um dia no futuro, ele incrementa $dt por um dia (e imprime uma linha para CSV até mesmo como a data atual.

Desta forma, você não precisa de tabelas extras, e não precisa buscar todos os seus linhas de antecedência.

Eu espero que você vai descobrir o 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

Com

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

você terá uma coluna com números de 0 a max(n3)*100+max(n2)*10+max(n1)

Desde que aqui temos max n3 3, SELECIONE retornará 399, mais 0 -> 400 registros (datas no calendário).

Você pode sintonizar o seu calendário dinâmico, limitando, por exemplo, a partir min(data), você tem agora().

Como você não sabe de onde as lacunas são, e você ainda quer que todos os valores de (supostamente) a partir da primeira data em sua lista e o último, fazer 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, que acabou por ser mais complicado do que eu pensei que seria..Espero que isso faz sentido!

Eu acho que a mais simples solução geral para o problema seria a criação de um Ordinal tabela com o maior número de linhas que você precisa (no seu 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

Em seguida, faça uma LEFT JOIN a partir de Ordinal para os seus dados.Aqui está um caso simples, ficar todos os dias na última semana:

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

Duas coisas que você precisará mudar esta são o ponto de partida e o de intervalo.Eu tenho usado SET @var = 'value' a sintaxe para maior clareza.

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;

Assim, o código final ficaria algo como isso, se você se juntar para obter o número de mensagens por dia nos últimos três 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`

Dicas e Comentários:

  • Provavelmente a parte mais difícil da sua consulta foi determinar o número de dias para usar quando a limitação Ordinal.Por comparação, transformar inteiro seqüência de datas foi fácil.
  • Você pode usar Ordinal para todas as suas ininterrupta sequência de necessidades.Apenas certifique-se de que ele contém mais linhas do que a sua mais longa sequência.
  • Você pode usar várias consultas em Ordinal para várias sequências, por exemplo, a listagem de todos os dias da semana (de 1 a 5) durante os últimos sete (1-7) semanas.
  • Você pode torná-lo mais rápido armazenando datas na sua Ordinal tabela, mas seria menos flexível.Desta forma, você só precisa de uma Ordinal tabela, não importa quantas vezes você usá-lo.Ainda, se a velocidade é a pena, tente o INSERT INTO ... SELECT a sintaxe.

O uso de alguns módulos Perl para fazer cálculos de data, como recomendado tipo DateTime ou de Tempo::Peça (núcleo de 5.10).Apenas incrementar a data e a data de impressão e a 0 até a data de correspondência atual.

Eu não sei se isso iria funcionar, mas se você tiver criado uma nova tabela que continha todas as datas possíveis (que pode ser o problema com essa idéia de que, se o intervalo de datas vai mudar de forma imprevisível...) e, em seguida, fazer um left join em duas tabelas?Eu acho que é um louco solução se houver um grande número de datas possíveis, ou nenhuma maneira de prever a primeira e a última data, mas se o intervalo de datas é fixa ou fácil de trabalhar, então isso poderia funcionar.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top