Oracle Hierarchical Query at depth level - sql

I have a requirement to build a table from a hierarchical table. Table structure as below:
emp_hier table:
emp_id
supervisorId
100
null
1
100
2
1
3
2
New table:
I have to write a select query on the emp_heir table and the selected data should look like this:
sel_emp_id
rel_emp_id
relation
depth_lvl
100
100
self
0
100
1
My Repotee
-1
100
2
My Repotee
-2
100
3
My Repotee
-3
1
100
My Mgr
1
1
1
self
0
1
2
My Repotee
-1
1
3
My Repotee
-2
2
1
My Mgr
1
2
2
self
0
2
3
My Repotee
-1
3
100
My Mgr
3
3
1
My Mgr
2
3
2
My Mgr
1
3
3
self
0

You can use UNION ALL to combine a hierarchical query to get each row and its children to another hierarchical query to get all the ancestors:
SELECT CONNECT_BY_ROOT emp_id AS sel_emp_id,
emp_id AS rel_emp_id,
CASE LEVEL WHEN 1 THEN 'Self' ELSE 'My Reportee' END AS relation,
1 - LEVEL AS depth_lvl
FROM emp_hier
CONNECT BY PRIOR emp_id = supervisorid
UNION ALL
SELECT CONNECT_BY_ROOT emp_id,
emp_id,
'My Mgr',
LEVEL - 1
FROM emp_hier
WHERE LEVEL > 1
CONNECT BY PRIOR supervisorid = emp_id
ORDER BY sel_emp_id, depth_lvl DESC
Which, for your sample data:
CREATE TABLE emp_hier (emp_id, supervisorId) AS
SELECT 100, null FROM DUAL UNION ALL
SELECT 1, 100 FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 3, 2 FROM DUAL;
Outputs:
SEL_EMP_ID
REL_EMP_ID
RELATION
DEPTH_LVL
1
100
My Mgr
1
1
1
Self
0
1
2
My Reportee
-1
1
3
My Reportee
-2
2
100
My Mgr
2
2
1
My Mgr
1
2
2
Self
0
2
3
My Reportee
-1
3
100
My Mgr
3
3
1
My Mgr
2
3
2
My Mgr
1
3
3
Self
0
100
100
Self
0
100
1
My Reportee
-1
100
2
My Reportee
-2
100
3
My Reportee
-3
db<>fiddle here

Using CONNECT BY, you can connect all the employees and their relationships to each other. Then by joining that information together, you can print out the information in the format you desire.
WITH
hier
AS
( SELECT e.*, LEVEL AS lvl
FROM emp_hier e
CONNECT BY PRIOR emp_id = supervisorid
START WITH supervisorid IS NULL)
SELECT h1.emp_id AS sel_emp_id,
h2.emp_id AS rel_emp_id,
CASE
WHEN h1.lvl - h2.lvl = 0 THEN 'self'
WHEN h1.lvl - h2.lvl > 0 THEN 'My Mgr'
ELSE 'My Reportee'
END AS relation,
h1.lvl - h2.lvl AS depth_level
FROM hier h1, hier h2
ORDER BY CASE WHEN h1.supervisorid IS NULL THEN 0 ELSE 1 END, h1.emp_id, h1.lvl - h2.lvl DESC;
SEL_EMP_ID REL_EMP_ID RELATION DEPTH_LEVEL
_____________ _____________ ______________ ______________
100 100 self 0
100 1 My Reportee -1
100 2 My Reportee -2
100 3 My Reportee -3
1 100 My Mgr 1
1 1 self 0
1 2 My Reportee -1
1 3 My Reportee -2
2 100 My Mgr 2
2 1 My Mgr 1
2 2 self 0
2 3 My Reportee -1
3 100 My Mgr 3
3 1 My Mgr 2
3 2 My Mgr 1
3 3 self 0

You can get the entire desired result with a single pass through the hierarchy (a single CONNECT BY query), with no self-join and no union all.
Instead, I use a helper inline view (with just one row and two numeric columns, with the values -1 and 1); since each "relationship" appears exactly twice in the output, with the exception of "Self", I use this to do an ad-hoc duplication of the rows from the hierarchical query.
I used the table in MT0's post for testing. I don't show the result - it's the same (just ordered differently).
with
h (x) as (select 1 from dual union all select -1 from dual)
, p (ancestor, emp, depth) as (
select connect_by_root(emp_id), emp_id, level - 1
from emp_hier
connect by supervisorid = prior emp_id
)
select case h.x when 1 then emp else ancestor end as emp_self,
case h.x when 1 then ancestor else emp end as emp_related,
case when h.x = 1 then 'Mgr'
when p.depth != 0 then 'Reportee'
else 'Self' end as rel,
h.x * p.depth as depth_level
from p join h on h.x = -1 or p.depth != 0 -- do not duplicate "Self"
order by emp_self, depth_level desc, emp_related -- or whatever (if/as needed)
;

Related

Oracle hierarchical queries data

The link gives a good example for overview on how to use Oracle hierarchical queries. I was trying to generate the below combination of data with the example table tab1 given in the link but struck for a while.Using Oracle version 12.2
PARENT_ID CHILD_ID
-------- ---------
1 2 # the root node and id combination (This is evident by using ROOT_ID,ID)
1 3
1 4
. .
. .
2 5 # How to generate below combination in a select statement ont TAB1 table.
2 6
. .
. .
9 11
SELECT id,
parent_id,
RPAD('.', (level-1)*2, '.') || id AS tree,
level,
CONNECT_BY_ROOT id AS root_id,
LTRIM(SYS_CONNECT_BY_PATH(id, '-'), '-') AS path,
CONNECT_BY_ISLEAF AS leaf
FROM tab1
START WITH parent_id IS NULL
CONNECT BY parent_id = PRIOR id
ORDER SIBLINGS BY id;
Output of the above select statement
ID PARENT_ID TREE LEVEL ROOT_ID PATH LEAF
---------- ---------- -------------------- ---------- ---------- -------------------- ----------
1 1 1 1 1 0
2 1 ..2 2 1 1-2 0
3 2 ....3 3 1 1-2-3 1
4 2 ....4 3 1 1-2-4 0
5 4 ......5 4 1 1-2-4-5 1
6 4 ......6 4 1 1-2-4-6 1
7 1 ..7 2 1 1-7 0
8 7 ....8 3 1 1-7-8 1
9 1 ..9 2 1 1-9 0
10 9 ....10 3 1 1-9-10 0
11 10 ......11 4 1 1-9-10-11 1
12 9 ....12 3 1 1-9-12 1
Diagraph:
The sql query to get the desired output are below:
Solution 1: using CTE
with h ( id, parent_id ) as (
select id ,parent_id from tab1
),
r ( id , parent_id, steps ) as (
select id , id , 0
from h
union all
select r.id, h.parent_id, steps + 1
from h join r
on h.id = r.parent_id
)
select parent_id, id
from r
where parent_id != id
order by parent_id asc;
Solution 2: Using Oracle only connect by query. The CONNECT BY NOCYCLE clause can be used to not traverse cyclical hierarchies if any.
with hier_data as
(
select
connect_by_root id as parent_id
,id
from tab1
connect by parent_id = prior id
order by parent_id,id
)
select * from hier_data
where
parent_id != id;

Select top year and term for each student

I want to return each student final semester record from these tables.
Table: dbo.Stdetail
StID YearID TermID
2 1 1
3 1 1
3 2 1
3 2 2
3 3 1
3 3 2
4 1 1
4 1 2
5 1 1
5 1 2
Table: dbo.lastyear
StID YearID TermID
1 5 1
2 5 1
2 6 2
3 5 1
3 6 2
From these two tables I want to return final yearID and term ID.
Desired output:
StID yearID TermID
1 5 1
2 6 2
3 6 2
4 1 2
5 1 2
I think you want to union together dbo.Stdetail and dbo.lastyear and then apply use row_number() to identify the most last record for each student. Like this:
;with cte as (select *
, row_number() over (partition by StID order by YearID desc, TermID desc) rn
from (select StID, YearID, TermID from dbo.Stdetail
union
select StID, YearID, TermID from dbo.lastyear) x
)
select *
from cte
where rn = 1

Conditional transpose columns into rows in oracle sql

I have this row in my table mtinre:
INREPRCO INRESELO INRECAPI INRECFRA INRECAPO
---------- ---------- ---------- ---------- ----------
32.42 1.87 1 5
I build a query to transpose this row as 5 different rows for each column.
SELECT CASE pivot
WHEN 1 THEN 'VAPRS'
WHEN 2 THEN 'VAFRC'
WHEN 3 THEN 'VACTA'
WHEN 4 THEN 'VIMSL'
WHEN 5 THEN 'VINEM'
END
component,
CASE pivot
WHEN 1 THEN inreprco
WHEN 2 THEN inrecfra
WHEN 3 THEN inrecapo
WHEN 4 THEN inreselo
WHEN 5 THEN inreinem
ELSE NULL
END
VALUE,
CASE pivot
WHEN 4
THEN
(NVL (inreprco, 0) + NVL (inrecfra, 0) + NVL (inrecapo, 0))
WHEN 5
THEN
(NVL (inreprco, 0) + NVL (inrecfra, 0) + NVL (inrecapo, 0))
ELSE
NULL
END
AS base
FROM mtinre,
( SELECT ROWNUM pivot
FROM DUAL
CONNECT BY LEVEL <= 5)
The output is:
COMPONENT VALUE BASE
--------- ---------- ----------
VAPRS 32.42
VAFRC
VACTA 5
VIMSL 1.87 37.42
VINEM .94 37.42
But these 5 fields(INREPRCO,INRESELO,INRECAPI,INRECFRA,INRECAPO) can have null or zero(0) values. So I need to select only those how have values.
In the last example, just show me:
COMPONENT VALUE BASE
--------- ---------- ----------
VAPRS 32.42
VACTA 5
VIMSL 1.87 37.42
VINEM .94 37.42
I've tried to put some where conditions, but the connect by level statement creates me always 5 rows.
So, I changed my query and made this:
SELECT *
FROM (SELECT CASE pivot
WHEN 1 THEN 'VAPRS'
WHEN 2 THEN 'VAFRC'
WHEN 3 THEN 'VACTA'
WHEN 4 THEN 'VIMSL'
WHEN 5 THEN 'VINEM'
END
component,
CASE pivot
WHEN 1 THEN inreprco
WHEN 2 THEN inrecfra
WHEN 3 THEN inrecapo
WHEN 4 THEN inreselo
WHEN 5 THEN inreinem
ELSE NULL
END
VALUE,
CASE pivot
WHEN 4
THEN
( NVL (inreprco, 0)
+ NVL (inrecfra, 0)
+ NVL (inrecapo, 0))
WHEN 5
THEN
( NVL (inreprco, 0)
+ NVL (inrecfra, 0)
+ NVL (inrecapo, 0))
ELSE
NULL
END
AS base
FROM mtinre,
( SELECT ROWNUM pivot
FROM DUAL
CONNECT BY LEVEL <= 5))
WHERE VALUE IS NOT NULL
It works, but is there any other way to do that without using a sub select statement?
Any suggestion?
Thanks
Filipe
Using UNPIVOT and a little trick can do the job. Almost all tables have an id column (primary or unique key). Assuming that the table has id_col as id column, this query will do the job
SQL> WITH table_(id_col, inreprco,inreselo,inrecapi,inrecfra,inrecapo) AS
2 (SELECT 1, 32.42,1.87,0.94,NULL,5 FROM dual UNION ALL
3 SELECT 2, 33.43,2.87,0.87,12,9 FROM dual ),
4 ---------
5 -- End of data preparation
6 ---------
7 table2_ AS (SELECT id_col, component, VALUE
8 FROM table_
9 UNPIVOT (VALUE FOR component IN (inreprco AS 'VAPRS', inrecfra AS 'VAFRC', inrecapo AS 'VACTA', inreselo AS 'VIMSL', inrecapi AS 'VINEM')))
10 select a.id_col,
11 a.COMPONENT,
12 a.VALUE,
13 CASE WHEN a.component IN ('VIMSL', 'VINEM') THEN nvl(b.inreprco, 0) + nvl(b.inrecfra, 0) + NVL(b.inrecapo, 0) ELSE NULL END AS base
14 FROM table2_ a
15 INNER JOIN table_ b
16 ON b.id_col = a.id_col;
ID_COL COMPONENT VALUE BASE
---------- --------- ---------- ----------
1 VAPRS 32.42
1 VACTA 5
1 VIMSL 1.87 37.42
1 VINEM 0.94 37.42
2 VAPRS 33.43
2 VAFRC 12
2 VACTA 9
2 VIMSL 2.87 54.43
2 VINEM 0.87 54.43
9 rows selected
But if there are no Id column, then modifying the join as cross join will do but that will return correct result if there is only one row in the table.
SQL> WITH table_(inreprco,inreselo,inrecapi,inrecfra,inrecapo) AS
2 (SELECT 32.42,1.87,0.94,NULL,5 FROM dual),
3 ---------
4 -- End of data preparation
5 ---------
6 table2_ AS (SELECT component, VALUE
7 FROM table_
8 UNPIVOT (VALUE FOR component IN (inreprco AS 'VAPRS', inrecfra AS 'VAFRC', inrecapo AS 'VACTA', inreselo AS 'VIMSL', inrecapi AS 'VINEM')))
9 select a.COMPONENT,
10 a.VALUE,
11 CASE WHEN a.component IN ('VIMSL', 'VINEM') THEN nvl(b.inreprco, 0) + nvl(b.inrecfra, 0) + NVL(b.inrecapo, 0) ELSE NULL END AS base
12 FROM table2_ a
13 CROSS JOIN table_ b
14 /
COMPONENT VALUE BASE
--------- ---------- ----------
VAPRS 32.42
VACTA 5
VIMSL 1.87 37.42
VINEM 0.94 37.42
Or wait for someone who comes with some other approach ;)

How to get this below output from DUAL in oracle?

1 L R
1 1 1
1 1 2
1 1 3
1 2 1
1 2 2
1 2 3
1 3 1
1 3 2
1 3 3
Using this query but not able to get for L column
Select 1,level R
from DUAL
Connect by level <=3
You could do a Cartesian join in the row generator query you have to generate 3 rows. Thus, the Cartesian product would generate total 9 rows.
For example,
SQL> WITH DATA AS
2 ( SELECT LEVEL rn FROM dual CONNECT BY LEVEL <=3
3 )
4 SELECT 1, A.rn L, b.rn R FROM DATA A, DATA b
5 /
1 L R
---------- ---------- ----------
1 1 1
1 1 2
1 1 3
1 2 1
1 2 2
1 2 3
1 3 1
1 3 2
1 3 3
9 rows selected.
SQL>
select 1, L, R
from (Select level R
from DUAL
Connect by level <=3),
(Select level L
from DUAL
Connect by level <=3)
You might try something like this:
SELECT 1, CEIL(lvl/3) AS l
, ROW_NUMBER() OVER ( PARTITION BY CEIL(lvl/3) ORDER BY lvl ) AS r
FROM (
SELECT LEVEL AS lvl FROM dual
CONNECT BY LEVEL <= 9
);
The above avoids a cartesian join. See SQLFiddle here.

T-SQL Reverse Pivot on every character of a string

We have a table like below in an sql server 2005 db:
event_id staff_id weeks
1 1 NNNYYYYNNYYY
1 2 YYYNNNYYYNNN
2 1 YYYYYYYYNYYY
This is from a piece of timetabling software and is basically saying which staff members are assigned to an event (register) and the set of weeks they are teaching that register. So staff_id 1 isn't teaching the first 3 weeks of event 1 but is teaching the following 4....
Is there an easy way to convert that to an easier form such as:
event_id staff_id week
1 1 4
1 1 5
1 1 6
1 1 7
1 1 10
1 1 11
1 1 12
1 2 1
1 2 2
1 2 3
1 2 7
1 2 8
1 2 9
2 1 1
2 1 2
2 1 3
2 1 4
2 1 5
2 1 6
2 1 7
2 1 8
2 1 10
2 1 11
2 1 12
WITH cte AS
(
SELECT 1 AS [week]
UNION ALL
SELECT [week] + 1
FROM cte
WHERE [week] < 53
)
SELECT t.event_id, t.staff_id, cte.[week]
FROM your_table AS t
INNER JOIN cte
ON LEN(ISNULL(t.weeks, '')) >= cte.[week]
AND SUBSTRING(t.weeks, cte.[week], 1) = 'Y'
ORDER BY t.event_id, t.staff_id, cte.[week]