Fetch recursive tree with only certain elements "expanded" - oracle

We have a table with a self-referencing tree structure (id, parent_id). Let's assume the following tree structure:
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
I'd like to fetch this data for displaying it in a tree. But only certain records expanded. I'm currently using the following query:
SELECT ID, NAME "PATH"
FROM GROUPS
WHERE PRIOR ID IN(1, 4)
CONNECT BY PARENT_ID = PRIOR ID
START WITH PARENT_ID IS NULL;
This works very well and returns the following records:
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
The problem is that this query returns every record of which the direct parent is expanded, but not the whole parent chain. So if we're just expanding id 4, records 5, 6, 7 still shouldn't be returned as 1 is not expanded.
What I have been trying so far is to fetch a custom column which indicates whether the element is expanded, which computes out of whether it is explicitly expanded AND the parent is expanded as well.
SELECT ...
CASE WHEN (ID IN (4) AND PRIOR EXPANDED = 1) THEN 1 ELSE 0 end "EXPANDED"
...
WHERE "EXPANDED" = 1
This does not work as I can use the EXPANDED alias neither in the WHERE statement nor the PRIOR EXPANDED statement.
Is there a simple way of achieving this using a simple query?

Oracle Setup:
CREATE TABLE hierarchy ( id, parent_id ) AS
SELECT 1, NULL FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 3, 2 FROM DUAL UNION ALL
SELECT 4, 1 FROM DUAL UNION ALL
SELECT 5, 4 FROM DUAL UNION ALL
SELECT 6, 5 FROM DUAL UNION ALL
SELECT 7, NULL FROM DUAL UNION ALL
SELECT 8, 7 FROM DUAL UNION ALL
SELECT 9, 8 FROM DUAL UNION ALL
SELECT 10, 9 FROM DUAL UNION ALL
SELECT 11, 8 FROM DUAL;
Query - IN clause has all parents explanded:
SELECT LPAD( '+ ', LEVEL*2, ' ' ) || id
FROM hierarchy
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id
AND parent_id IN ( 1, 2, 4, 5, 7, 8, 9 );
Output:
+ 1
+ 2
+ 3
+4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
Query - IN clause has all parents expanded except 4 and 8:
SELECT LPAD( '+ ', LEVEL*2, ' ' ) || id
FROM hierarchy
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id
AND parent_id IN ( 1, 2, 5, 7, 9 );
Output:
+ 1
+ 2
+ 3
+4
+ 7
+ 8
Update - Showing leaf nodes:
SELECT LPAD( '+ ', LEVEL*2, ' ' ) || id AS value,
isleaf
FROM (
-- Find the leaves first (as if all parents are expanded)
SELECT h.*,
CONNECT_BY_ISLEAF AS isLeaf
FROM hierarchy h
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id
)
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id
AND parent_id IN ( 1, 2, 4, 7, 9 );
Output:
VALUE ISLEAF
---------------- ----------
+ 1 0
+ 2 0
+ 3 1
+ 4 0
+ 5 0
+ 7 0
+ 8 0
1 Indicates that the node has no children and 0 indicates that the node has children (even though they might not be expanded).

OK, just saw your note about requiring the entire parent chain to be expanded. The following does this by using sys_connect_by_path to build that chain, then checking that it is all 1's (have to lop off the last node which is at the current level):
With 1 and 4 expanded you get:
WITH hier as (
SELECT 1 id, NULL parent_id, 1 expanded FROM DUAL UNION ALL
SELECT 2, 1, 0 FROM DUAL UNION ALL
SELECT 3, 2, 0 FROM DUAL UNION ALL
SELECT 4, 1, 1 FROM DUAL UNION ALL
SELECT 5, 4, 0 FROM DUAL UNION ALL
SELECT 6, 4, 0 FROM DUAL UNION ALL
SELECT 7, 4, 0 FROM DUAL UNION ALL
SELECT 8, 1, 0 FROM DUAL UNION ALL
SELECT 9, 1, 0 FROM DUAL UNION ALL
SELECT 10, 9, 0 FROM DUAL UNION ALL
SELECT 11, 9, 0 FROM DUAL )
SELECT LPAD( '+ ', lvl*2, ' ' ) || id
FROM (
SELECT ID
, parent_id
, level as lvl
, sys_connect_by_path(expanded,'-') as path_expanded
FROM hier
CONNECT BY PARENT_ID = PRIOR ID
START WITH PARENT_ID IS NULL
)
WHERE --every node in the path from the parent is expanded.
instr(substr(path_expanded,1,length(path_expanded)-2),'0') = 0
OR parent_id is null ;
LPAD('+',LVL*2,'')||ID
+ 1
+ 2
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
Change to un-expand at node 1 and you get:
WITH hier as (
SELECT 1 id, NULL parent_id, 0 expanded FROM DUAL UNION ALL
SELECT 2, 1, 0 FROM DUAL UNION ALL
SELECT 3, 2, 0 FROM DUAL UNION ALL
SELECT 4, 1, 1 FROM DUAL UNION ALL
SELECT 5, 4, 0 FROM DUAL UNION ALL
SELECT 6, 4, 0 FROM DUAL UNION ALL
SELECT 7, 4, 0 FROM DUAL UNION ALL
SELECT 8, 1, 0 FROM DUAL UNION ALL
SELECT 9, 1, 0 FROM DUAL UNION ALL
SELECT 10, 9, 0 FROM DUAL UNION ALL
SELECT 11, 9, 0 FROM DUAL )
SELECT LPAD( '+ ', lvl*2, ' ' ) || id
FROM (
SELECT ID
, parent_id
, level as lvl
, sys_connect_by_path(expanded,'-') as path_expanded
FROM hier
CONNECT BY PARENT_ID = PRIOR ID
START WITH PARENT_ID IS NULL
)
WHERE --every node in the path from the parent is expanded.
instr(substr(path_expanded,1,length(path_expanded)-2),'0') = 0
OR parent_id is null ;
LPAD('+',LVL*2,'')||ID
+ 1

Related

Oracle transform table column to two columns

Let say I have the next table:
ID_1
ID_2
Value
1
11
A
2
12
A
2
13
A
2
13
B
3
12
A
3
13
B
I want to transform it to:
ID_1
ID_2
Value_A
Value_B
1
11
1
0
2
12
1
0
2
13
1
1
3
12
1
0
3
13
0
1
When there is a value A but no Value B, so put 0 in Value B
When there is a value B but no Value A, so put 0 in Value A
How can I do it in oracle?
Use a PIVOT:
SELECT *
FROM table_name
PIVOT ( COUNT(*) FOR value IN ('A' AS Value_A, 'B' AS Value_B) )
Which, for the sample data:
CREATE TABLE table_name (ID_1, ID_2, Value) AS
SELECT 1, 11, 'A' FROM DUAL UNION ALL
SELECT 2, 12, 'A' FROM DUAL UNION ALL
SELECT 2, 13, 'A' FROM DUAL UNION ALL
SELECT 2, 13, 'B' FROM DUAL UNION ALL
SELECT 3, 12, 'A' FROM DUAL UNION ALL
SELECT 3, 13, 'B' FROM DUAL;
Outputs:
ID_1
ID_2
VALUE_A
VALUE_B
1
11
1
0
3
12
1
0
2
12
1
0
3
13
0
1
2
13
1
1
fiddle
You could use below solution to get the job done
select ID_1, ID_2
, decode(Value_A, null, 0, 1) Value_A
, decode(Value_B, null, 0, 1) Value_B
from your_Table t
pivot (
max(Value) for value in (
'A' as Value_A
, 'B' as Value_B
)
)
order by ID_1, ID_2
;
demo

Multiple joins display student course name

I have the following setup, which is working perfectly. I am difficulty figuring out the syntax how to display the course name in the output. In my test CASE all the rows should have the value Geometry.
In addition, how could I use rank or rank_dense to limit the output to display only 1 row with the highest average?
CREATE TABLE students(student_id, first_name, last_name) AS
SELECT 1, 'Faith', 'Aaron' FROM dual UNION ALL
SELECT 2, 'Lisa', 'Saladino' FROM dual UNION ALL
SELECT 3, 'Leslee', 'Altman' FROM dual UNION ALL
SELECT 4, 'Patty', 'Kern' FROM dual UNION ALL
SELECT 5, 'Betty', 'Bowers' FROM dual;
CREATE TABLE courses(course_id, course_name) AS
SELECT 1, 'Geometry' FROM dual UNION ALL
SELECT 2, 'Trigonometry' FROM dual UNION ALL
SELECT 3, 'Calculus' FROM DUAL;
CREATE TABLE grades(student_id,
course_id, grade) AS
SELECT 1, 1, 75 FROM dual UNION ALL
SELECT 1, 1, 81 FROM dual UNION ALL
SELECT 1, 1, 76 FROM dual UNION ALL
SELECT 2, 1, 100 FROM dual UNION ALL
SELECT 2, 1, 95 FROM dual UNION ALL
SELECT 2, 1, 96 FROM dual UNION ALL
SELECT 3, 1, 80 FROM dual UNION ALL
SELECT 3, 1, 85 FROM dual UNION ALL
SELECT 3, 1, 86 FROM dual UNION ALL
SELECT 4, 1, 88 FROM dual UNION ALL
SELECT 4, 1, 85 FROM dual UNION ALL
SELECT 4, 1, 91 FROM dual UNION ALL
SELECT 5, 1, 98 FROM dual UNION ALL
SELECT 5, 1, 74 FROM dual UNION ALL
SELECT 5, 1, 81 FROM dual;
/* average grade of each student */
select s.student_id
, s.first_name
, s.last_name
, round(avg(g.grade), 1) as student_avg
from students s
join grades g
on s.student_id = g.student_id
group by s.student_id, s.first_name, s.last_name
ORDER BY avg(g.grade) DESC;
Something like this?
SQL> with temp as
2 (select s.student_id
3 , s.first_name
4 , s.last_name
5 , c.course_name
6 , round(avg(g.grade), 1) as student_avg
7 , rank() over (order by avg(g.grade) desc) rnk
8 from students s join grades g on s.student_id = g.student_id
9 join courses c on c.course_id = g.course_id
10 group by s.student_id, s.first_name, s.last_name, c.course_name
11 )
12 select student_id, first_name, last_name, course_name, student_avg
13 from temp
14 where rnk <= 3
15 order by rnk;
STUDENT_ID FIRST_ LAST_NAM COURSE_NAME STUDENT_AVG
---------- ------ -------- ------------ -----------
2 Lisa Saladino Geometry 97
4 Patty Kern Geometry 88
5 Betty Bowers Geometry 84.3
SQL>

Direct-Path INSERT query generates ORA-00918 error

can you please explain why the error ORA-00918 is generated while executing this query
INSERT INTO CLG_TEST_2 (CLG_TEST_2.record_id, CLG_TEST_2.chain_id,
CLG_TEST_2.chain_n,
CLG_TEST_2.contact_info)
select * from (
SELECT 1, 1, 0, '2222' from dual UNION ALL
SELECT 2, 2, 0, '4444' from dual UNION ALL
SELECT 3, 3, 0, '6666' from dual
)
Error at line 1
ORA-00918: column ambiguously defined
Script Terminated on line 2.
The issue is in the fact that you are using a select * over a query without giving aliases to the columns; this will work:
INSERT INTO CLG_TEST_2 (CLG_TEST_2.record_id,
CLG_TEST_2.chain_id,
CLG_TEST_2.chain_n,
CLG_TEST_2.contact_info)
select *
from (
SELECT 1 a, 1 b, 0 c, '2222' d from dual UNION ALL
SELECT 2 , 2 , 0 , '4444' from dual UNION ALL
SELECT 3 , 3 , 0 , '6666' from dual
)
However, you can simplify your code:
INSERT INTO CLG_TEST_2 (record_id, chain_id, chain_n, contact_info)
SELECT 1, 1, 0, '2222' from dual UNION ALL
SELECT 2, 2, 0, '4444' from dual UNION ALL
SELECT 3, 3, 0, '6666' from dual
Something more about the reason of the error.
Your code:
SQL> INSERT INTO CLG_TEST_2 (
2 CLG_TEST_2.record_id,
3 CLG_TEST_2.chain_id,
4 CLG_TEST_2.chain_n,
5 CLG_TEST_2.contact_info)
6 select * from (
7 SELECT 1, 1, 0, '2222' from dual UNION ALL
8 SELECT 2, 2, 0, '4444' from dual UNION ALL
9 SELECT 3, 3, 0, '6666' from dual
10 );
select * from (
*
ERROR at line 6:
ORA-00918: column ambiguously defined
Slightly different:
SQL> INSERT INTO CLG_TEST_2 (
2 CLG_TEST_2.record_id,
3 CLG_TEST_2.chain_id,
4 CLG_TEST_2.chain_n,
5 CLG_TEST_2.contact_info)
6 select * from (
7 SELECT 1, 2, 0, '2222' from dual UNION ALL
8 SELECT 2, 2, 0, '4444' from dual UNION ALL
9 SELECT 3, 3, 0, '6666' from dual
10 );
3 rows created.
What's different?
In the first row, I changed
SELECT 1, 1, 0, '2222' --> SELECT 1, 2, 0, '2222'
^ ^
The reason:
SQL> SELECT 1, 2, 0, '2222' from dual UNION ALL
2 SELECT 2, 2, 0, '4444' from dual UNION ALL
3 SELECT 3, 3, 0, '6666' from dual;
1 2 0 '222
---------- ---------- ---------- ----
1 2 0 2222
2 2 0 4444
3 3 0 6666
SQL> SELECT 1, 1, 0, '2222' from dual UNION ALL
2 SELECT 2, 2, 0, '4444' from dual UNION ALL
3 SELECT 3, 3, 0, '6666' from dual;
1 1 0 '222
---------- ---------- ---------- ----
1 1 0 2222
2 2 0 4444
3 3 0 6666
SQL>
Here you have two columns with the same alias '1', and this is confusing for the external select *.
Also, a direct-path insert is something different
I don't see any "Direct-Path" insert. Anyway, try this one
INSERT INTO CLG_TEST_2 (CLG_TEST_2.record_id, CLG_TEST_2.chain_id,
CLG_TEST_2.chain_n,
CLG_TEST_2.contact_info)
SELECT 1, 1, 0, '2222' from dual UNION ALL
SELECT 2, 2, 0, '4444' from dual UNION ALL
SELECT 3, 3, 0, '6666' from dual
btw, why do you use string from numbers?

transpose row into a single column with pivot or decode in dual table - Oracle

If i give this query,
Select 1,2,3,4,5,6,7,8,9 from dual;
It will look like this
1 2 3 4 5 6 7 8 9 - column names
1 2 3 4 5 6 7 8 9 - Associated values
But i want to show like this
1
2
3
4
5
6
7
8
9
I don't know how to do this with dual table
"Unpivot version":
select val from (select 1, 2, 3, 4, 5, 6, 7, 8, 9 from dual)
unpivot (val for tmp in ("1", "2", "3", "4", "5", "6", "7", "8", "9"))
Simpler alternative giving the same results:
select * from table(sys.odcinumberlist(1, 2, 3, 4, 5, 6, 7, 8, 9))
Just use union all:
select 1 as name from dual union all
select 2 as name from dual union all
select 3 as name from dual union all
select 4 as name from dual union all
select 5 as name from dual union all
select 6 as name from dual union all
select 7 as name from dual union all
select 8 as name from dual union all
select 9 as name from dual;

Faster way to calculate percentage?

I currently use this method to come up with a percentage:
declare #height table
(
UserId int,
tall bit
)
insert into #height
select 1, 1 union all
select 2, 1 union all
select 6, 0 union all
select 3, 0 union all
select 7, 0 union all
select 4, 1 union all
select 8, 0 union all
select 5, 0
declare #all decimal(8,5)
select
#all = count(distinct UserId)
from #height
select
count(distinct UserId) / #all Pct
from #height
where tall = 1
Result: 0.375000000
Is there a better performing way to do this? As you can see the #height
table is hit twice.
Thanks!
This allows you to hit the table only once, and gives you the same result for your given dataset.
declare #height table
(
UserId int,
tall bit
)
insert into #height
select 1, 1 union all
select 2, 1 union all
select 6, 0 union all
select 3, 0 union all
select 7, 0 union all
select 4, 1 union all
select 8, 0 union all
select 5, 0
select SUM(convert(decimal(8,5), tall)) / convert(decimal(8,5), COUNT(*)) Pct
from #height
Depending on your requirements, this might work for duplicate userids. At least it gives the same result as yours does.
select SUM(convert(decimal(8,5), tall)) / convert(decimal(8,5), COUNT(distinct userid)) Pct
from
(select distinct UserId, tall
from #height) t
Here is an alternative query that produces your expected results. I don't know how the performance of this query compares to others, but I suspect it would be easy enough for you to test this.
declare #height table
(
UserId int,
tall bit
)
insert into #height
select 1, 1 union all
select 2, 1 union all
select 4, 1 union all
select 3, 0 union all
select 5, 0 union all
select 6, 0 union all
select 7, 0 union all
select 8, 0
Select 1.0 * Count(Distinct Case When Tall = 1 Then UserId End)
/ Count(Distinct UserId)
From #height

Resources