web-dev-qa-db-ja.com

SQL結果(mysqlまたはPerl側)で空の日付を埋める最も簡単な方法は何ですか?

次のようなクエリを使用して、mysqlテーブルからクイックcsvを作成しています。

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

そしてそれらをPerlのファイルにダンプするだけです:

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

ただし、データには日付のギャップがあります。

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

データを埋めて、欠落している日をゼロカウントのエントリで埋めて、最終的に次のようにします。

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

私は月ごとの日数の配列といくつかの数学を使って本当に厄介な(そしてほぼ間違いなくバグのある)回避策をまとめましたが、mysql側またはPerl側のどちらかでもっと簡単なものが必要です。

なぜ私がそんなに馬鹿なのか、天才的なアイデアや平手打ちはありますか?


いくつかの理由で、問題の日付範囲の一時テーブルを生成するストアドプロシージャを使用することになりました。

  • 毎回探している日付範囲を知っています
  • 残念ながら、問題のサーバーはatmにPerlモジュールをインストールできるサーバーではなく、その状態は十分に老朽化していて、リモートで何もインストールされていませんでしたDate ::-y install

PerlのDate/DateTimeを繰り返す回答も非常に良かったので、複数の回答を選択できればと思います。

33
user13196

サーバー側でそのようなものが必要な場合は、通常、2つの時点の間のすべての可能な日付を含むテーブルを作成し、このテーブルをクエリ結果と左結合します。このようなもの:

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

この特定のケースでは、クライアント側で少しチェックを入れたほうがよいでしょう。現在の日付がprevios + 1でない場合は、いくつかの追加文字列を入れてください。

20
GSerg

この問題に対処する必要があったとき、不足している日付を入力するために、実際には、関心のあるすべての日付を含む参照テーブルを作成し、日付フィールドのデータテーブルに結合しました。粗雑ですが、機能します。

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;

出力に関しては、CSVを手動で生成する代わりに、 SELECT INTO OUTFILE を使用します。特別なキャラクターを逃れる心配もありません。

7
Aeon

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

上記のコードは、最後に印刷された日付をDateTimeオブジェクト$dtに格納し、現在の日付が1日以上先の場合、$dtを次のようにインクリメントします。現在の日付と同じになるまで、ある日(そして、それをCSVに1行出力します)。

このように、余分なテーブルは必要ありません。また、事前にすべての行をフェッチする必要もありません。

4
8jean

ばかげているわけではありません。これはMySQLが行うことではなく、空の日付値を挿入します。私はこれをPerlで2段階のプロセスで行います。まず、クエリのすべてのデータを日付順に整理されたハッシュに読み込みます。次に、Date :: EzDateオブジェクトを作成し、日ごとにインクリメントします。

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

ここで、最終日付は別のEzDateオブジェクト、または日付範囲の終わりを含む文字列です。

EzDateは現在CPANにありませんが、日付の比較を行い、日付の増分を提供する別のPerlmodをおそらく見つけることができます。

4
coffeepac

私はあなたが残りを理解することを願っています。

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

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

0からmax(n3)* 100 + max(n2)* 10 + max(n1)までの数値の列が表示されます

ここでは最大n3が3であるため、SELECTは399に加えて、0-> 400レコード(カレンダーの日付)を返します。

たとえば、min(date)からnow()に制限することで、動的カレンダーを調整できます。

2
Igor Kryltsov

ギャップがどこにあるかわからないが、リストの最初の日付から最後の日付までのすべての値(おそらく)が必要なので、次のようにします。

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

うーん、それは私が思っていたよりも複雑であることが判明しました..それが理にかなっていることを願っています!

1
castaway

この問題の最も簡単な一般的な解決策は、必要な行数が最も多いOrdinalテーブルを作成することだと思います(この場合は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

次に、Ordinalからデータに対してLEFT JOINを実行します。先週の毎日を取得する簡単なケースを次に示します。

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

これについて変更する必要がある2つのことは、開始点と間隔です。わかりやすくするために、SET @var = 'value'構文を使用しました。

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;

したがって、過去3か月間の1日あたりのメッセージ数を取得するために参加した場合、最終的なコードは次のようになります。

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`

ヒントとコメント:

  • おそらく、クエリで最も難しい部分は、Ordinalを制限するときに使用する日数を決定することでした。比較すると、その整数列を日付に変換するのは簡単でした。
  • 中断されないシーケンスのすべてのニーズにOrdinalを使用できます。最長のシーケンスよりも多くの行が含まれていることを確認してください。
  • Ordinalで複数のシーケンスに対して複数のクエリを使用できます。たとえば、過去7週間の平日(1-5)ごとに一覧表示します。
  • Ordinalテーブルに日付を保存することで高速化できますが、柔軟性は低くなります。このように、何度使用しても、必要なOrdinalテーブルは1つだけです。それでも、速度に見合うだけの場合は、INSERT INTO ... SELECT構文を試してください。
1
theazureshadow

推奨されるDateTimeやTime :: Piece(5.10からのコア)など、いくつかのPerlモジュールを使用して日付計算を実行します。日付をインクリメントして日付を印刷し、日付が現在と一致するまで0にします。

0