PostgreSQL: дни / месяцы / годы между двумя датами


94

Я ищу способ реализовать SQLServer-функцию dateiff в PostgreSQL. То есть,

Эта функция возвращает количество (в виде целого числа со знаком) указанных границ datepart, пересеченных между указанными startdate и enddate.

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

Я знаю, что могу сделать 'dd', просто используя вычитание, но есть ли идеи о двух других?


Ответы:


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;

Это даст вам полные годы, месяц, дни ... между двумя датами:

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

Более подробная информация о датировке .


8
+1 из-за функции возраста, но я думаю, что это аргументы в неправильном порядке :)
dcarneiro

2
select date_part('month', age('2010-04-01', '2012-03-05'));дает -11. Это неправильная разница в месяцах
smac89

1
выберите date_part ('месяц', возраст ('2012-03-05', '2010-04-01'));
Зуко

35
это не учитывает месяцы + годы и т. д. Это просто проверка скользящего дня, т.е. SELECT date_part('day', age('2016-10-05', '2015-10-02'))возврат3
Дон Чидл

Вопрос в том, как подсчитать, сколько пересечений границы в год и сколько месяцев пересечений границы произошло между двумя датами. Использование age()всегда будет неправильным делать это годами и месяцами. Между 2014-12-31и 2015-01-01назад даст 0 для месяца и года, тогда как datediff()даст 1 и 1. Вы не можете просто прибавить единицу к age()результату, потому age()+1что даст 1, когда между 2015-01-01и 2015-01-02, тогда как datediff()теперь дает 0.
Андре К. Андерсен

147

Просто вычтите их:

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

Результат:

 days
------
   11

2
Да, это работает для PostgreSQL, когда вам нужно знать количество дней между двумя датами.
icl7126 06

Большой! К вашему сведению, я использовал его, чтобы упорядочить записи по меньшему диапазону между двумя датами, например:range_end - range_start ASC, id DESC
Эдисон Мачадо,

1
Имейте в виду interval, что этот метод возвращает тип , который нельзя просто преобразовать как int. Как мне преобразовать интервал в количество часов с помощью postgres? . Использование комбо date_part/ agefunction, как указано в ответе @IgorRomanchenko, вернет типdouble precision
WebWanderer

2
Если подумать, это правильный путь. Использование этого метода для получения количества дней между двумя датами будет считать дни между месяцами и годами, а ответ date_part/ age, как принято, предоставит дни как разницу в днях, составляющих две даты. date_part('day', age('2016-9-05', '2015-10-02'))возвращает 3.
WebWanderer

1
Вопрос не в днях, а в количестве месяцев и годов, которые должны быть пересечены между двумя датами. Спрашивающий уже знал, как делать дни.
André C. Andersen

41

Я потратил некоторое время на поиск лучшего ответа, и я думаю, что он у меня есть.

Этот sql даст вам количество дней между двумя датами как integer:

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

..который при запуске today ( 2017-3-28) предоставляет мне:

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

Заблуждение о принятом ответе:

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

... вы получите буквальную разницу между частями строк даты, а не количество времени между двумя датами.

IE:

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

Years(double precision)=-1;

Months(double precision)=-11;

Days(double precision)=-4;


7
Это должен быть принятый ответ. Единственная настройка, которую я предлагаю, - использовать ceil () вместо преобразования в int (чтобы округлить неполные дни вместо усечения).
Роб

2
Хороший вопрос @Rob. Читатели должны это отметить. Я считаю, что это скорее предпочтение. Я думаю, что в большинстве моих случаев я бы хотел усечь свое значение как буквальное количество дней между датами, но я вижу, где также следует использовать округление количества дней.
WebWanderer

2
Этот подход может быть сбит с толку из-за перехода на летнее время в Великобритании - один день в году будет
длиться

Вау @qcodepeter! Хороший улов! Вы абсолютно правы! Как это исправить?
WebWanderer

1
Это не совсем ответ на вопрос. Вопрос в том, как подсчитать, сколько пересечений границы за год и сколько месяцев пересечений границы произошло между двумя датами. Этот ответ отвечает только за количество дней и не учитывает високосные годы.
Андре К. Андерсен

9

Почти та же функция, что и вам (на основе ответа атируза, сокращенная версия UDF отсюда )

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

Применение:

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

Это неверный ответ. DATEDIFF()Функции в MS SQL возвращает 1для datediff(year, '2015-02-14', '2016-01-03'). Это связано с тем, что вы должны один раз пройти границу года между этими датами: docs.microsoft.com/en-us/sql/t-sql/functions/…
André C. Andersen

Ответ правильный, потому что исходный вопрос касался PostgreSQL, а не MS SQL.
Riki_tiki_tavi

Я понимаю, что это может быть сложно уловить, но исходный вопрос заключался в том, чтобы найти «способ реализовать SQLServer-функцию dateiff в PostgreSQL». Пользователи MS SQL Server обычно называют его просто SQL Server. Попробуйте поискать в Google "SQL Server": i.imgur.com/USCHdLS.png Пользователь хотел перенести функцию с одной конкретной технологии на другую.
Андре К. Андерсен

Простите, вы были правы. Похоже, я торопился и не понял смысла вашего первого комментария. Ответ был обновлен.
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

Результат: 6


1

Этот вопрос полон недоразумений. Сначала давайте полностью поймем вопрос. Запрашивающий хочет получить тот же результат, что и при запуске функции MS SQL Server, DATEDIFF ( datepart , startdate , enddate ) гдеdatepart принимает dd, mmили yy.

Эта функция определяется:

Эта функция возвращает количество (в виде целого числа со знаком) указанных границ datepart, пересеченных между указанными startdate и enddate.

Это означает, сколько дней, месяцев или лет пересекаются. Не сколько дней, месяцев или лет между ними. Поэтомуdatediff(yy, '2010-04-01', '2012-03-05') 2, а не 1. Между этими датами меньше 2 лет, то есть прошел только 1 полный год, но пересеклись 2-летние границы: с 2010 по 2011 и с 2011 по 2012.

Следующее - моя лучшая попытка правильно воспроизвести логику.

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

Я хотел бы расширить ответ Riki_tiki_tavi и получить данные оттуда. Я создал функцию dateiff, которая делает почти все, что делает sql server. Таким образом, мы можем учесть любую единицу.

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

Ответ @WebWanderer очень близок к DateDiff с использованием SQL-сервера, но неточен. Это из-за использования функции age ().

например, дней между '2019-07-29' и '2020-06-25' должно возвращать 332, однако, используя функцию age (), она вернет 327. Поскольку age () возвращает '10 mons 27 days "и обрабатывает каждый месяц как 30 дней, что неверно.

Вы должны использовать временную метку, чтобы получить точный результат. например

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


0

Вот полный пример с выводом. psql (10.1, сервер 9.5.10).

Вы получите 58, а не какое-то значение меньше 30.
Функция удаления age () решила проблему, упомянутую в предыдущем посте.

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
Этот ответ не отвечает на вопрос о том, как реплицировать dateiff (yy, '2010-04-01', '2012-03-05') = 2 и версию для месяца.
Андре К. Андерсен

0

Еще одно решение, версия для разницы в "годах":

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

    2

(1 ряд)

И тот же трюк на месяцы:

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

   23

(1 ряд)

В реальном запросе может быть несколько последовательностей временных меток, сгруппированных по часам / дням / неделям и т. Д. Вместо generate_series.

Это 'count(distinct(date_trunc('month', ts)))'можно использовать прямо в «левой» части выбора:

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

Я использовал здесь generate_series () для краткости.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.