18

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

select avg(endtime-starttime) from timings;

但是得到:

SQL 错误:ORA-00932:不一致的数据类型:预期的 NUMBER 得到 INTERVAL DAY TO SECOND

这有效:

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

6 回答 6

25

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

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

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

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

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


请注意,自定义 pl/sql 函数具有显着的性能开销,可能不适合繁重的查询。

于 2011-12-06T22:22:35.590 回答
9

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;
于 2009-01-16T15:10:58.367 回答
2

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;
于 2009-01-16T15:01:47.777 回答
2

SQL小提琴

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

结果

| AVERAGE(DIFF) |
|---------------|
|     0 2:0:0.0 |
于 2017-12-07T10:08:43.127 回答
1

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?

于 2009-01-16T15:05:07.770 回答
0

不幸的是,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 天或超过 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 聚合对象,这可能是不希望或不允许的;
  • 您无法命名它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从 a 中减去 aDATE将返回具有第二精度的天数(归功于@jimmyorr):

select avg(cast(endtime as date)-cast(starttime as date))*24*60*60 
  from timings;
于 2020-06-15T16:05:12.287 回答