I have a table like this:
[Date] [hour] [valid] [value] [type]
And I need to do the averange for each hour of the 23 previous values and itself (24 values) and only if I have 18 or more valid values.
I do with one hour with this query:
select type, avg(value)
from table
where type = 10
and valid = 1
and ((date = '11/10/2016' and hour <= 22) or
(date = '11/09/2016' and hour > 22))
group by type
having count(*)>=18;
But I need to do it with a date interval for each hour in that interval.
It can be possible?
Thank you.
Sorry but keeping date separately with hour is not good. So I'll merge it into date in with clause. If you don't want that you can use to_date(date || ':' || hour, 'dd/mm/yyyy:hh24') everywhere where I have d and ignore with clause
with dat as (select to_date(date || ':' || hour, 'dd/mm/yyyy:hh24') d, valid, value, type from table)
select d1.d, d1.type, avg(d2.value)
from dat d1 cross join dat d2
where d1.d < (d2.d + 1) and d1.d >= d2.d and d2.valid = 1
group by d1.d, d1.type
having count(*) >= 18;
I hope it will work. If you provided any test data I would test it.
you may write next, I assume column valid contains Y and N values.
select type
,max(avg1)
from (
select type
,avg(value) over (order by trunc(date,'HH') range between interval '24' hour preceding and interval '0' hour following ) as avg1
,count(decode(valid,'Y',1)) over (order by trunc(date,'HH') range between interval '24' hour preceding and interval '0' hour following ) as count1
from table
where type = 10
and valid = 1)
where count1 >= 18
group by type;
Related
I need to aggregate date ranges allowing for max 2 days gaps in between for each id. Any help would be much appreciated
create table tt ( id int, startdate date, stopdate date);
Insert into TT values (1,'24/05/2010', '29/05/2010');
Insert into TT values (1,'30/05/2010', '22/06/2010');
Insert into TT values (10,'26/06/2012', '28/06/2012');
Insert into TT values (10,'29/06/2012', '30/06/2012');
Insert into TT values (10,'01/07/2012', '30/07/2012');
Insert into TT values (10,'03/08/2012', '30/12/2012');
insert into TT values (90,'08/03/2002', '16/03/2002');
insert into TT values (90,'31/01/2002', '15/02/2002');
insert into TT values (90,'15/02/2002', '28/02/2002');
insert into TT values (90,'31/01/2002', '15/02/2004');
insert into TT values (90,'15/02/2004', '15/04/2004');
insert into TT values (90,'01/03/2002', '07/03/2002');
expected output would be:
1 24/05/2010 22/06/2010
10 26/06/2012 30/07/2012
10 03/08/2012 30/12/2012
90 31/01/2002 15/04/2004
If you're on 12c, you can use one of my favourite SQL features: pattern matching (match_recognize).
With this you need to define a pattern variable. This is where you'll check that the start date of the current row is within two days of the stop date for the previous row. Which is:
startdate <= prev ( stopdate ) + 2
The pattern you're searching for is any row, followed by zero or more rows that meet this criterium.
So you have an "always true" strt variable, followed by * (regular expression zero-or-more quantifier) occurrences of the within2 variable:
( strt within2* )
I'm guessing you also need to split the ranges up by ID. So I've added a partition by for this.
Put it all together and you get:
select *
from tt match_recognize (
partition by id
order by startdate, stopdate
measures
first ( startdate ) startdate,
last ( stopdate ) stopdate
pattern ( strt within2* )
define
within2 as startdate <= prev ( stopdate ) + 2
);
ID STARTDATE STOPDATE
1 24/05/2010 22/06/2010
10 26/06/2012 30/07/2012
10 03/08/2012 30/12/2012
If you want to know more about this, you can find several match_recognize examples here.
I have the following query that gets the week of a date:
SELECT pdm.serie, rta.matricula_ant, TO_CHAR (fecha, 'ww') semana,
SUM (rta.kms_acumulados) kms,
COUNT
(DISTINCT (CASE
WHEN v.secuencia BETWEEN rta.sec_origen AND rta.sec_destino
THEN v.cod_inc
ELSE '0'
END
)
)
- 1 numincidencias
FROM (SELECT ms.tren, ms.fecha_origen_tren, ms.secuencia, ri.cod_inc
FROM r_incidencias ri, mer_sitra ms
WHERE ri.cod_serv = ms.tren
AND ri.fecha_origen_tren = ms.fecha_origen_tren
AND ri.cod_tipoin IN (SELECT cod_tipo_iincidencia
FROM v_tipos_incidencias
WHERE grupo = '45')
AND ri.punto_desde = ms.cod_estacion) v,
r_trenes_asignar rta,
r_maquinas rm,
planificador.pl_dh_material pdm
WHERE rta.fecha BETWEEN TO_DATE ('21/09/2018', 'dd/mm/yyyy') AND TO_DATE ('21/09/2018',
'dd/mm/yyyy'
)
AND rta.serie >= 4000
AND rta.matricula_ant IS NOT NULL
AND rm.matricula_maq = rta.matricula_ant
AND rm.cod_serie = pdm.id_material
AND rta.grafico BETWEEN pdm.desde AND pdm.hasta
AND v.tren(+) = rta.tren
AND v.fecha_origen_tren(+) = rta.fecha
GROUP BY pdm.serie, rta.matricula_ant, TO_CHAR (fecha, 'ww')
ORDER BY pdm.serie, rta.matricula_ant, TO_CHAR (fecha, 'ww')
For example week 1
I want to display
week 1 : 1 january - 7 january
How can I get this?
Oracle offers the TRUNC(datestamp, format) function to manipulate dates this way. You may use a variety of format strings to get the first day of a quarter, year, or even the top of the hour.
Given a particular datestamp value, Oracle returns midnight on the first day of the present week with this expression:
TRUNC(datestamp,'DY')
You can add days to a datestamp. Therefore this expression gives you midnight on the last day of the week
TRUNC(datestamp,'DY') + 6
A WHERE-clause selector for all rows in the present week might be this.
WHERE datestamp >= TRUNC(SYSDATE,'DY')
AND datestamp < TRUNC(SYSDATE,'DY') + 7
Notice that the end of the range is just before (<) midnight on the first day of the next week. You need that because you may have datestamps after midnight on the last day of the week. (Beware using BETWEEN for datestamp ranges.)
And,
SELECT TO_CHAR(TRUNC(SYSDATE,'DY'),'YYYY-MM-DD'),
TO_CHAR(TRUNC(SYSDATE,'DY')+6,'YYYY-MM-DD')
FROM DUAL;
displays the first and last dates of the present week in ISO-like format.
Date arithmetic is cool. It's worth your trouble to study the date-arithmetic functions in your DBMS at least once a year.
I have say, start and end date as "6/11/1996" and "3/1/2002" in "mm/dd/yyyy" format respectively.
I need to get all the monthly periods as given below.
Start Date End Date
6/11/1996 - 6/30/1996
7/01/1996 - 7/31/1996
8/01/1996 - 8/31/1996
.
.
.
Till
2/01/2002 - 2/28/2002
Any help would be highly appreciated.
Assuming the interval is given by way of two bind variables, :from_dt and :to_dt (strings in the indicated format):
with
inputs ( f_dt, t_dt ) as (
select to_date(:from_dt, 'mm/dd/yyyy'), to_date(:to_dt, 'mm/dd/yyyy') from dual
),
ld ( l_day, lvl ) as (
select add_months(last_day(f_dt), level - 1), level
from inputs
connect by level <= months_between(last_day(t_dt), last_day(f_dt)) + 1
)
select case when ld.lvl = 1 then i.f_dt else add_months(ld.l_day, -1) + 1 end
as start_date,
least(i.t_dt, ld.l_day) as end_date
from inputs i cross join ld
;
This assumes that in the original post you did, in fact, mean to have one more interval, from 3/1/2002 to 3/1/2002; and the query deals correctly with the case when the from-date and the to-date are in the same month: if the inputs are 6/11/1996 to 6/21/1996, then the output is exactly that interval.
Added: creating column aliases in the declaration of factored subqueries (in the WITH clause), as I have done, requires Oracle 11.2 or above. For earlier versions, it is necessary to write it a little differently, like so:
with
inputs as (
select to_date(:from_dt, 'mm/dd/yyyy') as f_dt,
to_date(:to_dt , 'mm/dd/yyyy') as t_dt
from dual
),
ld as (
select add_months(last_day(f_dt), level - 1) as l_day, level as lvl
from inputs ...............
You could use connect by query with functions add_months and months_between:
with p as ( select date '1996-06-11' d1, date '2002-03-01' d2 from dual )
select greatest(trunc(add_months(d1, level - 1), 'month'), d1) as d1,
trunc(add_months(d1, level), 'month') - 1 as d2
from p connect by level <= months_between(trunc(d2, 'month'), trunc(d1, 'month'))
The output is exactly as requested.
DECLARE
L_min_date DATE := '11-Jun-1996';
L_max_date DATE := '28-Feb-2002';
L_number_of_months NUMBER := 0;
L_new_date DATE := NULL;
BEGIN
L_number_of_months := MONTHS_BETWEEN(L_max_date, L_min_date);
DBMS_OUTPUT.PUT_LINE('Start_Date End_Date');
FOR i IN 1..L_number_of_months
LOOP
SELECT (ADD_MONTHS(L_min_date, 1) - 1) INTO L_new_date FROM dual;
DBMS_OUTPUT.PUT_LINE(L_min_date || ' - ' || L_new_date);
L_min_date := L_new_date + 1;
END LOOP;
END;
/
Something like above can help.
CREATE OR REPLACE FUNCTION exlude_weekends (p_date_start DATE,
p_date_end DATE)
RETURN NUMBER
AS
l_no_of_days NUMBER := NULL;
BEGIN
SELECT COUNT ( * ) INTO l_no_of_days
FROM (SELECT date_extraction, TO_CHAR (date_extraction, 'DAY')
FROM (SELECT TO_DATE(p_date_start,'DD-MON-RRRR')
+ LEVEL - 1 date_extraction FROM DUAL CONNECT BY LEVEL <
(TO_DATE (p_date_end, 'DD-MON-RRRR')- TO_DATE (p_date_start,'DD-MON-RRRR'))+ 2)
WHERE TRIM (TO_CHAR (date_extraction, 'DAY')) NOT IN ('SATURDAY', 'SUNDAY'));
RETURN l_no_of_days;
EXCEPTION
WHEN OTHERS
THEN
RETURN 0;
END exlude_weekends;
As you mention in the comments, the key of the function is the hierarchical sub-query:
SELECT TO_DATE(p_date_start,'DD-MON-RRRR') + LEVEL - 1 date_extraction
FROM DUAL
CONNECT BY LEVEL <
(TO_DATE(p_date_end,'DD-MON-RRRR')-
TO_DATE(p_date_start,'DD-MON-RRRR'))+ 2
Hierarchical queries try to traverse a tree (CONNECT BY clause specifies how parents and children are related). In this example we find a tricky use (or abuse) of the connect by.
This sub-query generates date from p_date_start to p_date_end (both inclusive). How it does it?
Note that the expression being compare with LEVEL in the CONNECT BY is a constant, and it is the number of days between start and the day after the end date (why the day after the end date? because it is using < and the day after the end date is the first day out of the interval):
(TO_DATE(p_date_end,'DD-MON-RRRR')-TO_DATE(p_date_start,'DD-MON-RRRR'))+2
The select get the DUAL row (it has only one row) this row has LEVEL 1 (hierarchical query use the pseudo-column LEVEL to indicate the depth from the root where it started to evaluate).
The CONNECT BY checks that this level (1) is in the range of days to be generated.
Evaluates the expression:
TO_DATE(p_date_start,'DD-MON-RRRR') + LEVEL - 1
This is the start date plus the level minus one: this is, the start date.
Now a new cycle in hierarchical evaluation starts: the row generated in the previous cycle (the start date) is evaluated again (the new row will have level 2).
If it is in the range of days to be generated (controlled by the CONNECT BY clause) a new date is generated (the day after the start date).
A new cycle start (level 3)....
And the process iterates until LEVEL is greater than the number of days to be generated (which is the same than the number of levels required to iterate from the start date to the end date).
The outer queries in the function only filter SATURDAYS and SUNDAYS and count the remaining days.
Although oracle is very efficient evaluating this query, this function uses a brute force solution.
A more elegant and mathematical solution can be used (with no iterations). We have an equation that computes the number of a particular day of week between two dates:
TRUNC(( END – START – DAYOFWEEK(END-DAYOFWEEKTOBECOUNTED) + 8) / 7)
where DAYOFWEEK is a function that returns 0-6 (0 Sunday, 1 Monday ... 6 Saturday). And DAYOFWEEKTOBECOUNTED is the number of the day to be counted in the same format.
Note that TO_CHAR(date, 'd') returns the day of week in 1..7 format we must rectify to 0..6 format (In my region monday is the first day of week, so i get sunday as 0 and saturday as 6 with the mod function as follows):
MOD(TO_NUMBER(TO_CHAR(p_date_end, 'd')), 7)
Finally we want the number of days in the interval minus the number of sundays (day 0) and saturdays (day 6). So the final procedure with the mathematical approach will be:
CREATE OR REPLACE FUNCTION exlude_weekends (p_date_start DATE,
p_date_end DATE)
RETURN NUMBER
AS
l_no_of_days NUMBER := NULL;
BEGIN
SELECT TRUNC(p_date_end - p_date_start) + 1 -
( TRUNC((p_date_end - p_date_start -
MOD(to_number(to_char(p_date_end - 0, 'd')), 7)+8)/7)
+ TRUNC((p_date_end - p_date_start -
MOD(to_number(to_char(p_date_end - 6, 'd')), 7)+8)/7)
)
INTO l_no_of_days
FROM DUAL;
RETURN l_no_of_days;
EXCEPTION
WHEN OTHERS
THEN
RETURN 0;
END exlude_weekends;
I have a query which I run on a table TXN_DEC(id, resourceid, usersid, date, eventdesc) which return distinct count of users for a given date-range and resourceid, group by date and eventdesc (each resource can have 4 to 5 eventdesc)
if there is no value of distinct users count on a date in the range, for an eventdesc, then it skips that date row in the resultset.
I need to have all date rows in my resultset or collection such that if there is no value of count for a date,eventdesc combination, then its value is set to 0 but that date still exists in the collection..
How do I go about getting such a collection
I know getting the final dataset entirely from the query result would be too complicated,
but I can use collections in groovy to modify and populate my map/list to get the data in the required format
something similar to following: if
input date range = 5th Feb to 3 March 2011
DataMap = [dateval: '02/05/2011' eventdesc: 'Read' dist_ucnt: 23,
dateval: '02/06/2011' eventdesc: 'Read' dist_ucnt: 23,
dateval: '02/07/2011' eventdesc: 'Read' dist_ucnt: 0, -> this row was not present in query resultset, but row exists in the map with value 0
....and so on till 3 march 2011 and then whole range repeated for each eventdesc
]
If you want all dates (including those with no entries in your TXN_DEC table) for a given range, you could use Oracle to generate your date range and then use an outer join to your existing query. Then you would just need to fill in null values. Something like:
select
d.dateInRange as dateval,
'Read' as eventdesc,
nvl(td.dist_ucnt, 0) as dist_ucnt
from (
select
to_date('02-FEB-2011','dd-mon-yyyy') + rownum - 1 as dateInRange
from all_objects
where rownum <= to_date('03-MAR-2011','dd-mon-yyyy') - to_date('02-FEB-2011','dd-mon-yyyy') + 1
) d
left join (
select
date,
count(distinct usersid) as dist_ucnt
from
txn_dec
where eventDesc = 'Read'
group by date
) td on td.date = d.dateInRange
That's my purely Oracle solution since I'm not a Groovy guy (well, actually, I am a pretty groovy guy...)
EDIT: Here's the same version wrapped in a stored procedure. It should be easy to call if you know the API....
create or replace procedure getDateRange (
p_begin_date IN DATE,
p_end_date IN DATE,
p_event IN txn_dec.eventDesc%TYPE,
p_recordset OUT SYS_REFCURSOR)
AS
BEGIN
OPEN p_recordset FOR
select
d.dateInRange as dateval,
p_event as eventdesc,
nvl(td.dist_ucnt, 0) as dist_ucnt
from (
select
p_begin_date + rownum - 1 as dateInRange
from all_objects
where rownum <= p_end_date - p_begin_date + 1
) d
left join (
select
date,
count(distinct usersid) as dist_ucnt
from
txn_dec
where eventDesc = p_event
group by date
) td on td.date = d.dateInRange;
END getDateRange;