Wat is die mees eenvoudige manier om pad leë datums in SQL resultate (op óf MySQL of perl einde)?
Vra
Ek is die bou van 'n vinnige CSV uit 'n MySQL tabel met 'n navraag soos:
select DATE(date),count(date) from table group by DATE(date) order by date asc;
en hulle net storting na 'n lêer in perl oor 'n:
while(my($date,$sum) = $sth->fetchrow) {
print CSV "$date,$sum\n"
}
Daar is op datum gapings in die data, al is:
| 2008-08-05 | 4 |
| 2008-08-07 | 23 |
Ek wil graag pad die data in die vermiste dae met 'n nul-telling inskrywings te eindig met te vul:
| 2008-08-05 | 4 |
| 2008-08-06 | 0 |
| 2008-08-07 | 23 |
Ek geklap saam 'n baie ongemaklike (en byna seker karretjie) tydelike oplossing met 'n verskeidenheid van dae-per-maand en 'n paar wiskunde, maar daar moet iets meer eenvoudig óf op die MySQL of perl kant wees.
Enige geniale idees / klappe in die gesig vir hoekom my is die feit dat so dom?
Ek het uiteindelik gaan met 'n gestoor proses wat 'n tydelike tabel gegenereer vir die periode in vraag vir 'n paar redes:
- Ek weet die datum bereik Ek sal die uitkyk wees vir elke keer as
- Die bediener betrokke was ongelukkig nie een wat ek perl modules op OTM kan installeer, en die toestand van dit was afgeleefde genoeg dat dit niks gehad het nie naastenby Datum :: - y geïnstalleer
Die perl Datum / Date Time-iterating antwoorde was ook baie goed, ek wens ek kon meer as een antwoord kies!
Oplossing
As jy iets soos dit op die bediener kant nodig, jy gewoonlik te skep 'n tafel wat alle moontlike datums tussen twee punte in die tyd bevat, en dan links by hierdie tafel met navraag resultate. Iets soos hierdie:
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 hierdie spesifieke geval is dit beter sou wees om 'n bietjie tjek op die kliënt kant sit, as die huidige datum is nie previos + 1, sit 'n paar Daarbenewens snare.
Ander wenke
As ek te doen gehad met hierdie probleem, in ontbreek datums ek 'n verwysing tafel wat net soos vervat al die datums Ek stel belang in en by die datatabel op die veld datum eintlik geskep in te vul. Dis ru, maar dit werk.
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;
As vir uitvoer, ek wil net gebruik KIES IN outfile 'n> in plaas van die opwekking van die CSV met die hand. Laat ons vry van sorge te maak oor die ontsnapping spesiale karakters sowel.
nie stom, dit is nie iets wat MySQL doen, plaas die leë datum waardes. Ek doen dit in perl met 'n twee-stap proses. In die eerste plek te laai al die data van die navraag in 'n gemors georganiseer deur datum. Dan, Ek skep 'n Datum :: EzDate voorwerp en inkrementeer dit oordag, so ...
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++;
}
waar finale datum is nog 'n EzDate voorwerp of 'n string met die einde van jou datum bereik.
EzDate is nie op CPAN nou nie, maar jy kan waarskynlik 'n ander perl mod daardie datum vergelyk sal doen en gee 'n datum incrementor.
Jy kan 'n Datum tyd gebruik voorwerp:
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";
}
Wat die bo-kode doen, is dit hou die laaste gedrukte datum gestoor in 'n
DateTime
voorwerp $dt
, en wanneer die huidige datum is meer as een dag
in die toekoms, dit vermeerderings $dt
deur een dag (en druk dit 'n lyn te
CSV
) totdat dit is dieselfde as die huidige datum.
Op hierdie manier wat jy nie ekstra tafels nodig, en hoef nie te gaan haal al jou rye in advance.
Ek hoop jy sal uit te vind die res.
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
Met
select n3.num*100+n2.num*10+n1.num as date
jy sal 'n kolom met getalle van 0 kry om maksimum (N3) * 100 + max (N2) * 10 + max (N1)
Sedert hier het ons maksimum n3 as 3, sal SELECT terugkeer 399, plus 0 -.> 400 rekords (datums in kalender)
Jy kan inskakel jou dinamiese kalender deur die beperking van dit, byvoorbeeld, van min (datum) jy nou moet ().
Aangesien jy weet nie waar die leemtes is, en nog jy wil al die waardes (vermoedelik) van die eerste datum in jou lys tot die laaste een, iets soos te doen:
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, wat blyk meer ingewikkeld as wat ek gedink het dit sou wees om te wees .. Ek hoop dit maak sin!
Ek dink die eenvoudigste algemene oplossing vir die probleem sou wees om 'n Ordinal
tafel met die hoogste aantal rye wat jy nodig het te skep (in jou geval 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
Volgende, doen 'n LEFT JOIN
van Ordinal
op jou data. Hier is 'n eenvoudige saak, om elke dag in die laaste week:
SELECT CURDATE() - INTERVAL `n` DAY AS `day`
FROM `Ordinal` WHERE `n` <= 7
ORDER BY `n` ASC
Die twee dinge wat jy nodig sou wees om te verander oor hierdie is die beginpunt en die interval. Ek het SET @var = 'value'
sintaksis gebruik vir duidelikheid.
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;
So die finale kode iets sou lyk soos hierdie, as jy vas teen die aantal boodskappe per dag kry oor die afgelope drie maande:
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`
Wenke en Kommentaar:
- Waarskynlik die moeilikste deel van jou navraag is die bepaling van die aantal dae om te gebruik wanneer die beperking van
Ordinal
. In vergelyking met die transformasie wat heelgetal volgorde in datums was maklik. - Jy kan
Ordinal
gebruik vir al jou ononderbroke-volgorde nodig het. Maak net seker dit bevat meer rye as jou langste ry. - Jy kan verskeie navrae gebruik op
Ordinal
vir verskeie rye, byvoorbeeld notering elke weekdag (1-5) vir die afgelope sewe (1-7) weke. - Jy kan dit maak vinniger deur datums stoor in jou
Ordinal
tafel, maar dit sal minder buigsaam wees. Hierdie manier waarop jy net een nodig hetOrdinal
tafel, maak nie saak hoeveel keer jy dit gebruik. Tog, as die spoed is die moeite werd, probeer dieINSERT INTO ... SELECT
sintaksis.
Gebruik 'n paar Perl module tot op datum berekeninge te doen, soos aanbeveel Datum tyd of Time :: Stuk (kern van 5.10). Net inkrementeer datum en gedrukte datum en 0 tot datum sal die huidige pas.
Ek weet nie of dit sal werk, maar hoe gaan dit as jy 'n nuwe tabel wat al die moontlike datums vervat geskep (wat dalk die probleem wees met hierdie idee, as die omvang van die datums gaan onvoorspelbaar verander .. .) en dan 'n links te sluit op die twee tafels? Ek dink dit is 'n gek oplossing indien daar 'n groot aantal moontlike datums, of geen manier om die eerste en laaste datum voorspel, maar as die omvang van die datums óf vaste is of maklik om uit te werk, dan is dit dalk werk.