Generando series de tiempo entre dos fechas en PostgreSQL

92

Tengo una consulta como esta que genera muy bien una serie de fechas entre 2 fechas determinadas:

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

Genera 162 fechas entre 2004-03-07y 2004-08-16y esto es lo que quiero. El problema con este código es que no da la respuesta correcta cuando las dos fechas son de años diferentes, por ejemplo, cuando intento 2007-02-01y 2008-04-01.

¿Existe una solución mejor?

f.ashouri
fuente

Respuestas:

175

Se puede hacer sin conversión a / desde int (pero a / desde la marca de tiempo en su lugar)

SELECT date_trunc('day', dd):: date
FROM generate_series
        ( '2007-02-01'::timestamp 
        , '2008-04-01'::timestamp
        , '1 day'::interval) dd
        ;
wildplasser
fuente
3
¿Por qué se date_truncnecesita?
Idefixx
2
Es solo una presentación. Elimina la impresión de la parte de tiempo de la marca de tiempo que siempre es ceros en este caso.
beemtee
73

Para generar una serie de fechas esta es la forma óptima :

SELECT t.day::date 
FROM   generate_series(timestamp '2004-03-07'
                     , timestamp '2004-08-16'
                     , interval  '1 day') AS t(day);
  • No date_trunc()se necesita más. La conversión a date( day::date) lo hace implícitamente.

  • Pero tampoco tiene sentido convertir literales de fecha datecomo parámetro de entrada. Au contraire, timestampes la mejor opción . La ventaja en el rendimiento es pequeña, pero no hay razón para no aprovecharla. Y ni falta que involucra a horario de verano (horario de verano) Normas junto con la conversión de datea timestamp with time zoneida y vuelta. Vea abajo.

Sintaxis corta equivalente, menos explícita:

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

O con la función de devolución de conjuntos en la SELECTlista:

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

La ASpalabra clave es necesaria en la última variante, de lo daycontrario , Postgres malinterpretaría el alias de la columna . Y no recomendaría esa variante antes de Postgres 10, al menos no con más de una función de retorno de conjunto en la misma SELECTlista:

(Aparte de eso, la última variante suele ser la más rápida por un pequeño margen).

¿Por qué timestamp [without time zone]?

Hay una serie de variantes sobrecargadas de generate_series(). Actualmente (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 (entero, entero, entero) | entero                    
generate_series (entero, entero) | entero                    
generate_series (bigint, bigint, bigint) | Empezando                     
generate_series (bigint, bigint) | Empezando                     
generate_series (numérico, numérico, numérico) | numérico                    
generate_series (numérico, numérico) | numérico                    
generate_series (marca de tiempo sin zona horaria, marca de tiempo sin zona horaria, intervalo) | marca de tiempo sin zona horaria
generate_series (marca de tiempo con zona horaria, marca de tiempo con zona horaria, intervalo) | marca de tiempo con zona horaria

( numericSe agregaron variantes con Postgres 9.5.) Las relevantes son las dos últimas en negrita tomando y regresando timestamp/ timestamptz.

No hay ninguna variante de recogida o devolucióndate . Se necesita un elenco explícito para regresar date. La llamada con timestampargumentos se resuelve directamente en la mejor variante sin descender a las reglas de resolución de tipo de función y sin conversión adicional para la entrada.

timestamp '2004-03-07'es perfectamente válido, por cierto. La parte de tiempo omitida tiene por defecto el 00:00formato ISO.

Gracias a la resolución del tipo de función todavía podemos pasar date. Pero eso requiere más trabajo de Postgres. Hay un reparto implícito de datea timestampasí como uno de datea timestamptz. Sería ambiguo, pero timestamptzes "preferido" entre los "tipos de fecha / hora". Entonces, la coincidencia se decide en el paso 4d. :

Revise todos los candidatos y mantenga aquellos que aceptan tipos preferidos (de la categoría de tipo del tipo de datos de entrada) en la mayoría de las posiciones donde se requerirá la conversión de tipo. Conserve todos los candidatos si ninguno acepta los tipos preferidos. Si solo queda un candidato, utilícelo; de lo contrario, continúe con el siguiente paso.

Además del trabajo adicional en la resolución del tipo de función, esto agrega una conversión adicional timestamptz, lo que no solo agrega más costos, sino que también puede presentar problemas con DST que conducen a resultados inesperados en casos excepcionales. (El horario de verano es un concepto estúpido, por cierto, no puedo enfatizar esto lo suficiente).

Agregué demostraciones al violín que muestran el plan de consulta más caro:

db <> violín aquí

Relacionado:

Erwin Brandstetter
fuente
7
Versión aún más corta:SELECT generate_series(timestamp '2004-03-07', '2004-08-16', '1 day') :: DATE AS day;
Václav Kužel
¿Qué significa la sintaxis t (día)?
rendang
@rendang: AS t(day)en SELECT * FROM func() AS t(day)son alias de tabla y columna. La ASpalabra clave es ruido opcional en este contexto. Ver: stackoverflow.com/a/20230716/939860
Erwin Brandstetter
35

Puede generar series directamente con fechas. No es necesario utilizar ints o marcas de tiempo:

select date::date 
from generate_series(
  '2004-03-07'::date,
  '2004-08-16'::date,
  '1 day'::interval
) date;
fbonetti
fuente
Dependiendo de su zona horaria, esto puede devolver un resultado inesperado. Tuve este problema. En su lugar, utilice la marca de tiempo. ESTABLECER la zona HORARIA de la sesión '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' :: fecha, '2019-11-04' :: fecha, '1 día') d
palhares
1

También puedes usar esto.

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