Основные операции над значениями даты/времени в 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
без потери точности или ошибок.