As the old saying goes, “Time and tide wait for no man.” As database developers, we may not deal with tide-related information every day, but we deal with time-related information almost every single day. The hire date of an employee, your pay day, the rent or mortgage payment date, the time duration required for a financial investment to mature, and the start date and time of your new car insurance are all examples of temporal data that we deal with every single day.
The need for effective management of temporal information became critical at the turn of the century, when most of us had to devise ways to handle the two-digit year correctly as it increased from 99 to 00, and then to 01. In this age of global e-business, the concepts of time are even more involved than ever before, because business is carried out around the clock across time zone boundaries.
A database needs to effectively and efficiently handle the storage, retrieval, and manipulation of the following types of temporal data:
Dates
Times
Date and time intervals
Time zones
Oracle’s support for temporal data is mature and efficient. Oracle8i supports convenient manipulation of date and time data. Oracle9i enhanced this support by introducing a new set of features including support for fractional seconds, date and time intervals, and time zones.
In the global economy, business is carried out across geographical boundaries and time zones. It is common for a customer in Los Angeles to order an item through a supplier’s web site, and the supplier’s database could be located in New York. A manufacturer in China could update the status of an order of one of its U.S. customers. Conducting business across the globe is a requirement in today’s global economy, and the evolution of the Internet and related technologies have made it simple. Databases can’t be far behind, can they?
Oracle facilitates global business through its support for time zones. With Oracle 9i Database and higher, a database and a session can be associated with time zones. Having database and session time zones enables users in geographically distant regions to exchange temporal data with the database without having to bother about the time differences between the location of their clients and the location of the database server.
The list of valid time zone region names is provided in the data dictionary view V$TIMEZONE_NAMES.
The time zone of a database is usually set at the time of creation of the database. Alternatively, a database administrator can change the time zone using the ALTER DATABASE command, after a database is created. Both CREATE DATABASE and ALTER DATABASE commands take an optional SET TIME_ZONE clause:
SET TIME_ZONE = '+ | -HH:MI
' | 'time_zone_region
'
You can specify a time zone in one of two ways:
By specifying a displacement from Coordinated Universal Time (UTC) in hours and minutes. For example, United States Eastern Standard Time is UTC -05:00.
By specifying a time zone name or time zone abbreviation (columns TZNAME and TZABBREV in V$TIMEZONE_NAMES, respectively). Every time zone is given a name and abbreviation. For example, “U.S./Eastern” is the time zone name, and EST is the time zone abbreviation for Eastern Standard Time. You can use either the time zone name or the abbreviation to set the time zone of a database.
The following examples use the SET TIME_ZONE clause to set the time zone of a database:
CREATE DATABASE . . . SET TIME_ZONE = '-05:00'; ALTER DATABASE . . . SET TIME_ZONE = 'EST';
Both of these examples set the time zone to Eastern Standard Time. The first example uses a displacement (-05:00) from UTC. The second example uses the time zone abbreviation (EST). EST is 5 hours behind UTC, and is therefore equivalent to “-5:00”.
Each session can have a time zone as well. You can set the time zone of a session by using the ALTER SESSION SET TIME_ZONE statement. The syntax for the SET TIME_ZONE clause in the ALTER SESSION statement is the same as that in the CREATE DATABASE and ALTER DATABASE statements.
The following example shows two ways to set the time zone of a session to Pacific Standard Time:
ALTER SESSION SET TIME_ZONE = '-08:00'; ALTER SESSION SET TIME_ZONE = 'PST';
To set the session time zone to the local operating system time zone (e.g., the time zone of a PC initiating a remote user session), you can use the LOCAL keyword in the SET TIME_ZONE clause, as in the following example:
ALTER SESSION SET TIME_ZONE = LOCAL;
To set the session time zone to the database time zone, use the DBTIMEZONE keyword in the SET TIME_ZONE clause, as in the following example:
ALTER SESSION SET TIME_ZONE = DBTIMEZONE;
Oracle provides the following categories of data types to represent temporal data inside an Oracle database:
The DATE data type
The TIMESTAMP data types:
TIMESTAMP
TIMESTAMP WITH TIME ZONE
TIMESTAMP WITH LOCAL TIME ZONE
The INTERVAL data types:
INTERVAL YEAR TO MONTH
INTERVAL DAY TO SECOND
Up to release Oracle8i, Oracle had only one temporal data type: DATE, which held date as well as time information. Oracle9i introduced several new data types—three TIMESTAMP data types to hold time data with fractional seconds, and two INTERVAL data types to hold time intervals. The following sections discuss all these data types in detail.
Oracle’s DATE data type holds date as well as time information. Regardless of the date format you use for display purposes, Oracle stores dates internally in one standard format. Internal to the database, a date is a fixed-length, 7-byte field. The seven bytes represent the following pieces of information:
The Century
The Year
The Month
The Day
The Hour
The Minute
The Second
Even though the data type is called a DATE, it also stores the time. You choose the components to display (the date, the time, the date and the time, etc.) when you retrieve a DATE value from the database. Or, if you are putting a DATE value into a program (e.g., a Java program) you might choose to extract the components of interest after transferring the entire date/time value to that program.
To provide support for fractional seconds along with date and time data, and also to provide support for time zones, Oracle9i introduced the following temporal data types:
TIMESTAMP
TIMESTAMP WITH TIME ZONE
TIMESTAMP WITH LOCAL TIME ZONE
The TIMESTAMP data type extends the DATE type to support more precise time values. A TIMESTAMP includes all the components of the DATE data type (century, year, month, day, hour, minute, second) plus fractional seconds. A TIMESTAMP data type is specified as:
TIMESTAMP [ (precision for fractional seconds
) ]
The precision for fractional seconds
is
optional and is specified in parentheses. You can specify integer
values between 0 and 9 for fractional precision. A precision of 9
means that you can have 9 digits to the right of the
decimal—i.e., up to nanoseconds precision. If you
don’t specify the precision, it defaults to 6
(microseconds precision)—i.e., TIMESTAMP is the same as
TIMESTAMP(6).
The following example creates a table with a column of type TIMESTAMP:
CREATE TABLE transaction (
transaction_id NUMBER(10),
transaction_timestamp TIMESTAMP,
status VARCHAR2(12));
Table created.DESC transaction
Name Null? Type --------------------------- -------- --------------- TRANSACTION_ID NUMBER(10) TRANSACTION_TIMESTAMP TIMESTAMP(6) STATUS VARCHAR2(12)
Since we don’t specify a precision in this example
for the column transaction_timestamp
, Oracle uses
the default precision for the TIMESTAMP data type, and it appears as
TIMESTAMP(6) when we describe the table.
The TIMESTAMP WITH TIME ZONE data type further extends the TIMESTAMP type to include a time zone displacement. A TIMESTAMP WITH TIME ZONE data type is specified as:
TIMESTAMP [ (precision for fractional seconds
) ] WITH TIME ZONE
The precision for fractional seconds
is
the same as that for the TIMESTAMP data type. The time zone
displacement is the time difference in hours and minutes between the
local time and UTC. You supply such displacements when you store
values in the column, and the database retains the displacements so
that those values can later be translated into any target time zone
desired by your system’s users.
The following example creates a table with a column of type TIMESTAMP WITH TIME ZONE:
CREATE TABLE transaction_time_zone (
transaction_id NUMBER(10),
transaction_timestamp TIMESTAMP(3) WITH TIME ZONE,
status VARCHAR2(12));
Table created.DESC transaction_time_zone
Name Null? Type ------------------------------- -------- ------------------------ TRANSACTION_ID NUMBER(10) TRANSACTION_TIMESTAMP TIMESTAMP(3) WITH TIME ZONE STATUS VARCHAR2(12)
The TIMESTAMP WITH LOCAL TIME ZONE data type is a variant of the TIMESTAMP WITH TIME ZONE data type. A TIMESTAMP WITH LOCAL TIME ZONE data type is specified as:
TIMESTAMP [ (precision for fractional seconds
) ] WITH LOCAL TIME ZONE
The precision for fractional seconds
is
the same as that in the TIMESTAMP data type. TIMESTAMP WITH LOCAL
TIME ZONE differs from TIMESTAMP WITH TIME ZONE in the following
ways:
The time zone displacement is not stored as part of the column data.
The data stored in the database is normalized to the time zone of the database. To normalize an input value to the database time zone, the input time is converted to a time in the database time zone. The original time zone is not preserved.
When the data is retrieved, Oracle returns the data in the time zone of the user session.
The following example creates a table with a column of type TIMESTAMP WITH LOCAL TIME ZONE:
CREATE TABLE transaction_local_time_zone (
transaction_id NUMBER(10),
transaction_timestamp TIMESTAMP(3) WITH LOCAL TIME ZONE,
status VARCHAR2(12));
Table created.DESC transaction_local_time_zone
Name Null? Type ------------------------ -------- ------------------------ TRANSACTION_ID NUMBER(10) TRANSACTION_TIMESTAMP TIMESTAMP(3) WITH LOCAL TIME ZONE STATUS VARCHAR2(12)
Date and time interval data are an integral part of our day-to-day life. Common examples of interval data are the age of a person, the maturity period of a bond or certificate of deposit, and the warranty period of your car. Prior to Oracle9i Database, we all used the NUMBER data type to represent such data, and the logic needed to deal with interval data had to be coded at the application level. Oracle9i Database introduced two new data types to handle interval data:
INTERVAL YEAR TO MONTH
INTERVAL DAY TO SECOND
The following sections discuss the use of these data types.
The INTERVAL YEAR TO MONTH type stores a period of time expressed as a number of years and months. An INTERVAL YEAR TO MONTH data type is specified as:
INTERVAL YEAR [ (precision for year
) ] TO MONTH
The precision for year
specifies the
number of digits in the year field. The precision can range from 0 to
9, and the default value is 2. The default precision of two allows
for a maximum interval of 99 years, 11 months.
The following example creates a table with a column of type INTERVAL YEAR TO MONTH:
CREATE TABLE event_history (
event_id NUMBER(10),
event_duration INTERVAL YEAR TO MONTH);
Table created.DESC event_history
Name Null? Type ---------------------------- -------- ------------------------ EVENT_ID NUMBER(10) EVENT_DURATION INTERVAL YEAR(2) TO MONTH
The next example uses the NUMTOYMINTERVAL (NUMBER-TO-YEAR-MONTH INTERVAL) function to insert data into a database column of type INTERVAL YEAR TO MONTH. This function converts a NUMBER value into a value of type INTERVAL YEAR TO MONTH, using the units specified by the second argument:
INSERT INTO event_history VALUES (5001, NUMTOYMINTERVAL(2,'YEAR'));
1 row created.INSERT INTO event_history VALUES (5002, NUMTOYMINTERVAL(2.5,'MONTH'));
1 row created.SELECT * FROM event_history;
EVENT_ID EVENT_DURATION ---------- ------------------ 5001 +02-00 5002 +00-02
The second argument to the NUMTOYMINTERVAL function specifies the unit of the first argument. Therefore, in the first example, the number 2 is treated as 2 years, and in the second example, the number 2.5 is treated as 2 months. Any fractional part of a month is ignored. An INTERVAL YEAR TO MONTH value is only in terms of years and months, not fractional months.
The INTERVAL DAY TO SECOND type stores a period of time expressed as a number of days, hours, minutes, seconds, and fractions of a second. An INTERVAL DAY TO SECOND data type is specified as:
INTERVAL DAY [(precision for day
)] TO SECOND [(precision for fractional seconds
)]
The precision for day
specifies the number
of digits in the day field. This precision can range from 0 to 9, and
the default value is 2. The precision for
fractional
seconds
is the
number of digits in the fractional part of a second. It can range
from 0 to 9, and the default value is 6.
The following example creates a table with an INTERVAL DAY TO SECOND column:
CREATE TABLE batch_job_history (
job_id NUMBER(6),
job_duration INTERVAL DAY(3) TO SECOND(6));
Table created.DESC batch_job_history
Name Null? Type ----------------------- -------- ----------------------------- JOB_ID NUMBER(6) JOB_DURATION INTERVAL DAY(3) TO SECOND(6)
Here’s how to insert data into a table with an INTERVAL DAY TO SECOND column:
INSERT INTO batch_job_history VALUES
(6001, NUMTODSINTERVAL(5369.2589,'SECOND'));
1 row created.SELECT * FROM batch_job_history;
JOB_ID JOB_DURATION ---------- ---------------------------------------- 6001 +00 01:29:29.258900
For the INSERT in this example we used the function NUMTODSINTERVAL (NUMBER-TO-DAY-SECOND-INTERVAL). This function converts a NUMBER value into a value of type INTERVAL DAY TO SECOND, using the units specified in the second argument. It’s analogous to NUMTOYMINTERVAL discussed in the previous section.
Using literals of character and number types is pretty simple, because they don’t involve any special formatting. However, when using literals of temporal type, you need to pay special attention to the formats in which they are specified. The following sections describe date, timestamp, and interval literals.
DATE literals are specified in the format specified by SQL Standard, and take the following form:
DATE 'YYYY
-MM
-DD
'
Unlike Oracle’s DATE data type, a DATE literal
doesn’t specify any time information. You also
can’t specify a format. If you want to specify a
date literal, you must always use the
YYYY
-MM
-DD
date format. The following example illustrates the use of a DATE
literal in a SQL statement:
INSERT INTO employee
(emp_id, fname, lname, dept_id, manager_emp_id, salary, hire_date)
VALUES
(2304, 'John', 'Smith', 20, 1258, 20000, DATE '1999-10-22'),
1 row created.SELECT * FROM employee;
EMP_ID FNAME LNAME DEPT_ID MANAGER_EMP_ID SALARY HIRE_DATE ------- -------- ------- ---------- -------------- ---------- --------- 2304 John Smith 20 1258 20000 22-OCT-99
In this example, the date literal DATE
'1999-10-22
' is interpreted as 22-OCT-99.
A TIMESTAMP literal takes the following format:
TIMESTAMP 'YYYY
-MM
-DD
HH
:MI
:SS
.xxxxxxxxx'
A TIMESTAMP literal can have up to nine digits of fractional seconds. The fractional part is optional, but the date and time elements are mandatory and must be provided in the given format. Here’s an example in which data is inserted into a table with a TIMESTAMP column:
INSERT INTO transaction
VALUES (1001, TIMESTAMP '1998-12-31 08:23:46.368', 'OPEN'),
1 row created.SELECT * FROM transaction;
TRANSACTION_ID TRANSACTION_TIMESTAMP STATUS -------------- --------------------------------- --------- 1001 31-DEC-98 08.23.46.368000 AM OPEN
A TIMESTAMP literal with a time zone displacement can be used to represent a literal of type TIMESTAMP WITH TIME ZONE. It takes the following form:
TIMESTAMP 'YYYY
-MM
-DD
HH
:MI
:SS
.xxxxxxxxx
{+|-}HH
:MI
'
Here is an example that shows how to insert data into a table with a TIMESTAMP WITH TIME ZONE column:
INSERT INTO transaction_time_zone
VALUES (1002, TIMESTAMP '1998-12-31 08:23:46.368 -10:30', 'NEW'),
1 row created.SELECT * FROM transaction_time_zone;
TRANSACTION_ID TRANSACTION_TIMESTAMP STATUS -------------- ----------------------------------- ------- 1002 31-DEC-98 08.23.46.368 AM -10:30 NEW
Even though the data type is called TIMESTAMP WITH TIME ZONE, the
literal still uses just the TIMESTAMP keyword. However, the literal
also specifies a date/time displacement for time zone using the
{+|-}
HH
:MI
notation.
If you are specifying a time zone displacement in a TIMESTAMP literal, you must specify the sign of the displacement (i.e., + or -). The range of the hour in a time zone displacement is -12 to +13, and the range of a minute is 0 to 59. A displacement outside these ranges will generate an error.
The valid range of time zone displacement in Oracle differs from that specified by the SQL Standard. The SQL Standard requires the valid range to be from -12:59 to +13:00. However, Oracle enforces the range on the hour (-12 to +13) and minute (0 to 59) components separately. Therefore, the valid range of time zone displacement in Oracle is from -12:00 to +13:59.
When you don’t specify a time zone displacement, the displacement is not assumed to be zero; instead, the timestamp is assumed to be in your session time zone, and the value of the displacement defaults to the displacement of that time zone. For example, the TIMESTAMP literal in the following INSERT specifies no time zone displacement, yet the SELECT statement proves that a time zone is, in fact, assumed:
INSERT INTO transaction_time_zone
VALUES (1003, TIMESTAMP '1999-12-31 08:23:46.368', 'NEW'),
1 row created.SELECT * FROM transaction_time_zone;
TRANSACTION_ID TRANSACTION_TIMESTAMP STATUS -------------- ------------------------------------- ------- 1003 31-DEC-99 08.23.46.368 AM -05:00 NEW
There is no literal specifically for the TIMESTAMP WITH LOCAL TIME ZONE data type. To insert data into a column of this type, you use a TIMESTAMP literal with a time zone displacement. For example:
INSERT INTO transaction_local_time_zone
VALUES (2001, TIMESTAMP '1998-12-31 10:00:00 -3:00', 'NEW'),
1 row created.SELECT * FROM transaction_local_time_zone;
TRANSACTION_ID TRANSACTION_TIMESTAMP STATUS -------------- -------------------------- ------- 2001 31-DEC-98 08.00.00 AM NEW
In a case like this, the time zone displacement is not stored in the database. The data is stored in the database in normalized form with respect to the database time zone. By “normalized form” we mean the input time is converted into a time in the database time zone before being stored in the database. The database time zone in this example is -5:00. Therefore, -3:00 is 2 hours ahead of the database time zone, and 10:00:00 - 3:00 is the same as 08:00:00 - 5:00. Since the time is normalized with respect to the database time zone, the displacement doesn’t need to be stored in the database.
Just as Oracle supports DATE and TIMESTAMP literals, it supports INTERVAL literals, too. There are two interval data types, and two types of corresponding interval literals: YEAR TO MONTH and DAY TO SECOND.
A YEAR TO MONTH interval literal represents a time period in terms of years and months. A YEAR TO MONTH interval literal takes one of the following two forms:
INTERVAL 'y
[-m
]' YEAR[(precision
)] [TO MONTH] INTERVAL 'm
' MONTH[(precision
)]
The syntax elements are:
y
An integer value specifying the years.
m
An integer value specifying the months. You must include the TO MONTH keywords if you specify a month value.
precision
Specifies the number of digits to allow for the year or month. The default is 2. The valid range is from 0 to 9.
The default precision for the year value is 2. If the literal represents a time period greater than 99 years, then you must specify a high-enough precision to accommodate the number of years in question. The integer value for the month, as well as the MONTH keyword, are optional. If you specify a month value, it must be between 0 and 11. You do need to use the MONTH keyword when you specify a month value.
The following example inserts a YEAR TO MONTH interval literal into an INTERVAL YEAR TO MONTH column:
INSERT INTO event_history
VALUES (6001, INTERVAL '5-2' YEAR TO MONTH);
1 row created.SELECT * FROM event_history;
EVENT_ID EVENT_DURATION ---------- ------------------------------------------ 6001 +05-02
The following example uses a YEAR TO MONTH interval literal to specify a time period of exactly four years. No value for months is included:
SELECT INTERVAL '4' YEAR FROM DUAL;
INTERVAL'4'YEAR
-------------------------------------------
+04-00
A YEAR TO MONTH interval literal can also be used to represent months only:
SELECT INTERVAL '3' MONTH FROM DUAL;
INTERVAL'3'MONTH ------------------------------------------------- +00-03SELECT INTERVAL '30' MONTH FROM DUAL;
INTERVAL'30'MONTH ------------------------------------------------- +02-06
Notice that when you use a YEAR TO MONTH interval literal to represent only months, you can actually specify a month value larger than 11. In such a situation, Oracle normalizes the value into an appropriate number of years and months. This is the only situation in which the number of months can be greater than 11.
A DAY TO SECOND interval literal represents a time period in terms of days, hours, minutes, seconds, and fractions of seconds. DAY TO SECOND interval literals take on the following form:
INTERVAL 'd
[h
[:m
[:s
]]]'unit1
[(precision1
)] TOunit2
[(frac_precision
)]
The syntax elements are:
d
An integer value specifying the days.
h
An integer value specifying the hours.
m
An integer value specifying the minutes.
s
A number value specifying the seconds and fractional seconds.
unit1, unit2
Can be DAY, HOUR, MINUTE, or SECOND. The leading unit
(unit1
) must always be greater than the
trailing unit (unit2
). For example,
INTERVAL HOUR TO MINUTE is valid, but INTERVAL HOUR TO DAY is not
valid.
precision1
The number of digits to allow for the leading unit. The default is 2. The valid range is from 0 to 9.
frac_precision
The number of digits to allow for fractional seconds. The default is 6. The valid range is from 0 to 9.
By default, two digits are allowed for the number of days. If a literal represents a time period of greater than 99 days, then you must specify a precision high enough to accommodate the number of digits that you need. There’s no need to specify the precision for the hour and minute values. The value for hours can be between 0 and 23, and the value for the minutes can be between 0 and 59. While specifying fractional seconds, you can specify a precision for the fractional seconds as well. The precision for the fractional seconds can be between 0 and 9 (default 6), and the seconds value can be between 0 and 59.999999999.
The following example inserts a DAY TO SECOND interval literal into a column of data type INTERVAL DAY TO SECOND. The time period being represented is 0 days, 3 hours, 16 minutes, 23.45 seconds.
INSERT INTO batch_job_history
VALUES (2001, INTERVAL '0 3:16:23.45' DAY TO SECOND);
1 row created.SELECT * FROM batch_job_history;
JOB_ID JOB_DURATION ---------- ------------------------------------------------ 2001 +00 03:16:23.450000
The previous example uses all elements of the DAY TO SECOND interval literal. However, you can use fewer elements if that’s all you need. The following example shows several valid permutations:
SELECT INTERVAL '400' DAY(3) FROM DUAL;
INTERVAL'400'DAY(3) ----------------------------------------------------------------- +400 00:00:00SELECT INTERVAL '11:23' HOUR TO MINUTE FROM DUAL;
INTERVAL'11:23'HOURTOMINUTE ----------------------------------------------------------------- +00 11:23:00SELECT INTERVAL '11:23' MINUTE TO SECOND FROM DUAL;
INTERVAL'11:23'MINUTETOSECOND ----------------------------------------------------------------- +00 00:11:23.000000SELECT INTERVAL '20' MINUTE FROM DUAL;
INTERVAL'20'MINUTE ----------------------------------------------------------------- +00 00:20:00
The only requirement is that you must use a range of contiguous elements. You cannot, for example, specify an interval in terms of only hours and seconds, because you can’t omit the intervening minutes value. An interval of 4 hours, 36 seconds would need to be expressed as 4 hours, 0 minutes, 36 seconds.
In the real world, temporal data are not always represented using Oracle’s DATE, TIMESTAMP, and INTERVAL data types. At various times, you’ll need to convert temporal values to other data types, especially to character types, and vice versa. This is particularly true when you interface an Oracle database with an external system, for example when you are accepting date input from an external system in which dates are represented as strings of characters (or even as numbers), or when you are sending output from an Oracle database to another application that doesn’t understand Oracle’s native temporal data types. You also need to convert DATE and TIMESTAMP values to text when you display them on a screen or generate a printed report.
Oracle provides some extremely useful functions to enable such conversions:
TO_DATE
TO_TIMESTAMP
TO_TIMESTAMP_TZ
TO_YMINTERVAL
TO_DSINTERVAL
NUMTOYMINTERVAL
NUMTODSINTERVAL
TO_CHAR
The purpose of each of these functions is more or less self-explanatory. The following sections discuss each of these functions in detail.
TO_DATE, TO_TIMESTAMP, and TO_TIMESTAMP_TZ are built-in SQL functions that convert, respectively, a character string into a DATE, a TIMESTAMP, and a TIMESTAMP WITH TIME ZONE. Input to these functions can be string literals, PL/SQL variables, and database columns of the CHAR and VARCHAR2 data types.
These three conversion functions are similar in operation. The difference is only in the data type of the return value. You call them as follows:
TO_DATE(string
[,format
]) TO_TIMESTAMP (string
[,format
]) TO_TIMESTAMP_TZ (string
[,format
])
The syntax elements are:
string
Specifies a string literal, a PL/SQL variable, or a database column containing character data (or even numeric data) convertible to a date or timestamp.
format
Specifies the format of the input string. The format must be a valid combination of format codes shown in Table 6-1, which you’ll find later in Section 6.5.
Through the format
argument, Oracle
provides a great deal of flexibility when it comes to converting
between date and time values and text. Oracle provides far more
flexibility, at least in our experience, than do competing platforms,
such as DB2 and SQL Server.
Specifying a format is optional. When you don’t specify a format, the input string is assumed to be in a default format as specified by the NLS_DATE_FORMAT, NLS_TIMESTAMP_FORMAT, or NLS_TIMESTAMP_TZ parameter settings.
You can view your current NLS parameter settings by querying the view named NLS_SESSION_PARAMETERS.
Every Oracle session has a set of default formats to use in converting date and timestamp values to and from their textual representations. You can query the NLS_SESSION_PARAMETERS view as follows to see the default formats currently in effect:
SELECT parameter, value
FROM nls_session_parameters
WHERE parameter LIKE '%FORMAT';
PARAMETER VALUE ------------------------------ ------------------------------ NLS_DATE_FORMAT DD-MON-RR NLS_TIME_FORMAT HH.MI.SSXFF AM NLS_TIMESTAMP_FORMAT DD-MON-RR HH.MI.SSXFF AM NLS_TIME_TZ_FORMAT HH.MI.SSXFF AM TZR NLS_TIMESTAMP_TZ_FORMAT DD-MON-RR HH.MI.SSXFF AM TZR
Session-specific formats derive from settings for language and territory. If you connect without specifying a language and territory, your session will inherit the default conversion formats established for the database. You can query NLS_DATABASE_PARAMETERS for those.
When you invoke one of the TO_ conversion functions, say TO_DATE,
without explicitly specifying a format, Oracle expects your input
string to be in the default format for the target data type. The
following
INSERT statement
converts a string in the default date format into a DATE, which is
then inserted into the employee
table:
INSERT INTO employee
(emp_id, fname, lname, dept_id, manager_emp_id, salary, hire_date)
VALUES
(2304, 'John', 'Smith', 20, 1258, 20000, TO_DATE('22-OCT-99'));
1 row created.SELECT * FROM employee;
EMP_ID FNAME LNAME DEPT_ID MANAGER_EMP_ID SALARY HIRE_DATE ------- -------- ------- ---------- -------------- ---------- --------- 2304 John Smith 20 1258 20000 22-OCT-99
The hire_date
column, into which our date was
inserted, is of type DATE. Because the input character string of
'22-OCT-99
' matched Oracle’s
default date format, the string could be converted without reference
to a format string. In fact, since the supplied string is in the
default date format, you don’t even need the TO_DATE
function. Oracle automatically performs an
implicit type conversion, as in this
example:
INSERT INTO employee
(emp_id, fname, lname, dept_id, manager_emp_id, salary, hire_date)
VALUES
(2304, 'John', 'Smith', 20, 1258, 20000, '22-OCT-99'),
1 row created.
Even though Oracle provides means for implicit data type conversions, we recommend always using explicit conversions, because implicit conversions are not obvious and may lead to confusion. They may also suddenly fail should a DBA change the database’s default date format.
If you wish to specify a format to use in converting from text to one of the temporal data types, there are at least two approaches you can take:
Specify the format at the session level, in which case it applies to all implicit conversions, and to all TO_DATE, TO_TIMESTAMP, or TO_TIMESTAMP_TZ conversions for which you do not explicitly specify some other format.
Specify the format as a parameter in a TO_X function call.
The following example changes the default date format for the session, and then uses TO_DATE to convert a number to date:
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYY';
Session altered.INSERT INTO employee
(emp_id, fname, lname, dept_id, manager_emp_id, salary, hire_date)
VALUES
(2304, 'John', 'Smith', 20, 1258, 20000, TO_DATE(102299));
1 row created.
Since the default date format has been changed prior to the conversion, the conversion function TO_DATE doesn’t need the date format as an input parameter.
Although it is possible to pass a number such as 102299 to the TO_DATE function, relying on Oracle’s implicit conversion to change the number to a string, and then into a date, it’s probably best to pass a string as input to the TO_DATE function.
If you do not wish to change your session’s default date format, you must specify the date format as the second input parameter to whichever of the three functions you are using. For example, the following SELECT specifies a format as the second input parameter to the TO_TIMESTAMP_TZ function:
SELECT
TO_TIMESTAMP_TZ('12/10/01 08:15:00.50 EST','MM/DD/YY HH:MI:SSXFF TZR')
FROM DUAL;
TO_TIMESTAMP_TZ('12/10/0108:15:00.50EST','MM/DD/YYHH:MI:SSXFFTZR') --------------------------------------------------------------------------- 10-DEC-01 08.15.00.500000000 AM EST
Let’s look at one more example to see how a database
character column can be converted to a
TIMESTAMP. Let’s assume that the
report_id
column in the report
table actually stores the date on which the report was generated, and
that the date is in the format
“MMDDYYYY.” Now, you can use
TO_TIMESTAMP on that column to convert that date into a TIMESTAMP,
which is then displayed using the default timestamp format:
SELECT sent_to, report_id,
TO_TIMESTAMP(report_id,'MMDDYYYY') date_generated
FROM report;
SENT_TO REPORT_I DATE_GENERATED -------------------- -------- --------------------------------- Manager 01011999 01-JAN-99 12.00.00.000000000 AM Director 01121999 12-JAN-99 12.00.00.000000000 AM Vice President 01231999 23-JAN-99 12.00.00.000000000 AM
In this example, the TO_TIMESTAMP function converts the MMDDYYYY data in the column to a TIMESTAMP. That TIMESTAMP value is then implicitly converted into a character string for display purposes, using the default timestamp format.
Interestingly, Oracle provides no function specifically to convert a text value into the TIMESTAMP WITH LOCAL TIME ZONE data type. To convert a value to TIMESTAMP WITH LOCAL TIME ZONE, you must use the CAST function, as in the following example:
SELECT CAST('10-DEC-01' AS TIMESTAMP WITH LOCAL TIME ZONE) FROM DUAL;
CAST('10-DEC-01'ASTIMESTAMPWITHLOCALTIMEZONE)
---------------------------------------------------------------------------
10-DEC-01 12.00.00 AM
In this example, the input string is in the default date format. Therefore, no date format is required for conversion. Indeed, CAST does not support date formats.
What then do you do if you wish to convert to TIMESTAMP WITH LOCAL TIME ZONE and you also need to specify a format? One solution here is to use a conversion function along with a format to convert the string into a value TIMESTAMP WITH TIME ZONE, which you can then cast to a TIMESTAMP WITH LOCAL TIME ZONE:
SELECT CAST(TO_TIMESTAMP_TZ('12/10/01','MM/DD/YY')
AS TIMESTAMP WITH LOCAL TIME ZONE)
FROM DUAL;
CAST(TO_TIMESTAMP_TZ('12/10/01','MM/DD/YY')ASTIMESTAMPWITHLOCALTIMEZONE) --------------------------------------------------------------------------- 10-DEC-01 12.00.00 AM
The CAST function used in these examples is not a SQL function in the truest sense. CAST is actually a SQL expression like DECODE and CASE. The CAST expression converts a value in one data type to a value in another data type. You can generally CAST between any two, compatible data types.
The TO_YMINTERVAL and TO_DSINTERVAL functions are similar in purpose to the TO_DATE family of functions, and serve to convert character strings to the INTERVAL YEAR TO MONTH and INTERVAL DAY TO SECOND data types. You can pass literals, PL/SQL variables, and database columns of CHAR or VARCHAR2 data type to these functions, which you invoke as follows:
TO_YMINTERVAL (string
) TO_DSINTERVAL (string
)
In these invocations, string
must contain
character data convertible to an INTERVAL YEAR TO MONTH or INTERVAL
DAY TO SECOND value, and in one of the following formats:
The input string must be in
Y
-
M
format—i.e., the year and month values must be separated by a
dash (-). All components (year, month, and -) must be present in the
string.
The input string must be in D
HH
:MI
:SS
format. The day value of the interval is separated by a space from
the time value, which is expressed in hours, minutes, and seconds,
and is delimited by “:”. All
components must be present in the string for it to be converted to an
INTERVAL DAY TO SECOND value.
The following two INSERT statements demonstrate the use of these functions:
INSERT INTO event_history VALUES (5001, TO_YMINTERVAL('02-04')); INSERT INTO batch_job_history VALUES (6001, TO_DSINTERVAL('0 2:30:43'));
In this example, the string '02-04
' represents an
interval of 2 years and 4 months, while the string '0
2:30:43
' represents an interval of 0 days, 2 hours, 30
minutes, and 43 seconds.
The NUMTOYMINTERVAL (NUMBER-TO-YEAR-MONTH-INTERVAL) and NUMTODSINTERVAL (NUMBER-TO-DAY-SECOND-INTERVAL) functions convert numeric values into INTERVAL YEAR TO MONTH and INTERVAL DAY TO SECOND values, respectively. You invoke these functions as follows:
NUMTOYMINTERVAL (n
,unit
) NUMTODSINTERVAL (n
,unit
)
The syntax elements are:
n
Specifies a numeric value, or a value that is convertible to a numeric type.
unit
Specifies the unit of time that n
represents. When converting to an INTERVAL YEAR TO MONTH,
unit
may be either
'YEAR
' or 'MONTH
‘. When
converting to an INTERVAL DAY TO SECOND,
unit
may be any of
'DAY
', 'HOUR
',
'MINUTE
', or 'SECOND
‘. Case
does not matter. Upper, lower, or mixed-case are all the same.
The following example demonstrates the use of these two functions. The first INSERT specifies an interval of two years, while the second specifies an interval of 5369.2589 seconds:
INSERT INTO event_history VALUES (5001, NUMTOYMINTERVAL(2,'YEAR')); INSERT INTO batch_job_history VALUES (6001, NUMTODSINTERVAL(5369.2589,'SECOND'));
Unlike the case with TO_YMINTERVAL and TO_DSINTERVAL, you cannot pass mixed units to these NUMTOXXINTERVAL functions. However, you can build up values from mixed units as follows:
INSERT INTO event_history VALUES (7001, NUMTOYMINTERVAL(2,'YEAR') + NUMTOYMINTERVAL (4, 'MONTH'));
This INSERT creates a two-year and four-month interval by adding a two-year interval to a four-month interval.
The TO_CHAR function is the opposite of the TO_DATE and TO_TIMESTAMP functions, and converts a DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE, or TIMESTAMP WITH LOCAL TIME ZONE value into a string of characters. Call TO_CHAR as follows:
TO_CHAR(temporal_data
[,format
])
The syntax elements are:
temporal_data
Specifies a literal, PL/SQL variable, or a database column of type DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE, or TIMESTAMP WITH LOCAL TIME ZONE.
format
Specifies the format of the output string. The format must be a valid combination of date or timestamp format elements as described later in Section 6.5.
The format is optional. When the format is not specified, the format of the output depends upon the type of the input data:
The output string takes the format specified by the parameter NLS_DATE_FORMAT.
The output string takes the format specified by the parameter NLS_TIMESTAMP_FORMAT.
The output string takes the format specified by the parameter NLS_TIMESTAMP_TZ_FORMAT.
The output string takes the format specified by the parameter NLS_TIMESTAMP_FORMAT.
The database parameters NLS_DATE_FORMAT, NLS_TIMESTAMP_FORMAT, and NLS_TIMESTAMP_TZ_FORMAT are discussed in Section 6.5.6.
The following example uses TO_CHAR to convert an input date into a string using the default date format:
SELECT fname, TO_CHAR(hire_date) FROM employee;
FNAME TO_CHAR(H
-------------------- ---------
John 22-OCT-99
The following example uses TO_CHAR to convert a timestamp into a string, and explicitly specifies a timestamp format:
SELECT TO_CHAR(SYSTIMESTAMP, 'MM/DD/YYYY HH24:MI:SS.FF') FROM DUAL;
TO_CHAR(SYSTIMESTAMP,'MM/DD/Y
-----------------------------
12/12/2003 10:18:36.070000
The format element FF
in the preceding example
represents fractional seconds. Timestamp-specific formats are
discussed in Section 6.5.
There are situations when you may need to combine TO_CHAR with TO_DATE. For example, if you want to know on what day of the week January 1, 2000, fell, you can use the following query:
SELECT TO_CHAR(TO_DATE('01-JAN-2000','DD-MON-YYYY'),'Day') FROM DUAL;
TO_CHAR(T
---------
Saturday
In this example, the input string '01-JAN-2000
' is
first converted into a date and then the TO_CHAR function is used to
convert this date into a string representing the day of the week.
You can display dates and times in a number of ways. Every country, every industry has its own standard for representing temporal data. Oracle provides you with date and time format codes so that you can interpret and display dates and timestamps in a wide variety of formats.
A simple example of displaying a date is:
SELECT SYSDATE FROM DUAL;
SYSDATE
---------
03-OCT-01
By default, the date is displayed using the DD-MON-RR format. This format uses two digits for the date (zero padded on the left), three characters for the month (the first three characters of the English, or your local language, name of the month in uppercase), and two digits for the year of the century (zero padded on the left). The default date format for the database is controlled by the NLS_DATE_FORMAT initialization parameter. You can use ALTER SYSTEM or ALTER SESSION commands to change the default date format for the instance or the session, respectively. You can also use the TO_CHAR function to specify a format on a per-call basis:
SELECT TO_CHAR(SYSDATE,'MM/DD/YYYY') FROM DUAL;
TO_CHAR(SY
----------
10/03/2001
This example converts the current date into the format
MM
/
DD
/
YYYY
using the TO_CHAR function. The second argument is a format string
specifying how we want the date to be presented. Table 6-1 describes the various date format elements at
your disposal. Most of the examples in Table 6-1
are based on 03-OCT-2001 03:34:48 P.M. Those that involve B.C. dates
use the year 2105 B.C. Those that specifically demonstrate A.M. times
are based on 03-OCT-2001 11:00:00 AM.
Component |
Options |
Description |
Format |
Output |
Punctuation |
- / , ; : . * |
Simply reproduced in the output |
DD-MON-YY |
03-OCT-01 |
Space |
Simply reproduced in the output |
DD MM YYYY |
03 10 2001 | |
“Text” |
Simply reproduced in the output |
DD “of” Month |
03 of October | |
Day |
DD |
Day of the month |
MM/DD/YY |
10/03/01 |
DDD |
Day of the year; starts with 1st January as 1 |
DDD/YY |
276/01 | |
D |
Day of the week; starts with Sunday as 1 |
D MM/YY |
4 10/01 | |
DAY |
Name of the day, in uppercase |
DAY MM/YY |
WEDNESDAY 10/01 | |
Day |
Name of the day, in mixed case |
Day MM/YY |
Wednesday 10/01 | |
DY |
Abbreviated name of the day, in uppercase |
DY MM/YY |
WED 10/01 | |
Dy |
Abbreviated name of the day, in mixed case |
Dy MM/YY |
Wed 10/01 | |
Month |
MM |
Two-digit month |
MM/DD/YY |
10/03/01 |
MONTH |
Name of the month, in uppercase |
MONTH YY |
OCTOBER 0 | |
Month |
Name of the month, in mixed case |
Month YY |
October 0 | |
MON |
Abbreviated name of the month, in uppercase |
MON YY |
OCT 0 | |
Mon |
Name of the month, in mixed case |
Mon YY |
Oct 01 | |
RM |
Roman-numeral month |
DD-RM-YY |
03-X-01 | |
Year |
Y |
Last one digit of year |
MM Y |
10 1 |
YY |
Last two digits of year |
MM YY |
10 01 | |
YYY |
Last three digits of year |
MM YYY |
10 001 | |
YYYY |
Four digits of year |
MM YYYY |
10 2001 | |
Y,YYY |
Year with comma |
MM Y,YYY |
10 2,001 | |
YEAR |
Year spelled out, in uppercase |
MM YEAR |
10 TWO THOUSAND ONE | |
Year |
Year spelled out, in mixed case |
MM Year |
10 Two Thousand One | |
SYYYY |
Four digits of year with “-” sign for BC |
SYYYY |
-2105 | |
RR |
Round year depending on the current year |
DD-MON-RR |
03-OCT-01 | |
RRRR |
Round year depending on the current year |
DD-MON-RRRR |
03-OCT-2001 | |
I |
Last one digit of the ISO Standard year |
MM I |
10 1 | |
IY |
Last two digits of the ISO Standard year |
MM IY |
10 01 | |
IYY |
Last three digits of the ISO Standard year |
MM IYY |
10 001 | |
IYYY |
Four digits of the ISO Standard year |
MM IYYY |
10 2001 | |
Century |
CC |
Century |
CC |
21 |
SCC |
Century with “-” sign for BC |
SCC |
-22 | |
Wtdeek |
W |
Week of the month |
W |
1 |
WW |
Week of the year |
WW |
40 | |
IW |
Week of the year in ISO standard |
IW |
40 | |
Quarter |
Q |
Quarter of the year |
Q |
4 |
Hour |
HH |
Hour of the day 1-12 |
HH |
03 |
HH12 |
Hour of the day 1-12 |
HH12 |
03 | |
HH24 |
Hour of the day 0-23 |
HH24 |
15 | |
Minute |
MI |
Minute of hour 0-59 |
MI |
34 |
Second |
SS |
Second of minute 0-59 |
SS |
48 |
SSSSS |
Seconds past midnight |
SSSSS |
42098 | |
AM/PM |
AM |
Meridian indicator |
HH:MI AM |
11:00 AM |
A.M. |
Meridian indicator with dots |
HH:MI A.M. |
11:00 A.M. | |
PM |
Meridian indicator |
HH:MI PM |
03:34 PM | |
P.M. |
Meridian indicator with dots |
HH:MI P.M. |
03:34 P.M. | |
AD/BC |
AD |
AD indicator |
YY AD |
01 AD |
A.D. |
AD indicator with dots |
YY A.D. |
01 A.D. | |
BC |
BC indicator |
YY BC |
05 BC | |
B.C. |
BC indicator with dots |
YY B.C. |
05 B.C. | |
Julian day |
J |
Number of days since January 1, 4712 BC |
J |
2452186 |
Suffix |
TH or th |
Ordinal number |
DDTH or DDth |
03RD |
SP or sp |
Spelled number |
MMSP or MMsp |
TEN | |
SPTH |
Spelled ordinal number |
DDSPTH |
THIRD | |
THSP |
Spelled ordinal number |
DD THSP |
THIRD | |
Fractional seconds |
FF |
Always use FF, with two Fs. |
HH:MI:SS.FF or HH:MI:SSXFF |
11:47:26.336000 |
Time zone |
TZH |
Time zone hour |
HH:MI:SS.FF TZH |
08:23:46.368 -10 |
TZ |
Time zone minute |
HH:MI:SS:FF TZH:TZM |
08:23:46.368 -10:30 |
Oracle provides two formats, AD and BC (two more with dots—A.D., B.C.), to characterize a year with respect to the year 0. However, they both serve the same purpose, and you can use either of them with equivalent results. If you have used the format BC in a query, and the date you are applying this format to comes out to be an AD year, Oracle is intelligent enough to print AD instead of BC, and vice versa. For example:
SELECT TO_CHAR(SYSDATE, 'YYYY AD'),
TO_CHAR(SYSDATE, 'YYYY BC') FROM DUAL;
TO_CHAR( TO_CHAR( -------- -------- 2001 AD 2001 ADSELECT TO_CHAR(ADD_MONTHS(SYSDATE,-50000), 'YYYY AD'),
TO_CHAR(ADD_MONTHS(SYSDATE,-50000), 'YYYY BC') FROM DUAL;
TO_CHAR( TO_CHAR( -------- -------- 2165 BC 2165 BC
In the first example, even though we supplied the BC format with the SYSDATE, it printed 2001 AD in the output, and in the second example, even though we supplied AD with a date 50,000 months earlier (in the BC), it printed BC in the output. The function ADD_MONTHS is discussed later in the chapter.
The AM/PM indicators (as well as A.M. and P.M.) behave exactly the same as the AD/BC indicators. If you have used the AM format in a query, and the time you are applying this format to comes out to be a PM time, Oracle is intelligent enough to print PM instead of AM, and vice versa. For example:
SELECT TO_CHAR(SYSDATE, 'HH:MI:SS AM'),
TO_CHAR(SYSDATE, 'HH:MI:SS PM'),
TO_CHAR(SYSDATE - 8/24, 'HH:MI:SS AM'),
TO_CHAR(SYSDATE - 8/24, 'HH:MI:SS PM')
FROM DUAL;
TO_CHAR(SYS TO_CHAR(SYS TO_CHAR(SYS TO_CHAR(SYS ----------- ----------- ----------- ----------- 06:58:07 PM 06:58:07 PM 10:58:07 AM 10:58:07 AM
Some date formats are case-sensitive while others aren’t. The formats that represent numbers are not case-sensitive. For example:
SELECT TO_CHAR(SYSDATE, 'HH:MI') UPPER,
TO_CHAR(SYSDATE, 'hh:mi') LOWER,
TO_CHAR(SYSDATE, 'Hh:mI') MIXED
FROM DUAL;
UPPER LOWER MIXED ----- ----- ----- 03:17 03:17 03:17
You can see that the format
HH
:MI
is case-insensitive—no matter which case you use for the
format, the output is the same. The same applies to all other format
elements that represent numbers, for example, DD, MM, YY, etc.
Date formats that represent
textual date components are
case-sensitive. For example, the format DAY
is
different from day
. The following rules apply for
determining the case of the output when a textual date format is
used:
If the first character of the format is lowercase, then the output will be lowercase, regardless of the case of the other characters in the format:
SELECT TO_CHAR(SYSDATE, 'month'),
TO_CHAR(SYSDATE, 'mONTH'),
TO_CHAR(SYSDATE, 'moNTh')
FROM DUAL;
TO_CHAR(S TO_CHAR(S TO_CHAR(S --------- --------- --------- october october october
If the first character of the format element is uppercase and the second character is also uppercase, then the output will be uppercase, regardless of the case of the other characters in the format:
SELECT TO_CHAR(SYSDATE, 'MOnth'),
TO_CHAR(SYSDATE, 'MONTH')
FROM DUAL;
TO_CHAR(S TO_CHAR(S --------- --------- OCTOBER OCTOBER
If the first character of the format element is uppercase and the second character is lowercase, then the output will have an uppercase first character and all other characters lowercase, regardless of the case of the other characters in the format:
SELECT TO_CHAR(SYSDATE, 'MoNTH'), TO_CHAR(SYSDATE, 'Month')
FROM DUAL;
TO_CHAR(S TO_CHAR(S --------- --------- October October
These rules apply to all text elements, such as those used to represent month names, day names, and so forth.
Even though Oracle stores the century of the year internally, it allows you to use two-digit years. Therefore, it is important to know how the century is handled when you use a two-digit year. Oracle provides two two-digit year formats that you can use: YY and RR.
With the YY year format, the century is assumed to be the current century:
ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MON-YY';
Session altered.SELECT SYSDATE, TO_CHAR(SYSDATE,'DD-MON-YYYY') FROM DUAL;
SYSDATE TO_CHAR(SYS --------- ----------- 06-OCT-01 06-OCT-2001SELECT TO_CHAR(TO_DATE('10-DEC-99'),'DD-MON-YYYY'),
TO_CHAR(TO_DATE('10-DEC-01'),'DD-MON-YYYY') FROM DUAL;
TO_CHAR(TO_ TO_CHAR(TO_ ----------- ----------- 10-DEC-2099 10-DEC-2001
Since the current date was 06-OCT-2001 when these examples were executed, the first two digits of the years (the century component) in this example are assumed to be 20.
With the RR year format, the first two digits of the specified year are determined based upon the last two digits of the current year and the last two digits of year specified. The following rules apply:
If the specified year is less than 50, and the last two digits of the current year are less than 50, then the first two digits of the return date are the same as the first two digits of the current date.
If the specified year is less than 50, and the last two digits of the current year are greater than or equal to 50, then first two digits of the return date are one greater than the first two digits of the current date.
If the specified year is greater than or equal to 50, and the last two digits of the current year are less than 50, then first two digits of the return date are one less than the first two digits of the current date.
If the specified year is greater than or equal to 50, and the last two digits of the current year are greater than or equal to 50, then the first two digits of the return date are the same as the first two digits of the current date.
The following example demonstrates these rules:
ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MON-RR';
Session altered.SELECT SYSDATE, TO_CHAR(SYSDATE,'DD-MON-YYYY') FROM DUAL;
SYSDATE TO_CHAR(SYS --------- ----------- 06-OCT-01 06-OCT-2001SELECT TO_CHAR(TO_DATE('10-DEC-99'),'DD-MON-YYYY'),
TO_CHAR(TO_DATE('10-DEC-01'),'DD-MON-YYYY') FROM DUAL;
TO_CHAR(TO_ TO_CHAR(TO_ ----------- ----------- 10-DEC-1999 10-DEC-2001
The ALTER SESSION command sets the default date format to DD-MON-RR. The next SELECT uses SYSDATE to show the current date at the time the example was executed. The final SELECT demonstrates the use of the RR date format (both TO_DATE calls rely on the default format set earlier). Note that the DD-MON-RR date format treats 10-DEC-99 as 10-DEC-1999, whereas it treats 10-DEC-01 as 10-DEC-2001. Compare this output to the rules we just listed.
The year format RRRR (four Rs) allows you to enter either a two-digit year or a four- digit year. If you enter a four-digit year, Oracle behaves as if the year format was YYYY. If you enter a two-digit year, Oracle behaves as if the year format is RR. The RRRR format is rarely used. Most SQL programmers prefer to use either YYYY, or to explicitly specify RR.
The ISO 8601 standard determines the start date of the first week of the year based upon whether most of the days in the week belong to the new year or to the previous year. If January 1 is a Monday, Tuesday, Wednesday, or a Thursday, then January 1 belongs to the first week of the new ISO year. The first day of the ISO year is either January 1 (if it is a Monday) or the previous Monday (which actually goes back to the last calendar year). For example, if January 1 is a Tuesday, then the first day of the ISO year is Monday, December 31, of the prior calendar year.
If January 1 is a Friday, Saturday, or a Sunday, then January 1 belongs to the last week of the previous ISO year. The first day of the first week of the new ISO year is then considered to be the Monday following January 1. For example, if January 1 falls on a Saturday, then the first day of the ISO year is considered to be Monday, January 3.
If you need to work with ISO dates, Oracle provides date formats that treat ISO years differently from calendar years. These ISO formats are:
Represents the week of the year in ISO standard.
Represents the ISO year.
The following sections describe ISO weeks and years with examples.
In the ISO standard, weeks of the year are counted differently than regular calendar weeks. In a regular calendar, the first week of the year starts on January 1. 01-JAN is the first date of the first week. However, in the ISO standard, a week always starts on a Monday and ends on a Sunday. Therefore, the first date of the first week is considered to be the date of the nearest Monday. This date could be a couple of days later than 01-JAN, or it could be a couple of days earlier (in the previous year).
The format WW returns the week of the year in terms of the regular calendar, and the format IW returns the week of the year in terms of the ISO standard. Since 01- JAN-2001 was a Monday, it was considered the start date of the first week in terms of the regular calendar as well as in terms of the ISO standard. Therefore, if you compute the week number of any date in the year 2001, the results will be the same whether you use the regular calendar or the ISO calendar. For example:
SELECT TO_CHAR(TO_DATE('10-DEC-01'),'WW'),
TO_CHAR(TO_DATE('10-DEC-01'),'IW')
FROM DUAL;
TO TO -- -- 50 50
However, the year 1999 didn’t start on a Monday. Therefore, for some dates, the week number in the ISO standard could be different from that of the regular calendar. For example:
SELECT TO_CHAR(TO_DATE('10-DEC-99'),'WW'),
TO_CHAR(TO_DATE('10-DEC-99'),'IW')
FROM DUAL;
TO TO -- -- 50 49
The ISO Standard can cause a year to have 53 weeks. Here’s an example:
SELECT TO_CHAR(TO_DATE('01-JAN-99'),'IW'),
TO_CHAR(TO_DATE('01-JAN- 99'),'Day')
FROM DUAL;
TO TO_CHAR(T -- --------- 53 Friday
Note that the ISO standard treats January 1, 1999 to be in the 53rd week of 1998, because it falls on a Friday. The first week of 1999 starts on the subsequent Monday, which is January 4, as per the ISO standard.
The year formats I, IY, IYY, and IYYY represent the ISO year. IYYY represents the four-digit ISO year, IYY represents the last three digits of the ISO year, IY represents the last two digits of the ISO year, and I represents the last digit of the ISO year. Remember that the start date of an ISO year is not necessarily January 1. The following example returns the ISO and calendar years for January 1, 1999:
SELECT TO_CHAR(TO_DATE('01-JAN-99'),'IYYY'),
TO_CHAR(TO_DATE('01-JAN-99'),'YYYY') FROM DUAL;
TO_C TO_C ---- ---- 1998 1999
Notice that even though the calendar year is 1999, the ISO year is considered to be 1998. That’s because 01-Jan-1999 fell on a Friday—late in the week, which causes the week to be considered part of the previous ISO year. The following example demonstrates the opposite situation:
SELECT TO_CHAR(TO_DATE('31-DEC-90'),'IYYY'),
TO_CHAR(TO_DATE('31-DEC-90'),'YYYY') FROM DUAL;
TO_C TO_C ---- ---- 1991 1990
This time, the calendar year is 1990, but the date 31-Dec-1990 is considered to be in ISO year 1991. This is because 01-Jan-1991 fell on a Tuesday, early enough in the week for the entire week to be considered part of the next ISO year.
The default formats to use when converting temporal data to character form are determined by database parameters. The key parameters are:
Specifies the default format used by TO_DATE and TO_CHAR functions when converting character data into data of type DATE or vice versa.
Specifies the default format used by TO_TIMESTAMP and TO_CHAR functions when converting character data into data of type TIMESTAMP and TIMESTAMP WITH LOCAL TIME ZONE or vice versa.
Specifies the default format used by TO_TIMESTAMP_TZ and TO_CHAR when converting character data into data of type TIMESTAMP WITH TIME ZONE or vice versa.
If any of these parameters are not set explicitly, its default value is derived from the setting for the NLS_TERRITORY parameter. The NLS_TERRITORY parameter specifies the territory, such as “AMERICA” or “CZECH REPUBLIC.” For more details on the NLS parameters, refer to Oracle’s Globalization Support Guide.
You or your DBA can specify values for these default date format parameters in one of the following three ways:
By specifying a value in the instance’s initialization parameter file. For example:
NLS_DATE_FORMAT = 'YYYY-MM-DD'
A format string specified using this approach becomes the instance-wide default.
By issuing an ALTER SESSION command to change the default for your current session:
ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF';
By setting an environment variable on your client, to change the default value for all sessions initiated from your client:
setenv NLS_DATE_FORMAT 'YYYY-MM-DD'
The NLS_LANG environment variable must be set for any other NLS_ environment variable setting to take effect. Unless NLS_LANG is set, all settings for other NLS_ environment variables are ignored.
The session-level setting overrides the environment variable setting, and the environment variable setting overrides the initialization parameter setting.
Date arithmetic is an important aspect of our day-to-day life. We find the age of a person by subtracting his date of birth from today’s date. We compute the date a warranty expires by adding the warranty period to the purchase date. Drivers’ license expirations, bank interest calculation, and a host of other things all depend on date arithmetic. It is extremely important for any database to support such common date arithmetic operations.
Oracle provides a number of helpful functions, some of which you’ve seen used earlier in this chapter, that you can use to manipulate temporal values:
ADD_MONTHS(
date_value
, months
)
Adds months to a date. Add negative values to subtract months. If the initial date represents the last day of a month, the result is forced to the final day of the result month. Section 6.6.2 discusses this function in detail.
CURRENT_DATE
Returns the current date in the session time zone, as a DATE value.
CURRENT_TIMESTAMP
Returns the current date and time in the session time zone, as a TIMESTAMP WITH TIME ZONE value.
DBTIMEZONE
Returns the database time zone.
EXTRACT(
element
FROM
temporal_value
)
Returns the specified element from a date, timestamp, or interval. Valid elements, which are SQL keywords and not string values, are: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, TIMEZONE_HOUR, TIMEZONE_MINUTE, TIMEZONE_REGION, and TIMEZONE_ABBR. The temporal value may be any of Oracle’s date, timestamp, or interval types.
FROM_TZ(
timestamp
, time_zone
)
Converts a TIMESTAMP into a TIMESTAMP WITH TIME ZONE, essentially
merging the two values you provide into one. The
time_zone
argument must be a string in the
form
[+|-]
hh
:mi
.
LAST_DAY(
date_value
)
Computes the last day of the month in which the given DATE value falls.
LOCALTIMESTAMP
Returns the current date and time in the session timezone, as a TIMESTAMP value.
MONTHS_BETWEEN(
later_date
, earlier_date
)
Determines the number of months between two dates. The calculation is
performed as: later_date
-
earlier_date
. If
later_date
is actually earlier, than
you’ll get a negative result. See Section 6.6.3 for
a detailed look at this function.
NEW_TIME(
date
, source_time_zone
, target_time_zone
)
Translates the time component of date
from
the source_time_zone
to the
target_time_zone
. The time zone arguments
must be strings containing time zone abbreviations such as PST, EST,
CST. The list of time zones supported for use with NEW_TIME is
shorter, and distinct from, the list of time zones supported for the
timestamp types.
NEXT_DAY(
date
, weekday
)
Returns the date of the next specified weekday following the given
date
. The
weekday
argument must be a valid weekday
name or abbreviation in the current language—e.g.,
“Monday,”
“Tuesday,”
“Wed,”
“Thu.”
ROUND(
temporal_value
, format_element
)
Rounds a date or timestamp value to the specified element. See Section 6.6.5.
SESSIONTIMEZONE
Returns the session timezone.
SYSDATE
Returns the current date and time for the operating system on which the database resides.
SYSTIMESTAMP
Returns the current date and timestamp time for the operating system on which the database resides as a TIMESTAMP WITH TIME ZONE value.
SYS_EXTRACT_UTC (
timestamp_with timezone_value
)
Returns the UTC data and time value with respect to the input TIMESTAMP WITH TIME ZONE value.
TRUNC(
temporal_value
, format_element
)
Truncates a date/time value to a specific element. See Section 6.6.5.
TZ_OFFSET([
tz_name
|
tz_offset
])
Returns the time zone offset with respect to UTC. Input may be a time
zone name from V$TIMEZONE_NAMES or a time zone offset in the form
[+|-]
hh
:mi
.
SYSDATE is one of the most commonly used functions, and returns the current date and time as a DATE value:
ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MON-RR HH:MI:SS';
Session altered.SELECT SYSDATE FROM DUAL;
SYSDATE ------------------ 11-NOV-01 01:00:10
The following is an example of a function that takes arguments. The FROM_TZ function is used to add time zone information to the timestamp returned by a call to SYSTIMESTAMP. You can see that LOCALTIMESTAMP by itself returns no time zone information. FROM_TZ combines the TIMESTAMP with the time zone we specified, and returns a TIMESTAMP WITH TIME ZONE:
SELECT LOCALTIMESTAMP FROM dual;
LOCALTIMESTAMP -------------------------------------------------------- 18-DEC-03 03.31.24.974000 PMSELECT FROM_TZ(LOCALTIMESTAMP,'-5:00') FROM dual;
FROM_TZ(LOCALTIMESTAMP,'-5:00') -------------------------------------------------------- 18-DEC-03 03.31.25.024000 PM -05:00
The EXTRACT function is unusual in that its first argument is actually a SQL keyword, and the delimiter between arguments is also a keyword:
SELECT EXTRACT(YEAR FROM SYSDATE) FROM dual;
EXTRACT(YEARFROMSYSDATE)
------------------------
2003
A more useful and interesting example of EXTRACT is shown at the end of Section 6.6.3.
Many of Oracle’s temporal functions take only DATE values as inputs. This harks back to the day when DATE was the only temporal type. You have to be careful about this, because Oracle will implicitly convert timestamp types to DATEs, leading you to inadvertently write erroneous code. For example:
SELECT ADD_MONTHS(SYSTIMESTAMP,1) FROM dual;
ADD_MONTH
---------
18-JAN-04
The problem here isn’t terribly obvious, but what’s happened is that SYSTIMESTAMP has returned a TIMESTAMP WITH TIME ZONE value, which has been implicitly cast to a DATE, and thus both fractional seconds and the time zone have been lost. The results are the same as if you’d executed:
SELECT ADD_MONTHS(CAST(SYSTIMESTAMP AS DATE),1) FROM dual;
Be careful about passing TIMESTAMP values to functions that expect DATEs. If your code depends on fractional seconds or time zone information, you’ll lose that information, and your code won’t work as you expect.
Adding two datetime values doesn’t make sense. However, you can add days, months, years, hours, minutes, and seconds to a datetime to generate a future date and time. How you go about adding time intervals to datetime values depends on whether you are working with a DATE or one of the TIMESTAMP values.
The + operator allows you to add numbers to a DATE. The unit of a number added to a DATE is assumed to be days. Therefore, to find tomorrow’s date, you can add 1 to SYSDATE:
SELECT SYSDATE, SYSDATE+1 FROM DUAL;
SYSDATE SYSDATE+1
--------- ---------
05-OCT-01 06-OCT-01
Any time you add a number to a DATE, Oracle assumes that the number represents a number of days. Therefore, if you want to add multiples of a day (week, month, year, etc.) to a DATE, you first need to multiply by a conversion factor. For example, to add one week to today’s date, you add 7 (7 days in a week times 1 day) to SYSDATE:
SELECT SYSDATE+7 FROM DUAL;
SYSDATE+7
---------
12-OCT-01
Similarly, if you want to add fractions of a day (hour, minute, second) to a DATE, you first need to convert such fractions into a fractional number of days. Do this by dividing by a conversion factor. For example, to add 20 minutes to the current date and time, you need to add (20 minutes/1,440 minutes in a day) to SYSDATE:
SELECT TO_CHAR(SYSDATE,'DD-MON-YY HH:MI:SS'),
TO_CHAR(SYSDATE+(20/1440),'DD-MON-YY HH:MI:SS')
FROM DUAL;
TO_CHAR(SYSDATE,'D TO_CHAR(SYSDATE+(2 ------------------ ------------------ 05-OCT-01 01:22:03 05-OCT-01 01:42:03
Adding months to a DATE is not as easy as adding weeks, because all months don’t have the same number of days—some have 30, some 31, some 28, and at times even 29. To add one month to a DATE, you need to know how many days that calendar month will have. Therefore, adding months to a DATE by converting those months to a number of days involves lots of homework, which is error-prone. Fortunately, Oracle does all the homework for us, and provides a built-in SQL function to add months to DATE values. This function is called ADD_MONTHS, and you call it as follows:
SELECT fname, hire_date, ADD_MONTHS(hire_date, 6) review_date
FROM employee;
FNAME HIRE_DATE REVIEW_DA -------------------- --------- --------- John 22-OCT-99 22-APR-00
This example shows the computation of an employee’s
biannual review date by using ADD_MONTHS to add six months to the
employee’s hire_date
. The input
DATE and the result DATE both fall on the 22nd of the month. This
would not have happened if we had added 180 days to the input DATE.
ADD_MONTHS is “smart” in one other
way, too. The following example adds 6 months to 31 December 1999:
SELECT ADD_MONTHS('31-DEC-99',6) FROM DUAL;
ADD_MONTH
---------
30-JUN-00
The ADD_MONTHS function is intelligent enough to know that adding 6 months to 31 December should result in the last day of June. And since the last day of June is the 30th (not 31st), it returns 30 June, 2000.
You can use the + operator to add INTERVALs to DATE or TIMESTAMP values. For example, assume the flight time between New York and Los Angeles is 6 hours and 27 minutes. To find the arrival time of the flight, leaving New York now (3:39 PM in the example), at Los Angeles, you add the flight time to the current time:
select sysdate, sysdate + INTERVAL '0 6:27:00' DAY TO SECOND
from dual;
SYSDATE SYSDATE+INTERVAL'06 ------------------- ------------------- 12/29/2003 15:39:00 12/29/2003 22:06:00
The preceding example adds an interval literal INTERVAL '0
6:27:00' DAY TO SECOND
(6 hours and 27 minutes) to the DATE
value returned by SYSDATE. The result of the addition is a DATE value
and is in the New York (EST) time zone. However, since the
destination is Los Angeles, you would like the output to be in the
local time zone of the destination (PST). To achieve this, you can
use the NEW_TIME function, as shown in the following example:
select sysdate,
new_time(sysdate + INTERVAL '0 6:27:00' DAY TO SECOND, 'EST','PST')
from dual;
SYSDATE NEW_TIME(SYSDATE+IN ------------------- ------------------- 12/29/2003 15:39:00 12/29/2003 19:06:00
Therefore, a flight with flight time of 6 hours and 27 minutes that leaves New York at 3:39 PM will reach Los Angeles at 7:06 PM local time.
Similarly to adding intervals to DATE values, you can add intervals to TIMESTAMP values. For example:
SELECT LOCALTIMESTAMP, LOCALTIMESTAMP + INTERVAL '0 3:16:23' DAY TO SECOND
FROM DUAL;
LOCALTIMESTAMP --------------------------------------------- LOCALTIMESTAMP+INTERVAL'03:16:23'DAYTOSECOND --------------------------------------------- 28-MAR-04 04.30.19.208000 PM 28-MAR-04 07.46.42.208000000 PM
If you need to add some number of days to a TIMESTAMP value, you shouldn’t just directly add the number to the TIMESTAMP. In doing so, the TIMESTAMP will be implicitly converted to a DATE, which results in the loss of information. Specifically, you’ll lose your fractional seconds. Rather, you should convert the number to an interval, and then add the interval to the TIMESTAMP. The result will be a TIMESTAMP value, and no information will be lost. For example:
SELECT LOCALTIMESTAMP + INTERVAL '1 0:00:00' DAY TO SECOND
FROM DUAL;
LOCALTIMESTAMP+INTERVAL'10:00:00'DAYTOSECOND --------------------------------------------- 29-MAR-04 04.36.46.211000000 PM
As an alternative to using an INTERVAL literal as in the preceding example, you can use the NUMTODSINTERVAL function to convert a number to an interval, as shown in the following example:
SELECT LOCALTIMESTAMP + NUMTODSINTERVAL(1,'DAY')
FROM DUAL;
LOCALTIMESTAMP+NUMTODSINTERVAL(1,'DAY') ----------------------------------------- 29-MAR-04 04.37.16.077000000 PM
Even though no other arithmetic operation (addition, multiplication, division) between two DATEs makes any sense, subtracting one DATE from another DATE is a very common and useful operation. The - operator allows you to subtract a DATE from a DATE, or a number from a DATE, a TIMESTAMP from a TIMESTAMP, an interval from a DATE and an interval from a TIMESTAMP.
Subtracting one DATE from another DATE returns the number of days
between those two DATEs. The following example displays the lead time
of a set of orders by subtracting the date on which the order was
placed (order_dt
) from the expected ship date
(expected_ship_dt
):
SELECT order_nbr, expected_ship_dt - order_dt lead_time
FROM cust_order;
ORDER_NBR LEAD_TIME ---------- ---------- 1001 1 1000 5 1002 13 1003 10 1004 9 1005 2 1006 6 1007 2 1008 2 1009 4 1012 1 1011 5 1015 13 1017 10 1019 9 1021 2 1023 6 1025 2 1027 2 1029 4
Subtracting one TIMESTAMP from another TIMESTAMP returns an interval of type INTERVAL DAY TO SECOND. For example:
SELECT LOCALTIMESTAMP - transaction_timestamp FROM transaction;
SYSTIMESTAMP-TRANSACTION_TIMESTAMP
-----------------------------------
+000000453 07:04:39.086000
Along with subtracting one DATE from another, you can also subtract a number from a DATE. Subtracting a number from a DATE returns a DATE that number of days in the past. For example, subtracting 1 from SYSDATE gives yesterday, and subtracting 7 from SYSDATE yields the same day last week:
SELECT SYSDATE, SYSDATE - 1, SYSDATE - 7 FROM DUAL;
SYSDATE SYSDATE-1 SYSDATE-7
--------- --------- ---------
05-OCT-01 04-OCT-01 28-SEP-01
Unlike ADD_MONTHS, Oracle doesn’t provide a SUBTRACT_MONTHS function. To subtract months from a DATE, use the ADD_MONTHS function, and pass a negative number as the second parameter:
SELECT SYSDATE, ADD_MONTHS(SYSDATE, -6) FROM DUAL;
SYSDATE ADD_MONTH
--------- ---------
05-OCT-01 05-APR-01
Earlier in this section you saw that subtracting a DATE from another
DATE returns the number of days between the two dates. There are
times when you may want to know the number of months between two
DATEs. Consider that subtracting an employee’s
hire_date
from SYSDATE yields the number of days
of experience the employee has with her employer:
SELECT SYSDATE-hire_date FROM employee;
SYSDATE-HIRE_DATE
-----------------
714.0786
It’s better, in most cases, to find the number of months of experience rather than the number of days. You know that dividing the number of days between two DATEs by 30 won’t accurately calculate the number of months between those two DATEs. Therefore, Oracle provides the built-in SQL function MONTHS_BETWEEN for finding the number of months between two DATEs. MONTHS_BETWEEN is called as follows:
SELECT MONTHS_BETWEEN(SYSDATE,hire_date),
MONTHS_BETWEEN(hire_date, SYSDATE)
FROM employee;
MONTHS_BETWEEN(SYSDATE,HIRE_DATE) MONTHS_BETWEEN(HIRE_DATE,SYSDATE) --------------------------------- --------------------------------- 267.83499 -267.83499
MONTHS_BETWEEN subtracts the second DATE from the first. So, if the second DATE comes later than the first, then MONTHS_BETWEEN will return a negative value. You can see that behavior in this example. Both calls use the same two DATEs, but in different orders, and the difference in results is that one result is negative while the other is positive.
There is no YEARS_BETWEEN function. To find the number of years between two DATEs, you can either subtract the two DATEs to find the number of days and then divide by 365, or use MONTHS_BETWEEN to find the number of months and then divide by 12. All years don’t have the same number of days—some have 365 days and others have 366 days. Therefore, it is not accurate to divide the number of days by 365 to get the number of years. On the other hand, all years have 12 months, whether a leap year or not. Therefore, the most accurate way to calculate the number of years between two DATEs is to use the MONTHS_BETWEEN function to find the number of months and then divide by 12 to get the number of years.
As you saw in the preceding examples, subtracting one DATE from
another returns the number of days. However, at times, if the
difference is a fractional day, you would like to find the number of
hours, minutes, and seconds between two points in time. In the next
example, a pizza delivery center keeps track of the
order_receive_time
and the
delivery_time
of the orders it receives:
CREATE TABLE pizza_delivery (
order_id NUMBER(10),
order_receive_time DATE,
delivery_time DATE);
Table created.INSERT INTO pizza_delivery VALUES
(1, TO_DATE('12/20/03 08:47:53','MM/DD/YY HH:MI:SS'),
TO_DATE ('12/20/03 10:30:34','MM/DD/YY HH:MI:SS'));
1 row created.COMMIT;
Commit complete.
The manager of the pizza delivery center wants to know the time
difference between the order_receive_time
and the
delivery_time
. You can use the date subtraction
operation as in the following query:
SELECT delivery_time - order_receive_time FROM pizza_delivery;
DELIVERY_TIME-ORDER_RECEIVE_TIME
--------------------------------
.07130787
The fractional days returned by the preceding query doesn’t make much sense. The manager wants to know the time difference in hours, minutes, and seconds. One way to find this would be to convert the number returned by the date subtraction into an interval using the NUMTODSINTERVAL function, and then extract the hour, minute, and second components of this interval using the EXTRACT function:
SELECT EXTRACT(HOUR FROM
NUMTODSINTERVAL(delivery_time - order_receive_time, 'DAY'))
||':'||
EXTRACT(MINUTE FROM
NUMTODSINTERVAL(delivery_time - order_receive_time, 'DAY'))
||':'||
EXTRACT(SECOND FROM
NUMTODSINTERVAL(delivery_time - order_receive_time, 'DAY'))
"Lead Time"
FROM pizza_delivery;
Lead Time --------------------------------------------------------------------------- 1:42:41
This example uses three expressions to extract hour, minute, and second, respectively, and then concatenates those values using the : delimiter to return the result in the HH:MI:SS format, which is much easier to comprehend compared to a fractional day.
You can use the - operator to subtract an INTERVAL from a DATE or TIMESTAMP value. For example, if you need to reach your office at 8:00 AM, and it takes 30 minutes to reach from your home to office, you can use the following example to back calculate the start time:
SELECT TO_DATE('12/29/2003 08:00:00') - INTERVAL '0 0:30:00' DAY TO SECOND
FROM DUAL;
TO_DATE('12/29/2003 ------------------- 12/29/2003 07:30:00
The preceding example subtracts an interval literal
(INTERVAL '0 0:30:00' DAY TO
SECOND
) from a DATE value, and returns a DATE
value. Similarly, you can subtract an interval from a TIMESTAMP
value, using the “-” operator, as
shown in the following example:
SELECT LOCALTIMESTAMP - INTERVAL '0 0:30:00' DAY TO SECOND
FROM DUAL;
LOCALTIMESTAMP-INTERVAL'00:30:00'DAYTOSECOND --------------------------------------------- 28-MAR-04 03.42.59.819000000 PM
The preceding example subtracts an interval literal
(INTERVAL '0 0:30:00' DAY TO
SECOND
) from a TIMESTAMP value, and returns a
TIMESTAMP value.
If you need to subtract some number of days from a TIMESTAMP value, you shouldn’t just directly subtract the number from the TIMESTAMP. In doing so, the TIMESTAMP will be implicitly converted to a DATE and result in loss of information. Rather, you should convert the number to an interval, and then subtract the interval from the TIMESTAMP. This way, the result will be a TIMESTAMP value, and no information will be lost, as illustrated in the following example:
SELECT LOCALTIMESTAMP - INTERVAL '1 0:00:00' DAY TO SECOND
FROM DUAL;
LOCALTIMESTAMP-INTERVAL'10:00:00'DAYTOSECOND ---------------------------------------------- 27-MAR-04 04.23.09.248000000 PM
As an alternative to the preceding example, you can use the NUMTODSINTERVAL function to convert a number to an interval, as shown in the following example:
SELECT LOCALTIMESTAMP - NUMTODSINTERVAL(1,'DAY')
FROM DUAL;
LOCALTIMESTAMP-NUMTODSINTERVAL(1,'DAY') ----------------------------------------- 27-MAR-04 04.27.41.052000000 PM
Oracle provides a built-in function to get the last day of a month. The function returns the last day of the month containing the input date. For example, to find the last date of the current month, you can use the following SQL statement:
SELECT LAST_DAY(SYSDATE) "Next Payment Date" FROM DUAL;
Next Paym
---------
31-OCT-01
Sometimes it’s useful to be able to determine the first day of a given month; it would be nice if Oracle would provide a FIRST_DAY function. One approach to getting the first day of the month for a given date is to use the TRUNC function:
TRUNC(SYSDATE,'MM')
A side-effect of this approach is that any time-of-day component of
the input value is eliminated; the result will always have a time of
midnight at the beginning of the day. Such a truncation of time may
be good, especially if you are doing a range comparison. For example,
to find all employees hired in the current month, without applying
any sort of function to the hire_date
column:
SELECT * FROM employee WHERE hire_date >= TRUNC(SYSDATE,'MM') AND hire_date < TRUNC(LAST_DAY(SYSDATE)+1);
This SELECT statement works because
TRUNC(SYSDATE,'MM')
sets the time-of-day to the
very beginning of the first day of the month. The second TRUNC
expression resolves to the very beginning of the first day of the
following month, which is why less-than rather than
less-than-or-equal-to is used when comparing
hire_date
to that value.
In many cases,
TRUNC(
date
,'MM')
will work just fine for getting to the first day of a month. However,
if you need to determine the first day of the month in which a given
DATE value falls while also preserving the time-of-day, you can use
the following expression:
ADD_MONTHS((LAST_DAY(SYSDATE)+1), -1)
This expression finds the last day of the month represented by date. It then adds 1 to get to the first day of the subsequent month, and finally uses ADD_MONTHS with an argument of -1 to go back to the beginning of the month in which you started. The result is the first day of the month in which the given date falls. Other approaches to this problem are possible; this is just one that works well for us. This approach has the advantage of preserving the time component of the date in question.
Rounding and truncating dates is similar in concept to the rounding and truncating of numbers, but more involved because an Oracle DATE contains date as well as time information. Use the ROUND function to round a date/time value to a specific element; use the TRUNC function to truncate a date/time value to a specific element.
Take care when using ROUND and TRUNC on TIMESTAMP values. Such values are implicitly converted to type DATE before being passed to ROUND or TRUNC, and you’ll lose any information, such as time zone and fractional seconds, that a DATE value cannot hold.
The return value from ROUND or TRUNC depends upon the specified format, which is an optional parameter. If you don’t specify a format in the call to ROUND, the function returns a date by rounding the input to the nearest day. If you don’t specify a format in the call to TRUNC, that function returns the input date by setting the time component to the beginning of the day.
When using ROUND and TRUNC to round to the nearest day, or to truncate a date, the functions set the time fields of the return value to the beginning of the returned day—i.e., 12:00:00 AM (00:00:00 in HH24 format). For example:
SELECT TO_CHAR(SYSDATE, 'DD-MON-YY HH:MI:SS AM'),
TO_CHAR(ROUND(SYSDATE), 'DD-MON-YY HH:MI:SS AM'),
TO_CHAR(TRUNC(SYSDATE), 'DD-MON-YY HH:MI:SS AM')
FROM DUAL;
TO_CHAR(SYSDATE,'DD-M TO_CHAR(ROUND(SYSDATE TO_CHAR(TRUNC(SYSDATE --------------------- --------------------- --------------------- 06-OCT-01 07:35:48 AM 06-OCT-01 12:00:00 AM 06-OCT-01 12:00:00 AM
Notice that since the input time (SYSDATE) is before 12 noon, the output of ROUND and TRUNC are the same. However, if the input time were after 12 noon, the output of ROUND and TRUNC would be different, as in the following example:
SELECT TO_CHAR(SYSDATE, 'DD-MON-YY HH:MI:SS AM'),
TO_CHAR(ROUND(SYSDATE), 'DD-MON-YY HH:MI:SS AM'),
TO_CHAR(TRUNC(SYSDATE), 'DD-MON-YY HH:MI:SS AM')
FROM DUAL;
TO_CHAR(SYSDATE,'DD-M TO_CHAR(ROUND(SYSDATE TO_CHAR(TRUNC(SYSDATE --------------------- --------------------- --------------------- 06-OCT-01 05:35:48 PM 07-OCT-01 12:00:00 AM 06-OCT-01 12:00:00 AM
Since the input time is past 12 noon, ROUND returns the beginning of the next day. However, TRUNC still returns the beginning of the input date. This is similar to the rounding and truncating of numbers.
When you specify a format as an input to the ROUND and TRUNC functions, things become a bit more involved, but the concepts of rounding and truncating still remain the same. The difference is that the rounding and truncating are now based on the format you specify. For example, if you specify the format as YYYY, the input date will be truncated or rounded based on the year, which means that if the input date is before the middle of the year (July 1), both ROUND and TRUNC will return the first day of the year. If the input date is after July 1, ROUND will return the first day of the next year, whereas TRUNC will return the first day of the input year. For example:
SELECT TO_CHAR(SYSDATE-180, 'DD-MON-YYYY HH24:MI:SS'),
TO_CHAR(ROUND(SYSDATE-180,'YYYY'),'DD-MON-YYYY HH24:MI:SS'),
TO_CHAR(TRUNC(SYSDATE-180,'YYYY'),'DD-MON-YYYY HH24:MI:SS')
FROM DUAL;
TO_CHAR(SYSDATE-180, TO_CHAR(ROUND(SYSDAT TO_CHAR(TRUNC(SYSDAT -------------------- -------------------- -------------------- 09-APR-2001 20:58:33 01-JAN-2001 00:00:00 01-JAN-2001 00:00:00SELECT TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS'),
TO_CHAR(ROUND(SYSDATE,'YYYY'),'DD-MON-YYYY HH24:MI:SS'),
TO_CHAR(TRUNC(SYSDATE,'YYYY'),'DD-MON-YYYY HH24:MI:SS')
FROM DUAL;
TO_CHAR(SYSDATE,'DD- TO_CHAR(ROUND(SYSDAT TO_CHAR(TRUNC(SYSDAT -------------------- -------------------- -------------------- 06-OCT-2001 20:58:49 01-JAN-2002 00:00:00 01-JAN-2001 00:00:00
Similarly, you can round or truncate a date to a specific month, quarter, week, century, hour, minute, and so forth by using the appropriate format. Table 6-2 lists the formats (and their meanings) that can be used with the ROUND and TRUNC functions.
Rounding unit |
Format |
Remarks |
Century |
CC |
TRUNC returns the first date of the century. |
SCC |
ROUND returns the first date of the century if the input date is before the middle of the century (01-JAN-xx51); otherwise, ROUND returns the first date of the next century. | |
Year |
SYYYY |
TRUNC returns the first date of the year. |
YYYY YEAR SYEAR YYY YY Y |
ROUND returns the first date of the year if the input date is before the middle of the year (01-JUL); otherwise, ROUND returns the first date of the next year. | |
ISO |
IYYY |
TRUNC returns the first date of the ISO year. |
IYY IY I |
ROUND returns the first date of the ISO year if the input date is before the middle of the ISO year; otherwise, ROUND returns the first date of the next ISO year. | |
Quarter |
Q |
TRUNC returns the first date of the quarter. ROUND returns the first date of the quarter if the input date is before the middle of the quarter (the 16th day of the second month of the quarter); otherwise, ROUND returns the first date of the next quarter. |
Month |
MONTH |
TRUNC returns the first date of the month. |
MON MM RM |
ROUND returns the first date of the month if the input date is before the middle of the month (the 16th day of the month); otherwise, ROUND returns the first date of the next month. | |
Week |
WW |
TRUNC returns the first date of the week. ROUND returns the first date of the week if the input date is on or before the middle of the week (based on the first day of the year); otherwise, the first date of the next week. |
ISO Week |
IW |
TRUNC returns the first date of the ISO week. ROUND returns the first date of the week if the input date is before the middle of the week (based on the first day of the ISO year); otherwise, ROUND returns the first date of the next week. |
Week |
W |
TRUNC returns the first date of the week. ROUND returns the first date of the week if the input date is before the middle of the week (based on the first day of the month); otherwise, ROUND returns the first date of the next week. |
Day |
DDD |
TRUNC returns the beginning of the day. |
DD J |
ROUND returns the beginning of the day if the input time is before the middle of the day (12:00 noon); otherwise, ROUND returns the beginning of the next day. | |
Day of the week |
DAY |
TRUNC returns the first date of the week. |
DY D |
ROUND returns the first date of the week if the input date is before the middle of the week (based on the first day of the month); otherwise, ROUND returns the first date of the next week. | |
Hour |
HH |
TRUNC returns the beginning of the hour. |
HH12 HH24 |
ROUND returns the beginning of the hour if the input time is before the middle of the hour (00:30); otherwise, ROUND returns the beginning of the next hour. | |
Minute |
MI |
TRUNC returns the beginning of the minute. ROUND returns the beginning of the minute if the input time is before the middle of the minute (00:00:30); otherwise, ROUND returns the beginning of the next minute. |
There are times when you need to SELECT data from a table based on a given date range. Let’s say you have been asked to print all disputed orders placed on a given date, say 22-JUL-01. Most likely, your immediate response would be a query such as the following:
SELECT * FROM disputed_orders
WHERE order_dt = '22-JUL-01';
no rows selected
There’s no output. Surprised? Although you know
there are orders on 22-JUL-01, this query didn’t
return any rows. The reason is that order_dt
is a
DATE column, and contains time as well as date information. On the
other hand, the date literal '22-JUL-01
'
doesn’t contain any time information. When you
don’t specify the time portion in a date literal,
the time portion is assumed to be beginning of the day—i.e.,
12:00:00 A.M. (or 00:00:00 in 24 hour format). In the
disputed_orders
table, the time components in the
order_dt
column are other than 12:00:00 A.M. In
this case, the correct query to print orders placed on 22-JUL-01 is:
SELECT order_nbr, cust_nbr, order_dt, expected_ship_dt
FROM disputed_orders
WHERE order_dt BETWEEN
TO_DATE('22-JUL-01 00:00:00','DD-MON-YY HH24:MI:SS') AND
TO_DATE('22-JUL-01 23:59:59','DD-MON-YY HH24:MI:SS'),
ORDER_NBR CUST_NBR ORDER_DT EXPECTED_ --------- ---------- --------- --------- 1001 1 22-JUL-01 23-JUL-01 1005 8 22-JUL-01 24-JUL-01 1006 1 22-JUL-01 28-JUL-01 1012 1 22-JUL-01 23-JUL-01 1021 8 22-JUL-01 24-JUL-01 1023 1 22-JUL-01 28-JUL-01
The query treats the one day as a range: 22-JUL-01 00:00:00 through 22-JUL-01 23:59:59. Thus, the query returns any order placed at any time during 22-JUL-01.
Another way to solve this problem of needing to ignore the time components in a DATE column is to truncate the date, and then compare the truncated result with the input literal:
SELECT order_nbr, cust_nbr, order_dt, expected_ship_dt
FROM disputed_orders
WHERE TRUNC(order_dt) = '22-JUL-01';
ORDER_NBR CUST_NBR ORDER_DT EXPECTED_ --------- ---------- --------- --------- 1001 1 22-JUL-01 23-JUL-01 1005 8 22-JUL-01 24-JUL-01 1006 1 22-JUL-01 28-JUL-01 1012 1 22-JUL-01 23-JUL-01 1021 8 22-JUL-01 24-JUL-01 1023 1 22-JUL-01 28-JUL-01
The TRUNC function sets the time
portion to the beginning of the day. Therefore, the equality
comparison with the date literal '22-JUL-01
'
returns the expected output. The same result can be achieved by
converting order_dt
to a character string in a
format matching that of the input data:
SELECT * FROM disputed_orders WHERE TO_CHAR(order_dt,'DD-MON-YY') = '22-JUL-01';
The downside to the approach of using the TRUNC and TO_CHAR functions
is that the resulting query cannot make use of any index that happens
to be on the order_dt
column. This can have
significant performance implications. On the other hand, the date
range solution, while more complex to code, does not preclude the use
of any index on the column in question.
Oracle8i and higher support the use of function-based indexes, which, if created correctly, allow for the use of indexes even when functions are applied to columns in query predicates.
You can use the same techniques shown in this section to SELECT data based on any given date range, even if that range spans more than just one day.
For certain types of queries, it’s helpful to have a table with one row for each date over a period of time. For example, you might wish to have one row for each date in the current year. You can use the TRUNC function in conjunction with some PL/SQL code to create such a table:
CREATE TABLE dates_of_year (one_day DATE);
Table created.DECLARE
i NUMBER;
start_day DATE := TRUNC(SYSDATE,'YY'),
BEGIN
FOR i IN 0 .. (TRUNC(ADD_MONTHS(SYSDATE,12),'YY') - 1) - (TRUNC(SYSDATE,'YY'))
LOOP
INSERT INTO dates_of_year VALUES (start_day+i);
END LOOP;
END;
/
PL/SQL procedure successfully completed.SELECT COUNT(*) FROM dates_of_year;
COUNT(*) ---------- 365
The dates_of_year
table is now populated with the
365 days of the year 2001. You can now use this table to generate
various useful lists of dates.
Let’s say there are two paydays where you
work—the 15th of each month and the last day of each month. Use
the following query against the dates_of_year
table to generate a list of all paydays in the year 2001:
SELECT one_day payday FROM dates_of_year
WHERE TO_CHAR(one_day,'DD') = '15'
OR one_day = LAST_DAY(one_day);
PAYDAY --------- 15-JAN-01 31-JAN-01 15-FEB-01 28-FEB-01 15-MAR-01 31-MAR-01 15-APR-01 30-APR-01 15-MAY-01 31-MAY-01 15-JUN-01 30-JUN-01 15-JUL-01 31-JUL-01 15-AUG-01 31-AUG-01 15-SEP-01 30-SEP-01 15-OCT-01 31-OCT-01 15-NOV-01 30-NOV-01 15-DEC-01 31-DEC-01 24 rows selected.
Quite often you are told by a government organization that the
processing of a
document will take
“x” number of days. When someone
says something like that, they usually mean
“x” number of working days.
Therefore, to calculate the expected completion date, you need to
count “x” days from the current
date, skipping Saturdays and Sundays. Obviously, you
can’t use simple date arithmetic, because simple
date subtraction doesn’t exclude weekend days. What
you can do is use the dates_of_year
table. For
example:
SELECT COUNT(*) FROM dates_of_year
WHERE RTRIM(TO_CHAR(one_day,'DAY')) NOT IN ('SATURDAY', 'SUNDAY')
AND one_day BETWEEN '&d1' AND '&d2';
Enter value for d1:18-FEB-01
Enter value for d2:15-MAR-01
old 3: AND one_day BETWEEN '&d1' AND '&d2' new 3: AND one_day BETWEEN '18-FEB-01' AND '15-MAR-01' COUNT(*) ---------- 19
This query counts the number of days between the two dates you enter,
excluding Saturdays and the Sundays. The TO_CHAR function with the
`DAY’ format converts each
candidate date (from the dates_of_year
table) to a
day of the week, and the NOT IN operator excludes the days that are
Saturdays and Sundays. Notice the use of the
RTRIM function with TO_CHAR. We used
RTRIM because TO_CHAR produces the DAY as a nine-character string,
blank padded to the right. RTRIM eliminates those extra spaces.
There could be holidays between two dates, and the queries shown in
this section don’t deal with that possibility. To
take holidays into account, you need another table (perhaps named
holidays
) that lists all the holidays in the year.
You can then modify the previous query to exclude days listed in the
holidays
table. Try this as an exercise.
Let’s say you want to print a quarterly summary of all your orders. You want to print the total number of orders and total sale price for each quarter. The order table is as follows:
SELECT * FROM cust_order;
ORDER CUST SALES PRICE ORDER_DT EXPECTED_ CANCELLED SHIP STATUS
----- ---- ------ ------ --------- --------- --------- ---- -----------
1001 1 7354 99 22-JUL-01 23-JUL-01 DELIVERED
1000 1 7354 19-JUL-01 24-JUL-01 21-JUL-01 CANCELLED
1002 5 7368 12-JUL-01 25-JUL-01 14-JUL-01 CANCELLED
1003 4 7654 56 16-JUL-01 26-JUL-01 DELIVERED
1004 4 7654 34 18-JUL-01 27-JUL-01 PENDING
1005 8 7654 99 22-JUL-01 24-JUL-01 DELIVERED
1006 1 7354 22-JUL-01 28-JUL-01 24-JUL-01 CANCELLED
1007 5 7368 25 20-JUL-01 22-JUL-01 PENDING
1008 5 7368 25 21-JUL-01 23-JUL-01 PENDING
1009 1 7354 56 18-JUL-01 22-JUL-01 DELIVERED
1012 1 7354 99 22-JUL-01 23-JUL-01 DELIVERED
1011 1 7354 19-JUL-01 24-JUL-01 21-JUL-01 CANCELLED
1015 5 7368 12-JUL-01 25-JUL-01 14-JUL-01 CANCELLED
1017 4 7654 56 16-JUL-01 26-JUL-01 DELIVERED
1019 4 7654 34 18-JUL-01 27-JUL-01 PENDING
1021 8 7654 99 22-JUL-01 24-JUL-01 DELIVERED
1023 1 7354 22-JUL-01 28-JUL-01 24-JUL-01 CANCELLED
1025 5 7368 25 20-JUL-01 22-JUL-01 PENDING
1027 5 7368 25 21-JUL-01 23-JUL-01 PENDING
1029 1 7354 56 18-JUL-01 22-JUL-01 DELIVERED
20 rows selected.
There is no quarter column in the cust_order
table. You have to manipulate the order_dt
column
to generate the quarter. The following SQL statement does this using
the TO_CHAR function along with a date format. In addition to being
used in the SELECT list, notice that TO_CHAR is used in the GROUP BY
clause to group the results by quarter:
SELECT 'Q'||TO_CHAR(order_dt, 'Q') quarter, COUNT(*),
SUM(NVL(sale_price,0))
FROM cust_order
GROUP BY 'Q'||TO_CHAR(order_dt, 'Q'),
QU COUNT(*) SUM(NVL(SALE_PRICE,0)) -- ---------- ---------------------- Q3 20 788
Using this same technique, you can summarize data by week, month, year, hour, minute, or any other date/time unit that you choose.
18.117.187.62