Арифметические операции над значениями даты/времени в PL/SQL

Арифметические операции над значениями даты/времени в PL/SQL

Основные операции над значениями даты/времени в Oracle сводятся к следующему набору:

  • Прибавление или вычитание интервала из значения даты/времени.
  • Вычитание одного значения даты/времени из другого для определения интервала между двумя значениями.
  • Прибавление или вычитание одного интервала из другого.
  • Умножение или деление интервала на число.

По историческим причинам я раздельно рассматриваю арифметические операции со значениями типа DATE и операции, в которых задействованы типы семейств TIMESTAMP и INTERVAL.



 

Операции с типами TIMESTAMP и INTERVAL

Вычисления с интервалами «дни/секунды» легко выполняются при работе с типами данных семейства TIMESTAMP. Создайте значение INTERVAL DAY TO SECOND и используйте его при сложении и вычитании. Например, прибавление к текущей дате 1500 дней, 4 часов, 30 минут и 2 секунд выполняется следующим образом: 

DECLARE
   current_date TIMESTAMP;
   result_date TIMESTAMP;
BEGIN
   current_date := SYSTIMESTAMP;
   result_date:= current_date + INTERVAL '1500 4:30:2' DAY TO SECOND;
   DBMS_OUTPUT.PUT_LINE(result_date);
END;

С интервалами «годы/месяцы» дело обстоит сложнее. Продолжительность любого дня составляет 24 часа, или 1440 минут, или даже 86 400 секунд, но не все месяцы имеют одинаковую продолжительность в днях: 28, 29, 30 или 31 день. По этой причине простое прибавление одного месяца к дате может привести к неоднозначному результату. Допустим, вы прибавляете один месяц к последнему дню мая; что получится — последний день июня или недействительная дата 31 июня? Все зависит от того, что должны представлять интервалы.

Oracle предоставляет необходимые средства для получения любого из этих результатов. Программист сам решает, какой вариант поведения должен реализоваться системой. Если конец месяца должен быть преобразован в конец месяца (31 мая + 1 месяц = 30 июня), используйте функцию ADD_MONTHS. Если изменение дня месяца нежелательно, используйте значение INTERVAL YEAR TO MONTH. В этом случае при прибавлении к 31 мая 2008 года INTERVAL '1' MONTH будет получено значение 31 июня 2008 года, а СУБД выдаст сообщение об ошибке.

Вычисления с типом INTERVAL YEAR TO MONTH лучше зарезервировать для тех значений даты/времени, которые усекаются по началу месяца или, скажем, по 15 числу — они плохо подходят для конца месяца. Если вам потребуется прибавить или вычесть сколько-то месяцев (или лет — аналогичная проблема возникает при прибавлении одного кода к 29 февраля 2008 года) из даты, в которой может быть задействован конец месяца, используйте функцию ADD_MONTHS. Эта функция, возвращающая тип DATE, решает проблему преобразованием соответствующих дат в последний день месяца вместо выдачи ошибки. Например, ADD_MONTHS('31-May-2008',1) вернет 30 июня 2008 года. Полученное значение DATE не содержит данных часового пояса (или долей секунд); если в результате должны присутствовать эти компоненты, вам придется реализовать дополнительную логику извлечения и повторного внесения этих компонентов в результат. 

DECLARE
   end_of_may2008 TIMESTAMP;
   next_month TIMESTAMP;
BEGIN
   end_of_may2008 := TO_TIMESTAMP('31-May-2008', 'DD-Mon-YYYY');
   next_month := TO_TIMESTAMP(ADD_MONTHS(end_of_may2008, 1));
   DBMS_OUTPUT.PUT_LINE(next_month);
END;

Результат:

30-Jun-2008 00:00:00.000000 

Аналогичной функции вычитания SUBTRACT_MONTHS не существует, но ADD_MONTHS можно вызывать с отрицательным количеством месяцев. Например, вызов ADD_MONTHS(current_date, -1) в приведенном примере вернет дату за один месяц до последнего дня апреля.

 

Операции с типом DATE

В операциях с типом DATE можно использовать как значения INTERVAL, так и числовые значения, представляющие дни и их доли. Например, прибавление одного дня к текущей дате и времени выполняется так: 

SYSDATE + 1

Прибавление четырех часов к текущей дате и времени:

SYSDATE + (4/24) 

Обратите внимание на использование дроби 4/24 вместо 1/6. При чтении кода сразу становится ясно, что значение, возвращаемое SYSDATE, увеличивается на 4 часа; а если использовать 1/6, программист, который будет заниматься сопровождением кода, будет долго ломать голову над тем, что должна означать эта таинственная дробь. Для еще более явного выражения намерений можно воспользоваться именованной константой:

DECLARE
   four_hours NUMBER := 4/24;
BEGIN
   DBMS_OUTPUT.PUT_LINE(
         'Now + 4 hours =' || TO_CHAR (SYSDATE + four_hours));
END;

В табл. 10.4 приведены дробные значения, представляющие часы, минуты и секунды при работе с DATE. Также в нее включены некоторые дробные значения, которые могут использоваться для построения этих значений.

Значение Выражение Представляет
1/24 1/24 Один час
1/1440 1/24/60 Одна минута
1/86400 1/24/60/60 Одна секунда

Используйте значения из табл. 10.4, и ваш код станет более понятным. Программист, привыкший к этим трем делителям, без труда поймет, что 40/86400 означает 40 секунд. Понять, что дробь 1/21610 означает то же самое, будет намного сложнее.

 

Вычисление интервала между двумя значениями DATE

Интервал между двумя значениями из семейства TIMESTAMP вычисляется простым вычитанием одного значения из другого. Результат всегда относится к типу INTERVAL DAY TO SECOND. Например: 

DECLARE
   leave_on_trip TIMESTAMP := TIMESTAMP '2005-03-22 06:11:00.00';
   return_from_trip TIMESTAMP := TIMESTAMP '2005-03-25 15:50:00.00';
   trip_length INTERVAL DAY TO SECOND;
BEGIN
   trip_length := return_from_trip - leave_on_trip;
   DBMS_OUTPUT.PUT_LINE('Длина в формате дни часы:минуты:секунды
                        ' || trip_length);
END;

Результат:

Длина в формате дни часы:минуты:секунды +03 09:39:00.000000 

Интервалы могут быть отрицательными или положительными. Отрицательный интервал означает, что более недавняя дата вычитается из даты, находящейся в прошлом: 

18 июня 1961 г. - 15 ноября 1961 г. = -150

Знак результата задает направленность интервала. К сожалению, не существует функции абсолютного значения для интервалов — аналога функции ABS для числовых значений.

Если вы вычисляете интервал между двумя значениями типа DATE, результатом является количество 24-часовых периодов (не то же самое, что количество дней) между ними. Если получено целочисленное значение, то разность представляет точное количество дней. Если же значение является дробным, то разность включает также некоторое количество часов, минут и секунд. Следующий пример взят из предыдущего раздела, но в нем используется тип DATE:

BEGIN
   DBMS_OUTPUT.PUT_LINE (
      TO_DATE('25-Mar-2005 3:50 pm','dd-Mon-yyyy hh:mi am')
      - TO_DATE('22-Mar-2005 6:11 am','dd-Mon-yyyy hh:mi am')
   );
END;

Результат:

3.40208333333333333333333333333333333333 

Три дня — понятно, но что такое 0.40208333333333333333333333333333333333? Даты часто усекаются перед вычитанием или же усекается полученный результат. Использование типов INTERVAL и TIMESTAMP значительно упрощает правильное усечение длинной дробной строки до часов, минут и секунд.

Для вычисления интервалов между двумя значениями DATE также удобно пользоваться функцией MONTHS_BETWEEN. Функция имеет следующий синтаксис: 

FUNCTION MONTHS_BETWEEN (дата_1 IN DATE, дата_2 IN DATE)
RETURN NUMBER

Функция MONTHS_BETWEEN работает по следующим правилам:

  • Если дата_1 наступает позже даты_2, MONTHS_BETWEEN возвращает положительное число.
  • Если дата_1 наступает раньше даты_2, MONTHS_BETWEEN возвращает отрицательное число.
  • Если дата_1 и дата_2 относятся к одному месяцу, функция возвращает дробное значение из диапазона от –1 до +1.
  • Если дата_1 и дата_2 приходятся на последние дни соответствующих месяцев, функция возвращает целое число (без дробного компонента).
  • Если дата_1 и дата_2 относятся к разным месяцам и хотя бы одна из них не приходится на последний день месяца, функция возвращает дробное значение. (Дробный компонент вычисляется для месяцев, состоящих из 31 дня, с учетом разницы компонентов времени двух дат.)

Несколько примеров использования MONTHS_BETWEEN

BEGIN
   -- Разность между двумя последними днями месяцев,
   -- первый месяц наступает раньше второго:
   DBMS_OUTPUT.PUT_LINE(
      MONTHS_BETWEEN ('31-JAN-1994', '28-FEB-1994'));

   -- Разность между двумя последними днями месяцев,
   -- первый месяц наступает позже второго:
   DBMS_OUTPUT.PUT_LINE(
      MONTHS_BETWEEN ('31-MAR-1995', '28-FEB-1994'));

   -- Две даты одного месяца:
   DBMS_OUTPUT.PUT_LINE(
      MONTHS_BETWEEN ('28-FEB-1994', '15-FEB-1994'));

   -- Вычисления с дробным компонентом:
   DBMS_OUTPUT.PUT_LINE(
      MONTHS_BETWEEN ('31-JAN-1994', '1-MAR-1994'));
   DBMS_OUTPUT.PUT_LINE(
      MONTHS_BETWEEN ('31-JAN-1994', '2-MAR-1994'));
   DBMS_OUTPUT.PUT_LINE(
      MONTHS_BETWEEN ('31-JAN-1994', '10-MAR-1994'));
END;

Результаты:

-1
13
.4193548387096774193548387096774193548387
-1.03225806451612903225806451612903225806
-1.06451612903225806451612903225806451613
-1.32258064516129032258064516129032258065 

Вероятно, вы заметили здесь определенную закономерность. Как уже было сказано, функция MONTHS_BETWEEN вычисляет дробный компонент количества месяцев исходя из предположения, что каждый месяц содержит 31 день. Поэтому на каждый день сверх полного месяца к результату прибавляется 1/31 месяца:

1/31 = .032258065...

В соответствии с этим правилом количество месяцев между 31 января 1994 года и 28 февраля 1994 года равно 1 — удобное целое число. Однако количество месяцев между 31 января 1994 года и 1 марта 1994 года увеличивается на .032258065. Как и в случае с вычитанием DATE, при работе с MONTHS_BETWEEN часто используется функция TRUNC.

 

Смешанное использование DATE и TIMESTAMP

Результатом вычитания двух TIMESTAMP является значение типа INTERVAL DAY TO SECOND. Результат вычитания с двумя значениями DATE представляет собой числовое значение. Соответственно, если требуется вычесть одно значение DATE из другого и вернуть значение INTERVAL DAY TO SECOND, вам придется преобразовать DATE в TIMESTAMP функцией CAST. Пример: 

DECLARE
   dt1 DATE;
   dt2 DATE;
   d2s INTERVAL DAY(3) TO SECOND(0);
BEGIN
   dt1 := TO_DATE('15-Nov-1961 12:01 am','dd-Mon-yyyy hh:mi am');
   dt2 := TO_DATE('18-Jun-1961 11:59 pm','dd-Mon-yyyy hh:mi am');
   d2s := CAST(dt1 AS TIMESTAMP) - CAST(dt2 AS TIMESTAMP);
   DBMS_OUTPUT.PUT_LINE(d2s);
END;

Результат:

+149 00:02:00 

Если значения DATE и TIMESTAMP смешиваются в одном выражении вычитания, PL/SQL выполняет неявное преобразование DATE в TIMESTAMP. Пример: 

DECLARE
   dt DATE;
   ts TIMESTAMP;
   d2s1 INTERVAL DAY(3) TO SECOND(0);
   d2s2 INTERVAL DAY(3) TO SECOND(0);
BEGIN
   dt := TO_DATE('15-Nov-1961 12:01 am','dd-Mon-yyyy hh:mi am');
   ts := TO_TIMESTAMP('18-Jun-1961 11:59 pm','dd-Mon-yyyy hh:mi am');
   d2s1 := dt - ts;
   d2s2 := ts - dt;
   DBMS_OUTPUT.PUT_LINE(d2s1);
   DBMS_OUTPUT.PUT_LINE(d2s2);
END;

Результат:

+149 00:02:00
-149 00:02:00 

Как обычно при работе с типами данных даты и времени, в программе желательно использовать явные преобразования.

 

Сложение и вычитание интервалов

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

DECLARE
   dts1 INTERVAL DAY TO SECOND := '2 3:4:5.6';
   dts2 INTERVAL DAY TO SECOND := '1 1:1:1.1';
   ytm1 INTERVAL YEAR TO MONTH := '2-10';
   ytm2 INTERVAL YEAR TO MONTH := '1-1';
   days1 NUMBER := 3;
   days2 NUMBER := 1;
BEGIN
   DBMS_OUTPUT.PUT_LINE(dts1 - dts2);
   DBMS_OUTPUT.PUT_LINE(ytm1 — ytm2);
   DBMS_OUTPUT.PUT_LINE(days1 — days2);
END;

Результат:

+000000001 02:03:04.500000000
+000000001-09
2 

Пример демонстрирует результаты трех вычитаний интервалов. В первых двух операциях участвуют интервалы INTERVAL DAY TO SECOND и INTERVAL YEAR TO MONTH. В третьей операции используется вычитание двух чисел. Запомните: при работе с типами DATE интервал между двумя значениями DATE выражается типом NUMBER. Так как месяц может состоять из 28, 29, 30 или 31 дня, при попытке суммирования или вычитания интервала «дни/секунды» с интервалом «годы/месяцы» происходит ошибка.

 

Умножение и деление интервалов

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

DECLARE
   dts1 INTERVAL DAY TO SECOND := '2 3:4:5.6';
   dts2 INTERVAL YEAR TO MONTH := '2-10';
   dts3 NUMBER := 3;
BEGIN
   -- Умножение интервала
   DBMS_OUTPUT.PUT_LINE(dts1 * 2);
   DBMS_OUTPUT.PUT_LINE(dts2 * 2);
   DBMS_OUTPUT.PUT_LINE(dts3 * 2);

   -- Деление интервала
   DBMS_OUTPUT.PUT_LINE(dts1 / 2);
   DBMS_OUTPUT.PUT_LINE(dts2 / 2);
   DBMS_OUTPUT.PUT_LINE(dts3 / 2);
END;

Результат:

+000000004 06:08:11.200000000
+000000005-08
6
+000000001 01:32:02.800000000
+000000001-05
1.5 

 

Типы данных INTERVAL без ограничений

Интервалы можно объявлять с разным уровнем точности, причем значения разной точностью не полностью совместимы между собой. Проблема особенно наглядно проявляется при написании процедур и функций, получающих параметры типа INTERVAL. Обратите внимание на потерю точности в следующем примере, где значение переменной dts удваивается с помощью функции double_my_interval

DECLARE
   dts INTERVAL DAY(9) TO SECOND(9);
   FUNCTION double_my_interval (
      dts_in IN INTERVAL DAY TO SECOND) RETURN INTERVAL DAY TO SECOND
IS
BEGIN
   RETURN dts_in * 2;
END;
BEGIN
   dts := '1 0:0:0.123456789';
   DBMS_OUTPUT.PUT_LINE(dts);
   DBMS_OUTPUT.PUT_LINE(double_my_interval(dts));
END;

Результат выполнения кода:

+000000001 00:00:00.123456789
+02 00:00:00.246914 

Цифры были потеряны не только в дробной части секунд, но и в значении количества дней. А если бы переменной dts было присвоено значение, равное 100 или более дням, попытка вызова функции double_my_interval привела бы к ошибке!

Дело в том, что задаваемая по умолчанию точность типов данных INTERVAL не равна максимально возможной точности. Обычно вызывающая программа передает точность параметров программе PL/SQL, но с типами данных INTERVAL используется принятая по умолчанию точность 2. Для решения этой проблемы можно воспользоваться типами данных INTERVAL, явно объявляемыми без ограничения точности:

  • YMINTERVAL_UNCONSTRAINED — принимает любое значение типа INTERVAL YEAR TO MONTH без потери точности;
  • DSINTERVAL_UNCONSTRAINED — принимает любое значение типа INTERVAL DAY TO SECOND без потери точности.

Воспользовавшись типом DSINTERVAL_UNCONSTRAINED, приведенный выше пример можно переписать следующим образом: 

DECLARE
   dts INTERVAL DAY(9) TO SECOND(9);
   FUNCTION double_my_interval (
   dts_in IN DSINTERVAL_UNCONSTRAINED) RETURN DSINTERVAL_UNCONSTRAINED
IS
BEGIN
   RETURN dts_in * 2;
END;
BEGIN
   dts := '100 0:0:0.123456789';
   DBMS_OUTPUT.PUT_LINE(dts);
   DBMS_OUTPUT.PUT_LINE(double_my_interval(dts));
END;

Результат будет таким:

+000000100 00:00:00.123456789
+000000200 00:00:00.246913578 

Обратите внимание на то, что тип данных DSINTERVAL_UNCONSTRAINED используется дважды: один раз для задания типа формального параметра функции double_my_interval, а второй — для задания типа возвращаемого значения. В результате эту функцию можно вызывать для любого значения типа INTERVAL DAY TO SECOND без потери точности или ошибок.

 

Вас заинтересует / Intresting for you:

Управление приложениями PL/SQL...
Управление приложениями PL/SQL... 4634 просмотров Stas Belkov Thu, 16 Jul 2020, 06:20:48
Встроенные методы коллекций PL...
Встроенные методы коллекций PL... 14738 просмотров sepia Tue, 29 Oct 2019, 09:54:01
Тип данных RAW в PL/SQL
Тип данных RAW в PL/SQL 12239 просмотров Doctor Thu, 12 Jul 2018, 08:41:33
Символьные функции и аргументы...
Символьные функции и аргументы... 18546 просмотров Анатолий Wed, 23 May 2018, 18:54:01
Войдите чтобы комментировать

apv аватар
apv ответил в теме #9521 4 года 5 мес. назад
Качественный материал с хорошими примерами. Особое спасибо за то, что отформатировали код. Удобочитаемость просто великолепна теперь!
Doc аватар
Doc ответил в теме #9166 5 года 6 мес. назад
Хорошие примеры по вычислению интервала между датами. Это очень часто используется при разработке программ PL/SQL под БД Oracle