如何平均时间间隔?
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
数据类型获取数字的唯一方法。
在这种特殊情况下,您唯一可以尝试的另一件事是在减去它们之前转换为数字,但由于这会产生两倍的 extract
ions,它可能会更慢:
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;
【讨论】:
以上是关于如何平均时间间隔?的主要内容,如果未能解决你的问题,请参考以下文章