【问题标题】:How to average time intervals?如何平均时间间隔?
【发布时间】:2010-10-01 19:46:11
【问题描述】:

在 Oracle 10g 中,我有一个包含时间戳的表,其中显示了某些操作花费了多长时间。它有两个时间戳字段:开始时间和结束时间。我想找到这些时间戳给出的持续时间的平均值。我试试:

select avg(endtime-starttime) from timings;

但是得到:

SQL 错误:ORA-00932:不一致 数据类型:预期 NUMBER 得到 间隔天到秒

这行得通:

select
     avg(extract( second from  endtime - starttime) +
        extract ( minute from  endtime - starttime) * 60 +
        extract ( hour   from  endtime - starttime) * 3600) from timings;

但是真的很慢。

有没有更好的方法将时间间隔转换为秒数,或者其他方法?

编辑: 真正减慢这一速度的是我在开始时间之前有一些结束时间。出于某种原因,这使得这个计算变得异常缓慢。我的根本问题是通过从查询集中消除它们来解决的。我还刚刚定义了一个函数来更轻松地进行这种转换:

FUNCTION fn_interval_to_sec ( i IN INTERVAL DAY TO SECOND )
RETURN NUMBER
IS
  numSecs NUMBER;
BEGIN
  numSecs := ((extract(day from i) * 24
         + extract(hour from i) )*60
         + extract(minute from i) )*60
         + extract(second from i);
  RETURN numSecs;
END;

【问题讨论】:

    标签: sql oracle timestamp ora-00932


    【解决方案1】:

    在 Oracle 中,有一种更短、更快、更好的方法来获得 DATETIME 差异(以秒为单位),而不是具有多个提取的毛茸茸的公式。

    试试这个以获得以秒为单位的响应时间:

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

    在减去 TIMESTAMP 时,它还会保留秒的小数部分。

    有关详细信息,请参阅http://kennethxu.blogspot.com/2009/04/converting-oracle-interval-data-type-to.html


    请注意,custom pl/sql functions have significant performace overhead 可能不适合繁重的查询。

    【讨论】:

    • 似乎是迄今为止最简单的解决方案。如果 Oracle 可以为此创建一个正常的函数会很好。
    • 这会将间隔差乘以24*60*60 = 86400,然后将其添加到日期中,这将给出结果作为日期并丢失任何小数秒 - 所以如果时间戳精确到微秒(或任何小于 1/86400 秒的),那么它将失去准确性。
    • @MT0,你是对的。 TIMESTAMP(9) 的纳秒精度可以通过 (sysdate + (end_ts - start_ts)*24*60*60*1000000 - sysdate)/1000000.0 实现。
    • 非常感谢您的回答。对我来说这很有帮助。
    【解决方案2】:

    如果您的结束时间和开始时间不在一秒之内,您可以将时间戳转换为日期并进行日期算术:

    select avg(cast(endtime as date)-cast(starttime as date))*24*60*60 
      from timings;
    

    【讨论】:

    • 这将丢失时间戳中的任何小数秒(无论它们是否在一秒内)。
    【解决方案3】:

    在 Oracle 中似乎没有任何函数可以将 INTERVAL DAY TO SECOND 显式转换为 NUMBER。请参阅this document 末尾的表格,这意味着没有这样的转换。

    其他来源似乎表明您使用的方法是从 INTERVAL DAY TO SECOND 数据类型获取数字的唯一方法。

    在这种特殊情况下,您唯一可以尝试的另一件事是在减去它们之前转换为数字,但由于这会产生两倍的 extractions,它可能会更慢:

    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;
    

    【讨论】:

      【解决方案4】:

      SQL Fiddle

      Oracle 11g R2 架构设置

      创建执行自定义聚合时使用的类型:

      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;
      /
      

      然后可以创建自定义聚合函数:

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

      查询 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 |
      

      【讨论】:

        【解决方案5】:

        嗯,这是一个非常快速和肮脏的方法,但是如何将秒差存储在单独的列中(如果记录发生更改,您需要使用触发器或手动更新它)并对该列进行平均?

        【讨论】:

        • 如果您想这样做,您可以使用基于函数的索引 (fbi),它可以为您节省触发器或手动更新列。 fbi 可以在 where 子句中使用,也可以在 select 子句中使用。
        【解决方案6】:

        不幸的是,Oracle 不支持大多数带间隔的函数。有许多解决方法,但它们都有一些缺点(值得注意的是,没有一个是 ANSI-SQL 兼容的)。

        最好的答案(正如@justsalt 后来发现的那样)是编写一个自定义函数来将区间转换为数字,平均数字,然后(可选)转换回区间。 Oracle 12.1 及更高版本支持使用WITH 块来声明函数:

        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;
        

        如果您使用的是 11.2 或更早版本,或者您不想在 SQL 语句中包含函数,则可以将其声明为存储函数:

        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;
        

        然后您可以按预期在 SQL 中使用它:

        select numtodsinterval(avg(fn_interval_to_sec(endtime-starttime)), 'SECOND') 
          from timings;
        

        使用dsinterval_unconstrained

        对函数参数使用 PL/SQL 类型别名 dsinterval_unconstrained 可确保您拥有最大的精度/规模; INTERVAL DAY TO SECOND 默认 DAY 精度为 2 位(意味着 ±100 天或以上的任何内容都会溢出并引发异常),SECOND 默认为 6 位。

        此外,如果您尝试在参数中指定任何精度/小数位数,Oracle 12.1 将引发 PL/SQL 错误:

        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

        替代方案

        自定义聚合函数

        Oracle 支持用 PL/SQL 编写的自定义聚合函数,这将允许您对语句进行最少的更改:

        select ds_avg(endtime-starttime) from timings;
        

        但是,这种方法有几个主要缺点:

        • 您必须在数据库中创建PL/SQL aggregate objects,这可能是不希望或不允许的;
        • 您不能将其命名为 avg,因为 Oracle 将始终使用内置的 avg 函数而不是您自己的函数。 (技术上你可以,但是你必须用模式来限定它,这违背了目的。)
        • 正如 @vadzim 所指出的,聚合 PL/SQL 函数具有显着的性能开销。

        日期算术

        如果您的价值观相差不大,@vadzim 的方法也可以:

        select avg((sysdate + (endtime-starttime)*24*60*60*1000000 - sysdate)/1000000.0) 
          from timings;
        

        但请注意,如果间隔太大,(endtime-starttime)*24*60*60*1000000 表达式将溢出并抛出 ORA-01873: the leading precision of the interval is too small。在这个精度 (1μs) 下,差异的幅度不能大于或等于00:16:40,因此对于小间隔是安全的,但不是全部。

        最后,如果您愿意失去所有亚秒级精度,您可以将TIMESTAMP 列转换为DATE;从DATE 中减去DATE 将返回秒精度的天数(归功于@jimmyorr):

        select avg(cast(endtime as date)-cast(starttime as date))*24*60*60 
          from timings;
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-04-14
          • 1970-01-01
          • 2019-11-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-03-25
          相关资源
          最近更新 更多