Unfortunately Oracle does not support most functions with intervals. There are a number of workarounds for this, but they all have some kind of drawback (and notably, none are ANSI-SQL compliant).
The best answer (as @justsalt later discovered) is to write a custom function to convert the intervals into numbers, average the numbers, then (optionally) convert back to intervals. Oracle 12.1 and later support doing this using a WITH
block to declare a function:
with
function fn_interval_to_sec(i in dsinterval_unconstrained)
return number is
begin
return ((extract(day from i) * 24
+ extract(hour from i) )*60
+ extract(minute from i) )*60
+ extract(second from i);
end;
select numtodsinterval(avg(fn_interval_to_sec(endtime-starttime)), 'SECOND')
from timings;
If you are on 11.2 or earlier, or if you prefer not to include functions in your SQL statements, you can declare it as a stored function:
create or replace function fn_interval_to_sec(i in dsinterval_unconstrained)
return number is
begin
return ((extract(day from i) * 24
+ extract(hour from i) )*60
+ extract(minute from i) )*60
+ extract(second from i);
end;
You can then use it in SQL as expected:
select numtodsinterval(avg(fn_interval_to_sec(endtime-starttime)), 'SECOND')
from timings;
Using dsinterval_unconstrained
Using the PL/SQL type alias dsinterval_unconstrained
for the function parameter ensures you have maximum precision/scale; INTERVAL DAY TO SECOND
defaults DAY
precision to 2 digits (meaning anything at or over ±100 days is an overflow and throws an exception) and SECOND
scale to 6 digits.
Additionally, Oracle 12.1 will raise a PL/SQL error if you try to specify any precision/scale in your parameter:
with
function fn_interval_to_sec(i in interval day(9) to second(9))
return number is
...
ORA-06553: PLS-103: Encountered the symbol "(" when expecting one of the following: to
Alternatives
Custom aggregate function
Oracle supports custom aggregate functions written in PL/SQL, which would allow you to make minimal changes to the statement:
select ds_avg(endtime-starttime) from timings;
However, this approach has several major drawbacks:
- You have to create the PL/SQL aggregate objects in your database, which may not be desired or allowed;
- You cannot name it
avg
, as Oracle will always use the builtin avg
function rather than your own. (Technically you can, but then you have to qualify it with schema, which defeats the purpose.)
- As @vadzim noted, aggregate PL/SQL functions have significant performance overhead.
Date arithmetic
If your values are not significantly far apart, @vadzim's approach works as well:
select avg((sysdate + (endtime-starttime)*24*60*60*1000000 - sysdate)/1000000.0)
from timings;
Be aware, though, that if the interval is too great, the (endtime-starttime)*24*60*60*1000000
expression will overflow and throw ORA-01873: the leading precision of the interval is too small
. At this precision (1μs) the difference cannot be greater than or equal to 00:16:40
in magnitude, so it is safe for small intervals, but not all.
Finally, if you are comfortable losing all subsecond precision, you can cast the TIMESTAMP
columns to DATE
; subtracting a DATE
from a DATE
will return the number of days with second precision (credit to @jimmyorr):
select avg(cast(endtime as date)-cast(starttime as date))*24*60*60
from timings;