8

私の 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 固有のソリューションで問題なく機能することに注意してください。もちろん、これはストアド プロシージャを使用して行うこともできますが、1 つのクエリでタスクを達成したいと考えていました。

4

4 に答える 4

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

編集:いつものように、ジャスティンに殴られました... :-)

于 2012-06-26T20:35:04.693 に答える
6

何かのようなもの

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)

于 2012-06-26T20:27:47.600 に答える
4

JustinとSebasの回答は、LEFT JOINを使用して拡張し、「ギャップ」をなくすことができます。これは、多くの場合望ましいことです。

それが必要でない場合は、別の方法として、古い学校のOracleDATE演算を使用できます...

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秒であり、1日に86400秒あることがわかります。したがって、秒数を300で除算し、そのFLOOR(整数部分のみ)を取得して、最も近い5分の境界に切り捨てることができます。これを(300で)乗算して秒を再度取得し、それを1日の秒数(86400)で除算すると、(切り捨てられた)日付部分に追加できます。

はい、痛いです。しかし、非常に高速です。

注:これは、丸められた時間値をとして返します。これは、DATE必要に応じてタイムスタンプにキャストバックできますが、5分の境界でも、DATE十分な解像度があります。

このアプローチの利点として、大きなテーブルの場合、このクエリのカバーインデックスを追加することで、クエリのパフォーマンスを向上させることができます。

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

補遺:

MiMoはSQLServerに対する回答を提供し、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

0001 ADの基準日としての選択は任意ですが、負の値をいじって、FLOORが正しいかどうか、または負の数のCEILを使用する必要があるかどうかを判断したくありませんでした。(魔法の数288は、1日の1440分を5で割った結果です)。この場合、小数日を取得し、1440を掛けて5で除算し、その整数部分を取得して、小数日に戻します。

その「基準日」をPL/SQLパッケージから取得したり、サブクエリから取得したりするのは魅力的ですが、これらのいずれかを実行すると、この式が決定論的でなくなる可能性があります。そして、関数ベースのインデックスを作成するオプションを開いたままにしておきたいと思います。

私の好みは、計算に「基準日」を含める必要を避けることです。

于 2012-06-26T20:48:21.107 に答える
1

これは SQL Server のソリューションです。

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

declare @interval int = 5

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

開始日は任意です。アイデアは、開始日からの分数を計算し、この数を間隔で割った値でグループ化するというものです。

これは、同等の forDATEADDおよびDATEDIFF

于 2012-06-26T20:44:27.037 に答える