How to average time intervals?

后端 未结 6 1087
故里飘歌
故里飘歌 2020-12-09 17:03

In Oracle 10g I have a table that holds timestamps showing how long certain operations took. It has two timestamp fields: starttime and endtime. I want to find averages of t

相关标签:
6条回答
  • 2020-12-09 17:23

    There is a shorter, faster and nicer way to get DATETIME difference in seconds in Oracle than that hairy formula with multiple extracts.

    Just try this to get response time in seconds:

    (sysdate + (endtime - starttime)*24*60*60 - sysdate)
    

    It also preserves fractional part of seconds when subtracting TIMESTAMPs.

    See http://kennethxu.blogspot.com/2009/04/converting-oracle-interval-data-type-to.html for some details.


    Note that custom pl/sql functions have significant performace overhead that may be not suitable for heavy queries.

    0 讨论(0)
  • 2020-12-09 17:26

    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;
    
    0 讨论(0)
  • 2020-12-09 17:31

    If your endtime and starttime aren't within a second of eachother, you can cast your timestamps as dates and do date arithmetic:

    select avg(cast(endtime as date)-cast(starttime as date))*24*60*60 
      from timings;
    
    0 讨论(0)
  • 2020-12-09 17:38

    Well, this is a really quick and dirty method, but what about storing the seconds difference in a separate column (you'll need to use a trigger or manually update this if the record changes) and averaging over that column?

    0 讨论(0)
  • 2020-12-09 17:40

    SQL Fiddle

    Oracle 11g R2 Schema Setup:

    Create a type to use when performing a custom aggregation:

    CREATE TYPE IntervalAverageType AS OBJECT(
      total INTERVAL DAY(9) TO SECOND(9),
      ct    INTEGER,
    
      STATIC FUNCTION ODCIAggregateInitialize(
        ctx         IN OUT IntervalAverageType
      ) RETURN NUMBER,
    
      MEMBER FUNCTION ODCIAggregateIterate(
        self        IN OUT IntervalAverageType,
        value       IN     INTERVAL DAY TO SECOND
      ) RETURN NUMBER,
    
      MEMBER FUNCTION ODCIAggregateTerminate(
        self        IN OUT IntervalAverageType,
        returnValue    OUT INTERVAL DAY TO SECOND,
        flags       IN     NUMBER
      ) RETURN NUMBER,
    
      MEMBER FUNCTION ODCIAggregateMerge(
        self        IN OUT IntervalAverageType,
        ctx         IN OUT IntervalAverageType
      ) RETURN NUMBER
    );
    /
    
    CREATE OR REPLACE TYPE BODY IntervalAverageType
    IS
      STATIC FUNCTION ODCIAggregateInitialize(
        ctx         IN OUT IntervalAverageType
      ) RETURN NUMBER
      IS
      BEGIN
        ctx := IntervalAverageType( INTERVAL '0' DAY, 0 );
        RETURN ODCIConst.SUCCESS;
      END;
    
      MEMBER FUNCTION ODCIAggregateIterate(
        self        IN OUT IntervalAverageType,
        value       IN     INTERVAL DAY TO SECOND
      ) RETURN NUMBER
      IS
      BEGIN
        IF value IS NOT NULL THEN
          self.total := self.total + value;
          self.ct    := self.ct + 1;
        END IF;
        RETURN ODCIConst.SUCCESS;
      END;
    
      MEMBER FUNCTION ODCIAggregateTerminate(
        self        IN OUT IntervalAverageType,
        returnValue    OUT INTERVAL DAY TO SECOND,
        flags       IN     NUMBER
      ) RETURN NUMBER
      IS
      BEGIN
        IF self.ct = 0 THEN
          returnValue := NULL;
        ELSE
          returnValue := self.total / self.ct;
        END IF;
        RETURN ODCIConst.SUCCESS;
      END;
    
      MEMBER FUNCTION ODCIAggregateMerge(
        self        IN OUT IntervalAverageType,
        ctx         IN OUT IntervalAverageType
      ) RETURN NUMBER
      IS
      BEGIN
        self.total := self.total + ctx.total;
        self.ct    := self.ct + ctx.ct;
        RETURN ODCIConst.SUCCESS;
      END;
    END;
    /
    

    Then you can create a custom aggregation function:

    CREATE FUNCTION AVERAGE( difference INTERVAL DAY TO SECOND )
    RETURN INTERVAL DAY TO SECOND
    PARALLEL_ENABLE AGGREGATE USING IntervalAverageType;
    /
    

    Query 1:

    WITH INTERVALS( diff ) AS (
      SELECT INTERVAL '0' DAY FROM DUAL UNION ALL
      SELECT INTERVAL '1' DAY FROM DUAL UNION ALL
      SELECT INTERVAL '-1' DAY FROM DUAL UNION ALL
      SELECT INTERVAL '8' HOUR FROM DUAL UNION ALL
      SELECT NULL FROM DUAL
    )
    SELECT AVERAGE( diff ) FROM intervals
    

    Results:

    | AVERAGE(DIFF) |
    |---------------|
    |     0 2:0:0.0 |
    
    0 讨论(0)
  • 2020-12-09 17:41

    It doesn't look like there is any function to do an explicit conversion of INTERVAL DAY TO SECOND to NUMBER in Oracle. See the table at the end of this document which implies there is no such conversion.

    Other sources seem to indicate that the method you're using is the only way to get a number from the INTERVAL DAY TO SECOND datatype.

    The only other thing you could try in this particular case would be to convert to number before subtracting them, but since that'll do twice as many extractions, it will likely be even slower:

    select
         avg(
           (extract( second from endtime)  +
            extract ( minute from endtime) * 60 +
            extract ( hour   from  endtime ) * 3600) - 
           (extract( second from starttime)  +
            extract ( minute from starttime) * 60 +
            extract ( hour   from  starttime ) * 3600)
          ) from timings;
    
    0 讨论(0)
提交回复
热议问题