oracle group by when value is zero, aggregate = 0 - sql

i am trying to build a query but it's taking me too much time to resolve it.
Oracle database v18
this is my table1
Date1
tagname
Value
01/01/2021 0:01
a
2
01/01/2021 0:02
a
4
01/01/2021 0:01
b
2
01/01/2021 0:02
b
4
01/01/2021 0:01
c
2
01/01/2021 0:02
c
4
02/01/2021 0:01
a
0
02/01/2021 0:02
a
0
02/01/2021 0:01
b
2
02/01/2021 0:02
b
4
02/01/2021 0:01
c
2
02/01/2021 0:02
c
4
i am doing an average by day
select avg(value) value, tagname, to_date(date1,'dd/MM/yyyy')
from table1
group by date1, tagname
Result:
Date1
tagname
Value
01/01/2021
a
3
01/01/2021
b
3
01/01/2021
c
3
02/01/2021
a
0
02/01/2021
b
3
02/01/2021
c
3
now i need to add a new tagname
select sum(value), 'newtag' tagname
from result
where tagname= 'a' or tagname = 'b' or tagname= 'c'
group by date1
but when a=0 newtag value = 0
How could i archieve this?
example
Date1
tagname
Value
01/01/2021
a
3
01/01/2021
b
3
01/01/2021
c
3
01/01/2021
newtag
9
02/01/2021
a
0
02/01/2021
b
3
02/01/2021
c
3
02/01/2021
newtag
0
could i use case in this query?
thanks in advance
Edit: table1 have more tagnames, but only need to sum(a+b+c)

So, this is easy to do with UNION ALL, of course. I guess your concern is that you do not want to read through your table twice (once to calculate date/tag aggregates and again to calculate date aggregates).
Anytime you want to aggregate query results at multiple levels, you should at least consider GROUPING SETS functionality.
The trick in your case isn't the multiple-level aggregates. Rather, it is that you want the 2nd level aggregate (by date) to be the SUM() of aggregates calculated at the first level (by date/tag).
To do that, you can use a window function to compute the AVG() by date/tag before any aggregates are done at all. That makes it possible to SUM() them later. Here is a working example (Oracle 12.1):
-- Create table with test data
create table my_table1 (Date1, tagname, Value) AS (
SELECT TO_DATE('01/01/2021 0:01','DD/MM/YYYY HH24:MI'), 'a', 2 FROM DUAL UNION ALL
SELECT TO_DATE('01/01/2021 0:02','DD/MM/YYYY HH24:MI'), 'a', 4 FROM DUAL UNION ALL
SELECT TO_DATE('01/01/2021 0:01','DD/MM/YYYY HH24:MI'), 'b', 2 FROM DUAL UNION ALL
SELECT TO_DATE('01/01/2021 0:02','DD/MM/YYYY HH24:MI'), 'b', 4 FROM DUAL UNION ALL
SELECT TO_DATE('01/01/2021 0:01','DD/MM/YYYY HH24:MI'), 'c', 2 FROM DUAL UNION ALL
SELECT TO_DATE('01/01/2021 0:02','DD/MM/YYYY HH24:MI'), 'c', 4 FROM DUAL UNION ALL
SELECT TO_DATE('02/01/2021 0:01','DD/MM/YYYY HH24:MI'), 'a', 0 FROM DUAL UNION ALL
SELECT TO_DATE('02/01/2021 0:02','DD/MM/YYYY HH24:MI'), 'a', 0 FROM DUAL UNION ALL
SELECT TO_DATE('02/01/2021 0:01','DD/MM/YYYY HH24:MI'), 'b', 2 FROM DUAL UNION ALL
SELECT TO_DATE('02/01/2021 0:02','DD/MM/YYYY HH24:MI'), 'b', 4 FROM DUAL UNION ALL
SELECT TO_DATE('02/01/2021 0:01','DD/MM/YYYY HH24:MI'), 'c', 2 FROM DUAL UNION ALL
SELECT TO_DATE('02/01/2021 0:02','DD/MM/YYYY HH24:MI'), 'c', 4 FROM DUAL
)
;
-- Compute the averages and the use GROUPING SETS to use those those
-- averages conditionally at multiple levels of aggregation
with date_tag_summary as (
select trunc(date1) date1, tagname, avg(value) avg_value
from my_table1
group by trunc(date1), tagname )
select date1,
case when grouping(tagname)=1 then 'newtag' ELSE tagname END tagname,
case when grouping(tagname)=1 AND COUNT(DECODE(avg_value,0,1,NULL)) > 0 THEN 0
when grouping(tagname)=1 THEN sum(avg_value)
ELSE min(avg_value) END value
from date_tag_summary
group by grouping sets ( (date1, tagname), (date1) )
order by 1,2;
+-----------+---------+-------+
| DATE1 | TAGNAME | VALUE |
+-----------+---------+-------+
| 01-JAN-21 | a | 3 |
| 01-JAN-21 | b | 3 |
| 01-JAN-21 | c | 3 |
| 01-JAN-21 | newtag | 9 |
| 02-JAN-21 | a | 0 |
| 02-JAN-21 | b | 3 |
| 02-JAN-21 | c | 3 |
| 02-JAN-21 | newtag | 0 |
+-----------+---------+-------+
And, to illustrate that the data is not being read twice, here is the execution plan for that query:
-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 6 (100)| |
| 1 | SORT ORDER BY | | 3 | 63 | 6 (50)| 00:00:01 |
| 2 | SORT GROUP BY ROLLUP| | 3 | 63 | 6 (50)| 00:00:01 |
| 3 | VIEW | | 9 | 189 | 4 (25)| 00:00:01 |
| 4 | HASH GROUP BY | | 9 | 117 | 4 (25)| 00:00:01 |
| 5 | TABLE ACCESS FULL| MY_TABLE1 | 12 | 156 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------------

One method generates the rows using a cross join and then brings in the existing results:
select d.date1, t.tagname, avg(value) value
from (select distinct to_date(date1, 'dd/MM/yyyy') as date1 from table1
) d cross join
(select 'a' as tagname from dual union all
select 'b' as tagname from dual union all
select 'c' as tagname from dual union all
select 'd' as tagname from dual
) t
table1 t1
on to_date(t1.date1, 'dd/MM/yyyy') = d.date1 and
t1.tagname = t.tagname
group by date1, tagname

You may use grouping sets and then replace avg total for group with sum of averages, calculated by analytic function.
select /*+ gather_plan_statistics */
trunc(date1) as dt
, case grouping_id(tagname)
when 0
then tagname
else 'newtag'
end as tagname
, case grouping_id(tagname)
when 0
then avg(value)
else (
/*Total sum except total avg*/
sum(avg(value)) over(
partition by trunc(date1)
) - avg(value))
* decode(min(avg(value)) over(partition by trunc(date1)), 0, 0, 1)
end as val
from a
group by grouping sets( (trunc(date1), tagname), trunc(date1))
DT | TAGNAME | VAL
:-------- | :------ | --:
01-JAN-21 | a | 3
01-JAN-21 | b | 3
01-JAN-21 | c | 3
01-JAN-21 | newtag | 9
02-JAN-21 | a | 0
02-JAN-21 | b | 3
02-JAN-21 | c | 3
02-JAN-21 | newtag | 0
db<>fiddle here

you can use following query. Of course it is set in SQL
;WITH cte AS
(SELECT convert(date,date1) as date1,tagname,avg(value) value
FROM table1
GROUP BY convert(date,date1),tagname)
select date1,tagname,
case when tagname = 'newtag'
then
case (select cte.value from cte where cte.date1 = result.date1 and cte.tagname = 'a')
when 0 then 0
else (select top 1 sum(c.value) from cte c where convert(date,c.date1,103) = result.date1)
end
else value end
from
(select date1,tagname,value ,ROW_NUMBER() over(partition by date1,tagname order by date1) as seq
from
(
select convert(date,date1) as date1,tagname,avg(value) as value
from table1
group by convert(date,date1),tagname
union all
select convert(date,date1),'newtag', 0
from table1
group by convert(date,date1),tagname
) T
) result
where result.seq = 1
order by convert(date,date1)

first average by day
with avgday as (select avg(value) value, tagname, to_date(date1,'dd/MM/yyyy')
from table1 group by date1, tagname)
transform row's to columns and doing a case to filter and operate.
with query1 as (SELECT * FROM avgday PIVOT ( MAX(value) FOR tagname IN ('a','b','c')))
select date1, case
when query1.a=0
then 0
else a + b + c end value,
'newtag' tagname
from query1
I finally came up with a solution, sure it's not the best answer, but it solves my problem

Related

Sorting the result based on 2 columns Oracle

I have two columns Date and Number with null values in date. Can I sort with date and number col together checking if date is null then sort with number.
dt num
3/20/2022 1
3/16/2022 3
3/17/2022 4
3/18/2022 5
NULL 6
NULL 7
3/19/2022 8
*Expected Output*
dt num
3/16/2022 3
3/17/2022 4
3/18/2022 5
NULL 6
NULL 7
3/19/2022 8
3/20/2022 1
We need to sort by the date if there is one and if there is not we search the previous row where the date is not null.
This does mean that we are running a sub-query per line so it will be slow for large queries.
create table d(
dt date,
num int);
insert into d (dt, num)
select to_date('2022-03-20','YYYY-MM-DD'),1 from dual union all
select to_date('2022-03-16','YYYY-MM-DD'),3 from dual union all
select to_date ('2022-03-17','YYYY-MM-DD'), 4 from dual union all
select to_date('2022-03-17','YYYY-MM-DD'),5 from dual union all
select to_date('2022-03-18','YYYY-MM-DD'),6 from dual union all
select to_date('2022-03-16','YYYY-MM-DD'),10 from dual union all
select to_date('2022-03-19','YYYY-MM-DD'),9 from dual;
insert into d ( num)
select 7 from dual union all
select 8 from dual ;
select
dt,
num,
( select dt
from d
where num <= d1.num and dt is not null
order by num desc
fetch next 1 rows only
) as dt_plus
from d d1
order by dt_plus,num;
DT | NUM | DT_PLUS
:-------- | --: | :--------
16-MAR-22 | 3 | 16-MAR-22
16-MAR-22 | 10 | 16-MAR-22
17-MAR-22 | 4 | 17-MAR-22
17-MAR-22 | 5 | 17-MAR-22
18-MAR-22 | 6 | 18-MAR-22
null | 7 | 18-MAR-22
null | 8 | 18-MAR-22
19-MAR-22 | 9 | 19-MAR-22
20-MAR-22 | 1 | 20-MAR-22
db<>fiddle here

How can I write a SQL query to map rows into columns?

Consider the following query from a database,
SELECT propertyname,propertyvalue,value,
FROM db
which returns
| propertyname | propertyvalue | value |
+--------------------+----------------+----------+
| AnimalNumber | 1 | 1.3 |
| Group | 1 | 1.3 |
| TimePoint | 24 days | 1.3 |
| Treatment method | vehicle | 1.3 |
| Treatment Conc | 0 | 1.3 |
| AnimalNumber | 2 | 0.5 |
| Group | 3 | 0.5 |
| TimePoint | 7 days | 0.5 |
| Treatment method | vehicle | 0.5 |
| Treatment Conc | 0 | 0.5 |
We can see it's the case that multiple rows map to the same 'value' datapoint. Really, it should be the case that each of those 5 properties (animal number, group, time point, treatment method, treatment conc) should be columns and the first 5 data points should be condensed into a single row with a value=1.3. In other words, it should be something like
| AnimalNumber | Group | TimePoint | Treatment method | Treatment Conc | value
+--------------+-------+-----------+------------------+-----------------+------+
| 1 | 1 | 24 days | vehicle | 0 | 1.3 |
| 2 | 3 | 7 days | vehicle | 0 | 0.5 |
Also note that it is not necessarily the case that there is a 1-1 mapping to value, multiple combinations of animal number, group, timepoint, treatment method, and treatment conc can map to to the same value. So grouping by value I do not believe is the proper approach. Also note that all data provided here is, of course, fabricated and not real.
You can use PIVOT:
SELECT *
FROM table_name
PIVOT(
MAX(propertyvalue)
FOR propertyname IN (
'AnimalNumber' AS animalnumber,
'Group' AS "GROUP",
'TimePoint' AS TimePoint,
'Treatment method' AS TreatementMethod,
'Treatment Conc' AS TreatmentConc
)
)
Which, for your sample data:
CREATE TABLE table_name ( propertyname, propertyvalue, value ) AS
SELECT 'AnimalNumber', '1', 1.3 FROM DUAL UNION ALL
SELECT 'Group', '1', 1.3 FROM DUAL UNION ALL
SELECT 'TimePoint', '24 days', 1.3 FROM DUAL UNION ALL
SELECT 'Treatment method', 'vehicle', 1.3 FROM DUAL UNION ALL
SELECT 'Treatment Conc', '0', 1.3 FROM DUAL UNION ALL
SELECT 'AnimalNumber', '2', 0.5 FROM DUAL UNION ALL
SELECT 'Group', '3', 0.5 FROM DUAL UNION ALL
SELECT 'TimePoint', '7 days', 0.5 FROM DUAL UNION ALL
SELECT 'Treatment method', 'vehicle', 0.5 FROM DUAL UNION ALL
SELECT 'Treatment Conc', '0', 0.5 FROM DUAL;
Outputs:
VALUE
ANIMALNUMBER
GROUP
TIMEPOINT
TREATEMENTMETHOD
TREATMENTCONC
0.5
2
3
7 days
vehicle
0
1.3
1
1
24 days
vehicle
0
db<>fiddle here
one way is to group by value :
select max(propertyvalue) filter(where propertyname='AnimalNumber') AnimalNumber
, max(propertyvalue) filter(where propertyname='TimePoint') TimePoint
, max(propertyvalue) filter(where propertyname='Treatment method') "Treatment method"
, max(propertyvalue) filter(where propertyname='Treatment Conc') "Treatment Conc"
, max(propertyvalue) filter(where propertyname='Group') "Group"
from tablename
group by value

Filtering a table via another table's values

I have 2 tables:
Value
+----+-------+
| id | name |
+----+-------+
| 1 | Peter |
| 2 | Jane |
| 3 | Joe |
+----+-------+
Filter
+----+---------+------+
| id | valueid | type |
+----+---------+------+
| 1 | 1 | A |
| 2 | 1 | B |
| 3 | 1 | C |
| 4 | 1 | D |
| 5 | 2 | A |
| 6 | 2 | C |
| 7 | 2 | E |
| 8 | 3 | A |
| 9 | 3 | D |
+----+---------+------+
I need to retrieve the values from the Value table where the related Filter table does not contain the type 'B' or 'C'
So in this quick example this would be only Joe.
Please note this is a DB2 DB and i have limited permissions to run selects only.
Or also a NOT IN (<*fullselect*) predicate:
Only that my result is 'Joe', not 'Jane' - and the data constellation would point to that ...
WITH
-- your input, sans reserved words
val(id,nam) AS (
SELECT 1,'Peter' FROM sysibm.sysdummy1
UNION ALL SELECT 2,'Jane' FROM sysibm.sysdummy1
UNION ALL SELECT 3,'Joe' FROM sysibm.sysdummy1
)
,
filtr(id,valueid,typ) AS (
SELECT 1,1,'A' FROM sysibm.sysdummy1
UNION ALL SELECT 2,1,'B' FROM sysibm.sysdummy1
UNION ALL SELECT 3,1,'C' FROM sysibm.sysdummy1
UNION ALL SELECT 4,1,'D' FROM sysibm.sysdummy1
UNION ALL SELECT 5,2,'A' FROM sysibm.sysdummy1
UNION ALL SELECT 6,2,'C' FROM sysibm.sysdummy1
UNION ALL SELECT 7,2,'E' FROM sysibm.sysdummy1
UNION ALL SELECT 8,3,'A' FROM sysibm.sysdummy1
UNION ALL SELECT 9,3,'D' FROM sysibm.sysdummy1
)
-- real query starts here
SELECT
*
FROM val
WHERE id NOT IN (
SELECT valueid FROM filtr WHERE typ IN ('B','C')
)
;
-- out id | nam
-- out ----+-------
-- out 3 | Joe
Or also, a failing left join:
SELECT
val.*
FROM val
LEFT JOIN (
SELECT valueid FROM filtr WHERE typ IN ('B','C')
) filtr
ON filtr.valueid = val.id
WHERE valueid IS NULL
You can use EXISTS, as in:
select *
from value v
where not exists (
select null from filter f
where f.valueid = v.id and f.type in ('B', 'C')
);
Result:
ID NAME
--- -----
3 Joe
See running example at db<>fiddle.

How collpase all subgroups into one line and keep the same order

This is a simplified version of my table
+----+----------+------------+------------+
| ID | Category | Start Date | End Date |
+----+----------+------------+------------+
| 1 | 'Alpha' | 2018/04/12 | 2018/04/15 |
| 2 | null | 2018/04/17 | 2018/04/21 |
| 3 | 'Gamma' | 2018/05/02 | 2018/05/07 |
| 4 | 'Gamma' | 2018/05/09 | 2018/05/11 |
| 5 | 'Gamma' | 2018/05/11 | 2018/05/17 |
| 6 | 'Alpha' | 2018/05/17 | 2018/05/23 |
| 7 | 'Alpha' | 2018/05/23 | 2018/05/24 |
| 8 | null | 2018/05/24 | 2018/06/02 |
| 9 | 'Beta' | 2018/06/12 | 2018/06/16 |
| 10 | 'Beta' | 2018/06/16 | 2018/06/20 |
+----+----------+------------+------------+
All Start Date are unique, not nullable and they have the same order as the IDs (if a and b are IDs and a < b then StartDate[a] < StartDate[b]). The Start Date is not always equal to the End Date of the previous row for the same Category (look at id 3 and 4).
I'm looking for a query that will give me the following result
+----------+------------+------------+
| Category | Start Date | End Date |
+----------+------------+------------+
| 'Alpha' | 2018/04/12 | 2018/04/15 |
| null | 2018/04/17 | 2018/04/21 |
| 'Gamma' | 2018/05/02 | 2018/05/17 |
| 'Alpha' | 2018/05/17 | 2018/05/24 |
| null | 2018/05/24 | 2018/06/02 |
| 'Beta' | 2018/06/12 | 2018/06/20 |
+----------+------------+------------+
Note: The End Date will be equal to End Date of the last row in the subgroup (same continuous Category).
This is a gaps-and-islands problem. I think you can use the difference of row numbers:
select category, min(startdate), max(enddate)
from (select t.*,
row_number() over (order by id) as seqnum,
row_number() over (partition by category order by id) as seqnum_c
from t
) t
group by category, (seqnum - seqnum_c)
order by min(startdate);
This is a gaps and islands question, you can use such a logic below
select category, min(start_date) as start_date, max(end_date) as end_date
from
(
select tt.*, sum(grp) over (order by id, start_date) sm
from
(
with t( ID, Category, Start_Date, End_Date) as
(
select 1 , 'Alpha' , date'2018-04-12',date'2018-04-15' from dual union all
select 2 , null , date'2018-04-17',date'2018-04-21' from dual union all
select 3 , 'Gamma' , date'2018-05-02',date'2018-05-07' from dual union all
select 4 , 'Gamma' , date'2018-05-09',date'2018-05-11' from dual union all
select 5 , 'Gamma' , date'2018-05-11',date'2018-05-17' from dual union all
select 6 , 'Alpha' , date'2018-05-17',date'2018-05-23' from dual union all
select 7 , 'Alpha' , date'2018-05-23',date'2018-05-24' from dual union all
select 8 , null , date'2018-05-24',date'2018-06-02' from dual union all
select 9 , 'Beta' , date'2018-06-12',date'2018-06-16' from dual union all
select 10 , 'Beta' , date'2018-06-16',date'2018-06-20' from dual
)
select id, Category,
decode(nvl(lag(end_date) over
(order by end_date),start_date),start_date,0,1)
as grp, --> means prev. value equals or not
row_number() over (order by id, end_date) as rn, start_date, end_date
from t
) tt
order by rn
)
group by Category, sm
order by end_date;
CATEGORY START_DATE END_DATE
Alpha 12.04.2018 15.04.2018
NULL 17.04.2018 21.04.2018
Gamma 02.05.2018 07.05.2018
Gamma 09.05.2018 17.05.2018
Alpha 17.05.2018 24.05.2018
NULL 24.05.2018 02.06.2018
Beta 12.06.2018 20.06.2018

Oracle query for selecting sampling of number values from a table

I have a data field in my Oracle DB Table whose datatype is NUMBER. I have tried a query below using order by.
SELECT Value
FROM Table
ORDER BY value;
I am getting the result as
Value |
------|
1 |
1 |
2 |
2 |
3 |
3 |
4 |
4 |
5 |
5 |
6 |
6 |
Instead I want a result as
Value |
------|
1 |
2 |
3 |
4 |
5 |
6 |
1 |
2 |
3 |
4 |
5 |
6 |
You can use the row_number to evaluate if an occurrence of a value is the first one, the second, and so on; an order by based on this value and then for the value in the table will do the work.
For example:
/* a test case */
with someTable(value) as (
select 1 from dual union all
select 2 from dual union all
select 3 from dual union all
select 4 from dual union all
select 5 from dual union all
select 6 from dual union all
select 1 from dual union all
select 2 from dual union all
select 3 from dual union all
select 4 from dual union all
select 5 from dual union all
select 6 from dual
)
/* the query */
select value
from someTable
order by row_number() over ( partition by value order by null), value
How it works:
select value, row_number() over ( partition by value order by null) rowNumber
from someTable
order by row_number() over ( partition by value order by null), value
gives:
VALUE ROWNUMBER
---------- ----------
1 1
2 1
3 1
4 1
5 1
6 1
1 2
2 2
3 2
4 2
5 2
6 2
Please try this. I'm using ROW_NUMBER() to arrange the values based on their occurrences,
SELECT VALUE
FROM (
SELECT VALUE
, ROW_NUMBER() OVER (PARTITION BY VALUE ORDER BY VALUE ASC) RNK
FROM MY_TABLE
)
ORDER BY RNK
, VALUE;
1 Value | ------| 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 | 6 |
SELECT ROW_NUMBER() OVER (PARTITION BY VALUE ORDER BY VALUE) AS RN, TABLE .* FROM TABLE
2 Value | ------| 1 | 2 | 3 | 4 | 5 | 6 | 1 | 2 | 3 | 4 | 5 | 6 |
SELECT ROWNUM,TABLE.* FROM TABLE