2012-04-05 15 views
6

Oto mój SQL:Jak generować dane w MySQL?

SELECT 
    COUNT(id), 
    CONCAT(YEAR(created_at), '-', MONTH(created_at), '-', DAY(created_at)) 
FROM my_table 
GROUP BY YEAR(created_at), MONTH(created_at), DAY(created_at) 

Chcę rząd, aby pokazać się nawet przez kilka dni, gdzie nie było ID utworzony. W tej chwili brakuje mi mnóstwa dat na dni, w których nie było żadnej aktywności.

Jakieś przemyślenia dotyczące zmiany tego zapytania w celu wykonania tego?

Odpowiedz

1

Sposób to zrobić w jednym zapytaniu:

SELECT COUNT(my_table.id) AS total, 
CONCAT(YEAR(dates.ddate), '-', MONTH(dates.ddate), '-', DAY(dates.ddate)) 
FROM (
    -- Creates "on the fly" 65536 days beginning from 2000-01-01 (179 years) 
    SELECT DATE_ADD("2000-01-01", INTERVAL (b1.b + b2.b + b3.b + b4.b + b5.b + b6.b + b7.b + b8.b + b9.b + b10.b + b11.b + b12.b + b13.b + b14.b + b15.b + b16.b) DAY) AS ddate FROM 
    (SELECT 0 AS b UNION SELECT 1) b1, 
    (SELECT 0 AS b UNION SELECT 2) b2, 
    (SELECT 0 AS b UNION SELECT 4) b3, 
    (SELECT 0 AS b UNION SELECT 8) b4, 
    (SELECT 0 AS b UNION SELECT 16) b5, 
    (SELECT 0 AS b UNION SELECT 32) b6, 
    (SELECT 0 AS b UNION SELECT 64) b7, 
    (SELECT 0 AS b UNION SELECT 128) b8, 
    (SELECT 0 AS b UNION SELECT 256) b9, 
    (SELECT 0 AS b UNION SELECT 512) b10, 
    (SELECT 0 AS b UNION SELECT 1024) b11, 
    (SELECT 0 AS b UNION SELECT 2048) b12, 
    (SELECT 0 AS b UNION SELECT 4096) b13, 
    (SELECT 0 AS b UNION SELECT 8192) b14, 
    (SELECT 0 AS b UNION SELECT 16384) b15, 
    (SELECT 0 AS b UNION SELECT 32768) b16 
) dates 
LEFT JOIN my_table ON dates.ddate = my_table.created_at 
GROUP BY dates.ddate 
ORDER BY dates.ddate 

następny kod jest konieczne tylko, jeśli chcesz przetestować i nie mieć „my_table” wskazany na pytanie:

create table `my_table` (
    `id` int (11), 
    `created_at` date 
); 
insert into `my_table` (`id`, `created_at`) values('1','2000-01-01'); 
insert into `my_table` (`id`, `created_at`) values('2','2000-01-01'); 
insert into `my_table` (`id`, `created_at`) values('3','2000-01-01'); 
insert into `my_table` (`id`, `created_at`) values('4','2001-01-01'); 
insert into `my_table` (`id`, `created_at`) values('5','2100-06-06'); 
9

SQL jest bardzo zły przy zwracaniu danych, których nie ma w bazie danych. Możesz znaleźć początkowe i końcowe wartości dla luk dat, ale uzyskanie wszystkich dat jest trudne.

Rozwiązaniem jest utworzenie tabeli kalendarzy z jednym rekordem dla każdej daty i OUTER DOŁĄCZ do zapytania.

Oto przykład zakładając, że created_at jest typ DATE:

SELECT calendar_date, COUNT(`id`) 
FROM calendar LEFT OUTER JOIN my_table ON calendar.calendar_date = my_table.created_at 
GROUP BY calendar_date 

(zgaduję, że jest naprawdę created_at DATETIME, więc będziesz musiał zrobić trochę więcej gimnastyki, aby przyłączyć się do tabel).

+1

To jest naprawdę jedyna opcja, chyba że jesteś w stanie utworzyć brakujące wpisy w swoim kodzie po sele podsumowanie posiadanych rekordów. Pamiętaj jednak, że będziesz musiał wypełniać tę datę calendar_date datami i mam nadzieję, że nie zapomnisz dodać więcej, niż aktualnie potrzebujesz. (Ile lat w przyszłości odejdziesz?) Osobiście nie podoba mi się ten pomysł, ponieważ ogranicza on również do grupowania według wybranego przedziału czasowego. Co jeśli jutro chciałbyś pokazać rzeczy pogrupowane według godziny? – Vyrotek

+1

Aby było jasne, w rzeczywistości nie ma dobrego rozwiązania tego problemu za pomocą SQL. –

+0

Pliki kalendarza są przydatne dla wielu rzeczy (szczególnie w sytuacjach sprzedaży detalicznej, gdzie kalendarz fiskalny nie zawsze jest odwzorowany na kalendarz gregoriański), w tym ten konkretny problem. Możesz tworzyć wirtualne w-instrukcja ... z rekurencyjnymi CTE (nieobecne w mySQL). –

7

Ogólny pomysł

Istnieją dwa główne podejścia do generowania danych w MySQL. Jedną z nich jest generowanie danych w locie podczas uruchamiania kwerendy, a druga to przechowywanie danych w bazie danych i używanie jej w razie potrzeby. Oczywiście drugi będzie szybszy niż pierwszy, jeśli często będziesz przeprowadzać zapytanie. Jednak drugi będzie wymagał tabeli w bazie danych, której jedynym celem będzie generowanie brakujących danych. Będzie również wymagać posiadania uprawnień wystarczających do utworzenia tego stołu.

Dynamiczne generowanie danych

Takie podejście wymaga podejmowania UNION s wygenerować fałszywy stolik, który można wykorzystać do przyłączenia się do rzeczywistej tabeli z. Straszliwe i powtarzające się zapytanie to:

select aDate from (
    select @maxDate - interval (a.a+(10*b.a)+(100*c.a)+(1000*d.a)) day aDate from 
    (select 0 as a 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) a, /*10 day range*/ 
    (select 0 as a 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) b, /*100 day range*/ 
    (select 0 as a 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) c, /*1000 day range*/ 
    (select 0 as a 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) d, /*10000 day range*/ 
    (select @minDate := '2001-01-01', @maxDate := '2002-02-02') e 
) f 
where aDate between @minDate and @maxDate 

W każdym razie jest to prostsze, niż się wydaje. Tworzy on kartezjańskie produkty tabel pochodnych z wartościami liczbowymi 10, więc wynik będzie miał 10^X wierszy, gdzie X jest ilością tabel pochodnych w zapytaniu. W tym przykładzie istnieje zakres dnia wynoszący 10000, więc możesz reprezentować okresy o wartości ponad 27 lat. Jeśli potrzebujesz więcej, dodaj kolejne zapytanie UNION do kwerendy i zaktualizuj interwał, a jeśli nie potrzebujesz tak wielu, możesz usunąć wartości UNION s lub poszczególne wartości z wyprowadzonych tabel. Aby wyjaśnić, możesz dostosować okres daty, stosując filtr z klauzulą ​​WHERE na zmiennych @minDate i @maxDate (ale nie używaj dłuższego okresu niż ten, który stworzyłeś z produktami kartezjańskimi).

Static generowania danych

To rozwiązanie będzie wymagać, aby wygenerować tabelę w bazie danych. Podejście jest podobne do poprzedniego. Najpierw musisz wstawić dane do tej tabeli: zakres liczb całkowitych od 1 do X, gdzie X to maksymalny wymagany zakres.Ponownie, jeśli nie masz pewności, po prostu wstaw wartości 100000, a będziesz mógł tworzyć przedziały dzienne dla ponad 273 lat. Tak, raz masz sekwencję liczb całkowitych, można przekształcić go w przedziale czasowym tak:

select '2012-01-01' + interval value - 1 day aDay from seq 
having aDay <= '2012-01-05' 

Zakładając tabelę o nazwie seq z kolumną o nazwie value. Na górze od daty i na dole daty do.

skrętu to w coś użytecznego

Ok, teraz mamy nasze okresy data generowane ale my wciąż brakuje sposób do kwerendy danych i wyświetlania brakujących wartości jako rzeczywisty 0. Tutaj na ratunek przychodzi left join. Aby upewnić się, że wszyscy jesteśmy na tej samej stronie, numer left join jest podobny do inner join, ale z jedną różnicą: zachowa wszystkie rekordy z lewej tabeli łączenia, niezależnie od tego, czy istnieje pasujący rekord w tabeli prawo. Innymi słowy, inner join usunie wszystkie nie dopasowane wiersze na łączeniu, podczas gdy left join zachowa te z lewej tabeli, a dla rekordów po lewej, które nie mają pasujących rekordów na prawym stole, left join wypełni to. "spacja" z wartością null.

Tak więc powinniśmy dołączyć do naszej tabeli domen (tej, która ma "brakujące" dane) z nowo wygenerowaną tabelą umieszczając tę ​​ostatnią po lewej stronie połączenia, a pierwszą po prawej, aby wszystkie elementy były brane pod uwagę, niezależnie od ich obecności w tabeli domeny.

Na przykład, jeśli mieliśmy stolik domainTable z polami ID, birthDate i chcielibyśmy, aby zobaczyć liczbę wszystkich birthDate w pierwszych 5 dni 2012 dziennie i jeśli liczba jest 0 pokazać tę wartość, to zapytanie może być prowadzony:

select allDays.aDay, count(dt.id) from (
    select '2012-01-01' + interval value - 1 day aDay from seq 
    having aDay <= '2012-01-05' 
) allDays 
left join domainTable dt on allDays.aDay = dt.birthDate 
group by allDays.aDay 

ten generuje tabelę pochodzącą ze wszystkimi wymagane na dniach (zauważ używam statyczną generowanie danych) i wykonuje left join przed naszym stole domeny, więc zostaną wyświetlone wszystkie dni, niezależnie czy mają pasujące wartości w naszych tabelach domeny. Zauważ także, że count należy wykonać na polu, które będzie miało wartości null, ponieważ te nie są zliczane.

Uwagi należy uznać

1) Zapytania mogą być używane do zapytań innych odstępach czasu (miesiące, lata) wykonywanie drobnych zmian w kodzie

2) zamiast sztywno daty można wyszukać dla min i max wartości z tabel domen tak:

select (select min(aDate) from domainTable) + interval value - 1 day aDay 
from seq 
having aDay <= (select max(aDate) from domainTable) 

Pozwoliłoby to uniknąć generowania więcej rekordów niż to konieczne.

rzeczywistości odpowiadając na pytanie

myślę, że powinien już zorientowali się, jak to zrobić, co chcesz. W każdym razie, oto kroki, aby inni mogli z nich skorzystać.Najpierw utwórz tabelę całkowitą . Po drugie, należy uruchomić tę kwerendę:

select allDays.aDay, count(mt.id) aCount from (
    select (select date(min(created_at)) from my_table) + interval value - 1 day aDay 
    from seq s 
    having aDay <= (select date(max(created_at)) from my_table) 
) allDays 
left join my_table mt on allDays.aDay = date(mt.created_at) 
group by allDays.aDay 

Chyba created_at jest datetime i dlatego jesteś łącząc w ten sposób. Jednak tak się dzieje, ponieważ MySQL natywnie przechowuje daty, więc po prostu grupuję według pola daty, ale przesyłając created_at do rzeczywistego typu danych date. Możesz grać z nim za pomocą tego fiddle.

A oto dane rozwiązanie generujące dynamicznie:

select allDays.aDay, count(mt.id) aCount from (
    select @maxDate - interval a.a day aDay from 
    (select 0 as a 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) a, /*10 day range*/ 
    (select @minDate := (select date(min(created_at)) from my_table), 
      @maxDate := (select date(max(created_at)) from my_table)) e 
    where @maxDate - interval a.a day between @minDate and @maxDate 
) allDays 
left join my_table mt on allDays.aDay = date(mt.created_at) 
group by allDays.aDay 

Jak widać szkielet zapytania jest taki sam jak poprzedni. Jedyną zmianą jest generowanie wygenerowanej tabeli allDays. Teraz sposób generowania tabeli pochodnej również nieznacznie różni się od tego, który dodałem wcześniej. Dzieje się tak dlatego, że w przykładowej filddle potrzebowałem tylko 10-dniowego zasięgu. Jak widać, jest bardziej czytelny niż dodanie zakresu dziennego 1000. Oto fiddle dla dynamicznego rozwiązania, abyś mógł z nim grać.

Mam nadzieję, że to pomoże!

+1

Woah - super kompleksowy. Czy jest możliwe utworzenie tabeli tymczasowej do zapytania w jednej instrukcji? –

+0

Możesz [tworzyć tymczasowe tabele] (http://dev.mysql.com/doc/refman/5.6/en/create-table.html) w jednej instrukcji. Jednak nie sądzę, że byłoby to użyteczne, ponieważ (co myślę, że planujesz zrobić) utworzysz tabelę, wypełnisz ją, zapytasz, a następnie usuniesz.Lepiej już mieć wypełnioną tabelę lub użyć wyprowadzonej tabeli (jak w dynamicznym podejściu: 'wybierz * z (derived_table) dt left join ...'), ponieważ kosztowną częścią procedury jest populacja stół. –

0

Testbed:

create table testbed (id integer, created_at date); 
insert into testbed values 
     (1, '2012-04-01'), 
     (1, '2012-04-30'), 
     (2, '2012-04-02'), 
     (3, '2012-04-03'), 
     (3, '2012-04-04'), 
     (4, '2012-04-04'); 

Używam również any_table, który stworzyłem sztucznie tak:

create table any_table (id integer); 
insert into any_table values (1), (2), (3), (4), (5), (6), (7), (8), (9), (10); 
insert into any_table select * from any_table; -- repeat this insert 7-8 times 

można użyć dowolnej tabeli w bazie danych, które powinna mieć więcej wierszy następnie max(created_dt) - min(created_dt) zakres co najmniej 365 na pokrycie roku.

Zapytanie:

SELECT concat(year(dr._date),'-',month(dr._date),'-',day(dr._date)), 
     -- or, instead of concat(), simply: dr._date 
     count(id) 
    FROM (
     SELECT date_add(r.mindt, INTERVAL @dist day) _date, 
       @dist := @dist + 1 AS days_away 
      FROM any_table t 
      JOIN (SELECT min(created_at) mindt, 
         max(created_at) maxdt, 
         @dist := 0 
        FROM testbed) r 
     WHERE date_add(r.mindt, INTERVAL @dist day) <= r.maxdt) dr 
    LEFT JOIN testbed tb ON dr._date = tb.created_at 
GROUP BY dr._date; 
Powiązane problemy