PostgreSQL: días / meses / años entre dos fechas

94

Estoy buscando una forma de implementar la función de SQLServer dateiff en PostgreSQL. Es decir,

Esta función devuelve el recuento (como un valor entero con signo) de los límites de la parte de fecha especificados cruzados entre la fecha de inicio y la fecha de finalización especificadas.

datediff(dd, '2010-04-01', '2012-03-05') = 704 // 704 changes of day in this interval
datediff(mm, '2010-04-01', '2012-03-05') = 23  // 23 changes of month
datediff(yy, '2010-04-01', '2012-03-05') = 2   // 2 changes of year

Sé que podría hacer 'dd' simplemente usando la resta, pero ¿alguna idea sobre los otros dos?

gefei
fuente
4
Compruebe aquí
revoua

Respuestas:

116
SELECT
  AGE('2012-03-05', '2010-04-01'),
  DATE_PART('year', AGE('2012-03-05', '2010-04-01')) AS years,
  DATE_PART('month', AGE('2012-03-05', '2010-04-01')) AS months,
  DATE_PART('day', AGE('2012-03-05', '2010-04-01')) AS days;

Esto te dará años completos , meses, días ... entre dos fechas:

          age          | years | months | days
-----------------------+-------+--------+------
 1 year 11 mons 4 days |     1 |     11 |    4

Información más detallada sobre la fecha .

Igor Romanchenko
fuente
8
+1 debido a la función de edad, pero creo que sus argumentos están en el orden incorrecto :)
dcarneiro
2
select date_part('month', age('2010-04-01', '2012-03-05'));da -11. Esa no es una diferencia correcta en meses
smac89
1
seleccione date_part ('mes', edad ('2012-03-05', '2010-04-01'));
Zuko
35
esto no tiene en cuenta meses + años, etc. Solo hace una verificación diaria continua - es decir, SELECT date_part('day', age('2016-10-05', '2015-10-02'))devoluciones3
Don Cheadle
La pregunta es cómo contar cuántos cruces de límites anuales y cuántos cruces de límites mensuales hay entre dos fechas. Usar age()siempre será incorrecto para hacer esto durante años y meses. Entre 2014-12-31y 2015-01-01ago dará 0 para mes y año, mientras datediff()que dará 1 y 1. No se puede simplemente agregar uno al age()resultado porque age()+1dará 1 cuando entre 2015-01-01y 2015-01-02, mientras datediff()que ahora da 0.
André C. Andersen
147

Simplemente réstelos:

SELECT ('2015-01-12'::date - '2015-01-01'::date) AS days;

El resultado:

 days
------
   11
mehdi
fuente
2
Sí, esto funciona para PostgreSQL cuando necesita saber el número de días entre dos fechas.
icl7126
¡Excelente! Para su información, lo usé para ordenar registros por rango más pequeño entre dos fechas, por ejemplo:range_end - range_start ASC, id DESC
Edison Machado
1
Tenga en cuenta que este método devuelve el tipo interval, que no se puede convertir simplemente como int. ¿Cómo convierto un intervalo en varias horas con postgres? . Usando el combo date_part/ agefunction, como se menciona en la respuesta de @IgorRomanchenko, se devolverá el tipodouble precision
WebWanderer
2
Pensándolo bien, esta es la forma correcta. El uso de este método para obtener el número de días entre dos fechas contará los días entre meses y años, mientras que date_part/ ageanswer, según se acepta, proporcionará días como la diferencia entre los días de las dos fechas. date_part('day', age('2016-9-05', '2015-10-02'))devuelve 3.
WebWanderer
1
La pregunta no se trata de días, se trata de la cantidad de límites de meses y años que se deben cruzar entre dos fechas. El autor de la pregunta ya sabía hacer días.
André C. Andersen
41

Pasé un tiempo buscando la mejor respuesta y creo que la tengo.

Este sql le dará el número de días entre dos fechas como integer:

SELECT
    (EXTRACT(epoch from age('2017-6-15', now())) / 86400)::int

..que, cuando se ejecuta hoy ( 2017-3-28), me proporciona:

?column?
------------
77

El concepto erróneo sobre la respuesta aceptada:

select age('2010-04-01', '2012-03-05'),
   date_part('year',age('2010-04-01', '2012-03-05')),
   date_part('month',age('2010-04-01', '2012-03-05')),
   date_part('day',age('2010-04-01', '2012-03-05'));

... es que obtendrá la diferencia literal entre las partes de las cadenas de fechas, no la cantidad de tiempo entre las dos fechas.

ES DECIR:

Age(interval)=-1 years -11 mons -4 days;

Years(double precision)=-1;

Months(double precision)=-11;

Days(double precision)=-4;

WebWanderer
fuente
7
Esta debería ser la respuesta aceptada. El único ajuste que sugiero es usar ceil () en lugar de convertir a int (para redondear días parciales, en lugar de truncar).
Rob
2
Buen punto @Rob. Los lectores deben tomar nota de esto. Puedo ver esto como una preferencia. Creo que en la mayoría de mis casos, querría truncar mi valor como el número literal de días entre fechas, pero puedo ver dónde se debería usar también el redondeo de la cantidad de días.
WebWanderer
2
Este enfoque puede verse afectado por el horario de verano del Reino Unido: habrá un día al año con solo 23 horas y otro con 25.
qcode peter
¡Guau @qcodepeter! ¡Buena atrapada! ¡Tienes toda la razón! ¿Cómo arreglamos eso?
WebWanderer
1
Esto realmente no responde a la pregunta. La pregunta es cómo contar cuántos cruces de límites anuales y cuántos cruces de límites mensuales hay entre dos fechas. Esta respuesta solo responde cuántos días hay y no tiene en cuenta los años bisiestos.
André C. Andersen
9

Casi la misma función que necesitaba (según la respuesta de atiruz, versión abreviada de UDF desde aquí )

CREATE OR REPLACE FUNCTION datediff(type VARCHAR, date_from DATE, date_to DATE) RETURNS INTEGER LANGUAGE plpgsql
AS
$$
DECLARE age INTERVAL;
BEGIN
    CASE type
        WHEN 'year' THEN
            RETURN date_part('year', date_to) - date_part('year', date_from);
        WHEN 'month' THEN
            age := age(date_to, date_from);
            RETURN date_part('year', age) * 12 + date_part('month', age);
        ELSE
            RETURN (date_to - date_from)::int;
    END CASE;
END;
$$;

Uso:

/* Get months count between two dates */
SELECT datediff('month', '2015-02-14'::date, '2016-01-03'::date);
/* Result: 10 */

/* Get years count between two dates */
SELECT datediff('year', '2015-02-14'::date, '2016-01-03'::date);
/* Result: 1 */

/* Get days count between two dates */
SELECT datediff('day', '2015-02-14'::date, '2016-01-03'::date);
/* Result: 323 */

/* Get months count between specified and current date */
SELECT datediff('month', '2015-02-14'::date, NOW()::date);
/* Result: 47 */
Riki_tiki_tavi
fuente
Esta respuesta es incorrecta. La DATEDIFF()función en MS SQL devuelve 1a datediff(year, '2015-02-14', '2016-01-03'). Esto se debe a que debe pasar el límite del año una vez entre esas fechas: docs.microsoft.com/en-us/sql/t-sql/functions/…
André C. Andersen
La respuesta es correcta porque la pregunta original era sobre PostgreSQL, no sobre MS SQL.
Riki_tiki_tavi
Entiendo que puede ser difícil de entender, pero la pregunta original era encontrar "una manera de implementar la función de SQLServer con fecha en PostgreSQL". Los usuarios de MS SQL Server tienden a llamarlo simplemente SQL Server. Intente buscar en Google "SQL Server": i.imgur.com/USCHdLS.png El usuario quería migrar una función de una tecnología específica a otra.
André C. Andersen
Lo siento, tenías razón. Parece que me estaba apurando y no entendí el significado de tu primer comentario. La respuesta se actualizó.
Riki_tiki_tavi
7
SELECT date_part ('year', f) * 12
     + date_part ('month', f)
FROM age ('2015-06-12'::DATE, '2014-12-01'::DATE) f

Resultado: 6

atiruz
fuente
1

Esta pregunta está llena de malentendidos. Primero, entendamos la pregunta completamente. El autor de la pregunta quiere obtener el mismo resultado que para cuando se ejecuta la función de MS SQL Server DATEDIFF ( datepart , startdate , enddate ) , donde dateparttoma dd, mmo yy.

Esta función está definida por:

Esta función devuelve el recuento (como un valor entero con signo) de los límites de la parte de fecha especificados cruzados entre la fecha de inicio y la fecha de finalización especificadas.

Eso significa cuántos límites de días, meses o años se cruzan. No cuántos días, meses o años hay entre ellos. Es por eso que datediff(yy, '2010-04-01', '2012-03-05')es 2, y no 1. Hay menos de 2 años entre esas fechas, lo que significa que solo ha pasado 1 año completo, pero se han cruzado límites de 2 años, de 2010 a 2011 y de 2011 a 2012.

Los siguientes son mi mejor intento de replicar la lógica correctamente.

-- datediff(dd`, '2010-04-01', '2012-03-05') = 704 // 704 changes of day in this interval
select ('2012-03-05'::date - '2010-04-01'::date );
-- 704 changes of day

-- datediff(mm, '2010-04-01', '2012-03-05') = 23  // 23 changes of month
select (date_part('year', '2012-03-05'::date) - date_part('year', '2010-04-01'::date)) * 12 + date_part('month', '2012-03-05'::date) - date_part('month', '2010-04-01'::date)
-- 23 changes of month

-- datediff(yy, '2010-04-01', '2012-03-05') = 2   // 2 changes of year
select date_part('year', '2012-03-05'::date) - date_part('year', '2010-04-01'::date);
-- 2 changes of year
André C. Andersen
fuente
1

Me gustaría ampliar la respuesta de Riki_tiki_tavi y obtener los datos. He creado una función dateiff que hace casi todo lo que hace el servidor SQL. De esa forma podemos tener en cuenta cualquier unidad.

create function datediff(units character varying, start_t timestamp without time zone, end_t timestamp without time zone) returns integer
language plpgsql
 as
 $$
DECLARE
 diff_interval INTERVAL; 
 diff INT = 0;
 years_diff INT = 0;
BEGIN
 IF units IN ('yy', 'yyyy', 'year', 'mm', 'm', 'month') THEN
   years_diff = DATE_PART('year', end_t) - DATE_PART('year', start_t);

   IF units IN ('yy', 'yyyy', 'year') THEN
     -- SQL Server does not count full years passed (only difference between year parts)
     RETURN years_diff;
   ELSE
     -- If end month is less than start month it will subtracted
     RETURN years_diff * 12 + (DATE_PART('month', end_t) - DATE_PART('month', start_t)); 
   END IF;
 END IF;

 -- Minus operator returns interval 'DDD days HH:MI:SS'  
 diff_interval = end_t - start_t;

 diff = diff + DATE_PART('day', diff_interval);

 IF units IN ('wk', 'ww', 'week') THEN
   diff = diff/7;
   RETURN diff;
 END IF;

 IF units IN ('dd', 'd', 'day') THEN
   RETURN diff;
 END IF;

 diff = diff * 24 + DATE_PART('hour', diff_interval); 

 IF units IN ('hh', 'hour') THEN
    RETURN diff;
 END IF;

 diff = diff * 60 + DATE_PART('minute', diff_interval);

 IF units IN ('mi', 'n', 'minute') THEN
    RETURN diff;
 END IF;

 diff = diff * 60 + DATE_PART('second', diff_interval);

 RETURN diff;
END;
$$;
Daniel L. VanDenBosch
fuente
1

La respuesta de @WebWanderer es muy cercana al DateDiff usando el servidor SQL, pero inexacta. Eso se debe al uso de la función age ().

por ejemplo, los días entre '2019-07-29' y '2020-06-25' deberían devolver 332, sin embargo, al usar la función age (), devolverá 327. Porque age () devuelve '10 mons 27 días "y trata cada mes como 30 días, lo cual es incorrecto.

Debe utilizar la marca de tiempo para obtener un resultado preciso. p.ej

ceil((select extract(epoch from (current_date::timestamp - <your_date>::timestamp)) / 86400))

Shengfeng Li
fuente
0

Aquí hay un ejemplo completo con salida. psql (10.1, servidor 9.5.10).

Obtienes 58, no un valor menor que 30.
Eliminar la función age (), resolvió el problema que mencionaba la publicación anterior.

drop table t;
create table t(
    d1 date
);

insert into t values(current_date - interval '58 day');

select d1
, current_timestamp - d1::timestamp date_diff
, date_part('day', current_timestamp - d1::timestamp)
from t;

     d1     |        date_diff        | date_part
------------+-------------------------+-----------
 2018-05-21 | 58 days 21:41:07.992731 |        58
Charlie 木匠
fuente
1
Esta respuesta no responde a la pregunta de cómo replicar la versión de fecha y mes (yy, '2010-04-01', '2012-03-05') = 2.
André C. Andersen
0

Una solución más, versión para la diferencia de 'años':

SELECT count(*) - 1 FROM (SELECT distinct(date_trunc('year', generate_series('2010-04-01'::timestamp, '2012-03-05', '1 week')))) x

    2

(1 fila)

Y el mismo truco para los meses:

SELECT count(*) - 1 FROM (SELECT distinct(date_trunc('month', generate_series('2010-04-01'::timestamp, '2012-03-05', '1 week')))) x

   23

(1 fila)

En la consulta de la vida real, puede haber algunas secuencias de marcas de tiempo agrupadas por hora / día / semana / etc.en lugar de generate_series.

Esto 'count(distinct(date_trunc('month', ts)))'se puede usar a la derecha en el lado 'izquierdo' de la selección:

SELECT sum(a - b)/count(distinct(date_trunc('month', c))) FROM d

Usé generate_series () aquí solo por brevedad.

Vladimir Kunschikov
fuente