How to get parent child with auto skip in sql oracle - sql

I have a table in SQL Oracle with some steps.
step
auto_skip
0
10
20
skip
21
skip
30
I want to get a table with child and parent. The parent is the next step. It is pretty easy to do with a lead:
select step as child,LEAD (step,1) OVER (ORDER BY step) AS parent from STEPS
but this is the tricky part.
When there is "skip" value in the auto_skip column, a second child/parent has to been made. Then we had to take the next step without a skip value.
so the result of my child/parent has to be for this example:
child
parent
0
10
10
20
20
21
21
30
30
10
30
Added this to be clearer: The skip can be sometimes different. When the same skip is added, the next step is till the step has different skip or no skip.
For example:
step
auto_skip
0
10
20
skip1
21
skip1
22
skip2
30
should become
child
parent
0
10
10
20
10
22
21
22
21
30
22
30
20
21
30

How about this (according to your sample data & result). if your data is very large, there maybe better solutions out there. Also you did not specify if 30 is also a parent of 20:
with steps as (
select 0 step, cast(null as varchar2(10)) auto_skip from dual
union ALL
select 10,null from dual
union all
select 20, 'skip' from dual
union all
select 21,'skip' from dual
union all
select 30 ,null from dual
)
select step as child,LEAD (step,1) OVER (ORDER BY step) AS parent from STEPS
union
select step as child,LEAD (step,1) OVER (ORDER BY step) AS parent from STEPS where auto_skip is null;
CHILD PARENT 
----- ------ 
    0     10 
   10     20 
   20     21 
   21     30 
   30        
   10     30 

Try this solution, working by UNION the 2 different ways to calculate the fathers, the second one works by recursion and must make a distinction between several skips or only one. It works on the test data but should be checked on more data.
with data(step,auto_skip) as (
select 0, null from dual union all
select 10, null from dual union all
select 20, 'skip1' from dual union all
select 21, 'skip1' from dual union all
select 22, 'skip2' from dual union all
select 30, null from dual -- union all
),
pdata as (
select d.* from (
select d.step, d.auto_skip,
lead(d.step,1) over(order by d.step) as natural_father,
lead(d.auto_skip,1) over(order by d.step) as natural_father_skip
from data d
) d
),
rdata as (
select level as lvl, connect_by_isleaf as isleaf, connect_by_root(step) as rstep, d.*
from pdata d
start with natural_father_skip <> nvl(auto_skip,'*')
connect by
prior natural_father = step and prior natural_father_skip = auto_skip
)
select * from (
select step as child, natural_father as parent from pdata
union all
select rstep, case when lvl > 2 then step else natural_father end from rdata
where isleaf = 1
)
order by child, parent
;
0 10
10 20
10 22
20 21
21 22
21 30
22 30
30

Related

Oracle SQL generate random output with listaggs

I have the values 30, 31, 32 and 33
How could I generate a output (random) which may contain single values of that list, listaggs of them or nulls?
id
val
1
30, 31
2
null
3
32
4
33
5
null
6
31, 33
7
null
8
30
Here is a way - generating the strings quasi-randomly (using ora_hash to do the trick), while in a perfectly deterministic, reproducible way. If you want to get different (but similar) results, use the third argument to ora_hash to provide a seed different from the default (which is 0). If you want different results every time, provide a dbms_random.value() value as the seed; this will still require just one "random" value to be generated for the entire query. You can also play with the upper bound (in my example, 280) to get more or fewer null (and shorter vs. longer comma-separated strings, more generally).
WITH data ( value ) AS (
SELECT 30 FROM DUAL UNION ALL
SELECT 31 FROM DUAL UNION ALL
SELECT 32 FROM DUAL UNION ALL
SELECT 33 FROM DUAL
),
ids ( id ) AS (
SELECT LEVEL
FROM DUAL
CONNECT BY LEVEL <= 8
)
select id,
( select listagg(case when ora_hash(id * value, 1000) < 280
then value end, ',')
within group(order by value)
from data
) as vals
from ids
;
ID VALS
-- ---------------
1 33
2 32
3
4 30,32
5 30,31
6 32
7
8
You can do it but you need to add some seemingly irrelevant filters to force the SQL optimiser to not materialize the sub-query and to ensure the values are randomly generated for each row:
WITH data ( value ) AS (
SELECT 30 FROM DUAL UNION ALL
SELECT 31 FROM DUAL UNION ALL
SELECT 32 FROM DUAL UNION ALL
SELECT 33 FROM DUAL
),
ids ( id ) AS (
SELECT LEVEL
FROM DUAL
CONNECT BY LEVEL <= 8
)
SELECT id,
(
SELECT LISTAGG( value, ',' ) WITHIN GROUP ( ORDER BY value )
FROM (
SELECT value,
DBMS_RANDOM.VALUE() AS rnd
FROM data
WHERE ROWNUM > 0 -- force a new random on each row.
)
WHERE rnd < 0.5
AND id > 0 -- force the query to correlate with the outer query.
) AS vals
FROM ids;
Which may output:
ID
VALS
1
33
2
30,31,32
3
31,32
4
<null>
5
30,32
6
31,33
7
30,31,32,33
8
30,31,32,33
db<>fiddle here

Flag individuals that share common features with Oracle SQL

Consider the following table:
ID Feature
1 1
1 2
1 3
2 3
2 4
2 6
3 5
3 10
3 12
4 12
4 18
5 10
5 30
I would like to group the individuals based on overlapping features. If two of these groups again have overlapping features, I would consider both as one group. This process should be repeated until there are no overlapping features between groups. The result of this procedure on the table above would be:
ID Feature Flag
1 1 A
1 2 A
1 3 A
2 3 A
2 4 A
2 6 A
3 5 B
3 10 B
3 12 B
4 12 B
4 18 B
5 10 B
5 30 B
So actually the problem I am trying to solve is finding connected components in a graph. Here [1,2,3] is the graph with ID 1 (see https://en.wikipedia.org/wiki/Connectivity_(graph_theory)). The problem is equivalent to this problem, however I would like to solve it with Oracle SQL.
Here is one way to do this, using a hierarchical ("connect by") query. The first step is to extract the initial relationships from the base data; the hierarchical query is built on the result from this first step. I added one more row to the inputs to illustrate a node that is a connected component by itself.
You marked the connected components as A and B - of course, that won't work if you have, say, 30,000 connected components. In my solution, I use the minimum node name as the marker for each connected component.
with
sample_data (id, feature) as (
select 1, 1 from dual union all
select 1, 2 from dual union all
select 1, 3 from dual union all
select 2, 3 from dual union all
select 2, 4 from dual union all
select 2, 6 from dual union all
select 3, 5 from dual union all
select 3, 10 from dual union all
select 3, 12 from dual union all
select 4, 12 from dual union all
select 4, 18 from dual union all
select 5, 10 from dual union all
select 5, 30 from dual union all
select 6, 40 from dual
)
-- select * from sample_data; /*
, initial_rel(id_base, id_linked) as (
select distinct s1.id, s2.id
from sample_data s1 join sample_data s2
on s1.feature = s2.feature and s1.id <= s2.id
)
-- select * from initial_rel; /*
select id_linked as id, min(connect_by_root(id_base)) as id_group
from initial_rel
start with id_base <= id_linked
connect by nocycle prior id_linked = id_base and id_base < id_linked
group by id_linked
order by id_group, id
;
Output:
ID ID_GROUP
------- ----------
1 1
2 1
3 3
4 3
5 3
6 6
Then, if you need to add the ID_GROUP as a FLAG to the base data, you can do so with a trivial join.

Want a SQL to group values by value scope in oracle

I have a table t1 and there is a column named days, so I want to group the days by 1 to 5 days and 6-15 days and more than 15 days then calculate the count for each group, but I don't know how to write the sql, can anyone tell me? the result should be looked like below:
Number Scope(days)
5 1-5
7 6-15
10 15+
If I understand well, this could be a way:
with t1(days) as (
select 1 from dual union all
select 2 from dual union all
select 5 from dual union all
select 6 from dual union all
select 7 from dual union all
select 8 from dual union all
select 10 from dual union all
select 11 from dual union all
select 16 from dual union all
select 17 from dual union all
select 19 from dual union all
select 19 from dual
)
/* the query */
select count(*),
case
when days between 1 and 5 then '1-5'
when days between 6 and 15 then '6-15'
else '+15'
end
from t1
group by case
when days between 1 and 5 then '1-5'
when days between 6 and 15 then '6-15'
else '+15'
end
that gives:
COUNT(*) CASE
---------- ----
3 1-5
4 +15
5 6-15
The idea is to aggregate by something that say the "group" where every number is, and you can easily build such an information with a CASE.
Accordin to Jarlh's suggestion, this can be re-written as
select count(*), the_group
from (
select case
when days between 1 and 5 then '1-5'
when days between 6 and 15 then '6-15'
else '+15'
end the_group
from t1
)
group by the_group
This obviously assumes that you only have positive numbers.

How can I find unoccupied id numbers in a table?

In my table I want to see a list of unoccupied id numbers in a certain range.
For example there are 10 records in my table with id's: "2,3,4,5,10,12,16,18,21,22" and say that I want to see available ones between 1 and 25. So I want to see a list like:
1,6,7,89,11,13,14,15,17,19,20,23,24,25
How should I write my sql query?
Select the numbers form 1 to 25 and show only those that are not in your table
select n from
( select rownum n from dual connect by level <= 25)
where n not in (select id from table);
Let's say you a #numbers table with three numbers -
CREATE TABLE #numbers (num INT)
INSERT INTO #numbers (num)
SELECT 1
UNION
SELECT 3
UNION
SELECT 6
Now, you can use CTE to generate numbers recursively from 1-25 and deselect those which are in your #numbers table in the WHERE clause -
;WITH n(n) AS
(
SELECT 1
UNION ALL
SELECT n+1 FROM n WHERE n < 25
)
SELECT n FROM n
WHERE n NOT IN (select num from #numbers)
ORDER BY n
OPTION (MAXRECURSION 25);
You can try using the "NOT IN" clause:
select
u1.user_id + 1 as start
from users as u1
left outer join users as u2 on u1.user_id + 1 = u2.id
where
u2.id is null
see also SQL query to find Missing sequence numbers
You need LISTAGG to get the output in a single row.
SQL> WITH DATA1 AS(
2 SELECT LEVEL rn FROM dual CONNECT BY LEVEL <=25
3 ),
4 data2 AS(
5 SELECT 2 num FROM dual UNION ALL
6 SELECT 3 FROM dual UNION ALL
7 SELECT 4 from dual union all
8 SELECT 5 FROM dual UNION ALL
9 SELECT 10 FROM dual UNION ALL
10 SELECT 12 from dual union all
11 SELECT 16 from dual union all
12 SELECT 18 FROM dual UNION ALL
13 SELECT 21 FROM dual UNION ALL
14 SELECT 22 FROM dual)
15 SELECT listagg(rn, ',')
16 WITHIN GROUP (ORDER BY rn) num_list FROM data1
17 WHERE rn NOT IN(SELECT num FROM data2)
18 /
NUM_LIST
----------------------------------------------------
1,6,7,8,9,11,13,14,15,17,19,20,23,24,25
SQL>

Correlated row-generating query in Oracle

Given this starting CTE:
WITH Sections AS (
SELECT 1 Section, 1 StartUnit, 5 EndUnit FROM DUAL
UNION ALL SELECT 2, 0, 2 FROM DUAL
UNION ALL SELECT 3, 1, 1 FROM DUAL
),
How do I generate a result set that has as many rows per row in Section as there are numbers between StartUnit and EndUnit (inclusive), with values ascending?
That is, I'd like to see a result set of:
Section Unit
1 1
1 2
1 3
1 4
1 5
2 0
2 1
2 2
3 1
Note that some of the values in the Sections CTE will be parameters, so it's not as simple as extending my UNIONs to the right number.
UPDATE
I've thought about this a little more and have another guideline. I'll take any answer that's correct, but was particularly hoping for someone to possibly show how to do this with CONNECT BY PRIOR and without an extra CTE in the middle...
I realized I could change the CTE to this:
WITH Sections AS (
SELECT 1 Section, LEVEL Unit FROM DUAL CONNECT BY LEVEL <= 5
UNION ALL SELECT 2, LEVEL - 1 FROM DUAL CONNECT BY LEVEL <= 3
UNION ALL SELECT 3, 1 FROM DUAL CONNECT BY LEVEL <= 1
)
But I'm leaning away from that here because it may come from a table rather than be selected from DUAL. So let's assume the Sections CTE is in fact a simple query from a table, something like:
SELECT Section, StartUnit, EndUnit FROM SectionData WHERE CallerID = 7
And the original question still stands.
Try this:
WITH Sections AS (
SELECT 1 Section, 1 StartUnit, 5 EndUnit FROM DUAL
UNION ALL SELECT 2, 0, 2 FROM DUAL
UNION ALL SELECT 3, 1, 1 FROM DUAL
),
Numbers AS (
SELECT ROWNUM-1 n
FROM DUAL
CONNECT BY LEVEL < 1000
)
select section, n
from sections, numbers
where n between startunit and endunit
order by section, n;
You may want to adjust the value of 1000 I used.