2012-06-26 64 views
7

我的SQL有点生疏,我在这个问题上遇到了相当多的困难。假设我有一个带Timestamp列和Number列的表。目标是返回一个包含任意选择的常规间隔的平均值的结果集。给定时间间隔内的聚合函数

因此,例如,如果我有以下初始数据,用5分钟的时间间隔所产生的输出将是如下:

time        value 
------------------------------- ----- 
06-JUN-12 12.40.00.000000000 PM  2 
06-JUN-12 12.41.35.000000000 PM  3 
06-JUN-12 12.43.22.000000000 PM  4 
06-JUN-12 12.47.55.000000000 PM  5 
06-JUN-12 12.52.00.000000000 PM  2 
06-JUN-12 12.54.59.000000000 PM  3 
06-JUN-12 12.56.01.000000000 PM  4 

OUTPUT: 

start_time       avg_value 
------------------------------- --------- 
06-JUN-12 12.40.00.000000000 PM  3 
06-JUN-12 12.45.00.000000000 PM  5 
06-JUN-12 12.50.00.000000000 PM  2.5 
06-JUN-12 12.55.00.000000000 PM  4 

注意,这是Oracle数据库,所以Oracle特定的解决方案会正常工作。当然,这可以通过存储过程完成,但我希望能够在单个查询中完成任务。

+0

Oracle版本10g +? – Sebas

+1

是的,对不起 - 10g – Nick

回答

8
CREATE TABLE tt (time TIMESTAMP, value NUMBER); 

INSERT INTO tt (time, value) VALUES ('06-JUN-12 12.40.00.000000000 PM', 2); 
INSERT INTO tt (time, value) VALUES ('06-JUN-12 12.41.35.000000000 PM', 3); 
INSERT INTO tt (time, value) VALUES ('06-JUN-12 12.43.22.000000000 PM', 4); 
INSERT INTO tt (time, value) VALUES ('06-JUN-12 12.47.55.000000000 PM', 5); 
INSERT INTO tt (time, value) VALUES ('06-JUN-12 12.52.00.000000000 PM', 2); 
INSERT INTO tt (time, value) VALUES ('06-JUN-12 12.54.59.000000000 PM', 3); 
INSERT INTO tt (time, value) VALUES ('06-JUN-12 12.56.01.000000000 PM', 4); 


WITH tmin AS (
    SELECT MIN(time) t FROM tt 
), tmax AS (
    SELECT MAX(time) t FROM tt 
) 
SELECT ranges.inf, ranges.sup, AVG(tt.value) 
FROM 
    (
     SELECT 
      5*(level-1)*(1/24/60) + tmin.t as inf, 
      5*(level)*(1/24/60) + tmin.t as sup 
     FROM tmin, tmax 
     CONNECT BY (5*(level-1)*(1/24/60) + tmin.t) < tmax.t 
    ) ranges JOIN tt ON tt.time BETWEEN ranges.inf AND ranges.sup 
GROUP BY ranges.inf, ranges.sup 
ORDER BY ranges.inf 

小提琴:http://sqlfiddle.com/#!4/9e314/11

编辑:由Justin打倒,像往常一样... :-)

+0

谢谢你的帮助 - 真棒回答! – Nick

+0

这个解决方案对我来说很慢,对于100k条记录,执行需要5分钟,尽管它确实有效。 – tosi

+0

tt.time索引? – Sebas

5

喜欢的东西

with st 
    as (SELECT to_timestamp('2012-06-06 12:40:00', 'yyyy-mm-dd hh24:mi:ss') + 
       numtodsinterval((level-1)*5, 'MINUTE') start_time, 
      to_timestamp('2012-06-06 12:40:00', 'yyyy-mm-dd hh24:mi:ss') + 
       numtodsinterval(level*5, 'MINUTE') end_time 
     from dual 
    connect by level <= 10) 
SELECT st.start_time, avg(yt.value) 
    FROM your_table yt, 
     st 
WHERE yt.time between st.start_time and st.end_time 

应该工作。不是生成10个间隔并对最低间隔进行硬编码,您可以增强查询以获取表中的起始点和MIN(time)MAX(time)的行数。

+0

感谢您的帮助 - 您肯定是您的工艺大师。 – Nick

1

这是SQL Server的解决方案:

declare @startDate datetime = '2000-01-01T00:00:00' 

declare @interval int = 5 

select 
    DATEADD(mi, DATEDIFF(mi, @startDate, time)/@interval, @startDate), 
    AVG(value) 
from 
    table 
group by 
    DATEDIFF(mi, @startDate, s_modifiedDate)/@interval 
order by 
    DATEDIFF(mi, @startDate, s_modifiedDate)/@interval 

的开始日期是任意的。这个想法是,你计算从开始日期开始的分钟数,然后按这个数除以间隔。

应该容易使用等效为DATEADDDATEDIFF

+0

适用于Oracle,作为我的答案的附录。 Oracle不提供DATEADD或DATEDIFF函数,但使用简单的算术代替。 – spencer7593

+0

请你解释一下's_modifiedDate'究竟代表什么? – zvonicek

+0

这是如何执行的?因为如果我有每秒数据,然后我去聚合1年以上。 AVG()函数会做一些认真的工作吗? – Zapnologica

3

Justin的和Sebas的答案可以与LEFT扩展JOIN以消除‘间隙’,其是通常希望能适应的Oracle。

如果这是没有必要的,作为替代,我们可以去老同学Oracle日期算术......

SELECT TRUNC(t.time)+FLOOR(TO_CHAR(t.time,'sssss')/300)*300/86400 AS time 
    , AVG(t.value) AS avg_value 
    FROM foo t 
WHERE t.time IS NOT NULL 
GROUP BY TRUNC(t.time)+FLOOR(TO_CHAR(t.time,'sssss')/300)*300/86400 
ORDER BY TRUNC(t.time)+FLOOR(TO_CHAR(t.time,'sssss')/300)*300/86400 

让我们解开这一点。我们可以分开日期和时间组件,使用TRUNC获取日期部分,并使用TO_CHAR返回自午夜以来的秒数。我们知道5分钟是300秒,而我们知道一天有86400秒。因此,我们可以将秒数除以300,并将FLOOR(只是整数部分),它将我们向下舍入到最近的5分钟边界。我们将它乘以300(再乘以300),再次得到秒,然后将其除以一天中的秒数(86400),然后我们可以将它加回到(截断的)日期部分。

痛苦,是的。但非常快。

注:本传回四舍五入的时间值作为DATE,这可能是如果需要转换回时间戳,但即使5分钟边界,一个DATE具有足够的分辨率。

CREATE INDEX foo_FBX1 
ON foo (TRUNC(t.time)+FLOOR(TO_CHAR(t.time,'sssss')/300)*300/86400,value); 

附录:

由于这种方法的好处,对于一个大表,我们可以通过添加一个覆盖索引为此查询提升查询的性能MiMo为SQL Server提供了一个答案,表明它将适用于Oracle。这是Oracle对这种方法的改编。请注意,Oracle不提供DATEDIFF和DATEADD函数的等价物。 Oracle使用简单的算术代替。

SELECT TO_DATE('00010101','YYYYMMDD')+FLOOR((t.time-TO_DATE('00010101','YYYYMMDD'))*288)/288 
     AS time 
    , AVG(t.value) AS avg_value 
    FROM foo t 
WHERE t.time IS NOT NULL 
GROUP BY TO_DATE('00010101','YYYYMMDD')+FLOOR((t.time-TO_DATE('00010101','YYYYMMDD'))*288)/288 
ORDER BY TO_DATE('00010101','YYYYMMDD')+FLOOR((t.time-TO_DATE('00010101','YYYYMMDD'))*288)/288 

1月1日的选择,公元0001的基准日是任意的,但我不想与负值的混乱,并搞清楚,如果地板是正确的,或者我们是否需要使用负数的CEIL。 (神奇数字288是一天中1440分钟除以5的结果)。在这种情况下,我们将分数日,乘以1440并除以5,并取整数部分,然后再回到分数日。

很容易从PL/SQL包中获取“基准日期”,或从子查询中获取“基准日期”,但执行其中任何一项都可能会阻止此表达式成为确定性表达式。而且我们真的很想打开创建基于函数的索引的选项。

我的首选是避免在计算中包含“基准日期”。

+0

感谢您的分解!非常丰富和有益的。 – Nick