Gerando séries temporais entre duas datas no PostgreSQL

93

Tenho uma consulta como esta que gera muito bem uma série de datas entre 2 datas fornecidas:

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

Gera 162 datas entre 2004-03-07e 2004-08-16e isso que eu quero. O problema com esse código é que ele não daria a resposta certa quando as duas datas são de anos diferentes, por exemplo, quando tento 2007-02-01e 2008-04-01.

Existe uma solução melhor?

f.ashouri
fonte

Respostas:

178

Pode ser feito sem conversão para / de int (mas para / de carimbo de data / hora)

SELECT date_trunc('day', dd):: date
FROM generate_series
        ( '2007-02-01'::timestamp 
        , '2008-04-01'::timestamp
        , '1 day'::interval) dd
        ;
Wildplasser
fonte
3
porque é date_truncnecessário?
Idefixx
2
É apenas uma apresentação. Elimina a impressão da parte da hora do carimbo de data / hora, que neste caso é sempre zeros.
beemtee
76

Para gerar uma série de datas, esta é a maneira ideal :

SELECT t.day::date 
FROM   generate_series(timestamp '2004-03-07'
                     , timestamp '2004-08-16'
                     , interval  '1 day') AS t(day);
  • Adicional date_trunc()não é necessário. A conversão para date( day::date) faz isso implicitamente.

  • Mas também não faz sentido converter literais de data datecomo parâmetro de entrada. Au contraire, timestampé a melhor escolha . A vantagem em desempenho é pequena, mas não há razão para não aproveitá-la. E você não desnecessariamente envolver DST (horário de verão) Regras juntamente com a conversão de datepara timestamp with time zonee volta. Ver abaixo.

Sintaxe curta equivalente e menos explícita:

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

Ou com a função set-return na SELECTlista:

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

A ASpalavra-chave é necessária na última variante; daycaso contrário , o Postgres interpretaria incorretamente o alias da coluna . E eu não recomendaria essa variante antes do Postgres 10 - pelo menos não com mais de uma função de retorno de conjunto na mesma SELECTlista:

(Tirando isso, a última variante é normalmente mais rápida por uma pequena margem.)

Por quê timestamp [without time zone]?

Existem várias variantes sobrecarregadas de generate_series(). Atualmente (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 (inteiro, inteiro, inteiro) | inteiro                    
generate_series (inteiro, inteiro) | inteiro                    
generate_series (bigint, bigint, bigint) | bigint                     
generate_series (bigint, bigint) | bigint                     
generate_series (numérico, numérico, numérico) | numérico                    
generate_series (numérico, numérico) | numérico                    
generate_series (timestamp sem fuso horário, timestamp sem fuso horário, intervalo) | carimbo de data / hora sem fuso horário
generate_series (timestamp com fuso horário, timestamp com fuso horário, intervalo) | carimbo de data / hora com fuso horário

(as numericvariantes foram adicionadas com Postgres 9.5.) Os relevantes são os dois últimos em negrito e retornando timestamp/ timestamptz.

Não há variante de pegar ou devolverdate . Um elenco explícito é necessário para retornar date. A chamada com timestampargumentos é resolvida para a melhor variante diretamente, sem cair nas regras de resolução de tipo de função e sem conversão adicional para a entrada.

timestamp '2004-03-07'é perfeitamente válido, aliás. A parte do tempo omitida é padronizada 00:00com o formato ISO.

Graças à resolução do tipo de função ainda podemos passar date. Mas isso requer mais trabalho do Postgres. Há um elenco implícito de datepara timestampe um de datepara timestamptz. Seria ambíguo, mas timestamptzé "preferido" entre os "tipos de data / hora". Portanto, a partida é decidida na etapa 4d. :

Percorra todos os candidatos e mantenha aqueles que aceitam tipos preferidos (da categoria de tipo do tipo de dados de entrada) na maioria das posições onde a conversão de tipo será necessária. Mantenha todos os candidatos se nenhum aceitar os tipos preferidos. Se restar apenas um candidato, use-o; caso contrário, continue para a próxima etapa.

Além do trabalho extra na resolução do tipo de função, isso adiciona um elenco extra timestamptz- o que não apenas adiciona mais custo, mas também pode introduzir problemas com o DST, levando a resultados inesperados em casos raros. (DST é um conceito idiota, aliás, não consigo enfatizar isso o suficiente.) Relacionado:

Eu adicionei demonstrações ao violino mostrando o plano de consulta mais caro:

db <> fiddle aqui

Relacionado:

Erwin Brandstetter
fonte
7
Versão ainda mais curta:SELECT generate_series(timestamp '2004-03-07', '2004-08-16', '1 day') :: DATE AS day;
Václav Kužel
O que significa a sintaxe t (dia)?
rendang
@rendang: AS t(day)em SELECT * FROM func() AS t(day)são alias de tabela e coluna. A ASpalavra-chave é ruído opcional neste contexto. Consulte: stackoverflow.com/a/20230716/939860
Erwin Brandstetter
35

Você pode gerar séries diretamente com datas. Não há necessidade de usar ints ou timestamps:

select date::date 
from generate_series(
  '2004-03-07'::date,
  '2004-08-16'::date,
  '1 day'::interval
) date;
fbonetti
fonte
Dependendo do seu fuso horário, isso pode retornar um resultado inesperado. Eu tive esse problema. Em vez disso, use o carimbo de data / hora. SET fuso horário da sessão 'América / São_Paulo' SELECT d :: data FROM generate_series ('2019-11-01' :: data, '2019-11-03' :: data, '1 dia') d SELECT d :: data FROM generate_series ('2019-11-01' :: date, '2019-11-04' :: date, '1 day') d
palhares
1

Você também pode usar isso.

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