Generowanie szeregów czasowych między dwiema datami w PostgreSQL

92

Mam takie zapytanie, które ładnie generuje serię dat między 2 podanymi datami:

select date '2004-03-07' + j - i as AllDate 
from generate_series(0, extract(doy from date '2004-03-07')::int - 1) as i,
     generate_series(0, extract(doy from date '2004-08-16')::int - 1) as j

Generuje 162 daty od 2004-03-07i 2004-08-16do tego czego chcę. Problem z tym kodem polega na tym, że nie dałby on prawidłowej odpowiedzi, gdy dwie daty pochodzą z różnych lat, na przykład gdy próbuję 2007-02-01i 2008-04-01.

Czy jest lepsze rozwiązanie?

f.ashouri
źródło

Odpowiedzi:

175

Można to zrobić bez konwersji do / z int (ale zamiast tego do / z timestamp)

SELECT date_trunc('day', dd):: date
FROM generate_series
        ( '2007-02-01'::timestamp 
        , '2008-04-01'::timestamp
        , '1 day'::interval) dd
        ;
wildplasser
źródło
3
dlaczego jest date_truncpotrzebny?
Idefixx
2
To tylko prezentacja. Eliminuje drukowanie części czasowej znacznika czasu, która w tym przypadku jest równa zerom.
beemtee
73

Aby wygenerować serię dat, jest to optymalny sposób:

SELECT t.day::date 
FROM   generate_series(timestamp '2004-03-07'
                     , timestamp '2004-08-16'
                     , interval  '1 day') AS t(day);
  • Dodatkowe date_trunc()nie są potrzebne. Rzutowanie do date( day::date) robi to niejawnie.

  • Ale nie ma też sensu rzutowanie literałów daty na dateparametr wejściowy. Au contraire, timestampto najlepszy wybór . Zaleta wydajności jest niewielka, ale nie ma powodu, aby jej nie brać. I nie trzeba niepotrzebnie stosować reguł czasu letniego (DST) w połączeniu z konwersją z datena timestamp with time zoneiz powrotem. Zobacz poniżej.

Równoważna, mniej jednoznaczna krótka składnia:

SELECT day::date 
FROM   generate_series(timestamp '2004-03-07', '2004-08-16', '1 day') day;

Lub z funkcją zwracania zestawu na SELECTliście:

SELECT generate_series(timestamp '2004-03-07', '2004-08-16', '1 day')::date AS day;

Słowo ASkluczowe jest wymagane w ostatnim wariancie, w dayprzeciwnym razie Postgres błędnie zinterpretowałby alias kolumny . I nie radziłbym tego wariantu przed Postgresem 10 - przynajmniej nie z więcej niż jedną funkcją zwracającą set na tej samej SELECTliście:

(Pomijając to, ostatni wariant jest zwykle najszybszy z niewielkim marginesem).

Dlaczego timestamp [without time zone]?

Istnieje wiele przeciążonych wariantów generate_series(). Obecnie (Postgres 11):

SELECT oid::regprocedure   AS function_signature
     , prorettype::regtype AS return_type
FROM   pg_proc
where  proname = 'generate_series';
podpis_funkcji | typ_zwrotu                
: ------------------------------------------------- ------------------------------- | : --------------------------
gene_series (liczba całkowita, liczba całkowita, liczba całkowita) | liczba całkowita                    
gene_series (liczba całkowita, liczba całkowita) | liczba całkowita                    
gene_series (bigint, bigint, bigint) | bigint                     
gene_series (bigint, bigint) | bigint                     
gene_series (numeryczne, numeryczne, numeryczne) | numeryczny                    
gene_series (numeryczne, numeryczne) | numeryczny                    
gene_series (sygnatura czasowa bez strefy czasowej, sygnatura czasowa bez strefy czasowej, interwał) | sygnatura czasowa bez strefy czasowej
gene_series (znacznik czasu ze strefą czasową, znacznik czasu ze strefą czasową, interwał) | sygnatura czasowa ze strefą czasową

( numericwarianty zostały dodane z Postgres 9.5.) Odpowiednie są dwa ostatnie wytłuszczone i powracające timestamp/ timestamptz.

Nie ma wariantu przyjmowania ani zwrotudate . Aby powrócić, potrzebne jest wyraźne rzutowanie date. Wywołanie z timestampargumentami jest rozwiązywane bezpośrednio do najlepszego wariantu bez schodzenia do reguł rozpoznawania typu funkcji i bez dodatkowego rzutowania danych wejściowych.

timestamp '2004-03-07'jest całkowicie ważny, przy okazji. Pominięta część czasu ma domyślnie 00:00format ISO.

Dzięki rozdzielczości typu funkcji nadal możemy przejść date. Ale to wymaga więcej pracy od Postgres. Istnieje niejawne rzutowanie od datedo timestamporaz od datedo timestamptz. Byłoby niejednoznaczne, ale timestamptzjest „preferowane” wśród „typów daty / godziny”. Tak więc mecz jest rozstrzygany w kroku 4d. :

Przeanalizuj wszystkich kandydatów i zatrzymaj te, które akceptują preferowane typy (z kategorii typu danych wejściowych) na większości stanowisk, na których wymagana będzie konwersja typów. Zachowaj wszystkich kandydatów, jeśli żaden nie akceptuje preferowanych typów. Jeśli zostanie tylko jeden kandydat, użyj go; w przeciwnym razie przejdź do następnego kroku.

Oprócz dodatkowej pracy związanej z rozwiązywaniem typów funkcji, dodaje to dodatkowe rzutowanie timestamptz- co nie tylko zwiększa koszty, ale może również powodować problemy z czasem letnim, prowadząc w rzadkich przypadkach do nieoczekiwanych wyników. (Czas letni to kretyński pomysł, nawiasem mówiąc, nie można tego wystarczająco podkreślić).

Dodałem dema do skrzypiec pokazujące droższy plan zapytań:

db <> skrzypce tutaj

Związane z:

Erwin Brandstetter
źródło
7
Jeszcze krótsza wersja:SELECT generate_series(timestamp '2004-03-07', '2004-08-16', '1 day') :: DATE AS day;
Václav Kužel
Co oznacza składnia t (day)?
Rendang
@rendang: AS t(day)in SELECT * FROM func() AS t(day)to alias tabeli i kolumny. W AStym kontekście słowo kluczowe to opcjonalny szum. Zobacz: stackoverflow.com/a/20230716/939860
Erwin Brandstetter
35

Możesz generować serie bezpośrednio z datami. Nie ma potrzeby używania numerów int ani sygnatur czasowych:

select date::date 
from generate_series(
  '2004-03-07'::date,
  '2004-08-16'::date,
  '1 day'::interval
) date;
fbonetti
źródło
W zależności od Twojej strefy czasowej może to zwrócić nieoczekiwany wynik. Miałem ten problem. Zamiast tego użyj sygnatury czasowej. SET session TIME zone 'America / Sao_Paulo' SELECT d :: date FROM generated_series ('2019-11-01' :: date, '2019-11-03' :: date, '1 day') d SELECT d :: date FROM generation_series ('2019-11-01' :: date, '2019-11-04' :: date, '1 day') d
palhares
1

Możesz również użyć tego.

select generate_series  ( '2012-12-31'::timestamp , '2018-10-31'::timestamp , '1 day'::interval) :: date 
Meyyappan
źródło