Db2

Db2

Connect with Db2, open source, and other data experts to gain value from your data, share insights, and solve problems.

 View Only

Adding an INTERVAL data type 

Tue March 10, 2020 07:52 PM

Posted by: Serge Rielau

I recently got asked whether DB2 support the INTERVAL data types.
My answer was, no, it does not. But DB2 does support so called "labeled durations"!
Labeled durations are a transient type that can only appear when you are doing datetime arithmetic.
An example for a labeled duration is: "7 months".
So:
VALUES CURRENT TIMESTAMP + 7 MONTHS
Adds seven months to the current moment in time.

Now, labeled durations predate the SQL Standard and they do have the downside that you cannot store them in a column.
You can only store the values themselves in a column not the fact that they are durations.

The question arises whether DB2 can be extended to a proper interval date type.
Remember that we used to call DB2 "UDB" (Universal Database)?
Aside from the fact that "Universal" was a fashion word at the time (I think Informix started it ;-) there was actually meaning attached to that name.
A DB2 could call itself UDB when it supported:

  • User Defined Functions (UDF)
  • Large OBjects (LOB)
  • and User defined, Distinct Types (UDT)
Nowadays all DB2 family members support these features and so do most other DBMS vendors. So the name was dropped.
The properties however exist and are ready for usage by anyone who wants to.

So In the following scripts we will:
  • Use UDTs to define two new types:
    • YEAR_TO_MONTH_INTERVAL
      An interval that hold a multiple of months counted in years an months
    • DAY_TO_SECOND_INTERVAL
      An interval that folds multiples of seconds counted in fractions of seconds all the way up to days.
Note that there is a hard boundary between these two intervals. That is because you cannot precisely say just how many months a certain number of days make up. Blame the Gregorian Calendar for that one - and the fact that our earth doesn't like round numbers of days per year.
Also note that an INTERVAL is very different from a TIMESTAMP, a DATE or a TIME. These are all moments in time, while intervals describe elapsed time.

UDTs are interesting creatures. They are based on a regular DB2 data type and as such are to some extend just an alias.
In addition however UDTs are also strongly typed. That is you cannot use any functions defined on the base type against the UDT.
The entire library of functions needs to be explicitly defined with the exception of comparison and cast functions.
That leads us to...
  • Use UDF to define operations on intervals.
    There aren't terribly many things one can do to intervals. Here is a list:

    • NUMTODSINTERVAL(<number>, <unit>) => DAY_TO_SECOND_INTERVAL
      This function takes a numeric argument and turns into the DAY_TO_SECOND_INTERVAL.
      The unit can be a string such as 'DAY', 'second', or 'Minute'.
      It is fine to use fractions.

      Example:
      NUMTODSINTERVAL(123.45, 'SECOND'
      => 123.450000000
      NUMTODSINTERVAL(-12.5, 'HOUR')
      => -45000.000000000

    • NUMTOYMINTERVAL(<number>, <unit>) => YEAR_TO_MONTH_INTERVAL
      This function takes a number and turns it into a YEAR_TO_MONTH_INTERVAL.
      The unit can be a string such as 'Year', or 'month'.
      It is fine to use fractions, fractional months are truncated however.

      Example:
      NUMTOYMINTERVAL(10.25, 'YEAR')
      => 123
      NUMTOYMINTERVAL(-1244, 'MONTH')
      => -1244

    • TO_DSINTERVAL(<string>) => DAY_TO_SECOND_INTERVAL
      This function turns a string into a  DAY_TO_SECOND_INTERVAL.
      The string has be of the format  DD9 HH:MI:SS[.FF9]
      DD9 means you can specify up to 9 digits for the days. Also you can use + or - to denote positive or negative intervals.
      The fractional seconds are optional and can go down to nano seconds.

      Example:
      TO_DSINTERVAL('-12 08:12:59.12')
      => -1066379.120000000
      TO_DSINTERVAL('0 00:00:12.12345')
      => 12.123450000

    • TO_YMINTERVAL(<string>) => YEAR_TO_MONTH_INTERVAL
      This function turns a string into a  YEAR_TO_MONTH_INTERVAL.
      The string has to be of the format: YY9-MM
      YY9 means you can specify up to 9 digits for the year. Also you can use + and - to denote positive or negative intervals.
      Note that DB2 does not validate that MM is less than 12.

      Example:
      TO_YMINTERVAL('-134-11')
      => -1619
      TO_YMINTERVAL('0-10')
      => 10

    • TO_CHAR(<dtsinterval>) => VARCHAR(29)
      This function formats a DAY_TO_SECOND_INTERVAL value into a string.
      The format is of the form: SDD9 HH:MI:SS.FF9
      S is the plus or minus sign

      Example:
      TO_CHAR(NUMTODSINTERVAL(-193745.12, 'SECOND'))
      => -000000002 05:49:05.120000000
      TO_CHAR(NUMTODSINTERVAL(560.456, 'HOUR'))
      => +000000023 08:27:21.600000000

    • TO_CHAR(<ytminterval>)=>VARCHAR(13)
      This function formats a YEAR_TO_MONTH_INTERVAL.
      The format is of the form: SYY9-MM
      S is the plus or minus sign

      Example:
      TO_CHAR(NUMTOYMINTERVAL(-16.25, 'YEAR'))
      => -000000016-03
      TO_CHAR(NUMTOYMINTERVAL(67253, 'MONTH'))
      => +000005604-05

    • <timestamp> + <dtsinterval>
      <dtsinterval> + <timestamp>
      Adds an day to second interval to a timestamp.

      Example:
      TIMESTAMP '2010-10-30 18:16:00.123' + TO_DSINTERVAL('12 07:12:30')
      => 2010-11-12-01.28.30.123000000000
      TIMESTAMP '2010-10-30 18:16:00.123' + TO_DSINTERVAL('-12 07:12:30')
      => 2010-10-18-11.03.30.123000000000

    • <time_stamp> - <dtsinterval>
      Substracts a day to second interval from a timestamp

      Example:
      TIMESTAMP '2010-10-30 18:16:00.123' - TO_DSINTERVAL('12 07:12:30')
      => 2010-10-18-11.03.30.123000000000

    • <date> + <ytminterval>
      <timestamp> + <ytminterval>
      <ytminterval> + <date>
      <ytminterval> + <timestamp>
      Adds a year to month interval to a date or timestamp

      Example:
      DATE'2010-10-30' + NUMTOYMINTERVAL(23, 'MONTH')
      => 09/30/2012
      TIMESTAMP'2010-10-30 18:16:00.123' + TO_YMINTERVAL('80-02')
      => 2090-12-30-18.16.00.123000000000

    • <date> - <ytminterval>
      <timestamp> - <ytminterval>
      Substracts a year to month interval from a date or timestamp

      Example:
      DATE'2010-10-30' - TO_YMINTERVAL('11-05')
      => 05/30/1999
      TIMESTAMP'2010-10-30 18:16:00.123' - TO_YMINTERVAL('80-02')
      => 1930-08-30-18.16.00.123000000000

    • <dtsinterval> + <dtsinterval>
      <dtsinterval> - <dtsinterval>
      Adds or substracts two day to second intervals

      Example:
      TO_CHAR(TO_DSINTERVAL('45 10:00:12.123') + NUMTODSINTERVAL(88, 'SECOND'))
      => +000000045 10:01:40.123000000
      TO_CHAR(NUMTODSINTERVAL(14.5, 'DAY') - TO_DSINTERVAL('13 12:00:00'))
      => +000000001 00:00:00.000000000

    • <ytminterval> + <ytminterval>
      <ytminterval> - <ytminterval>
      Adds or substracts two year to month intervals

      Example:
      TO_CHAR(TO_YMINTERVAL('-30-03') + NUMTOYMINTERVAL(3.25, 'YEAR'))
      => -000000027-00
      TO_CHAR(TO_YMINTERVAL('100-00') - NUMTOYMINTERVAL(3, 'MONTH'))
      => +000000099-09

    • =, != (<>), <, >, <=, >= as well as BETWEEN and IN are also supported for both YEAR_TO_MONTH_INTERVAL and DAY_TO_SECOND_INTERVAL
Here is the definition of the functions:
SET PATH   = CURRENT PATH, INTERVAL;
SET SCHEMA = INTERVAL;

CREATE TYPE INTERVAL_DAY_TO_SECOND AS DECIMAL WITH COMPARISONS;
CREATE TYPE INTERVAL_YEAR_TO_MONTH AS DECIMAL WITH COMPARISONS;

CREATE OR REPLACE FUNCTION NUMTODSINTERVAL(val DECFLOAT, unit VARCHAR(10))
  RETURNS INTERVAL_DAY_TO_SECOND
  RETURN INTERVAL_DAY_TO_SECOND(TRUNC(CASE UPPER(unit)
                                WHEN 'SECOND' THEN val
                                WHEN 'MINUTE' THEN val * 60
                                WHEN 'HOUR'   THEN val * 60 * 60
                                WHEN 'DAY'    THEN val * 60 * 60 * 24
                                ELSE RAISE_ERROR('78000',
'Unknown unit ' || unit || ' for NUMTODSINTERVAL')
                                END), 9);

CREATE OR REPLACE FUNCTION NUMTOYMINTERVAL(val DECFLOAT, unit VARCHAR(10))
  RETURNS INTERVAL_YEAR_TO_MONTH
  RETURN INTERVAL_YEAR_TO_MONTH(TRUNC(CASE UPPER(unit)
                                WHEN 'MONTH' THEN val
                                WHEN 'YEAR'  THEN val * 12
                                ELSE RAISE_ERROR('78000',
'Unknown unit ' || unit || ' for NUMTOYMINTERVAL')
                                END));

CREATE OR REPLACE FUNCTION TO_DSINTERVAL(val VARCHAR(20))
 RETURNS INTERVAL_DAY_TO_SECOND
 RETURN INTERVAL_DAY_TO_SECOND(
        CASE WHEN LOCATE_IN_STRING(val, ' ') = 0
             THEN DECFLOAT(val) * 60 * 60 * 24
             ELSE DECFLOAT(SUBSTR(val, 1, LOCATE_IN_STRING(val, ' ') - 1)) * 60 * 60 * 24   
                + (CASE WHEN SUBSTR(val, 1, 1) = '-' THEN -1 ELSE 1 END
                   * CASE WHEN LOCATE_IN_STRING(val, '.') > 0
                          THEN MIDNIGHT_SECONDS(TIME(SUBSTR(val,
                                                         LOCATE_IN_STRING(val, ' ') + 1,
                                                            LOCATE_IN_STRING(val, '.')
- LOCATE_IN_STRING(val, ' ') - 1)))
                             + DECFLOAT(RPAD(SUBSTR(val,
                                                 LOCATE_IN_STRING(val, '.') + 1,
                                                 LENGTH(val) - LOCATE_IN_STRING(val, '.')),
                                          9, '0')) * 0.000000001
                         ELSE MIDNIGHT_SECONDS(TIME(SUBSTR(val, LOCATE_IN_STRING(val, ' ') + 1,
                                                           LENGTH(val) - LOCATE_IN_STRING(val, ' '))))
                      END)
             END);

CREATE OR REPLACE FUNCTION TO_YMINTERVAL(val VARCHAR(20))
 RETURNS INTERVAL_YEAR_TO_MONTH
 RETURN INTERVAL_YEAR_TO_MONTH(
         CASE WHEN SUBSTR(val, 1, 1) = '-'
             THEN CASE WHEN LOCATE_IN_STRING(val, '-', 2) > 0
                       THEN DECFLOAT(SUBSTR(val, 1, LOCATE_IN_STRING(val, '-', 2) - 1)) * 12
                          - DECFLOAT(SUBSTR(val, LOCATE_IN_STRING(val, '-', 2) + 1,
LENGTH(val) - LOCATE_IN_STRING(val, '-', 2)))
                       ELSE DECFLOAT(val) * 12 END
             ELSE CASE WHEN LOCATE_IN_STRING(val, '-') > 0
                       THEN DECFLOAT(SUBSTR(val, 1, LOCATE_IN_STRING(val, '-') - 1)) * 12
                          + DECFLOAT(SUBSTR(val, LOCATE_IN_STRING(val, '-') + 1,
LENGTH(val) - LOCATE_IN_STRING(val, '-', 2)))
                       ELSE DECFLOAT(val) END
              END);
  
CREATE OR REPLACE FUNCTION TO_CHAR(val INTERVAL_DAY_TO_SECOND)
  RETURNS VARCHAR(29)
  RETURN TO_CHAR(TRUNC(DECFLOAT(val) / (60 * 60 * 24)), 'S000000000')
      || TO_CHAR(MOD(TRUNC(ABS(DECFLOAT(val)) / (60 * 60)), 24), '00') || ':'
      || SUBSTR(TO_CHAR(MOD(TRUNC(ABS(DECFLOAT(val)) / 60), 60), '00'), 2) || ':'
      || SUBSTR(TO_CHAR(MOD(TRUNC(ABS(DECFLOAT(val)), 9), 60), '00.000000000'), 2);
 
CREATE OR REPLACE FUNCTION TO_CHAR(val INTERVAL_YEAR_TO_MONTH)
  RETURNS VARCHAR(13)
  RETURN TO_CHAR(TRUNC(DECFLOAT(val) / 12), 'S000000000') || '-'
      || SUBSTR(TO_CHAR(MOD(TRUNC(ABS(DECFLOAT(val))), 12), '00'), 2);

CREATE OR REPLACE FUNCTION "+"(INTERVAL_YEAR_TO_MONTH, INTERVAL_YEAR_TO_MONTH)
  RETURNS INTERVAL_YEAR_TO_MONTH
  SOURCE SYSIBM."+"(DECFLOAT, DECFLOAT);

CREATE OR REPLACE FUNCTION "-"(INTERVAL_YEAR_TO_MONTH, INTERVAL_YEAR_TO_MONTH)
  RETURNS INTERVAL_YEAR_TO_MONTH
  SOURCE SYSIBM."-"(DECFLOAT, DECFLOAT);

-- Put DATE before TIMESTAMP. In ORA mode the later will override DATE
CREATE OR REPLACE FUNCTION "+"(arg1 DATE, arg2 INTERVAL_YEAR_TO_MONTH)
  RETURNS DATE
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg1 + DECFLOAT(arg2) MONTHS;

CREATE OR REPLACE FUNCTION "-"(arg1 DATE, arg2 INTERVAL_YEAR_TO_MONTH)
  RETURNS DATE
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg1 - DECFLOAT(arg2) MONTHS;
 
CREATE OR REPLACE FUNCTION "+"(arg1 TIMESTAMP(12), arg2 INTERVAL_YEAR_TO_MONTH)
  RETURNS TIMESTAMP(12)
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg1 + DECFLOAT(arg2) MONTHS;

CREATE OR REPLACE FUNCTION "-"(arg1 TIMESTAMP(12), arg2 INTERVAL_YEAR_TO_MONTH)
  RETURNS TIMESTAMP(12)
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg1 - DECFLOAT(arg2) MONTHS;

CREATE OR REPLACE FUNCTION "+"(arg1 INTERVAL_YEAR_TO_MONTH, arg2 DATE)
  RETURNS DATE
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg2 + DECFLOAT(arg1) MONTHS;

CREATE OR REPLACE FUNCTION "+"(arg1 INTERVAL_YEAR_TO_MONTH, arg2 TIMESTAMP(12))
  RETURNS TIMESTAMP(12)
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg2 + DECFLOAT(arg1) MONTHS;

CREATE OR REPLACE FUNCTION "+"(INTERVAL_DAY_TO_SECOND, INTERVAL_DAY_TO_SECOND)
  RETURNS INTERVAL_DAY_TO_SECOND
  SOURCE SYSIBM."+"(DECFLOAT, DECFLOAT);

CREATE OR REPLACE FUNCTION "-"(INTERVAL_DAY_TO_SECOND, INTERVAL_DAY_TO_SECOND)
  RETURNS INTERVAL_DAY_TO_SECOND
  SOURCE SYSIBM."-"(DECFLOAT, DECFLOAT);

CREATE OR REPLACE FUNCTION "+"(arg1 TIMESTAMP(12), arg2 INTERVAL_DAY_TO_SECOND)
  RETURNS TIMESTAMP(12)
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg1 + DECFLOAT(arg2) SECONDS;

CREATE OR REPLACE FUNCTION "-"(arg1 TIMESTAMP(12), arg2 INTERVAL_DAY_TO_SECOND)
  RETURNS TIMESTAMP(12)
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg1 - DECFLOAT(arg2) SECONDS;

CREATE OR REPLACE FUNCTION "+"(arg1 INTERVAL_DAY_TO_SECOND, arg2 TIMESTAMP(12))
  RETURNS TIMESTAMP(12)
  NO EXTERNAL ACTION CONTAINS SQL DETERMINISTIC
  RETURN arg2 + DECFLOAT(arg1) SECONDS;

#Db2

Statistics
0 Favorited
3 Views
0 Files
0 Shares
0 Downloads