Chapter 6. Handling Temporal Data

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.

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.

Tip

The list of valid time zone region names is provided in the data dictionary view V$TIMEZONE_NAMES.

Database Time Zone

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

Tip

If you do not explicitly set the database time zone, Oracle defaults to the operating system time zone. If the operating system time zone is not a valid Oracle time zone, UTC is used as the default time zone.

Session Time Zone

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;

Tip

If you do not explicitly set the session time zone, Oracle defaults to the local operating system time zone. If the local operating system time zone is not a valid Oracle time zone, UTC is used as the default session time zone.

Temporal Data Types in Oracle

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.

The DATE Data Type

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.

The TIMESTAMP Data Types

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

TIMESTAMP

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.

TIMESTAMP WITH TIME ZONE

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)

TIMESTAMP WITH LOCAL TIME ZONE

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)

The INTERVAL Data Types

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.

INTERVAL YEAR TO MONTH

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.

INTERVAL DAY TO SECOND

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.

Literals of Temporal Types

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

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.

TIMESTAMP Literals

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.

Tip

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.

Tip

When TIMESTAMP WITH LOCAL TIME ZONE data is normalized to the database time zone, the time zone of the original data is not preserved.

INTERVAL Literals

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.

YEAR TO MONTH interval literals

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

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

DAY TO SECOND interval literals

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)] TO unit2[(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:00

SELECT INTERVAL '11:23' HOUR TO MINUTE FROM DUAL;

INTERVAL'11:23'HOURTOMINUTE
-----------------------------------------------------------------
+00 11:23:00

SELECT INTERVAL '11:23' MINUTE TO SECOND FROM DUAL;

INTERVAL'11:23'MINUTETOSECOND
-----------------------------------------------------------------
+00 00:11:23.000000

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

Getting Temporal Data In and Out of a Database

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

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.

Tip

You can view your current NLS parameter settings by querying the view named NLS_SESSION_PARAMETERS.

Using the default formats

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.

Specifying a 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.

Warning

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.

Tip

Later, in the section on the TO_CHAR function, you’ll learn how you can use formats to exercise great control over the textual representation of date and timestamp values.

Converting to TIMESTAMP WITH LOCAL TIME ZONE

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.

TO_YMINTERVAL and TO_DSINTERVAL

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:

TO_YMINTERVAL

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.

TO_DSINTERVAL

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.

NUMTOYMINTERVAL and NUMTODSINTERVAL

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.

Tip

Remember, that there is a “break” in the interval model between days and months. You cannot add an INTERVAL DAY TO SECOND value to an INTERVAL YEAR TO MONTH value.

TO_CHAR

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:

DATE

The output string takes the format specified by the parameter NLS_DATE_FORMAT.

TIMESTAMP

The output string takes the format specified by the parameter NLS_TIMESTAMP_FORMAT.

TIMESTAMP WITH TIME ZONE

The output string takes the format specified by the parameter NLS_TIMESTAMP_TZ_FORMAT.

TIMESTAMP WITH LOCAL TIME ZONE

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.

Date and Time Formats

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.

Table 6-1. Oracle date, timestamp, and time zone format elements

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

AD/BC Indicators

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 AD

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

AM/PM Indicators

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

Case-Sensitivity of Formats

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.

Two-Digit Years

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

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

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

ISO Standard Issues

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:

IW

Represents the week of the year in ISO standard.

I, IY, IYY, and IYYY

Represents the ISO year.

The following sections describe ISO weeks and years with examples.

ISO standard weeks

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.

ISO standard year

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.

Database Parameters

The default formats to use when converting temporal data to character form are determined by database parameters. The key parameters are:

NLS_DATE_FORMAT

Specifies the default format used by TO_DATE and TO_CHAR functions when converting character data into data of type DATE or vice versa.

NLS_TIMESTAMP_FORMAT

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.

NLS_TIMESTAMP_TZ_FORMAT

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'

Tip

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.

Manipulating Temporal Data

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.

Using the Built-in Temporal Functions

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 PM

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

Tip

We rather wish Oracle had overloaded all the existing DATE functions, such as ADD_MONTHS, to also accept the various TIMESTAMP data types.

Addition

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.

Adding numbers to a DATE

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

Warning

Oracle allows you to use the + operator to add a number (number of days) to a TIMESTAMP value. However, when you do that, the TIMESTAMP value will be implicitly converted to a DATE value, with consequent loss of information.

Adding months to a DATE

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.

Tip

ADD_MONTHS does not work for TIMESTAMP values. Any such values passed to ADD_MONTHS will be implicitly converted to DATE values, with consequent loss of information.

Adding true INTERVAL values rather than numbers

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

Subtraction

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

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

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

Subtracting a number from a DATE

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

Warning

Oracle lets you use the - operator to subtract a number (of days) from a TIMESTAMP value. However, when you do that, the TIMESTAMP value will be implicitly converted to a DATE value, with consequent loss of information.

Subtracting months from a DATE

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

Number of months between two DATEs

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.

Time interval between two DATEs

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.

Subtracting an INTERVAL from a DATE or TIMESTAMP

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

Determining the First Day of the Month

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

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.

Warning

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

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

Table 6-2. Date formats for use with ROUND and TRUNC

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.

SELECTing Data Based on Date Ranges

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.

Tip

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.

Creating a Date Pivot Table

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.

Summarizing by a Date/Time Element

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.187.62