PostgreSQL: dni / miesiące / lata między dwiema datami

95

Szukam sposobu na zaimplementowanie funkcji Datediff SQLServer w PostgreSQL. To jest,

Ta funkcja zwraca liczbę (jako wartość całkowitą ze znakiem) określonych granic części daty, które zostały przekroczone między określoną datą początkową i końcową.

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

Wiem, że mógłbym zrobić „dd” po prostu używając odejmowania, ale masz jakieś pojęcie o pozostałych dwóch?

gefei
źródło
4
Sprawdź tutaj
revoua

Odpowiedzi:

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

W ten sposób otrzymasz pełne lata, miesiąc, dni ... między dwiema datami:

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

Bardziej szczegółowe informacje o datach .

Igor Romanchenko
źródło
8
+1 z powodu funkcji wieku, ale myślę, że jej argumenty są w złej kolejności :)
dcarneiro
2
select date_part('month', age('2010-04-01', '2012-03-05'));daje -11. To nie jest poprawna różnica w miesiącach
smac89
1
select date_part ('miesiąc', wiek ('2012-03-05', '2010-04-01'));
Zuko
35
to nie uwzględnia miesięcy + lat itp. Po prostu wykonuje bieżącą kontrolę dnia - tj. SELECT date_part('day', age('2016-10-05', '2015-10-02'))zwroty3
Don Cheadle
1
Pytanie brzmi, jak policzyć, ile lat przekracza granice, a ile miesięcy występuje między dwiema datami. Używanie age()zawsze będzie złe przez lata i miesiące. Czas między 2014-12-31i 2015-01-01temu da 0 dla miesiąca i roku, a datediff()da 1 i 1. Nie możesz po prostu dodać jedynki do age()wyniku, ponieważ age()+1da 1 między 2015-01-01a 2015-01-02, a datediff()teraz daje 0.
André C. Andersen
152

Po prostu je odejmij:

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

Wynik:

 days
------
   11
Mehdi
źródło
2
Tak, działa to w przypadku PostgreSQL, gdy potrzebujesz znać liczbę dni między dwiema datami.
icl7126
Świetny! Do Twojej wiadomości użyłem go do uporządkowania płyt według mniejszego zakresu między dwoma datami, np .:range_end - range_start ASC, id DESC
Edison Machado
1
Należy pamiętać, że ta metoda zwraca typ interval, którego nie można po prostu rzutować jako int. Jak przekonwertować interwał na liczbę godzin za pomocą postgresów? . Użycie kombinacji funkcji date_part/ age, jak wspomniano w odpowiedzi @IgorRomanchenko, zwróci typdouble precision
WebWanderer
2
Po namyśle, to jest właściwy sposób. Użycie tej metody do uzyskania liczby dni między dwiema datami spowoduje policzenie dni między miesiącami i latami, podczas gdy zaakceptowana odpowiedź date_part/ agepoda dni jako różnicę w części dni w dwóch datach. date_part('day', age('2016-9-05', '2015-10-02'))zwraca 3.
WebWanderer
1
Pytanie nie dotyczyło dni, chodziło o liczbę granic miesięcy i lat, które należy przekroczyć między dwiema datami. Pytający wiedział już, jak robić dni.
André C. Andersen
41

Spędziłem trochę czasu na poszukiwaniu najlepszej odpowiedzi i myślę, że ją mam.

Ten sql poda liczbę dni między dwiema datami jako integer:

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

.. który po uruchomieniu dzisiaj ( 2017-3-28) zapewnia mi:

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

Błędne przekonanie o zaakceptowanej odpowiedzi:

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

..jest to, że otrzymasz dosłowną różnicę między częściami ciągów dat, a nie okres między dwiema datami.

TO ZNACZY:

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

Years(double precision)=-1;

Months(double precision)=-11;

Days(double precision)=-4;

WebWanderer
źródło
7
To powinna być akceptowana odpowiedź. Jedyne ulepszenie, które sugeruję, to użycie ceil () zamiast rzutowania na int (aby zaokrąglić częściowe dni zamiast obcinania).
Rob
2
Słuszna uwaga @Rob. Czytelnicy powinni to odnotować. Widzę to jako bardziej preferowane. Myślę, że w większości moich przypadków chciałbym skrócić moją wartość jako dosłowną liczbę dni między datami, ale widzę, gdzie należy również zastosować zaokrąglenie liczby dni.
WebWanderer
2
To podejście może zostać odrzucone przez czas letni w Wielkiej Brytanii - jeden dzień w roku będzie miał tylko 23 godziny, a inny 25.
qcode peter
Wow @qcodepeter! Dobry chwyt! Masz całkowitą rację! Jak to naprawimy?
WebWanderer
1
To naprawdę nie odpowiada na pytanie. Pytanie brzmi, jak policzyć, ile lat przekracza granice, a ile miesięcy występuje między dwiema datami. Ta odpowiedź dotyczy tylko liczby dni i nie uwzględnia lat przestępnych.
André C. Andersen
9

Prawie taka sama funkcja, jakiej potrzebujesz (na podstawie odpowiedzi atiruza, skrócona wersja UDF stąd )

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

Stosowanie:

/* 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
źródło
Ta odpowiedź jest nieprawidłowa. DATEDIFF()Funkcja MS SQL wraca 1do datediff(year, '2015-02-14', '2016-01-03'). Dzieje się tak, ponieważ musisz raz przekroczyć granicę roku między tymi datami: docs.microsoft.com/en-us/sql/t-sql/functions/ ...
André C. Andersen
Odpowiedź jest prawidłowa, ponieważ pierwotne pytanie dotyczyło PostgreSQL, a nie MS SQL.
Riki_tiki_tavi
Rozumiem, że może to być trudne do zrozumienia, ale pierwotnym pytaniem było znalezienie „sposobu na zaimplementowanie funkcji SQLServer datediff w PostgreSQL”. Użytkownicy MS SQL Server zwykle nazywają go SQL Server. Spróbuj wygooglować „SQL Server”: i.imgur.com/USCHdLS.png Użytkownik chciał przenieść funkcję z jednej określonej technologii do innej.
André C. Andersen
Przepraszam, miałeś rację. Wygląda na to, że się spieszyłem i nie rozumiałem znaczenia twojego pierwszego komentarza. Odpowiedź została zaktualizowana.
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

Wynik: 6

atiruz
źródło
3

Odpowiedź @WebWanderer jest bardzo zbliżona do DateDiff przy użyciu serwera SQL, ale jest niedokładna. Dzieje się tak z powodu użycia funkcji age ().

np. dni między „2019-07-29” a „2020-06-25” powinny zwrócić 332, jednak użycie funkcji age () zwróci 327. Ponieważ funkcja age () zwraca „10 mons 27 dni” i traktuje co miesiąc jako 30 dni, co jest niepoprawne.

Aby uzyskać dokładny wynik, należy użyć sygnatury czasowej. na przykład

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

Shengfeng Li
źródło
wygląda na najlepszą odpowiedź
urmurmur
1

To pytanie jest pełne nieporozumień. Najpierw w pełni zrozumiemy pytanie. Pytający chce uzyskać taki sam efekt, jak w przypadku, gdy uruchomienie funkcji serwera MS SQL DATEDIFF ( datepart , startdate , enddate ) , gdzie datepartzaczyna dd, mmalbo yy.

Tę funkcję definiują:

Ta funkcja zwraca liczbę (jako wartość całkowitą ze znakiem) określonych granic części daty, które zostały przekroczone między określoną datą początkową i końcową.

Oznacza to, ile granic dni, miesięcy lub lat zostało przekroczonych. Nie ile dni, miesięcy czy lat jest między nimi. Dlatego jest datediff(yy, '2010-04-01', '2012-03-05')to 2, a nie 1. Pomiędzy tymi datami są mniej niż 2 lata, co oznacza, że ​​minął tylko 1 rok, ale przekroczyły 2 lata, od 2010 do 2011 i od 2011 do 2012.

Oto moja najlepsza próba poprawnego odtworzenia logiki.

-- 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
źródło
1

Chciałbym rozwinąć odpowiedź Riki_tiki_tavi i pobrać tam dane. Stworzyłem funkcję Datediff, która robi prawie wszystko, co robi serwer sql. W ten sposób możemy wziąć pod uwagę dowolną jednostkę.

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
źródło
0

Oto kompletny przykład z danymi wyjściowymi. psql (10.1, serwer 9.5.10).

Otrzymujesz 58, nie mniej niż 30.
Usuń funkcję age (), rozwiązując problem, o którym wspominał poprzedni post.

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 木匠
źródło
1
Ta odpowiedź nie odpowiada na pytanie, jak replikować datę i datę (yy, „2010-04-01”, „2012-03-05”) = 2 i wersję miesiąca.
André C. Andersen
0

Jeszcze jedno rozwiązanie, wersja na różnicę lat:

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

    2

(1 rząd)

I ta sama sztuczka na miesiące:

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

   23

(1 rząd)

W prawdziwym zapytaniu mogą występować sekwencje znaczników czasu pogrupowane według godziny / dnia / tygodnia / itp. Zamiast generowania_serii.

To 'count(distinct(date_trunc('month', ts)))'może być stosowany bezpośrednio w „left” stronie wybierz:

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

Użyłem tutaj wygeneruj_series () tylko dla zwięzłości.

Vladimir Kunschikov
źródło