Close

Oracle Intervals – Day to Second

An INTERVAL DAY TO SECOND, can have up to 9 digits of sub-second precision (nanoseconds.) By default, a column or pl/sql variable will have 6 digits (microseconds.) In addition to the subsecond precision, a default INTERVAL DAY TO SECOND will also be limited to 2 digits in the day counter. These limits can be changed by specifying the limit of each (0-9) in the declaration.

select interval '123456789 00:00:00.123456789' day(9) to second(9) from dual;

DECLARE
     i   INTERVAL DAY(9) TO SECOND(9);
 BEGIN
     i := TO_DSINTERVAL('123456789 00:00:00.123456789');
     DBMS_OUTPUT.put_line(i);
 END;

As mentioned in the overview, INTERVALS do not represent any particular period of time. Thus time zones and daylight saving time are not supported within an INTERVAL DAY TO SECOND. So one day within an interval is always 24 hours.

Literals

Intervals have a variety of ways of being created. In the examples above I show one method of using an interval-literal and in the other I use a function. Interval-literals come in the form of the key word INTERVAL followed by either a number and a unit or a duration format followed by the range of units supplied. In either case, the units may optionally include precision limits . Templates of each would look like the following:

INTERVAL 'd' DAY
INTERVAL 'h' HOUR
INTERVAL 'm' MINUTE
INTERVAL 's' SECOND
INTERVAL 'd h:m:s' DAY TO SECOND
INTERVAL 'd h:m' DAY TO MINUTE
INTERVAL 'd h' DAY TO HOUR
INTERVAL 'h:m:s' HOUR TO SECOND
INTERVAL 'h:m' HOUR TO MINUTE
INTERVAL 'm:s' MINUTE TO SECOND

Where “d”, “h”, and “m” are integers, “s” can be a decimal number. When used in the duration  form, if a higher unit is included in the duration range then the time components are restricted to normal clock ranges.  For example  “h” is restricted to the range 0-23 when days are included in the format. “m” is restricted to 0-59 if hours or days are in the format. “s” is restricted to 0-59.999999999 or the upper limit of the precision specified for seconds if hours, minutes, or days are included.

Another oddity of literal precision is how it interacts with the day counter.
If you want to restrict the interval to only having 3 digits of days, you can specify DAY(3), but if your units or duration do not include days then the precision is applied to the largest unit that is included.  If you are only specifying a number of seconds then you should include the precision of both the day and the fractional portions in the precision, separated by a comma.  Thus the formats above might be written to include precision as follows:

INTERVAL 'd' DAY(3)
INTERVAL 'h' HOUR(3)
INTERVAL 'm' MINUTE(3)
INTERVAL 's' SECOND(3,5)
INTERVAL 'd h:m:s' DAY(3) TO SECOND(5)
INTERVAL 'd h:m' DAY(3) TO MINUTE
INTERVAL 'd h' DAY(3) TO HOUR
INTERVAL 'h:m:s' HOUR(3) TO SECOND(5)
INTERVAL 'h:m' HOUR(3) TO MINUTE(5)
INTERVAL 'm:s' MINUTE(3) TO SECOND(5)

Even though the “d”, “h”, “m”, and “s” values are all numeric, the interval-literal format requires them to be entered as character strings.  Also, since these are literals, they may not include bind variables for the character strings.  The query below, includes an example of each of the formats described above.

SELECT INTERVAL '7 2:7:38.123456789' DAY TO SECOND(9),
        INTERVAL '1' DAY,
        INTERVAL '1' HOUR,
        INTERVAL '1' MINUTE,
        INTERVAL '1' SECOND,
        INTERVAL '1234.2345' SECOND(3, 4),
        INTERVAL '1 2:3:4.567' DAY TO SECOND,
        INTERVAL '1 2:3' DAY TO MINUTE,
        INTERVAL '1 2' DAY TO HOUR,
        INTERVAL '1:2:3.45' HOUR TO SECOND,
        INTERVAL '1:2' HOUR TO MINUTE,
        INTERVAL '1:2.345' MINUTE TO SECOND,
        INTERVAL '1' DAY(3),
        INTERVAL '1' HOUR(3),
        INTERVAL '1' MINUTE(3),
        INTERVAL '1' SECOND(3, 5),
        INTERVAL '1 2:3:4.567' DAY(3) TO SECOND(5),
        INTERVAL '1 2:3' DAY(3) TO MINUTE,
        INTERVAL '1 2' DAY(3) TO HOUR,
        INTERVAL '1:2:3.45' HOUR(3) TO SECOND(5),
        INTERVAL '1:2' HOUR TO MINUTE,
        INTERVAL '9999:2.345' MINUTE(1) TO SECOND(2)
 FROM DUAL;

Functions

In addition to creating intervals with literals, Oracle also provides functions. As opposed to the literals – using the functions allows for passing variables or referencing column values. The functions are NUMTODSINTERVAL and TO_DSINTERVAL.

NUMTODSINTERVAL is fairly straight forward. It takes two parameters: a number, and a character string of one of the unit names: ‘DAY’, ‘HOUR’, ‘MINUTE’, or ‘SECOND’. It will return an INTERVAL DAY(9) TO SECOND(9) filled with the appropriate number of units, rolling them across the various fields of the interval as needed to canonicalize them into days, 0-23 hours, 0-59 minutes, and 0-59.999999999 seconds.

SQL> select numtodsinterval(3.8522463,'day') result from dual
   2  union all
   3  select numtodsinterval(3.8522463,'hour') from dual
   4  union all
   5  select numtodsinterval(3.8522463,'minute') from dual
   6  union all
   7  select numtodsinterval(3.8522463,'second') from dual;
 RESULT
 +000000003 20:27:14.080320000
 +000000000 03:51:08.086680000
 +000000000 00:03:51.134778000
 +000000000 00:00:03.852246300

TO_DSINTERVAL supports two input formats (since 11gR1.) A SQL format and an ISO 8601 format. Both formats consist of a single character string. In 10gR2 and lower a second parameter to specify NLS characters was supported. The syntax is still accepted in 11.1 and above, but the parameter is ignored.

The SQL format has only one form: TO_DSINTERVAL(‘d h:m:s’). As with full literal durations, the hour, minute, and second components must fall within 0-23 for hours, 0-59 for minutes, and 0-59.999999999 for seconds.

SQL> select TO_DSINTERVAL('1 23:45:06.987654321') sql_format from dual;
 SQL_FORMAT
 +000000001 23:45:06.987654321

The ISO 8601 format is identified with a P or -P (for period) as the leading characters. It is not legal syntax to include a + character for positive intervals, only negative intervals include a sign.

After the P each component of days, hours, minutes, and/or seconds may be included in order of descending magnitude with D, H, M, and S as suffixes to follow the numeric values. If you only need a day count in your interval then the format is simply PnD or -PnD where n is the positive integer number of days. If time components are to be included they need to be prefixed with T before adding them.
Thus the legal format templates (not counting the optional minus sign) are:

PnDTnHnMnS  -- days, hours, minutes, seconds
PnDTnHnM    -- days, hours, minutes
PnDTnHnS    -- days, hours, seconds
PnDTnH      -- days, hours
PnDTnMnS    -- days, minutes, seconds
PnDTnM      -- days, minutes
PnDTnS      -- days, seconds
PnD         -- days
PTnHnMnS    -- hours, minutes, seconds
PTnHnM      -- hours, minutes
PTnHnS      -- hours seconds
PTnH        -- hours
PTnMnS      -- minutes seconds
PTnM        -- minutes
PTnS        -- seconds

Again, the “n” stands for a numeric value for the suffixed units of days, hours, minutes, or seconds. Where the SQL model describes an interval in terms of its total amount of time in canonicalized clock-time scale, the ISO model is a summation of each time component. Thus the ISO model does not require hours to be 0-23 or minutes 0-59 or seconds 0-59.999999999. Any positive integer can be used for day, hour, and minute values. Any positive number can be used for the seconds. The function will sum the values and roll the values up to into larger units as needed. Thus TO_DSINTERVAL(‘PT99M’) will return an interval of one hour and thirty-nine minutes.

SQL> select TO_DSINTERVAL('PT99M') from dual;
 TO_DSINTERVAL('PT99M')
 +000000000 01:39:00.000000000

The ISO format supports multiple such additions. In the following example 10 days, 50 hours, 99 minutes, and 1000.365 seconds are added together to create a single interval of 12 days, 3 hours, 55 minutes, and 40.365 seconds.

SQL> select to_dsinterval('P10DT50H99M1000.365S') result from dual;
 RESULT
 +000000012 03:55:40.365000000

Timestamp Math

The default type for subtracting a timestamp from another timestamp or a date is INTERVAL DAY TO SECOND. The precision of the days will be maximized at 9 while the seconds will be based on the larger precision of the two values (DATE types have precision 0 since they can’t support sub-second detail.) Thus a TIMESTAMP(4) minus a TIMESTAMP(5) will result in an INTERVAL DAY(9) TO SECOND(5) value for the difference.

The default precision of the differences may be overridden by specifying the desired type after the subtraction-expression. The expression should be in parentheses to tell the parser the type declaration applies to the subtraction-expression and not the subtrahend. In the example below, a TIMESTAMP(5) will be subtracted from a TIMESTAMP(4). First the result will be returned without modification and in the second query the result will be forced to an INTERVAL DAY TO SECOND(3), this will result in rounding of the result to fit in the smaller precision.

SQL> SELECT (ts1 - ts2)  diff
    2    FROM (SELECT CAST(TIMESTAMP '2020-12-16 19:07:00.5678 US/Eastern' AS TIMESTAMP(4)) ts1,
    3                 CAST(TIMESTAMP '2020-11-02 14:07:43.44913 US/Eastern' AS TIMESTAMP(5)) ts2
    4            FROM DUAL);
  DIFF
  +000000044 04:59:17.11867
SQL> SELECT (ts1 - ts2) DAY TO SECOND(3) diff
    2    FROM (SELECT CAST(TIMESTAMP '2020-12-16 19:07:00.5678 US/Eastern' AS TIMESTAMP(4)) ts1,
    3                 CAST(TIMESTAMP '2020-11-02 14:07:43.44913 US/Eastern' AS TIMESTAMP(5)) ts2
    4            FROM DUAL);
  DIFF
  +44 04:59:17.119

Note, specifying a SECOND precision also forced a default precision of 2 when not specified on the DAY portion even though it has been 9 previously. In a similar way, specifying a DAY precision without a SECOND precision will change to the default 6. Here is the same subtraction as the previous two; but DAY(3) is specified while allowing the SECONDs to default.

SQL> SELECT (ts1 - ts2) DAY(3) TO SECOND diff
    2    FROM (SELECT CAST(TIMESTAMP '2020-12-16 19:07:00.5678 US/Eastern' AS TIMESTAMP(4)) ts1,
    3                 CAST(TIMESTAMP '2020-11-02 14:07:43.44913 US/Eastern' AS TIMESTAMP(5)) ts2
    4            FROM DUAL);
  DIFF
  +044 04:59:17.118670

An interesting quirk (bug?) arises if you have the same result twice in the result set, but with different precision specified for each. All of them will be forced to the precision of the last column defined in the result set. Here they are DAY(5)/SECOND(3) because it came last. In the second query they are reversed and DAY(4)/SECOND(2) is forced on all results.

SQL> SELECT (ts1 - ts2) DAY(4) TO SECOND(2) diff1, (ts1 - ts2) DAY(5) TO SECOND(3) diff2
    2    FROM (SELECT CAST(TIMESTAMP '2020-12-16 19:07:00.5678 US/Eastern' AS TIMESTAMP(4)) ts1,
    3                 CAST(TIMESTAMP '2020-11-02 14:07:43.44913 US/Eastern' AS TIMESTAMP(5)) ts2
    4            FROM DUAL);
  DIFF1                    DIFF2
  +00044 04:59:17.119      +00044 04:59:17.119

SQL> SELECT (ts1 - ts2) DAY(5) TO SECOND(3) diff1, (ts1 - ts2) DAY(4) TO SECOND(2) diff2
    2    FROM (SELECT CAST(TIMESTAMP '2020-12-16 19:07:00.5678 US/Eastern' AS TIMESTAMP(4)) ts1,
    3                 CAST(TIMESTAMP '2020-11-02 14:07:43.44913 US/Eastern' AS TIMESTAMP(5)) ts2
    4            FROM DUAL);
 DIFF1               DIFF2
 +0044 04:59:17.12   +0044 04:59:17.12

The last-defines-all effect even applies for defaults. In the first query below, we can see the DAY(4)/SECOND(3) rule of the last column is applied to both results; but in the second, the default precision of DAY(9) and SECOND inherited from the larger TIMESTAMP precision is enforced on all results.

SQL> SELECT (ts1 - ts2) diff1, (ts1 - ts2) DAY(4) TO SECOND(3) diff2
    2    FROM (SELECT CAST(TIMESTAMP '2020-12-16 19:07:00.5678 US/Eastern' AS TIMESTAMP(4)) ts1,
    3                 CAST(TIMESTAMP '2020-11-02 14:07:43.44913 US/Eastern' AS TIMESTAMP(5)) ts2
    4            FROM DUAL);
  DIFF1                  DIFF2
  +0044 04:59:17.119     +0044 04:59:17.119

SQL> SELECT (ts1 - ts2) DAY(4) TO SECOND(3) diff1, (ts1 - ts2) diff2
    2    FROM (SELECT CAST(TIMESTAMP '2020-12-16 19:07:00.5678 US/Eastern' AS TIMESTAMP(4)) ts1,
    3                 CAST(TIMESTAMP '2020-11-02 14:07:43.44913 US/Eastern' AS TIMESTAMP(5)) ts2
    4            FROM DUAL);
  DIFF1                         DIFF2
  +000000044 04:59:17.11867     +000000044 04:59:17.11867

And, of course, in addition to creating interval through subtraction, you can add intervals to a DATE or TIMESTAMP to create a new DATE or TIMESTAMP value of the same type. The resulting TIMESTAMPs will always have 9 digits of precision, regardless of the original TIMESTAMP’s or INTERVAL’s precision.

SQL> SELECT DATE '2020-12-20' + INTERVAL '1:2:3.45' HOUR TO SECOND FROM DUAL;
 DATE'2020-12-20'+IN
 2020-12-20 01:02:03

SQL> SELECT TIMESTAMP '2020-12-20 00:00:00' + INTERVAL '1:2:3.45' HOUR TO SECOND FROM DUAL;
 TIMESTAMP'2020-12-2000:00:00'+INTERVAL'1:2:3.45'HOURTOSECOND
 2020-12-20 01:02:03.450000000

SQL> SELECT TIMESTAMP '2020-12-20 00:00:00 US/Eastern' + INTERVAL '1:2:3.45' HOUR TO SECOND FROM DUAL;
 TIMESTAMP'2020-12-2000:00:00US/EASTERN'+INTERVAL'1:2:3.45'HOURTOSECOND
 2020-12-20 01:02:03.450000000 US/EASTERN

User-Defined Aggregates

Oracle does not (as of 21.1.0.0.0) support a SUM aggregate for INTERVAL types. However, since 9i we have been able to define our own aggregates using the Data Cartridge API. Using this as a model you could extend to MIN, MAX, and AVG aggregates as well.

CREATE OR REPLACE TYPE DSINTERVAL_SUM_TYPE                                          
    AS OBJECT
(
    v_result INTERVAL DAY(9) TO SECOND(9),  -- our working result

    STATIC FUNCTION odciaggregateinitialize(ctx IN OUT dsinterval_sum_type)
        RETURN NUMBER,

    MEMBER FUNCTION odciaggregateiterate(
        self         IN OUT dsinterval_sum_type,
        p_interval   IN     INTERVAL DAY TO SECOND
    )
        RETURN NUMBER,

    MEMBER FUNCTION odciaggregatemerge(
        self   IN OUT dsinterval_sum_type,
        ctx2   IN     dsinterval_sum_type
    )
        RETURN NUMBER,

    MEMBER FUNCTION odciaggregateterminate(
        self          IN     dsinterval_sum_type,
        returnvalue      OUT INTERVAL DAY TO SECOND,
        flags         IN     NUMBER
    )
        RETURN NUMBER
);
CREATE OR REPLACE TYPE BODY DSINTERVAL_SUM_TYPE 
IS
    STATIC FUNCTION odciaggregateinitialize(ctx IN OUT dsinterval_sum_type)
        RETURN NUMBER
    IS
    BEGIN
        ctx := dsinterval_sum_type(NULL);  -- initialize the sum to NULL
        RETURN odciconst.success;
    END odciaggregateinitialize;

    MEMBER FUNCTION odciaggregateiterate(
        self         IN OUT dsinterval_sum_type,
        p_interval   IN     INTERVAL DAY TO SECOND
    )
        RETURN NUMBER
    IS
    BEGIN
        IF self.v_result IS NULL
        THEN
            self.v_result := p_interval;  -- the first interval  is the sum
        ELSE
            self.v_result := (self.v_result + p_interval);  -- add each new interval to the running total
        END IF;

        RETURN odciconst.success;
    END odciaggregateiterate;

    MEMBER FUNCTION odciaggregatemerge(
        self   IN OUT dsinterval_sum_type,
        ctx2   IN     dsinterval_sum_type
    )
        RETURN NUMBER
    IS
    BEGIN
        IF self.v_result IS NULL
        THEN
            self.v_result := ctx2.v_result;  -- If our sum is NULL, use the result of the other set
        ELSIF ctx2.v_result IS NOT NULL
        THEN
            self.v_result := (self.v_result + ctx2.v_result); -- If both sets have sums, add them to get the total
        END IF;

        RETURN odciconst.success;
    END odciaggregatemerge;

    MEMBER FUNCTION odciaggregateterminate(
        self          IN     dsinterval_sum_type,
        returnvalue      OUT INTERVAL DAY TO SECOND,
        flags         IN     NUMBER
    )
        RETURN NUMBER
    IS
    BEGIN
        returnvalue := self.v_result;   -- the iterate and merge did all the work, return the result
        RETURN odciconst.success;
    END odciaggregateterminate;
END;

Using the ODCI type above, you can then define an aggregate function to use it.

CREATE OR REPLACE FUNCTION dsintervalsum(p_interval dsinterval_unconstrained)
    RETURN dsinterval_unconstrained
    PARALLEL_ENABLE
    AGGREGATE USING dsinterval_sum_type;

Once you have the function, you can use it anywhere you’d use a native aggregate.

SQL> select dsintervalsum(result) from
  2  (
  3  select numtodsinterval(3.8522463,'day') result from dual
  4  union all
  5  select numtodsinterval(3.8522463,'hour') from dual
  6  union all
  7  select numtodsinterval(3.8522463,'minute') from dual
  8  union all
  9  select numtodsinterval(3.8522463,'second') from dual);

DSINTERVALSUM(RESULT)
+000000004 00:22:17.154024000

In the next article I will cover the INTERVAL YEAR TO MONTH type. It is simpler in that it only has 2 components: years and months; but the type has its own challenges as well since years and months vary in size, whereas days, hours, minutes, and seconds are all consistent within the INTERVAL DAY TO SECOND type.

Related articles

Interval Overview
INTERVAL YEAR TO MONTH