如何平均时间间隔?

Posted

技术标签:

【中文标题】如何平均时间间隔?【英文标题】: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;

【问题讨论】:

【参考方案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;

【讨论】:

以上是关于如何平均时间间隔?的主要内容,如果未能解决你的问题,请参考以下文章

Prometheus 查询一个时间间隔下的总体平均值

如何将系列拆分为时间间隔? (Python)

SQL按时间戳间隔获取平均值

RDD的Pyspark平均间隔

数组中时间戳之间的平均间隔

平均无故障时间的计算方法及公式?