Date Delta SQL – What is Wrong with this SQL Statement?

2 06 2010

June 2, 2010

I found this interesting example a couple of months ago in a book, and something just did not seem right with the example.  What is the problem with the example?

select employee_id, first_name, last_name,
(sysdate - hire_date)*86400 Emp_Length_Seconds,
extract(year from (sysdate - hire_date) year to month) || ' years, ' ||
extract(month from (sysdate - hire_date) year to month) || ' months. '
Emp_Length_Readable
from hr.employees;

I have not used the EXTRACT SQL function much, if at all, prior to seeing this example, but I sensed that something was wrong.

Here is a test script to possibly help you find the problem with the above:

SET PAGESIZE 2000
COLUMN T FORMAT A20
SET TRIMSPOOL ON
SPOOL datedelta.txt

WITH DATES AS (
SELECT
  TO_DATE('01-JAN-2010','DD-MON-YYYY') + (ROWNUM-1) D
FROM
  DUAL
CONNECT BY
  LEVEL<=365)
SELECT
  D1.D D1,
  D2.D D2,
  D2.D-D1.D DATE_DELTA,
  extract(year from (D2.D-D1.D) year to month) || ' years, ' ||
  extract(month from (D2.D-D1.D) year to month) || ' months. ' T
FROM
  DATES D1,
  DATES D2
WHERE
  D1.D<D2.D;

SPOOL OFF

The above script scolls through every start and end date combination for this year, writing a modified version of the calculation from the original SQL statement to a text file.  The output should look something like this:

D1        D2        DATE_DELTA T
--------- --------- ---------- --------------------
01-JAN-10 02-JAN-10          1 0 years, 0 months.
01-JAN-10 03-JAN-10          2 0 years, 0 months.
01-JAN-10 04-JAN-10          3 0 years, 0 months.
01-JAN-10 05-JAN-10          4 0 years, 0 months.
01-JAN-10 06-JAN-10          5 0 years, 0 months.
01-JAN-10 07-JAN-10          6 0 years, 0 months.
01-JAN-10 08-JAN-10          7 0 years, 0 months.
01-JAN-10 09-JAN-10          8 0 years, 0 months.
01-JAN-10 10-JAN-10          9 0 years, 0 months.
...
05-MAR-10 16-JUN-10        103 0 years, 3 months.
05-MAR-10 17-JUN-10        104 0 years, 3 months.
05-MAR-10 18-JUN-10        105 0 years, 3 months.
05-MAR-10 19-JUN-10        106 0 years, 3 months.
05-MAR-10 20-JUN-10        107 0 years, 4 months.
05-MAR-10 21-JUN-10        108 0 years, 4 months.
05-MAR-10 22-JUN-10        109 0 years, 4 months.
...

What is wrong with this SQL statement, and more importantly, how do we fix it?


Actions

Information

17 responses

2 06 2010
Sokrates

seems to me that this example is copied from Oracle Documentation, isn’t it ?
http://download.oracle.com/docs/cd/E11882_01/server.112/e10592/sql_elements001.htm#SQLRF50992

haven’t a clue why

select (date'2010-01-17' - date'2010-01-02') year to month from dual;
(DATE'2010-01-17'-DATE'2010-01-02')YEARTOMONTH
---------------------------------------------------------------------------
+00-01

shows me one month difference between the 2nd Jan 2010 and 17th Jan 2010

However, how to define the “month-part” of the difference of two dates ?

2 06 2010
Charles Hooper

Sokrates,

Thank you for providing the link to the documentation – the example from the book does look very similar to the documentation. One of my concerns is that this example may lead to “incorrect” results or “unexpected” results as you have illustrated with your example.

3 06 2010
3 06 2010
Sokrates

this syntax was new for me and I agree with you, that it is “incorrect” or “unexpected”
However, I’m still not sure how to fix it

2 06 2010
Sokrates

do you expect the “month-part” of the difference of two dates, say d2 – d1, to be the largest integer n such that add_months(d1, n) is smaller than or equal to d2 ?

2 06 2010
Gary

Are you just talking about rounding, where 14 days is ’rounded’ down to 0 months and 15 days is ’rounded’ up to 1 month. Or vaguely around 14/15 days. If the solar system had been sensibly specified, then the rotation of the earth and the duration of its orbit around the sun would be a whole number ratio, preferably with some decent factors for each breakdown.

You can use cast((D2.D-D1.D)/(30.4) as number(3,0)) DATE_DELTA_months to see a similar rounding happening with numerics.

2 06 2010
Charles Hooper

Gary,

I am looking for anything that is not quite right with the demonstration. The book page states:
“For instance, we can use the next SQL statement to find the employment duration for employees in the HR.EMPLOYEES table, but then have to wrestle with part-days represented as fractional numbers…
The next SQL statement achieves both precise seconds and human-readable years and months as output.”

The SQL statement at the start of this blog article is then shown. Now consider a programmer reading that section of the book, or the documentation reference provided by Sokrates (seems to be down right now just like the documentation reference I provided). The programmer is asked to find the employee’s three month anniversary (start of insurance) and the one year anniversary (vacation time roll-over). Given the description, would the programmer expect the EXTRACT code to return a result similar to MONTHS_BETWEEN? For example:

SET PAGESIZE 2000
COLUMN T FORMAT A20
COLUMN MONTHS FORMAT 90.0000
COLUMN M FORMAT 90
COLUMN D FORMAT 90
SET TRIMSPOOL ON
SPOOL datedelta.txt
 
WITH DATES AS (
SELECT
  TO_DATE('01-JAN-2010','DD-MON-YYYY') + (ROWNUM-1) D
FROM
  DUAL
CONNECT BY
  LEVEL<=365)
SELECT
  D1.D D1,
  D2.D D2,
  D2.D-D1.D DATE_DELTA,
  extract(year from (D2.D-D1.D) year to month) || ' years, ' ||
  extract(month from (D2.D-D1.D) year to month) || ' months. ' T,
  MONTHS_BETWEEN(D2.D,D1.D) MONTHS,
  MOD(TRUNC(MONTHS_BETWEEN(D2.D,D1.D)),12) M,
  ROUND((MONTHS_BETWEEN(D2.D,D1.D)-TRUNC(MONTHS_BETWEEN(D2.D,D1.D)))*30.41667) D
FROM
  DATES D1,
  DATES D2
WHERE
  D1.D<D2.D;

SPOOL OFF
D1        D2        DATE_DELTA T                      MONTHS   M   D
--------- --------- ---------- -------------------- -------- --- ---
01-JAN-10 02-JAN-10          1 0 years, 0 months.     0.0323   0   1
01-JAN-10 03-JAN-10          2 0 years, 0 months.     0.0645   0   2
01-JAN-10 04-JAN-10          3 0 years, 0 months.     0.0968   0   3
01-JAN-10 05-JAN-10          4 0 years, 0 months.     0.1290   0   4
01-JAN-10 06-JAN-10          5 0 years, 0 months.     0.1613   0   5
01-JAN-10 07-JAN-10          6 0 years, 0 months.     0.1935   0   6
01-JAN-10 08-JAN-10          7 0 years, 0 months.     0.2258   0   7
01-JAN-10 09-JAN-10          8 0 years, 0 months.     0.2581   0   8
01-JAN-10 10-JAN-10          9 0 years, 0 months.     0.2903   0   9
01-JAN-10 11-JAN-10         10 0 years, 0 months.     0.3226   0  10
01-JAN-10 12-JAN-10         11 0 years, 0 months.     0.3548   0  11
01-JAN-10 13-JAN-10         12 0 years, 0 months.     0.3871   0  12
01-JAN-10 14-JAN-10         13 0 years, 0 months.     0.4194   0  13
01-JAN-10 15-JAN-10         14 0 years, 0 months.     0.4516   0  14
01-JAN-10 16-JAN-10         15 0 years, 1 months.     0.4839   0  15
01-JAN-10 17-JAN-10         16 0 years, 1 months.     0.5161   0  16
01-JAN-10 18-JAN-10         17 0 years, 1 months.     0.5484   0  17
01-JAN-10 19-JAN-10         18 0 years, 1 months.     0.5806   0  18
01-JAN-10 20-JAN-10         19 0 years, 1 months.     0.6129   0  19
01-JAN-10 21-JAN-10         20 0 years, 1 months.     0.6452   0  20
01-JAN-10 22-JAN-10         21 0 years, 1 months.     0.6774   0  21
01-JAN-10 23-JAN-10         22 0 years, 1 months.     0.7097   0  22
01-JAN-10 24-JAN-10         23 0 years, 1 months.     0.7419   0  23
01-JAN-10 25-JAN-10         24 0 years, 1 months.     0.7742   0  24
01-JAN-10 26-JAN-10         25 0 years, 1 months.     0.8065   0  25
01-JAN-10 27-JAN-10         26 0 years, 1 months.     0.8387   0  26
01-JAN-10 28-JAN-10         27 0 years, 1 months.     0.8710   0  26
01-JAN-10 29-JAN-10         28 0 years, 1 months.     0.9032   0  27
01-JAN-10 30-JAN-10         29 0 years, 1 months.     0.9355   0  28
01-JAN-10 31-JAN-10         30 0 years, 1 months.     0.9677   0  29
01-JAN-10 01-FEB-10         31 0 years, 1 months.     1.0000   1   0
01-JAN-10 02-FEB-10         32 0 years, 1 months.     1.0323   1   1
01-JAN-10 03-FEB-10         33 0 years, 1 months.     1.0645   1   2
01-JAN-10 04-FEB-10         34 0 years, 1 months.     1.0968   1   3
01-JAN-10 05-FEB-10         35 0 years, 1 months.     1.1290   1   4
01-JAN-10 06-FEB-10         36 0 years, 1 months.     1.1613   1   5
01-JAN-10 07-FEB-10         37 0 years, 1 months.     1.1935   1   6
01-JAN-10 08-FEB-10         38 0 years, 1 months.     1.2258   1   7
01-JAN-10 09-FEB-10         39 0 years, 1 months.     1.2581   1   8
01-JAN-10 10-FEB-10         40 0 years, 1 months.     1.2903   1   9
01-JAN-10 11-FEB-10         41 0 years, 1 months.     1.3226   1  10
01-JAN-10 12-FEB-10         42 0 years, 1 months.     1.3548   1  11
01-JAN-10 13-FEB-10         43 0 years, 1 months.     1.3871   1  12
01-JAN-10 14-FEB-10         44 0 years, 1 months.     1.4194   1  13
01-JAN-10 15-FEB-10         45 0 years, 1 months.     1.4516   1  14
01-JAN-10 16-FEB-10         46 0 years, 2 months.     1.4839   1  15
01-JAN-10 17-FEB-10         47 0 years, 2 months.     1.5161   1  16
01-JAN-10 18-FEB-10         48 0 years, 2 months.     1.5484   1  17
01-JAN-10 19-FEB-10         49 0 years, 2 months.     1.5806   1  18
...

Obviously, if someone else finds something else wrong with the example from the book or documentation, I would like to see any issues that are found.

2 06 2010
Gary

What is probably not quite right is that

http://download.oracle.com/docs/cd/E11882_01/server.112/e10592/sql_elements001.htm#g196492
indicates that a date_col – date_col should evaluate to a number
http://download.oracle.com/docs/cd/E11882_01/server.112/e10592/functions057.htm#SQLRF51389
indicates that you can only extract a month from a date, timestamp or INTERVAL YEAR TO MONTH.

Even if it was doing an implicit conversion of the dates to a timestamp, the timestamp subtraction would give an INTERVAL DAY TO SECOND which still isn’t valid.

In your example, you are explicitly doing a TRUNC. If you CAST(0.5161 AS NUMBER(2,0)) you get 1. You see the same if simply insert 0.51into a column defined as NUMBER(2,0) . Oracle implicity ROUNDs rather than TRUNCs. If you are looking for a precision of 1 day, then INTERVAL YEAR TO MONTH isn’t exact enough. And if you are looking for ‘month/year’ arithmetic, INTERVAL DAY TO SECOND doesn’t really work either. It might be ANSI standard, but it doesn’t mean its useful.

3 06 2010
Charles Hooper

The first documentation reference that you provided is the exact reason why I initially marked this SQL statement as being wrong. Assume that subtracting the second date from the first date results in the numeric value of 13 (just to keep things simple). The first question is “What is the assumed year?” – is it this year (2010), 2008, 1970, 1900, 1902, etc. The second question is “What is the assumed month” – is it January, February, March, etc. The third question is “What is the assumed day of the month for the zero point starting date?” – is it today’s day of the month, the first day of the current month, the first day of January, etc. What if the starting date and ending date span multiple years (the birth date and death date of someone famous, for example)?

Back to the example of the numeric value 13. Something odd is going on with this EXTRACT function. In this case, the result of subtracting a date from a date is not a number:

SELECT
  extract(year from (13) year to month) || ' years, ' ||
  extract(month from (13) year to month) || ' months. ' T
FROM
  DUAL;
  
ERROR at line 2:
ORA-30083: syntax error was found in interval value expression
  
SELECT
  (TO_DATE('03-MAR-2010','DD-MON-YYYY')-TO_DATE('18-FEB-2010','DD-MON-YYYY')) DAYS,
  extract(year from ((TO_DATE('03-MAR-2010','DD-MON-YYYY')-TO_DATE('18-FEB-2010','DD-MON-YYYY'))) year to month) || ' years, ' ||
  extract(month from ((TO_DATE('03-MAR-2010','DD-MON-YYYY')-TO_DATE('18-FEB-2010','DD-MON-YYYY'))) year to month) || ' months. ' T
FROM
  DUAL;
 
      DAYS T
---------- --------------------
        13 0 years, 1 months.
 
SELECT
  (TO_DATE('02-MAR-2008','DD-MON-YYYY')-TO_DATE('18-FEB-2008','DD-MON-YYYY')) DAYS,
  extract(year from ((TO_DATE('02-MAR-2008','DD-MON-YYYY')-TO_DATE('18-FEB-2008','DD-MON-YYYY'))) year to month) || ' years, ' ||
  extract(month from ((TO_DATE('02-MAR-2008','DD-MON-YYYY')-TO_DATE('18-FEB-2008','DD-MON-YYYY'))) year to month) || ' months. ' T
FROM
  DUAL;
 
      DAYS T
---------- --------------------
        13 0 years, 0 months.

The number 13 causes the SQL statement to fail. In the second of the above SQL statements, if Oracle assumed that 13 was in January of any year, months should evaluate to 0, but Oracle returned a value of 1 month. In the third of the above SQL statements, if Oracle assumed that 13 was in February of this year, like the second example, then Oracle should have returned a value of 0 months.

Another interesting result was provided in the code section of my previous response. It appears that Oracle is not strictly rounding the results of “extract(month from (D2.D-D1.D) year to month)” using the same calculation as used by the MONTHS_BETWEEN function. If that was the case, the date range of 01-JAN-2010 through 16-JAN-2010 would be rounded down to 0 months, but 16-JAN-2010 is indicated as the start of 1 month:

D1        D2        DATE_DELTA T                      MONTHS   M   D
--------- --------- ---------- -------------------- -------- --- ---
01-JAN-10 15-JAN-10         14 0 years, 0 months.     0.4516   0  14
01-JAN-10 16-JAN-10         15 0 years, 1 months.     0.4839   0  15
01-JAN-10 17-JAN-10         16 0 years, 1 months.     0.5161   0  16

I probably need to spend some more time working with the EXTRACT function.

3 06 2010
Charles Hooper

To keep people thinking about the SQL statement at the start of this article, the documentation reference that I provided in the article for the EXTRACT function (http://download.oracle.com/docs/cd/E11882_01/server.112/e10592/functions057.htm) includes the following SQL statement:

SELECT EXTRACT(month FROM order_date) "Month",
  COUNT(order_date) "No. of Orders"
  FROM orders
  GROUP BY EXTRACT(month FROM order_date)
  ORDER BY "No. of Orders" DESC;

Let’s put the SQL statement to the test:

CREATE TABLE T7 (ORDER_DATE DATE);
 
INSERT INTO
  T7
SELECT
  TO_DATE('01-JAN-2010','DD-MON-YYYY')+(ROWNUM-1)
FROM
  DUAL
CONNECT BY
  LEVEL<=300;
 
COMMIT;
 
SELECT EXTRACT(month FROM order_date) "Month",
  COUNT(order_date) "No. of Orders"
  FROM T7
  GROUP BY EXTRACT(month FROM order_date)
  ORDER BY "No. of Orders" DESC;
 
Month No. of Orders
----- -------------
    1            31
    5            31
    7            31
    3            31
    8            31
    9            30
    4            30
    6            30
    2            28
   10            27

That worked exactly as expected, but are we really seeing the 31 days that fall into January, or is Oracle rounding and stealing part of the days from February (the previous tests show that this is not the way the rounding was working – the days would actually be stolen from January and given to Feburary). Now to verify:

SELECT EXTRACT(month FROM order_date) "Month",
  COUNT(order_date) "No. of Orders"
  FROM T7
WHERE
  ORDER_DATE BETWEEN  TO_DATE('01-JAN-2010','DD-MON-YYYY') AND  TO_DATE('31-JAN-2010','DD-MON-YYYY')
  GROUP BY EXTRACT(month FROM order_date)
  ORDER BY "No. of Orders" DESC;
 
Month No. of Orders
----- -------------
    1            31

That worked exactly as I thought that it should.

Now back to the original question, what is wrong with that SQL statement? 🙂

3 06 2010
Sokrates

(13) year to month
is not a valid SQL Expression

((TO_DATE(’03-MAR-2010′,’DD-MON-YYYY’)-TO_DATE(’18-FEB-2010′,’DD-MON-YYYY’))) year to month
is a valid SQL Expression

see
http://download.oracle.com/docs/cd/E11882_01/server.112/e10592/expressions009.htm#sthref1885

the difference in the brackets is **NOT** “evaluated at first” to a number

3 06 2010
Sean Molloy

Gray is right in his analysis.

It helps me to think of it in these terms:

For:
select extract(month from ( dt1 – dt2 ) year to month) from dual;

Oracle calculate dt1 – dt2.
Oracle converts this number of days to a “interval year to month” value.
A “interval year to month” value cannot have a fractional month, so the month value is rounded.

So we do not get the same precision as months_between.

See http://forums.oracle.com/forums/thread.jspa?threadID=354651 for more discussion.

4 06 2010
Donatello Settembrino

Hi Charles,
I think I used only twice EXTRACT, perhaps in its simplest form, honestly I don’t know it very well.
curious 🙂 , I did a test and I think that a possible solution to this problem
could be the following:

WITH DATES AS (
SELECT
  TO_DATE('01-JAN-2010','DD-MON-YYYY') + (ROWNUM-1) D
FROM
  DUAL
CONNECT BY
  LEVEL<=365)
SELECT
  D1.D D1,
  D2.D D2,
  D2.D-D1.D DATE_DELTA, 
  to_char(EXTRACT(YEAR FROM D2.D) - EXTRACT(YEAR FROM  d1.d)) || ' years, ' ||
  to_char( EXTRACT(MONTH FROM D2.D) -  EXTRACT(MONTH FROM D1.D)) || ' months. ' as t 
  from
  DATES D1,
  DATES D2
WHERE
  D1.D<D2.D;

but if I extract month and year in this way must also be aware that the difference between two dates is only indicative, in this example I have a difference of 71 days indicated by two months

D1             D2             DATE_DELTA  T                         
-------------- -------------- ----------- ------------------
01-JAN-2010    13-MAR-2010            71  0 years, 2 months.

Regards

5 06 2010
Charles Hooper

Donatello,

That is certainly an improvement for the first day of the month:

D1        D2        DATE_DELTA T
--------- --------- ---------- ------------------
01-JAN-10 02-JAN-10          1 0 years, 0 months.
01-JAN-10 03-JAN-10          2 0 years, 0 months.
01-JAN-10 04-JAN-10          3 0 years, 0 months.
01-JAN-10 05-JAN-10          4 0 years, 0 months.
01-JAN-10 06-JAN-10          5 0 years, 0 months.
01-JAN-10 07-JAN-10          6 0 years, 0 months.
01-JAN-10 08-JAN-10          7 0 years, 0 months.
01-JAN-10 09-JAN-10          8 0 years, 0 months.
01-JAN-10 10-JAN-10          9 0 years, 0 months.
01-JAN-10 11-JAN-10         10 0 years, 0 months.
01-JAN-10 12-JAN-10         11 0 years, 0 months.
01-JAN-10 13-JAN-10         12 0 years, 0 months.
01-JAN-10 14-JAN-10         13 0 years, 0 months.
01-JAN-10 15-JAN-10         14 0 years, 0 months.
01-JAN-10 16-JAN-10         15 0 years, 0 months.
01-JAN-10 17-JAN-10         16 0 years, 0 months.
01-JAN-10 18-JAN-10         17 0 years, 0 months.
01-JAN-10 19-JAN-10         18 0 years, 0 months.
01-JAN-10 20-JAN-10         19 0 years, 0 months.
01-JAN-10 21-JAN-10         20 0 years, 0 months.
01-JAN-10 22-JAN-10         21 0 years, 0 months.
01-JAN-10 23-JAN-10         22 0 years, 0 months.
01-JAN-10 24-JAN-10         23 0 years, 0 months.
01-JAN-10 25-JAN-10         24 0 years, 0 months.
01-JAN-10 26-JAN-10         25 0 years, 0 months.
01-JAN-10 27-JAN-10         26 0 years, 0 months.
01-JAN-10 28-JAN-10         27 0 years, 0 months.
01-JAN-10 29-JAN-10         28 0 years, 0 months.
01-JAN-10 30-JAN-10         29 0 years, 0 months.
01-JAN-10 31-JAN-10         30 0 years, 0 months.
01-JAN-10 01-FEB-10         31 0 years, 1 months.
01-JAN-10 02-FEB-10         32 0 years, 1 months.

It appears that the solution you provided, and possibly any solution using EXACT, still has issues. For example, if you check January 5, you will see that the number of months advances when February 1 appears for the D2 column.

D1        D2        DATE_DELTA T
--------- --------- ---------- ------------------
...
05-JAN-10 30-JAN-10         25 0 years, 0 months.
05-JAN-10 31-JAN-10         26 0 years, 0 months.
05-JAN-10 01-FEB-10         27 0 years, 1 months.
05-JAN-10 02-FEB-10         28 0 years, 1 months.
05-JAN-10 03-FEB-10         29 0 years, 1 months.
05-JAN-10 04-FEB-10         30 0 years, 1 months.
05-JAN-10 05-FEB-10         31 0 years, 1 months.
...
05-JAN-10 27-FEB-10         53 0 years, 1 months.
05-JAN-10 28-FEB-10         54 0 years, 1 months.
05-JAN-10 01-MAR-10         55 0 years, 2 months.
05-JAN-10 02-MAR-10         56 0 years, 2 months.
05-JAN-10 03-MAR-10         57 0 years, 2 months.
05-JAN-10 04-MAR-10         58 0 years, 2 months.
05-JAN-10 05-MAR-10         59 0 years, 2 months.

It might simply be a problem with the precision of the EXACT functionality, as mentioned in the comment by Gary.

(Edit: In the comment I originally described how the number of months advances when January 5 is the start date (column D1) and February 1 appears in the D2 column. Unfortunately, the code output section of the comment showed what happened when March 1 appeared in the D2 column. I added the January 5/February 1 output to the code output section in the comment.)

7 06 2010
Donatello Settembrino

Charles
we should ask:

1) What does extract?
2) What I want as a result?

Reply

1) from what I understand, it extracts the month from a date,
so for example, EXTRACT (MONTH FROM to_date (’01-Mar-2010 ‘)) = 3
then:

SELECT EXTRACT(MONTH FROM to_date('01-mar-2010')) AS month1_ext ,
       EXTRACT(MONTH FROM to_date('05-jan-2010')) AS month2_ext
from dual

MONTH1_EXT             MONTH2_EXT              
---------------------- ---------------------- 
3                      1                     

it does is simply, 3 – 1 = 2, then in my opinion should not be
used when we want the difference between two dates.

2)
if I have to do a temporal analysis on the data expressed in month, I have two situations,
I think. Compare fixed dates (eg the first of each month), or to compare between any dates.
In the first case, I should not have problems using Extract. In the second case, should be
established a rule. Example: from 1 to 15 of every month as the first of this month, from
16 onwards consider the first of next month.In the absence of rules, I do not think you can
use Extract and I do not think it makes sense to have a difference between two
dates in months(1.5 months is a month or two months ???).

Regards

7 06 2010
Sokrates

“1) from what I understand, it extracts the month from a date,”

not only from a date, but also from an interval.

7 06 2010
Donatello Settembrino

yes, the meaning of my answer is that
apart from a date or a range returns a number

Leave a reply to Gary Cancel reply