2012-06-26 14 views
7

Il mio SQL è un po 'arrugginito e sto avendo un po' di difficoltà con questo problema. Supponiamo che abbia una tabella con una colonna Timestamp e una colonna Numero. L'obiettivo è restituire un set di risultati contenente il valore medio per alcuni intervalli regolari scelti arbitrariamente.Funzione aggregata su un dato intervallo di tempo

Così, per esempio, se ho avuto i seguenti dati iniziali, l'output risultante con un intervallo di 5 minuti sarebbero i seguenti:

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 

Si noti che questo è un database Oracle, quindi le soluzioni Oracle-specifiche funzionerebbe bene Questo potrebbe, naturalmente, essere fatto con una procedura memorizzata, ma speravo di realizzare l'operazione in una singola query.

+0

Oracle versione 10g +? – Sebas

+1

Sì, mi dispiace - 10g – Nick

risposta

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 

violino: http://sqlfiddle.com/#!4/9e314/11

edit: ha battuto da Justin, come al solito ... :-)

+0

Grazie mille per l'aiuto - risposta fantastica! – Nick

+0

questa soluzione è lenta per me, per i record di 100k ci vogliono 5 minuti per l'esecuzione, anche se funziona. – tosi

+0

tt.time indexed? – Sebas

5

Qualcosa di simile

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 

dovrebbe funzionare. Anziché generare 10 intervalli e codificare con precisione l'intervallo più basso, è possibile migliorare la query per ricavare il punto di partenza e il numero di righe da MIN(time) e MAX(time) nella tabella.

+0

Grazie per l'aiuto - sei sicuramente un maestro del tuo mestiere. – Nick

1

Questa è una soluzione per 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 

La data di inizio è arbitrario. L'idea è di calcolare il numero di minuti dalla data di inizio, quindi raggruppare per questo numero diviso per l'intervallo.

Dovrebbe essere adattabile a Oracle facilmente usando l'equivalente per DATEADD e DATEDIFF

+0

adattato a Oracle come addendum alla mia risposta. Oracle non fornisce funzioni DATEADD o DATEDIFF, ma utilizza invece l'aritmetica semplice. – spencer7593

+0

Per favore, puoi spiegare cosa rappresenta "s_modifiedDate" esattamente? – zvonicek

+0

Come funziona? Perché se ho dati al secondo e poi vado ad aggregarlo per 1 anno. Quella funzione 'AVG()' sta facendo un lavoro serio? – Zapnologica

3

risposte Sebas' di Justin e possono essere estese con un LEFT JOIN per eliminare 'buchi', che è spesso desiderabile.

Se questo non è necessario, in alternativa, possiamo andare vecchia scuola Oracle DATA aritmetica ...

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 

Diamo disfare che un po '. Possiamo separare i componenti di data e ora, utilizzando TRUNC per ottenere la parte di data e utilizzando un TO_CHAR per restituire il numero di secondi da mezzanotte. Sappiamo che 5 minuti sono 300 secondi e sappiamo che ci sono 86400 secondi in un giorno. Quindi possiamo dividere il numero di secondi per 300, e prendere il FLOOR di quello (solo la parte intera), che ci arrotonda al limite di 5 minuti più vicino. Moltiplichiamo quello indietro (per 300), per ottenere di nuovo secondi, e poi lo dividiamo per il numero di secondi in un giorno (86400), e possiamo aggiungerlo nuovamente alla porzione di data (troncata).

Doloroso, sì. Ma incredibilmente veloce.

NOTA: questo restituisce il valore di tempo arrotondato come DATE, questo potrebbe essere restituito a un timestamp se necessario, ma per i confini anche di 5 minuti, uno DATE ha una risoluzione sufficiente.

Come un vantaggio di questo approccio, per un grande tavolo, siamo in grado di migliorare le prestazioni della query con l'aggiunta di un indice di copertura per questa query:

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

APPENDICE:

MiMo ha fornito una risposta per SQL Server, suggerendo che sarebbe adattabile per Oracle. Ecco un adattamento di tale approccio in Oracle. Si noti che Oracle non fornisce equivalenti per le funzioni DATEDIFF e DATEADD. Oracle utilizza invece l'aritmetica semplice.

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 

La scelta del 1 gennaio, 0001 dC come data di base è arbitraria, ma non ho voglia di pasticciare con valori negativi, e cercare di capire se PIANO sarebbe giusto, o se ci sarebbe bisogno di usare CEIL con numeri negativi. (Il numero magico 288 è un risultato di 1440 minuti in un giorno diviso per 5). In questo caso, stiamo prendendo il giorno frazionario, moltiplicando per 1440 e dividendo per 5, e prendendo la parte intera di quello, e quindi rimettendola in giorni frazionari.

Si è tentati di estrarre quella "data di base" da un pacchetto PL/SQL o di ottenerla da una sottoquery, ma l'esecuzione di una di queste potrebbe impedire che questa espressione sia deterministica. E ci piacerebbe davvero mantenere aperta l'opzione di creare un indice basato su funzioni.

La mia preferenza è evitare la necessità di includere una "data base" nel calcolo.

+0

Grazie per il guasto! Molto informativo e utile. – Nick

Problemi correlati