Rows to Columns - Oracle - Not using Union ALL - sql

Is there any better way achieve the below result other than using union all?
The table has millions of records, so looking for a better option where the result set is fetched once.
create table test_tab (
rec_id number(3),
p_code varchar2(5),
q_code varchar2(5),
r_code varchar2(5),
p_amt number(8),
q_amt number(8),
r_amt number(8)
);
delete from test_tab;
insert into test_tab (rec_id, p_code, q_code,r_code , p_amt,q_amt,r_amt)
values (1, 'p1','q1','r1',18,9,9);
insert into test_tab (rec_id, p_code, q_code,r_code , p_amt,q_amt,r_amt)
values (2, 'p2','q2','r2',28,6,4);
insert into test_tab (rec_id, p_code, q_code,r_code , p_amt,q_amt,r_amt)
values (3, 'p1',null,null,18,null,null);
insert into test_tab (rec_id, p_code, q_code,r_code , p_amt,q_amt,r_amt)
values (4, null,'q3','r3',null,9,9);
commit;
select rec_id, p_code,p_amt from test_tab where p_code is not null
union all
select rec_id, q_code,q_amt from test_tab where q_code is not null
union all
select rec_id, r_code,r_amt from test_tab where r_code is not null;
Result:
REC_ID
P_CODE
P_AMT
1
q1
9
1
p1
18
1
r1
9
2
p2
28
2
q2
6
2
r2
4
3
p1
18
4
q3
9
4
r3
9

This is a basic application of the unpivot operator, available since Oracle 11.1.
select rec_id, code, amt
from test_tab
unpivot ((code, amt) for ord in
((p_code, p_amt) as 1, (q_code, q_amt) as 2, (r_code, r_amt) as 3))
order by rec_id, ord -- if needed
;
REC_ID CODE AMT
---------- ----- ----------
1 p1 18
1 q1 9
1 r1 9
2 p2 28
2 q2 6
2 r2 4
3 p1 18
4 q3 9
4 r3 9
9 rows selected.
Notice a few things. I call the output columns code and amt - it makes no sense to have the prefix p_ in the output column names. Also, "exclude nulls" is the default in unpivot, so I didn't need to mention it explicitly (although it wouldn't hurt anything). Finally, while perhaps not critical, I also created a column ord to reflect column order, and ordered the rows in the output in the same order as you had the columns in the input.

You can use Hiearchy query and cross join as follows:
select * from (select rec_id,
case lvl when 1 then p_code when 2 then q_code else r_code end as p_code,
case lvl when 1 then p_amt when 2 then q_amt else r_amt end as p_amount
from test_tab
cross join (select level as lvl from dual connect by level <= 3) )
where p_code is not null

You can use a lateral join:
select x.*
from test_tab t cross join lateral
(select t.rec_id, t.p_code as code, t.p_amount as amount from dual union all
select t.rec_id, t.q_code, t.q_amount from dual union all
select t.rec_id, t.r_code, t.r_amount from dual
) x
where code is not null;
Technical, this still has a union all, but it is only scanning the original table once.

Related

How to insert multiple values in table using subquery in Oracle?

I have created two new tables TYPE and OPERATION-
TYPE table has the following columns
TYPE
(TYPE_ID : integer, with sequence
TYPE_NAME : String
BASE_ID :
)
The default values for TYPE_NAME are (Type1, Type2, Unknown) which needs to be added for every BASE_ID which is 68 rows in the BASE table
So there need to be 3 entries for every BASE_ID in the TYPE table
OPERATION table has the following columns
OPERATION
(OPERATION_ID : integer, with sequence
OPERATION_NAME : String
BASE_ID :
)
The default values for OPERATION_NAME are (Operation1, Operation2, Unknown) which needs to be added for every BASE_ID which is 68 rows in the BASE table
So there need to be 3 entries for every BASE_ID in the OPERATION table
Can someone please help me with a query so that I can perform this process of multiple entry with ease.
Thank you in advance.
Had to individually add query like this
INSERT INTO TYPE(TYPE_ID, TYPE_NAME, BASE_ID) (
select TYPE_SEQ.nextval, 'Type 1', ID_BASE FROM BASE);
INSERT INTO TYPE(TYPE_ID, TYPE_NAME, BASE_ID) (
select TYPE_SEQ.nextval, 'Type 2', ID_BASE FROM BASE);
INSERT INTO TYPE(TYPE_ID, TYPE_NAME, BASE_ID) (
select TYPE_SEQ.nextval, 'Unknown', ID_BASE FROM BASE);
CTE with cross join might help. Here's how.
Creating tables and a sequence first; my base table contains only 3 rows (as opposed to your 68 rows; just for simplicity. You don't have to create the base table as you already have it):
SQL> create table base as
2 select 1 base_id from dual union all
3 select 2 base_id from dual union all
4 select 3 base_id from dual;
Table created.
SQL> create sequence seqankit;
Sequence created.
SQL> create table type (type_id number, type_name varchar2(20), base_id number);
Table created.
SQL> create table operation (operation_id number, operation_name varchar2(20), base_id number);
Table created.
SQL>
Inserting into type and operation tables:
SQL> insert into type (type_id, type_name, base_id)
2 with names (type_name) as
3 (select 'type1' from dual union all
4 select 'type2' from dual union all
5 select 'unknown' from dual
6 )
7 select seqankit.nextval, n.type_name, b.base_id
8 from names n cross join base b;
9 rows created.
SQL> insert into operation (operation_id, operation_name, base_id)
2 with names (operation_name) as
3 (select 'operation1' from dual union all
4 select 'operation2' from dual union all
5 select 'unknown' from dual
6 )
7 select seqankit.nextval, n.operation_name, b.base_id
8 from names n cross join base b;
9 rows created.
SQL>
What's the result?
SQL> select * from type order by base_id, type_name;
TYPE_ID TYPE_NAME BASE_ID
---------- -------------------- ----------
1 type1 1
4 type2 1
7 unknown 1
2 type1 2
5 type2 2
8 unknown 2
3 type1 3
6 type2 3
9 unknown 3
9 rows selected.
SQL> select * from operation order by base_id, operation_name;
OPERATION_ID OPERATION_NAME BASE_ID
------------ -------------------- ----------
10 operation1 1
13 operation2 1
16 unknown 1
11 operation1 2
14 operation2 2
17 unknown 2
12 operation1 3
15 operation2 3
18 unknown 3
9 rows selected.
SQL>
Looks OK to me.
You can create a trigger that inserts the value for column TYPE_ID.
create trigger TYPE_INS
before insert on TYPE
for each row
when (new.TYPE_ID is null)
begin
:new.TYPE_ID := TYPE_SEQ.nextval;
end;
Then you don't need the TYPE_ID column in the INSERT statement, which allows you to do...
insert into TYPE (TYPE_NAME, BASE_ID)
select 'Type1', BASE_ID
from BASE
union
select 'Type2', BASE_ID
from BASE
union
select 'Unknown', BASE_ID
from BASE

oracle lookup translation for a column value

I have a table with data below
create table A
(id number,
code varchar2(100));
insert into A values
(1, '20,21,22');
insert into A values
(2, '22,23,24');
commit;
create table code_descriptions
(
code_id number,
code_desc varchar2(100));
insert into code_descriptions values
(20, 'ABC');
insert into code_descriptions values
(21, 'BBC');
insert into code_descriptions values
(22, 'CAP');
insert into code_descriptions values
(23, 'INC');
insert into code_descriptions values
(24, 'ABC');
commit;
I would like to come up with sql to update table A code values with descriptions
Could you please suggest any functions or ideas, to achieve this?
You can tokenize your comma-separated lists:
select id, level as rn, regexp_substr(code, '(.*?)(,|$)', 1, level, null, 1) as code_id
from a
connect by level <= regexp_count(code, ',') + 1
and id = prior id
and prior dbms_random.value is not null;
ID RN CODE_ID
---------- ---------- -------
1 1 20
1 2 21
1 3 22
2 1 22
2 2 23
2 3 24
Then use that as a CTE (subquery factoring) and join to the descriptions (I've made it a left-join so you choose how to handle any missing codes):
with cte (id, rn, code_id) as (
select id, level, regexp_substr(code, '(.*?)(,|$)', 1, level, null, 1)
from a
connect by level <= regexp_count(code, ',') + 1
and id = prior id
and prior dbms_random.value is not null
)
select cte.id, cte.rn, cte.code_id, coalesce(cd.code_desc, '??') as code_desc
from cte
left join code_descriptions cd on cd.code_id = cte.code_id;
ID RN CODE_ID CODE_DESC
---------- ---------- ------- ---------
1 1 20 ABC
1 2 21 BBC
2 1 22 CAP
1 3 22 CAP
2 2 23 INC
2 3 24 ABC
and then aggregate those back into single comma-separated values:
with cte (id, rn, code_id) as (
select id, level, regexp_substr(code, '(.*?)(,|$)', 1, level, null, 1)
from a
connect by level <= regexp_count(code, ',') + 1
and id = prior id
and prior dbms_random.value is not null
)
select cte.id,
listagg(coalesce(cd.code_desc, '??'), ',') within group (order by rn) as code
from cte
left join code_descriptions cd on cd.code_id = cte.code_id
group by cte.id;
ID CODE
---------- ----------------
1 ABC,BBC,CAP
2 CAP,INC,ABC
and then merge or us a correlated update:
update a
set code = (
with cte (id, rn, code_id) as (
select id, level, regexp_substr(code, '(.*?)(,|$)', 1, level, null, 1)
from a
connect by level <= regexp_count(code, ',') + 1
and id = prior id
and prior dbms_random.value is not null
)
select listagg(coalesce(cd.code_desc, '??'), ',') within group (order by rn) as code
from cte
left join code_descriptions cd on cd.code_id = cte.code_id
where cte.id = a.id
group by cte.id
);
2 rows updated.
select * from a;
ID CODE
---------- ----------------
1 ABC,BBC,CAP
2 CAP,INC,ABC
or
rollback; -- just to undo previous update
merge into a
using (
with cte (id, rn, code_id) as (
select id, level, regexp_substr(code, '(.*?)(,|$)', 1, level, null, 1)
from a
connect by level <= regexp_count(code, ',') + 1
and id = prior id
and prior dbms_random.value is not null
)
select cte.id, listagg(coalesce(cd.code_desc, '??'), ',') within group (order by rn) as code
from cte
left join code_descriptions cd on cd.code_id = cte.code_id
group by cte.id
) tmp
on (tmp.id = a.id)
when matched then update set a.code = tmp.code;
2 rows merged.
select * from a;
ID CODE
---------- ----------------
1 ABC,BBC,CAP
2 CAP,INC,ABC
db<>fiddle
The simplest solution is if the number of elements in the "code" column of table A is fixed - by directly gluing the subqueries to the code_descriptions table by codes (we cut each code from the enumeration line).

Select where cumulative sum is less than a number (in order of priority)

I have a table with id, cost, and priority columns:
create table a_test_table (id number(4,0), cost number(15,2), priority number(4,0));
insert into a_test_table (id, cost, priority) values (1, 1000000, 10);
insert into a_test_table (id, cost, priority) values (2, 10000000, 9);
insert into a_test_table (id, cost, priority) values (3, 5000000, 8);
insert into a_test_table (id, cost, priority) values (4, 19000000, 7);
insert into a_test_table (id, cost, priority) values (5, 20000000, 6);
insert into a_test_table (id, cost, priority) values (6, 15000000, 5);
insert into a_test_table (id, cost, priority) values (7, 2000000, 4);
insert into a_test_table (id, cost, priority) values (8, 3000000, 3);
insert into a_test_table (id, cost, priority) values (9, 3000000, 2);
insert into a_test_table (id, cost, priority) values (10, 8000000, 1);
commit;
select
id,
to_char(cost, '$999,999,999') as cost,
priority
from
a_test_table;
ID COST PRIORITY
---------- ------------- ----------
1 $1,000,000 10
2 $10,000,000 9
3 $5,000,000 8
4 $19,000,000 7
5 $20,000,000 6
6 $15,000,000 5
7 $2,000,000 4
8 $3,000,000 3
9 $3,000,000 2
10 $8,000,000 1
Starting with the highest priority (descending), I want to select the rows where the cost adds up to less than (or equal to) $20,000,000.
The result would look like this:
ID COST PRIORITY
---------- ------------- ----------
1 $1,000,000 10
2 $10,000,000 9
3 $5,000,000 8
7 $2,000,000 4
Total: $18,000,000
How can I do this with Oracle SQL?
Here is a way to do it in pure SQL. I won't swear there isn't a better way.
Basically, it uses a recursive common table expression (i.e., WITH costed...) to
compute every possible combination of elements totaling less than 20,000,000.
Then it gets the first full path from that result.
Then, it gets all the rows in that path.
NOTE: the logic assumes that no id is longer than 5 digits. That's the LPAD(id,5,'0') stuff.
WITH costed (id, cost, priority, running_cost, path) as
( SELECT id, cost, priority, cost running_cost, lpad(id,5,'0') path
FROM a_test_table
WHERE cost <= 20000000
UNION ALL
SELECT a.id, a.cost, a.priority, a.cost + costed.running_Cost, costed.path || '|' || lpad(a.id,5,'0')
FROM costed, a_test_table a
WHERE a.priority < costed.priority
AND a.cost + costed.running_cost <= 20000000),
best_path as (
SELECT *
FROM costed c
where not exists ( SELECT 'longer path' FROM costed c2 WHERE c2.path like c.path || '|%' )
order by path
fetch first 1 row only )
SELECT att.*
FROM best_path cross join a_test_table att
WHERE best_path.path like '%' || lpad(att.id,5,'0') || '%'
order by att.priority desc;
+----+----------+----------+
| ID | COST | PRIORITY |
+----+----------+----------+
| 1 | 1000000 | 10 |
| 2 | 10000000 | 9 |
| 3 | 5000000 | 8 |
| 7 | 2000000 | 4 |
+----+----------+----------+
UPDATE - Shorter version
This version uses MATCH_RECOGNIZE to find all the rows in the best group following the recursive CTE:
WITH costed (id, cost, priority, running_cost, path) as
( SELECT id, cost, priority, cost running_cost, lpad(id,5,'0') path
FROM a_test_table
WHERE cost <= 20000000
UNION ALL
SELECT a.id, a.cost, a.priority, a.cost + costed.running_Cost, costed.path || '|' || lpad(a.id,5,'0')
FROM costed, a_test_table a
WHERE a.priority < costed.priority
AND a.cost + costed.running_cost <= 20000000)
search depth first by priority desc set ord
SELECT id, cost, priority
FROM costed c
MATCH_RECOGNIZE (
ORDER BY path
MEASURES
MATCH_NUMBER() AS mno
ALL ROWS PER MATCH
PATTERN (STRT ADDON*)
DEFINE
ADDON AS ADDON.PATH = PREV(ADDON.PATH) || '|' || LPAD(ADDON.ID,5,'0')
)
WHERE mno = 1
ORDER BY priority DESC;
UPDATE -- Even shorter version, using clever idea from the SQL*Server link the OP posted
*Edit: removed use of ROWNUM=1 in anchor part of recursive CTE, since it depended on the arbitrary order in which rows would be returned. I'm surprised no one dinged me on that. *
WITH costed (id, cost, priority, running_cost) as
( SELECT id, cost, priority, cost running_cost
FROM ( SELECT * FROM a_test_table
WHERE cost <= 20000000
ORDER BY priority desc
FETCH FIRST 1 ROW ONLY )
UNION ALL
SELECT a.id, a.cost, a.priority, a.cost + costed.running_Cost
FROM costed CROSS APPLY ( SELECT b.*
FROM a_test_table b
WHERE b.priority < costed.priority
AND b.cost + costed.running_cost <= 20000000
FETCH FIRST 1 ROW ONLY
) a
)
CYCLE id SET is_cycle TO 'Y' DEFAULT 'N'
select id, cost, priority from costed
order by priority desc
I'm too stupid to do it in plain SQL, so I tried PL/SQL - a function that return s a table. Here's how: looping through all rows in a table, I'm calculating the sum; if it is lower than a limit, fine - add row's ID into an array and go on.
SQL> create or replace function f_pri (par_limit in number)
2 return sys.odcinumberlist
3 is
4 l_sum number := 0;
5 l_arr sys.odcinumberlist := sys.odcinumberlist();
6 begin
7 for cur_r in (select id, cost, priority
8 from a_test_table
9 order by priority desc
10 )
11 loop
12 l_sum := l_sum + cur_r.cost;
13 if l_sum <= par_limit then
14 l_arr.extend;
15 l_arr(l_arr.last) := cur_r.id;
16 else
17 l_sum := l_sum - cur_r.cost;
18 end if;
19 end loop;
20 return (l_arr);
21 end;
22 /
Function created.
Preparing SQL*Plus environment so that the output looks prettier:
SQL> break on report
SQL> compute sum of cost on report
SQL> set ver off
Testing:
SQL> select t.id, t.cost, t.priority
2 from table(f_pri(&par_limit)) x join a_test_table t on t.id = x.column_value
3 order by t.priority desc;
Enter value for par_limit: 20000000
ID COST PRIORITY
---------- ---------- ----------
1 1000000 10
2 10000000 9
3 5000000 8
7 2000000 4
----------
sum 18000000
SQL> /
Enter value for par_limit: 30000000
ID COST PRIORITY
---------- ---------- ----------
1 1000000 10
2 10000000 9
3 5000000 8
7 2000000 4
8 3000000 3
9 3000000 2
----------
sum 24000000
6 rows selected.
SQL>
#ypercubeᵀᴹ on the DBA-SE chat posted this solution. It's pretty consise.
with rt (id, cost, running_total, priority) as
(
(
select
id,
cost,
cost as running_total,
priority
from
a_test_table
where cost <= 20000000
order by priority desc
fetch first 1 rows only
)
union all
select
t.id,
t.cost,
t.cost + rt.running_total,
t.priority
from a_test_table t
join rt
on t.priority < rt.priority -- redundant but throws
-- "cycle detected error" if omitted
and t.priority = -- needed
( select max(tm.priority) from a_test_table tm
where tm.priority < rt.priority
and tm.cost + rt.running_total <= 20000000 )
)
select *
from rt ;
(#ypercubeᵀᴹ is not interested in posting it himself.)

Return results from a table match on exact number of rows

I have two tables A and B, that are in a many to many relationship in a third table. What A want to achieve is get the "repeating" A rows based on B. For example:
table A table B table A_B
---------- ---------- ----------
1 A 1 A
2 B 1 B
3 C 2 A
4 D 2 B
5 3 A
3 B
3 C
4 A
4 D
5 A
What I want is, when searching table A_B by lets say '1', to get only 2, although 3 has both A and B and 4 has A, same goes for 5 too, it matches A but only A so it should be ignored as well. I've tried some suggestions form similar questions with cross join but I had no luck. I am trying to achieve this with just selects and joins, without stored procedures or temporary tables. Any suggestions is welcomed, thank you.
Repeat all base table rows for EACH left join row match
I want my output to look like:
table A_B
----------
2 A
2 B
Or if possible it would be even better if it matches the A_id by which the search is being done
table A_B
----------
1 A
1 B
2 A
2 B
However, the B_id column is not as important so if it is only
table A_B
----------
2
or
table A_B
----------
1
2
is acceptable as well.
EDIT 1:
Until now this is what I've came up with, although a bit unclean but it gets the expected result
select
A_id
from
tableA_B
where
A_id in
(
select
A_id
from
tableA_B
group by
A_id
having
count (A_id) IN (
select
count (A_id)
from
tableA_B
where
A_id = 1
)
)
AND
B_id IN (
select
B_id
from
tableA_B
where
A_id = 1
)
group by
A_id
Basically process of elimination, step by step. It would be ideal if it took only one step.
EDIT 2:
I'm sorry I left out some important information, my B values can be repeated for instance
table A table B table A_B
---------- ---------- ----------
1 A 1 A
2 B 1 B
3 c 2 A
4 D 2 B
5 AB 3 A
6 3 B
3 C
4 A
4 D
5 A
6 AB
so using XML path may return incorrect values. Because in my case it will return 6 as well which is incorrect. I apologies for leaving out this information.
Other solution which use INTERSECT could be:
CREATE TABLE tableA_B (A_id INT, B_id VARCHAR(8))
GO
INSERT INTO tableA_B VALUES
(1,'A'),(1,'B'),(2,'A'),(2,'B'),(3,'A'),(3,'B'),(3,'C'),(4,'A'),(4,'D'),(5,'A')
GO
DECLARE #x INT = 1;
SELECT A_id FROM tableA_B ab1
LEFT JOIN (
SELECT B_id FROM tableA_B
WHERE A_id=#x
) ab2 ON ab1.B_id=ab2.B_id
GROUP BY ab1.A_id
HAVING COUNT(*)=(SELECT COUNT(*) FROM tableA_B WHERE A_id=#x)
INTERSECT
SELECT A_id FROM tableA_B ab1
JOIN (
SELECT B_id FROM tableA_B
WHERE A_id=#x
) ab2 ON ab1.B_id=ab2.B_id
GROUP BY ab1.A_id
HAVING COUNT(*)=(SELECT COUNT(*) FROM tableA_B WHERE A_id=#x)
DROP TABLE tableA_B
GO
Try this,
declare #A_B table(col int,col2 varchar(30))
insert into #A_B VALUES
(1 ,'A') ,(1 ,'B') ,(2 ,'A') ,(2 ,'B') ,(3 ,'A') ,(3 ,'B')
,(3 ,'C') ,(4 ,'A') ,(4 ,'D') ,(5 ,'A'),(6 ,'AB')
declare #i int=1
declare #007 char(1)='-'
;with CTE as
(
select col,col2
,(select #007+col2 from #A_B y
where col=x.col for xml path(''))ConcateCol
from #A_B x
--where col=#i
)
select col,col2
from cte c
where
exists(select * from cte c1
where col=#i and c.ConcateCol=c1.ConcateCol)
you can further maniplate to get whatever desire output
;With tableA(ID)
AS
(
Select 1 uNION ALL
Select 2 uNION ALL
Select 3 uNION ALL
Select 4
)
, tableB(VAL)
As
(
SELECT 'A' UNION ALL
SELECT 'B' UNION ALL
SELECT 'C' UNION ALL
SELECT 'D'
)
SELECT ID,VAL FROM
(
SELECT *,ROW_NUMBER()OVER(PARTITION BY ID ORDER BY ID)AS Seq FROM tableA
CROSS JOIN tableB
)Dt
WHERE ID In (SELECT Id From tableA where id in(1,2) ) AND Dt.Seq<3
OutPut
table A_B
----------
1 A
1 B
2 A
2 B

Oracle Hierarchical Query

Using Oracle 10g. I have two tables:
User Parent
-------------
1 (null)
2 1
3 1
4 3
Permission User_ID
-------------------
A 1
B 3
The values in the permissions table get inherited down to the children. I would like to write a single query that could return me something like this:
User Permission
------------------
1 A
2 A
3 A
3 A
3 B
4 A
4 B
Is it possible to formulate such a query using 10g connect .. by syntax to pull in rows from previous levels?
you can achieve the desired result with a connect by (and the function CONNECT_BY_ROOT that returns the column value of the root node):
SQL> WITH users AS (
2 SELECT 1 user_id, (null) PARENT FROM dual
3 UNION ALL SELECT 2, 1 FROM dual
4 UNION ALL SELECT 3, 1 FROM dual
5 UNION ALL SELECT 4, 3 FROM dual
6 ), permissions AS (
7 SELECT 'A' permission, 1 user_id FROM dual
8 UNION ALL SELECT 'B', 3 FROM dual
9 )
10 SELECT lpad('*', 2 * (LEVEL-1), '*')||u.user_id u,
11 u.user_id, connect_by_root(permission) permission
12 FROM users u
13 LEFT JOIN permissions p ON u.user_id = p.user_id
14 CONNECT BY u.PARENT = PRIOR u.user_id
15 START WITH p.permission IS NOT NULL
16 ORDER SIBLINGS BY user_id;
U USER_ID PERMISSION
--------- ------- ----------
3 3 B
**4 4 B
1 1 A
**2 2 A
**3 3 A
****4 4 A
You could take a look at http://www.adp-gmbh.ch/ora/sql/connect_by.html
Kind of black magic, but you can use table-cast-multiset to reference one table from another in WHERE clause:
create table t1(
usr number,
parent number
);
create table t2(
usr number,
perm char(1)
);
insert into t1 values (1,null);
insert into t1 values (2,1);
insert into t1 values (3,1);
insert into t1 values (4,3);
insert into t2 values (1,'A');
insert into t2 values (3,'B');
select t1.usr
, t2.perm
from t1
, table(cast(multiset(
select t.usr
from t1 t
connect by t.usr = prior t.parent
start with t.usr = t1.usr
) as sys.odcinumberlist)) x
, t2
where t2.usr = x.column_value
;
In the subquery x I construct a table of all parents for the given user from t1 (including itself), then join it with permissions for these parents.
Here is a example for just one user id. you can use proc to loop all.
CREATE TABLE a_lnk
(user_id VARCHAR2(5),
parent_id VARCHAR2(5));
CREATE TABLE b_perm
(perm VARCHAR2(5),
user_id VARCHAR2(5));
INSERT INTO a_lnk
SELECT 1, NULL
FROM DUAL;
INSERT INTO a_lnk
SELECT 2, 1
FROM DUAL;
INSERT INTO a_lnk
SELECT 3, 1
FROM DUAL;
INSERT INTO a_lnk
SELECT 4, 3
FROM DUAL;
INSERT INTO b_perm
SELECT 'A', 1
FROM DUAL;
INSERT INTO b_perm
SELECT 'B', 3
FROM DUAL;
-- example for just for user id = 1
--
SELECT c.user_id, c.perm
FROM b_perm c,
(SELECT parent_id, user_id
FROM a_lnk
START WITH parent_id = 1
CONNECT BY PRIOR user_id = parent_id
UNION
SELECT parent_id, user_id
FROM a_lnk
START WITH parent_id IS NULL
CONNECT BY PRIOR user_id = parent_id) d
WHERE c.user_id = d.user_id
UNION
SELECT d.user_id, c.perm
FROM b_perm c,
(SELECT parent_id, user_id
FROM a_lnk
START WITH parent_id = 1
CONNECT BY PRIOR user_id = parent_id
UNION
SELECT parent_id, user_id
FROM a_lnk
START WITH parent_id IS NULL
CONNECT BY PRIOR user_id = parent_id) d
WHERE c.user_id = d.parent_id;