Générer des séries temporelles entre deux dates dans PostgreSQL

92

J'ai une requête comme celle-ci qui génère bien une série de dates entre 2 dates données:

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

Il génère 162 dates entre 2004-03-07et 2004-08-16et ce que je veux. Le problème avec ce code est qu'il ne donnerait pas la bonne réponse lorsque les deux dates sont d'années différentes, par exemple lorsque j'essaye 2007-02-01et 2008-04-01.

Y a-t-il une meilleure solution?

f.ashouri
la source

Réponses:

175

Peut être fait sans conversion vers / depuis int (mais vers / depuis horodatage à la place)

SELECT date_trunc('day', dd):: date
FROM generate_series
        ( '2007-02-01'::timestamp 
        , '2008-04-01'::timestamp
        , '1 day'::interval) dd
        ;
wildplasser
la source
3
pourquoi est-il date_truncnécessaire?
Idefixx
2
C'est juste une présentation. Il élimine l'impression de la partie horaire de l'horodatage qui est toujours à zéro dans ce cas.
beemtee
73

Pour générer une série de dates, c'est la manière optimale :

SELECT t.day::date 
FROM   generate_series(timestamp '2004-03-07'
                     , timestamp '2004-08-16'
                     , interval  '1 day') AS t(day);
  • Aucun supplément date_trunc()n'est nécessaire. Le cast to date( day::date) fait cela implicitement.

  • Mais il est également inutile de convertir des littéraux de date dateen paramètre d'entrée. Au contraire, timestampc'est le meilleur choix . L'avantage en termes de performances est faible, mais il n'y a aucune raison de ne pas en profiter. Et vous n'impliquez pas inutilement des règles DST (heure d'été) associées à la conversion de datevers timestamp with time zoneet inversement. Voir ci-dessous.

Syntaxe courte équivalente, moins explicite:

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

Ou avec la fonction de retour de set dans la SELECTliste:

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

Le ASmot-clé est obligatoire dans la dernière variante, Postgres interpréterait mal l'alias de colonne daysinon. Et je ne conseillerais pas cette variante avant Postgres 10 - du moins pas avec plus d'une fonction de retour d'ensemble dans la même SELECTliste:

(Cela mis à part, la dernière variante est généralement la plus rapide par une petite marge.)

Pourquoi timestamp [without time zone]?

Il existe un certain nombre de variantes surchargées de generate_series(). Actuellement (Postgres 11):

SELECT oid::regprocedure   AS function_signature
     , prorettype::regtype AS return_type
FROM   pg_proc
where  proname = 'generate_series';
function_signature | return_type                
: ------------------------------------------------- ------------------------------- | : --------------------------
generate_series (entier, entier, entier) | entier                    
generate_series (entier, entier) | entier                    
generate_series (bigint, bigint, bigint) | bigint                     
generate_series (bigint, bigint) | bigint                     
generate_series (numérique, numérique, numérique) | numérique                    
generate_series (numérique, numérique) | numérique                    
generate_series (horodatage sans fuseau horaire, horodatage sans fuseau horaire, intervalle) | horodatage sans fuseau horaire
generate_series (horodatage avec fuseau horaire, horodatage avec fuseau horaire, intervalle) | horodatage avec fuseau horaire

(Des numericvariantes ont été ajoutées avec Postgres 9.5.) Les plus pertinentes sont les deux dernières en gras prenant et retournant timestamp/ timestamptz.

Il n'y a pas de variante prise ou retourdate . Un cast explicite est nécessaire pour revenir date. L'appel avec des timestamparguments résout directement la meilleure variante sans descendre dans les règles de résolution de type de fonction et sans conversion supplémentaire pour l'entrée.

timestamp '2004-03-07'est parfaitement valide, btw. La partie de temps omise est par défaut au 00:00format ISO.

Grâce à la résolution du type de fonction, nous pouvons encore passer date. Mais cela nécessite plus de travail de la part de Postgres. Il y a un cast implicite de dateà timestampet un de dateà timestamptz. Serait ambigu, mais timestamptzest "préféré" parmi les "types de date / heure". Le match est donc décidé à l'étape 4d. :

Parcourez tous les candidats et conservez ceux qui acceptent les types préférés (de la catégorie de type du type de données d'entrée) dans la plupart des positions où la conversion de type sera requise. Conservez tous les candidats si aucun n'accepte les types préférés. S'il ne reste qu'un seul candidat, utilisez-le; sinon, passez à l'étape suivante.

En plus du travail supplémentaire dans la résolution des types de fonction, cela ajoute une conversion supplémentaire timestamptz- ce qui non seulement ajoute plus de coûts, mais peut également introduire des problèmes avec DST conduisant à des résultats inattendus dans de rares cas. (L'heure d'été est un concept idiot, d'ailleurs, je ne saurais trop insister sur ce point.)

J'ai ajouté des démos au violon montrant le plan de requête le plus cher:

db <> violon ici

En relation:

Erwin Brandstetter
la source
7
Version encore plus courte:SELECT generate_series(timestamp '2004-03-07', '2004-08-16', '1 day') :: DATE AS day;
Václav Kužel
Que signifie la syntaxe t (jour)?
rendang
@rendang: AS t(day)dans l' SELECT * FROM func() AS t(day)alias de table et de colonne. Le ASmot-clé est bruit optionnel dans ce contexte. Voir: stackoverflow.com/a/20230716/939860
Erwin Brandstetter
35

Vous pouvez générer des séries directement avec des dates. Pas besoin d'utiliser des ints ou des horodatages:

select date::date 
from generate_series(
  '2004-03-07'::date,
  '2004-08-16'::date,
  '1 day'::interval
) date;
fbonetti
la source
En fonction de votre fuseau horaire, cela peut renvoyer un résultat inattendu. J'ai eu ce problème. Utilisez plutôt l'horodatage. SET session TIME zone 'America / Sao_Paulo' SELECT d :: date FROM generate_series ('2019-11-01' :: date, '2019-11-03' :: date, '1 day') d SELECT d :: date FROM generate_series ('2019-11-01' :: date, '2019-11-04' :: date, '1 jour') d
palhares
1

Vous pouvez également l'utiliser.

select generate_series  ( '2012-12-31'::timestamp , '2018-10-31'::timestamp , '1 day'::interval) :: date 
Meyyappan
la source