Функция age () в PostgreSQL: разные / неожиданные результаты при посадке в другом месяце - PullRequest
0 голосов
/ 04 июля 2018

Сегодня я столкнулся с необъяснимым результатом в PostgreSQL 9.6 при выполнении этого запроса:

SELECT age('2018-06-30','2018-05-19') AS one,
       age('2018-07-01','2018-05-20') AS two; 

Ожидаемые результаты для обоих столбцов: 1 mon 11 days. Однако, только за интервал с 2018-05-19 по 2018-06-30 я получаю то, что ожидаю, а за 2018-05-20 до 2018-07-01 я получу еще один день: 1 mon 12 days

Я не понимаю, почему это так, и, насколько я понимаю, между 2018-05-20 и 2018-07-01 это просто интервал 1 mon 11 days, и результат Postgres здесь неверный.

Я не могу найти подробную информацию о том, как именно работает функция PostgreSQL- age(timestamp,timestamp). Тем не менее, я предположил, что функция выполняет что-то вроде: переходите от даты начала к шагу месяца, пока не достигнете конца месяца. Оттуда перейдите в день даты окончания. Подведите итоги по месяцам и дням.

Итак, в моем понимании, это то, что должно происходить в моем случае под капотом (извините, за столь многословность, но я чувствую, что это необходимо):

Начало в 2018-05-19. Идите на месяц вперед. Земля в 2018-06-19. Пройдите N дней вперед, пока не достигнете 2018-06-30:

1 day: 20
2 days: 21
3 days: 22
4 days: 23
5 days: 24
6 days: 25
7 days: 26
8 days: 27
9 days: 28
10 days: 29
11 days: 30

= 1 month 11 days.

Для времени между 2018-05-20 и 2018-07-01 оно должно быть почти таким же:

Начало в 2018-05-20. Идите на месяц вперед. Земля в 2018-06-20. Пройдите N дней вперед, пока не достигнете 2018-07-01:

1 day: 21
2 days: 22
3 days: 23
4 days: 24
5 days: 25
6 days: 26
7 days: 27
8 days: 28
9 days: 29
10 days: 30
11 days: 1

= 1 month 11 days.

Это моя ошибка или одна из PostgreSQL? Существуют ли альтернативные функции / алгоритмы, которые работают так, как я описал / ожидал?

Ответы [ 3 ]

0 голосов
/ 04 июля 2018

age рассчитывается функцией timestamptz_age в src/backend/utils/adt/timestamp.c. Комментарий гласит:

/* timestamptz_age()
 * Calculate time difference while retaining year/month fields.
 * Note that this does not result in an accurate absolute time span
 *  since year and month are out of context once the arithmetic
 *  is done.
 */

Код сначала преобразует аргументы в struct pg_tm переменные tm1 и tm2 (struct pg_tm аналогичен struct tm библиотеки C, но имеет дополнительные поля часового пояса), а затем вычисляет разницу tm за поле.

В случае age('2018-07-01','2018-05-20') соответствующие поля этой разницы будут выглядеть следующим образом:

tm_mday = -19
tm_mon  =   2
tm_year =   0

Теперь отрицательные поля корректируются. для tm_mday код выглядит так:

while (tm->tm_mday < 0)
{
    if (dt1 < dt2)
    {
        tm->tm_mday += day_tab[isleap(tm1->tm_year)][tm1->tm_mon - 1];
        tm->tm_mon--;
    }
    else
    {
        tm->tm_mday += day_tab[isleap(tm2->tm_year)][tm2->tm_mon - 1];
        tm->tm_mon--;
    }
}

Начиная с dt1 > dt2, берется ветвь else, и код добавляет количество дней в мае (31) и уменьшает месяц на 1, заканчивая

tm_mday = 12
tm_mon  =  1
tm_year =  0

Вот результат, который вы получите.

Теперь, на первый взгляд, кажется, что tm2->tm_mon не правильный месяц для выбора, и было бы лучше взять предыдущий месяц из левого аргумента:

day_tab[isleap(tm1->tm_year)][(tm1->tm_mon + 10) % 12]

Но я не могу сказать, будет ли этот выбор лучше во всех случаях, и в любом случае комментарий освобождает функцию, поэтому я не решусь назвать это ошибкой.

Возможно, вы захотите заняться списком рассылки хакеров.

0 голосов
/ 04 июля 2018

Для тех, кто заинтересован: я думаю, что нашел решение этой проблемы, используя функцию, которая дает мне желаемый результат. Он работает по моим собственным тестам, даже в течение високосных лет, но, к сожалению, я не могу гарантировать, что он всегда будет работать. Это также кажется немного хакерским.

CREATE OR REPLACE FUNCTION age_forward ("endDate" date,"startDate" date) 
     RETURNS interval AS $$

     /*

     Basic approach: actually do a culculation like this:
     SELECT age('2018-07-01','2018-06-01') + ((30 - 20) + 1||' days')::interval;

     So, basically:
     (1) truncate start and end to month level, so always FIRST of month
     (2) add one month to the start month
     (3) calculate the days
     (4) add the days as string and build the interval

     The crucial part is 3: calculate the days

     We do it like this:
     - get the number of days for the month in question. The month in question is the month BEFORE the end month. For our example it is JUNE
     - subtract the start date day number from the number of days (here 20)
     - add the end date day number (here 1)

     */

     SELECT CASE 

        /* First step: Check if the startDate day number is lower or equal the endDate day number.
           If this is the case: Do vanilla age(). Works perfectly here
        */

        WHEN (date_part('day', "startDate" )::integer) <= date_part('day', "endDate" )::integer

        THEN age("endDate","startDate")

        /* Special case to treat here: startDate day number is greater than endDate day number. Do the algorithm described above */

        ELSE  age
               (

                  date_trunc('month', "endDate"::date), /* Go just till month level, always using '1' as day */

                  date_trunc('month', "startDate"::date)
                  + '1 mons'::interval
                  /* Add one month so that interval to look for will become actually shorter for now. */
                ) 
              + 
                (
                   (

                     /* Calculate the last day of the month previous to the end month. See https://stackoverflow.com/questions/28186014/how-to-get-the-last-day-of-month-in-postgres  */
                     (date_part('day',(date_trunc('month', (date_trunc('month', "endDate"::date) - '1 mons'::interval)  ) + interval '1 month' - interval '1 day')::date))::integer

                     - 

                     /* endDate day number subtracted */
                     date_part('day', "startDate" )::integer
                   )

                   /* endDate day number added */
                   + date_part('day', "endDate" )::integer||' days'

                )::interval
        END

$$ LANGUAGE sql;
0 голосов
/ 04 июля 2018

Вышеуказанное неожиданное поведение не из-за возраста (). Но из-за интервального типа данных, который позволяет вычисления. Ниже ссылка содержит необходимые пояснения.

Арифметика нечетного месяца

В вашем первом, так как два раза подряд, вы не видите неожиданного. Но это второе, это не так. Это имеет тенденцию к арифметическому поведению выше нечетного месяца

...