PostgreSQL: giorni / mesi / anni tra due date


94

Sto cercando un modo per implementare la datiff della funzione SQLServer in PostgreSQL. Questo è,

Questa funzione restituisce il conteggio (come valore intero con segno) dei limiti della parte data specificati attraversati tra la data di inizio e la data di fine specificate.

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

So che potrei fare "dd" semplicemente usando la sottrazione, ma qualche idea sugli altri due?


Risposte:


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;

Questo ti darà anni interi , mesi, giorni ... tra due date:

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

Informazioni datate più dettagliate .


8
+1 a causa della funzione età, ma penso che gli argomenti siano nell'ordine sbagliato :)
dcarneiro

2
select date_part('month', age('2010-04-01', '2012-03-05'));-11. Non è una differenza corretta in mesi
smac89

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

35
questo non tiene conto di mesi + anni ecc. Fa solo un controllo giornaliero continuo, ovvero SELECT date_part('day', age('2016-10-05', '2015-10-02'))restituisce3
Don Cheadle

La domanda è come contare quanti anni di attraversamenti di confine e quanti mesi di attraversamenti di confine ci sono tra due date. L'uso age()sarà sempre sbagliato farlo per anni e mesi. Tra 2014-12-31e 2015-01-01fa darà 0 per mese e anno, mentre datediff()darà 1 e 1. Non puoi aggiungerne uno al age()risultato perché age()+1darà 1 quando tra 2015-01-01e 2015-01-02, mentre datediff()ora darebbe 0.
André C. Andersen

147

Semplicemente sottraendoli:

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

Il risultato:

 days
------
   11

2
Sì, questo funziona per PostgreSQL quando è necessario conoscere il numero di giorni tra due date.
icl7126

Grande! Cordiali saluti, l'ho usato per ordinare i record in un intervallo più piccolo tra due date, es .:range_end - range_start ASC, id DESC
Edison Machado

1
Tieni presente che questo metodo restituisce il tipo interval, che non può essere semplicemente lanciato come int. Come si converte un intervallo in un numero di ore con postgres? . Utilizzando la combinazione date_part/ agefunzione, come menzionato nella risposta di @IgorRomanchenko, verrà restituito il tipodouble precision
WebWanderer

2
A pensarci bene, questo è il modo corretto. L'utilizzo di questo metodo per ottenere il numero di giorni tra due date conterà i giorni tra mesi e anni, mentre la risposta date_part/ age, come accettata, fornirà i giorni come differenza nella parte dei giorni delle due date. date_part('day', age('2016-9-05', '2015-10-02'))restituisce 3.
WebWanderer

1
La domanda non riguardava i giorni, ma il numero di confini del mese e dell'anno che devono essere superati tra due date. Il richiedente sapeva già come fare i giorni.
André C. Andersen

41

Ho passato un po 'di tempo a cercare la risposta migliore e penso di averla.

Questo sql ti darà il numero di giorni tra due date come integer:

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

..che, se eseguito oggi ( 2017-3-28), mi fornisce:

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

L'idea sbagliata sulla risposta accettata:

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'));

..è che otterrai la differenza letterale tra le parti delle stringhe di data, non la quantità di tempo tra le due date.

IE:

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

Years(double precision)=-1;

Months(double precision)=-11;

Days(double precision)=-4;


7
Questa dovrebbe essere la risposta accettata. L'unico tweak che suggerisco è di usare ceil () invece di casting a int (per arrotondare i giorni parziali, invece di troncare).
Rob

2
Buon punto @ Rob. I lettori dovrebbero prenderne atto. Lo vedo come una preferenza. Penso che nella maggior parte dei miei casi, vorrei troncare il mio valore come numero letterale di giorni tra le date, ma posso vedere dove dovrebbe essere usato anche l'arrotondamento del numero di giorni.
WebWanderer

2
Questo approccio può essere bloccato dall'ora legale del Regno Unito: ci sarà un giorno all'anno con solo 23 ore e un altro con 25.
qcode peter

Wow @qcodepeter! Buona pesca! Hai assolutamente ragione! Come lo sistemiamo?
WebWanderer

1
Questo non risponde davvero alla domanda. La domanda è come contare quanti anni di attraversamenti di confine e quanti mesi di attraversamenti di confine ci sono tra due date. Questa risposta risponde solo a quanti giorni ci sono e non tiene conto degli anni bisestili.
André C. Andersen

9

Quasi la stessa funzione di cui avevi bisogno (in base alla risposta di atiruz, versione abbreviata di UDF da qui )

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;
$$;

Utilizzo:

/* 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 */

Questa risposta non è corretta. La DATEDIFF()funzione MS SQL restituisce 1per datediff(year, '2015-02-14', '2016-01-03'). Questo perché devi passare il limite dell'anno una volta tra quelle date: docs.microsoft.com/en-us/sql/t-sql/functions/…
André C. Andersen

La risposta è corretta perché la domanda originale riguardava PostgreSQL, non MS SQL.
Riki_tiki_tavi

Capisco che possa essere difficile da capire, ma la domanda originale era trovare "un modo per implementare la funzione SQLServer datiff in PostgreSQL". Gli utenti di MS SQL Server tendono a chiamarlo semplicemente SQL Server. Prova a cercare su Google "SQL Server": i.imgur.com/USCHdLS.png L'utente desiderava trasferire una funzione da una tecnologia specifica a un'altra.
André C. Andersen

Scusa, avevi ragione. Sembra che fossi di fretta e non ho capito il significato del tuo primo commento. La risposta è stata aggiornata.
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

Risultato: 6


1

Questa domanda è piena di incomprensioni. Per prima cosa capiamo completamente la domanda. Il richiedente vuole ottenere lo stesso risultato per quando si esegue la funzione di MS SQL Server DATEDIFF ( datepart , startdate , enddate ) in cui datepartprende dd, mmo yy.

Questa funzione è definita da:

Questa funzione restituisce il conteggio (come valore intero con segno) dei limiti della parte data specificati attraversati tra la data di inizio e la data di fine specificate.

Ciò significa quanti limiti del giorno, del mese o dell'anno vengono attraversati. Non quanti giorni, mesi o anni ci sono tra di loro. Ecco perché datediff(yy, '2010-04-01', '2012-03-05')è 2 e non 1. Sono trascorsi meno di 2 anni tra quelle date, il che significa che è trascorso solo 1 anno intero, ma i confini di 2 anni sono stati oltrepassati, dal 2010 al 2011 e dal 2011 al 2012.

I seguenti sono il mio miglior tentativo di replicare correttamente la logica.

-- 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

1

Vorrei espandere la risposta di Riki_tiki_tavi e ottenere i dati là fuori. Ho creato una funzione datiff che fa quasi tutto il server sql. In questo modo possiamo prendere in considerazione qualsiasi unità.

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;
$$;

1

La risposta di @WebWanderer è molto simile a quella di DateDiff che utilizza il server SQL, ma imprecisa. Ciò è dovuto all'utilizzo della funzione age ().

ad esempio, i giorni tra "2019-07-29" e "2020-06-25" dovrebbero restituire 332, tuttavia, utilizzando la funzione age () restituirà 327. Poiché age () restituisce "10 mons 27 giorni" e tratta ogni mese come 30 giorni che non è corretto.

È necessario utilizzare il timestamp per ottenere il risultato accurato. per esempio

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


0

Ecco un esempio completo con output. psql (10.1, server 9.5.10).

Ottieni 58, non un valore inferiore a 30.
Rimuovi la funzione age (), risolto il problema menzionato nel post precedente.

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

1
Questa risposta non risponde alla domanda su come replicare datiff (yy, '2010-04-01', '2012-03-05') = 2 e la versione del mese.
André C. Andersen

0

Un'altra soluzione, versione per la differenza degli anni:

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

    2

(1 riga)

E lo stesso trucco per i mesi:

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

   23

(1 riga)

Nella query della vita reale ci possono essere alcune sequenze di timestamp raggruppate per ora / giorno / settimana / ecc invece di generate_series.

Questo 'count(distinct(date_trunc('month', ts)))'può essere utilizzato a destra nella parte "sinistra" della selezione:

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

Ho usato generate_series () qui solo per la brevità.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.