PostgreSQL: dias / meses / anos entre duas datas

95

Estou procurando uma maneira de implementar o datediff de função SQLServer no PostgreSQL. Isso é,

Esta função retorna a contagem (como um valor inteiro assinado) dos limites da parte da data especificada cruzada entre a data de início e a data de término 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

Eu sei que poderia fazer 'dd' simplesmente usando subtração, mas alguma ideia sobre os outros dois?

gefei
fonte
4
Verifique aqui
revoua

Respostas:

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;

Isso lhe dará anos, meses, dias completos ... entre duas datas:

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

Informações mais detalhadas sobre datediff .

Igor Romanchenko
fonte
8
+1 por causa da função idade, mas acho que os argumentos estão na ordem errada :)
dcarneiro
2
select date_part('month', age('2010-04-01', '2012-03-05'));-11. Essa não é uma diferença correta em meses
smac89
1
selecione date_part ('mês', idade ('2012-03-05', '2010-04-01'));
Zuko
35
isso não leva em conta meses + anos etc. Ele apenas faz uma verificação do dia corrido - ou seja, SELECT date_part('day', age('2016-10-05', '2015-10-02'))retorna3
Don Cheadle
A questão é como contar quantos cruzamentos de limite de ano e quantos cruzamentos de limite de mês há entre duas datas. Usar age()sempre será errado por anos e meses. Entre 2014-12-31e 2015-01-01atrás vai dar 0 para mês e ano, enquanto datediff()vai dar 1 e 1. Você não pode simplesmente adicionar um ao age()resultado porque age()+1vai dar 1 quando entre 2015-01-01e 2015-01-02, enquanto datediff()que agora dá 0.
André C. Andersen
152

Basta subtraí-los:

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

O resultado:

 days
------
   11
mehdi
fonte
2
Sim, isso funciona para PostgreSQL quando você precisa saber o número de dias entre duas datas.
icl7126
Ótimo! Para sua informação, usei-o para ordenar registros por intervalo menor entre duas datas, ex .:range_end - range_start ASC, id DESC
Edison Machado
1
Esteja ciente de que esse método retorna tipo interval, que não pode ser simplesmente convertido como int. Como faço para converter um intervalo em várias horas com o postgres? . Usando o combo date_part/ agefunction, como mencionado na resposta de @IgorRomanchenko, retornará typedouble precision
WebWanderer
2
Pensando bem, esta é a maneira correta. Usar esse método para obter o número de dias entre duas datas contará os dias entre meses e anos, enquanto a resposta date_part/ age, conforme aceita, fornecerá dias como a diferença na parte dos dias das duas datas. date_part('day', age('2016-9-05', '2015-10-02'))retorna 3.
WebWanderer
1
A questão não era sobre dias, mas sobre o número de limites de meses e anos que precisam ser cruzados entre duas datas. O questionador já sabia fazer dias.
André C. Andersen
41

Passei algum tempo procurando a melhor resposta e acho que consegui.

Este sql fornecerá o número de dias entre duas datas como integer:

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

..que, quando executado hoje ( 2017-3-28), me fornece:

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

O equívoco sobre a resposta aceita:

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

..is que você obterá a diferença literal entre as partes das strings de data, não a quantidade de tempo entre as duas datas.

IE:

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

Years(double precision)=-1;

Months(double precision)=-11;

Days(double precision)=-4;

WebWanderer
fonte
7
Esta deve ser a resposta aceita. O único ajuste que sugiro é usar ceil () em vez de converter em int (para arredondar dias parciais, em vez de truncar).
Rob
2
Bom ponto, @Rob. Os leitores devem tomar nota disso. Eu posso ver isso como sendo mais uma preferência. Acho que, na maioria dos meus casos, gostaria de truncar meu valor como o número literal de dias entre as datas, mas também posso ver onde o arredondamento do número de dias deve ser usado.
WebWanderer
2
Esta abordagem pode ser interrompida pelo horário de verão do Reino Unido - haverá um dia por ano com apenas 23 horas e outro com 25.
qcode peter
Uau @qcodepeter! Boa pegada! Você está absolutamente correto! Como podemos consertar isso?
WebWanderer de
1
Isso realmente não responde à pergunta. A questão é como contar quantos cruzamentos de limite de ano e quantos cruzamentos de limite de mês há entre duas datas. Essa resposta responde apenas quantos dias existem e não leva em consideração os anos bissextos.
André C. Andersen
9

Quase a mesma função que você precisava (com base na resposta de atiruz, versão abreviada de UDF a partir daqui )

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
fonte
Esta resposta está incorreta. A DATEDIFF()função em MS SQL retorna 1para datediff(year, '2015-02-14', '2016-01-03'). Isso ocorre porque você deve ultrapassar o limite do ano uma vez entre essas datas: docs.microsoft.com/en-us/sql/t-sql/functions/…
André C. Andersen
A resposta está correta porque a pergunta original era sobre PostgreSQL, não MS SQL.
Riki_tiki_tavi
Eu entendo que pode ser difícil entender, mas a questão original era encontrar "uma maneira de implementar a função SQLServer datediff no PostgreSQL". Os usuários do MS SQL Server tendem a chamá-lo apenas de SQL Server. Tente pesquisar "SQL Server" no Google: i.imgur.com/USCHdLS.png O usuário queria portar uma função de uma tecnologia específica para outra.
André C. Andersen
Desculpe, você estava certo. Parece que estava com pressa e não entendi o significado do seu primeiro comentário. A resposta foi atualizada.
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
fonte
3

A resposta de @WebWanderer é muito próxima do DateDiff usando o servidor SQL, mas imprecisa. Isso se deve ao uso da função age ().

por exemplo, dias entre '2019-07-29' e '2020-06-25' devem retornar 332, no entanto, usando a função age () ele retornará 327. Porque age () retorna '10 mons 27 dias "e trata cada mês como 30 dias, o que está incorreto.

Você deve usar o carimbo de data / hora para obter o resultado preciso. por exemplo

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

Shengfeng Li
fonte
parece ser a melhor resposta
urmurmur
1

Esta questão está cheia de mal-entendidos. Primeiro, vamos entender a questão completamente. O autor da questão quer obter o mesmo resultado que quando executando a função de MS SQL Server DATEDIFF ( datepart , startdate , enddate ) , onde datepartleva dd, mmou yy.

Esta função é definida por:

Esta função retorna a contagem (como um valor inteiro assinado) dos limites da parte da data especificada cruzada entre a data de início e a data de término especificadas.

Isso significa quantos limites de dias, limites de meses ou limites de anos foram ultrapassados. Não quantos dias, meses ou anos entre eles. É por isso que datediff(yy, '2010-04-01', '2012-03-05')é 2, e não 1. Há menos de 2 anos entre essas datas, o que significa que apenas 1 ano inteiro se passou, mas os limites de 2 anos se cruzaram, de 2010 a 2011 e de 2011 a 2012.

A seguir, minha melhor tentativa de replicar a lógica corretamente.

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

Eu gostaria de expandir a resposta de Riki_tiki_tavi e divulgar os dados. Eu criei uma função datediff que faz quase tudo que o sql server faz. Assim podemos levar em consideração qualquer unidade.

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

Aqui está um exemplo completo com saída. psql (10.1, servidor 9.5.10).

Você obtém 58, não algum valor menor que 30.
Remova a função age (), resolveu o problema que o post anterior mencionou.

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 木匠
fonte
1
Esta resposta não responde à questão de como replicar datediff (aa, '2010-04-01', '2012-03-05') = 2 e a versão do mês.
André C. Andersen
0

Mais uma solução, versão para a diferença de 'anos':

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

    2

(1 linha)

E o mesmo truque para os meses:

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

   23

(1 linha)

Na consulta da vida real, pode haver algumas sequências de carimbo de data / hora agrupadas por hora / dia / semana / etc em vez de generate_series.

Isso 'count(distinct(date_trunc('month', ts)))'pode ser usado à direita no lado 'esquerdo' do select:

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

Usei generate_series () aqui apenas por uma questão de brevidade.

Vladimir Kunschikov
fonte