Sorting the result based on 2 columns Oracle - sql

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

Related

oracle group by when value is zero, aggregate = 0

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

Can Hierarchical queries be used in a condition?

I am going to build a Hierarchial Tree in Oracle Forms using the Hiearchical queries. As far as I know, the HQ works only for single table, can they be used in a condition where we have Master Details scenario.
Or can it be based on a view?
For example; I want to display an hierarchical with 3 of options from various tables. For example a Customer Order can have have various Items. First List of All CO, on clicking a specific CO, All the Items therein, An Item may have Sub Parts, and so on. Spanning multiple connected tables
If you have the test data:
CREATE TABLE customer_orders ( id, name ) AS
SELECT 1, 'Cust. Ord. XYZ' FrOM DUAL UNION ALL
SELECT 2, 'Cust. Ord. ABC' FrOM DUAL UNION ALL
SELECT 3, 'Cust. Ord. MNO' FrOM DUAL;
CREATE TABLE customer_order_items ( id, order_id, name ) AS
SELECT 1, 3, 'MNO_NNN' FROM DUAL UNION ALL
SELECT 2, 3, 'MNO_OOO' FROM DUAL UNION ALL
SELECT 3, 3, 'MNO_MMM' FROM DUAL UNION ALL
SELECT 4, 2, 'ABC_AAA' FROM DUAL;
CREATE TABLE customer_order_item_parts ( id, item_id, name ) AS
SELECT 1, 1, 'Part_2_N' FROM DUAL UNION ALL
SELECT 2, 1, 'Part_1_N' FROM DUAL UNION ALL
SELECT 3, 2, 'Part_1_O' FROM DUAL UNION ALL
SELECT 4, 2, 'Part_3_O' FROM DUAL UNION ALL
SELECT 5, 4, 'Part_5_A' FROM DUAL UNION ALL
SELECT 6, 4, 'Part_3_A' FROM DUAL UNION ALL
SELECT 7, 4, 'Part_2_A' FROM DUAL UNION ALL
SELECT 8, 4, 'Part_4_A' FROM DUAL;
Then you can use UNION ALL (and JOINs as required) to concatenate the data and once it is all compiled then you can apply a hierarchical query:
SELECT *
FROM (
SELECT id AS order_id,
NULL AS item_id,
NULL AS part_id,
name
FROM customer_orders
UNION ALL
SELECT order_id,
id,
NULL,
name
FROM customer_order_items
UNION ALL
SELECT i.order_id,
p.item_id,
p.id,
p.name
FROM customer_order_items i
INNER JOIN
customer_order_item_parts p
ON ( i.id = p.item_id )
)
START WITH item_id IS NULL
CONNECT BY NOCYCLE
PRIOR order_id = order_id
AND ( ( PRIOR item_id IS NULL
AND part_id IS NULL )
OR ( PRIOR part_id IS NULL
AND part_id IS NOT NULL
AND PRIOR item_id = item_id )
)
ORDER SIBLINGS BY name
Which outputs:
ORDER_ID | ITEM_ID | PART_ID | NAME
-------: | ------: | ------: | :-------------
2 | null | null | Cust. Ord. ABC
2 | 4 | null | ABC_AAA
2 | 4 | 7 | Part_2_A
2 | 4 | 6 | Part_3_A
2 | 4 | 8 | Part_4_A
2 | 4 | 5 | Part_5_A
3 | null | null | Cust. Ord. MNO
3 | 3 | null | MNO_MMM
3 | 1 | null | MNO_NNN
3 | 1 | 2 | Part_1_N
3 | 1 | 1 | Part_2_N
3 | 2 | null | MNO_OOO
3 | 2 | 3 | Part_1_O
3 | 2 | 4 | Part_3_O
1 | null | null | Cust. Ord. XYZ
db<>fiddle here

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

Fetching one to many relationship from same table

I have to pull one to many relationship data from the same table.
The structure of the table is like
____________________________________
| CUSTOMER_ID | SUBSCRIPTION_NAME |
-------------------------------------
| 1 | ABC |
| 1 | TNT |
| 1 | AMC |
| 2 | ABC |
| 2 | USA |
| 3 | TNT |
| 3 | AMC |
-------------------------------------
I would like to get the output like
| CUSTOMER_ID | SUBSCRIPTION_NAME |
| 1 | ABC,TNT,AMC |
| 2 | ABC,USA |
| 3 | TNT,AMC |
SELECT customer_id,
LISTAGG(subscription_name, '; ') WITHIN GROUP (ORDER BY subscription_name) AS subscription_name
FROM subscription
GROUP BY customer_id
ORDER BY customer_id;
Order by is optional
Just few alternatives to LISTAGG:
SQL> with t (CUSTOMER_ID, SUBSCRIPTION_NAME)
2 as (
3 select 1, 'ABC' from dual union all
4 select 1, 'TNT' from dual union all
5 select 1, 'AMC' from dual union all
6 select 2, 'ABC' from dual union all
7 select 2, 'USA' from dual union all
8 select 3, 'TNT' from dual union all
9 select 3, 'AMC' from dual
10 )
11 SELECT CUSTOMER_ID,
12 RTRIM(
13 XMLAGG (XMLELEMENT(e, SUBSCRIPTION_NAME||',') ORDER BY SUBSCRIPTION_NAME).EXTRACT('//text()')
14 ,',') AS x
15 FROM t
16 GROUP BY CUSTOMER_ID
17 /
CUSTOMER_ID X
----------- ----------------------------------------
1 ABC,AMC,TNT
2 ABC,USA
3 AMC,TNT
SQL> with t (CUSTOMER_ID, SUBSCRIPTION_NAME)
2 as (
3 select 1, 'ABC' from dual union all
4 select 1, 'TNT' from dual union all
5 select 1, 'AMC' from dual union all
6 select 2, 'ABC' from dual union all
7 select 2, 'USA' from dual union all
8 select 3, 'TNT' from dual union all
9 select 3, 'AMC' from dual
10 )
11 select customer_id,
12 max(substr(sys_connect_by_path( SUBSCRIPTION_NAME, ','),2)) x from
13 (
14 select customer_id, SUBSCRIPTION_NAME, row_number() over(partition by customer_id order by null) rn
15 from t
16 )
17 start with rn=1
18 connect by prior customer_id = customer_id and rn = prior rn+1
19 group by customer_id
20 /
CUSTOMER_ID X
----------- ----------------------------------------
1 ABC,TNT,AMC
2 ABC,USA
3 TNT,AMC

Select a row X times

I have a very specific sql problem.
I have a table given with order positions (each position belongs to one order, but this isn't a problem):
| Article ID | Amount |
|--------------|----------|
| 5 | 3 |
| 12 | 4 |
For the customer, I need an export with every physical item that is ordered, e.g.
| Article ID | Position |
|--------------|------------|
| 5 | 1 |
| 5 | 2 |
| 5 | 3 |
| 12 | 1 |
| 12 | 2 |
| 12 | 3 |
| 12 | 4 |
How can I build my select statement to give me this results? I think there are two key tasks:
1) Select a row X times based on the amount
2) Set the position for each physical article
You can do it like this
SELECT ArticleID, n.n Position
FROM table1 t JOIN
(
SELECT a.N + b.N * 10 + 1 n
FROM
(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b
) n
ON n.n <= t.amount
ORDER BY ArticleID, Position
Note: subquery n generates a sequence of numbers on the fly from 1 to 100. If you do a lot of such queries you may consider to create persisted tally(numbers) table and use it instead.
Here is SQLFiddle demo
or using a recursive CTE
WITH tally AS (
SELECT 1 n
UNION ALL
SELECT n + 1 FROM tally WHERE n < 100
)
SELECT ArticleID, n.n Position
FROM table1 t JOIN tally n
ON n.n <= t.amount
ORDER BY ArticleID, Position
Here is SQLFiddle demo
Output in both cases:
| ARTICLEID | POSITION |
|-----------|----------|
| 5 | 1 |
| 5 | 2 |
| 5 | 3 |
| 12 | 1 |
| 12 | 2 |
| 12 | 3 |
| 12 | 4 |
Query:
SQLFIDDLEExample
SELECT t1.[Article ID],
t2.number
FROM Table1 t1,
master..spt_values t2
WHERE t1.Amount >= t2.number
AND t2.type = 'P'
AND t2.number <= 255
AND t2.number <> 0
Result:
| ARTICLE ID | NUMBER |
|------------|--------|
| 5 | 1 |
| 5 | 2 |
| 5 | 3 |
| 12 | 1 |
| 12 | 2 |
| 12 | 3 |
| 12 | 4 |