Recursive hierarchical Oracle SQL query - oracle

I have a source table like below:
Emp_ID| Name| Manager_ID
001|abc|005
005|cde|010
010|xyz|050
050 | bcg| 100
100|sta|NULL
My requirement is to populate the target table like below:
Emp_ID| Name| Manager_1| Manager_2| Manager_3| Manager_4
005|cde|xyz|bcg|sta|NULL
050|bcg|sta| NULL|NULL|NULL
100|sta|NULL|NULL|NULL
001|abc|cde|xyz|bcg|sta
I am able to use recursive select through Connect by clause and populate the value for Manager_1 but not able to get through the logic to populate Manager_2, Manager_3 , Manager_4 values as different column values in a single row depending on how many level of hierarchy is present for a certain employee.
Please help.

I think the following query will help you. But to split the string to individual manager id, you need to know the max no of level of managers.
WITH data_set AS
(SELECT '001' emp_id, 'aaa' emp_name, '005' mgr_id
FROM DUAL
UNION
SELECT '005' emp_id, 'bbb' emp_name, '010' mgr_id
FROM DUAL
UNION
SELECT '010' emp_id, 'ccc' emp_name, '050' mgr_id
FROM DUAL
UNION
SELECT '020' emp_id, 'ddd' emp_name, '050' mgr_id
FROM DUAL
UNION
SELECT '050' emp_id, 'eee' emp_name, '100' mgr_id
FROM DUAL
UNION
SELECT '100' emp_id, 'fff' emp_name, '200' mgr_id
FROM DUAL
UNION
SELECT '200' emp_id, 'ggg' emp_name, NULL mgr_id
FROM DUAL)
SELECT emp_id, emp_name, mgr_id,
LTRIM (SYS_CONNECT_BY_PATH (emp_id, '-'), '-') chain
FROM data_set
START WITH mgr_id IS NULL
CONNECT BY mgr_id = PRIOR emp_id
ORDER SIBLINGS BY emp_id;

If your hierarchy only extends to 4 levels deep, the following query may be used:
select t1.Emp_ID,
t1.Name,
t2.Name as Manager_1,
t3.Name as Manager_2,
t4.Name as Manager_3,
t5.Name as Manager_4
from tmp t1
left join tmp t2 on t2.Emp_ID = t1.Manager_ID
left join tmp t3 on t3.Emp_ID = t2.Manager_ID
left join tmp t4 on t4.Emp_ID = t3.Manager_ID
left join tmp t5 on t5.Emp_ID = t4.Manager_ID;

Pivot option:
SELECT * FROM
(
SELECT emp_id, name, manager_id
FROM employees
)
PIVOT
(
COUNT(manager_id)
FOR manager_id IN ('005', '100', '050')
)
ORDER BY emp_id;

Related

Using JOIN with cte

I have the following setup, which seems to be working fine. I am having trouble modifying the query to include the department_name in the output.
I can't seem to get the JOIN working with the CTE. Its probably something trivial but after many attempts I can't get it to work.
Any help would be appreciated.
Below is my setup and test case.
CREATE TABLE departments( department_id, department_name) AS
SELECT 1, 'IT' FROM DUAL UNION ALL
SELECT 2, 'DBA' FROM DUAL;
CREATE TABLE employees (employee_id, first_name, last_name, hire_date, salary, department_id) AS
SELECT 1, 'Lisa', 'Saladino', DATE '2001-04-03', 100000, 1 FROM DUAL UNION ALL
SELECT 2, 'Abby', 'Abbott', DATE '2001-04-04', 50000, 1 FROM DUAL UNION ALL
SELECT 3, 'Beth', 'Cooper', DATE '2001-04-05', 60000, 1 FROM DUAL UNION ALL
SELECT 4, 'Carol', 'Orr', DATE '2001-04-06', 70000,1 FROM DUAL UNION ALL
SELECT 5, 'Vicky', 'Palazzo', DATE '2001-04-07', 88000,2 FROM DUAL UNION ALL
SELECT 6, 'Cheryl', 'Ford', DATE '2001-04-08', 110000,1 FROM DUAL UNION ALL
SELECT 7, 'Leslee', 'Altman', DATE '2001-04-10', 666666, 1 FROM DUAL UNION ALL
SELECT 8, 'Jill', 'Coralnick', DATE '2001-04-11', 190000, 2 FROM DUAL UNION ALL
SELECT 9, 'Faith', 'Aaron', DATE '2001-04-17', 122000,2 FROM DUAL;
WITH cte AS (
SELECT department_id,
first_name,
last_name,
salary,
DENSE_RANK() OVER(PARTITION BY department_id ORDER BY salary DESC) AS rnk
FROM employees
)
SELECT department_id,
/* department_name */
first_name,
last_name,
salary
FROM cte
WHERE rnk=1
You did not join the table.
WITH cte AS (
SELECT department_id,
first_name,
last_name,
salary,
DENSE_RANK() OVER(PARTITION BY department_id ORDER BY salary DESC) AS rnk
FROM employees
)
SELECT e.department_id,
d.department_name,
e.first_name,
e.last_name,
e.salary
FROM cte e
INNER JOIN departments d
ON (d.department_id = e.department_id)
WHERE rnk=1
or:
WITH cte AS (
SELECT e.department_id,
d.department_name,
e.first_name,
e.last_name,
e.salary,
DENSE_RANK() OVER(PARTITION BY e.department_id ORDER BY e.salary DESC) AS rnk
FROM employees e
INNER JOIN departments d
ON (d.department_id = e.department_id)
)
SELECT department_id,
department_name,
first_name,
last_name,
salary
FROM cte
WHERE rnk=1
or using a sub-query, instead of the sub-query factoring clause:
SELECT e.department_id,
d.department_name,
e.first_name,
e.last_name,
e.salary
FROM (
SELECT department_id,
first_name,
last_name,
salary,
DENSE_RANK() OVER(PARTITION BY department_id ORDER BY salary DESC) AS rnk
FROM employees
) e
INNER JOIN departments d
ON (d.department_id = e.department_id)
WHERE rnk=1
or:
SELECT department_id,
department_name,
first_name,
last_name,
salary
FROM (
SELECT e.department_id,
d.department_name,
e.first_name,
e.last_name,
e.salary,
DENSE_RANK() OVER(PARTITION BY e.department_id ORDER BY e.salary DESC) AS rnk
FROM employees e
INNER JOIN departments d
ON (d.department_id = e.department_id)
)
WHERE rnk=1
fiddle

Oracle rewrite query using rank or dense_rank

I have the following query and sample data, which is working fine. It finds the max salary for employee(s) for each department.
Although it works, I like to change it to use rank or dense_rank (unsure how to do this) to achieve the output I GENERATED below but
Note department_id=1 has 2 different employees making the exact salary and I need to keep the result like that. In addition, is there a way I can only compare e.department_id = d.department_id once?
Any help would be greatly appreciated. Below is my test CASE and sample data.
CREATE table dept (department_id, department_name) AS
SELECT 1, 'IT' FROM DUAL UNION ALL
SELECT 2, 'SALES' FROM DUAL;
CREATE TABLE employees (employee_id, manager_id, first_name, last_name, department_id, sal,
serial_number) AS
SELECT 1, NULL, 'Alice', 'Abbot', 1, 100000, 'D123' FROM DUAL UNION ALL
SELECT 2, 1, 'Beryl', 'Baron',1, 50000,'D124' FROM DUAL UNION ALL
SELECT 3, 1, 'Carol', 'Chang',1, 100000, 'A1424' FROM DUAL UNION ALL
SELECT 4, 2, 'Debra', 'Dunbar',1, 75000, 'A1425' FROM DUAL UNION ALL
SELECT 5, NULL, 'Emily', 'Eden',2, 90000, 'C1725' FROM DUAL UNION ALL
SELECT 6, 3, 'Fiona', 'Finn',1, 88500,'C1726' FROM DUAL UNION ALL
SELECT 7,5, 'Grace', 'Gelfenbein',2, 55000, 'C1727' FROM DUAL;
select
e.employee_id,
e.first_name,
e.last_name,
e.department_id,
d.department_name,
e.sal
from employees e join dept d on e.department_id = d.department_id
where not exists
( select null
from employees
where department_id = e.department_id
and sal > e.sal );
EMPLOYEE_ID FIRST_NAME LAST_NAME DEPARTMENT_ID DEPARTMENT_NAME SAL
1 Alice Abbot 1 IT 100000
3 Carol Chang 1 IT 100000
5 Emily Eden 2 SALES 90000
You may use RANK as follows:
WITH cte AS (
SELECT e.employee_id, e.first_name, e.last_name, e.department_id,
d.department_name, e.sal,
RANK() OVER (PARTITION BY e.department_id ORDER BY e.sal DESC) rnk
FROM employees e
INNER JOIN dept d ON e.department_id = d.department_id
)
SELECT employee_id, first_name, last_name, department_id, department_name, sal
FROM cte
WHERE rnk = 1;

Group By inside Rtrim(Xmlagg (Xmlelement (e,element || ',')).extract ( '//text()' ).GetClobVal(), ',')

I need to group values ​​inside a query using (or not) the command Rtrim(Xmlagg (Xmlelement (e,column || ',')).extract ( '//text()' ).GetClobVal(), ','), but I can't find any literature where explain a way to group the data inside this command. The code is very simple, as you can see below:
SELECT ID,
Rtrim(Xmlagg (Xmlelement (and, CONTRACTS || ',')).extract ( '//text()' ).GetClobVal(), ',') AS CONTRACTS
FROM TABLE_A
GROUP BY ID
The result in CONTRACTS is always repeated when the ID is found, thats ok, it´s working!
ID
CONTRACTS
876
1,1,1,2,3,3
But what I really need is this return:
ID
CONTRACTS
876
1,2,3
It´s not necessary to use the command Rtrim(Xmlagg (Xmlelement (e,column || ',')).extract ( '//text()' ).GetClobVal(), ','), instead, I just use to concatenate element with comma "," in the same column.
If anyone can help me, I would be very grateful!
If your values will fit into a VARCHAR2 data type (rather than a CLOB) then you can use a nested sub-query to get the DISTINCT values for each ID:
SELECT ID,
LISTAGG(contracts, ',') WITHIN GROUP (ORDER BY contracts) AS CONTRACTS
FROM ( SELECT DISTINCT id, contracts FROM TABLE_A)
GROUP BY ID
Or, from Oracle 19c, it is built-in to LISTAGG:
SELECT ID,
LISTAGG(DISTINCT contracts, ',') WITHIN GROUP (ORDER BY contracts) AS CONTRACTS
FROM TABLE_A
GROUP BY ID
If you want a CLOB then you can use the same technique as the first query:
SELECT ID,
Rtrim(
Xmlagg(
Xmlelement(name, CONTRACTS || ',')
ORDER BY contracts
).extract ( '//text()' ).GetClobVal(),
','
) AS CONTRACTS
FROM (SELECT DISTINCT id, contracts FROM TABLE_A)
GROUP BY ID
Which, for the sample data:
CREATE TABLE table_a (id, contracts) AS
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 2 FROM DUAL UNION ALL
SELECT 876, 2 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL;
All output:
ID
CONTRACTS
876
1,2,3
db<>fiddle here
It's much easier to do all those operation in XML functions: DBFiddle
SELECT--+ NO_XML_QUERY_REWRITE
id,
xmlquery(
'string-join(distinct-values($R/R/X/text()),",")'
passing
Xmlelement(
R,
Xmlagg(
Xmlelement (X, CONTRACTS)
order by CONTRACTS
)) as R
RETURNING CONTENT
) AS CONTRACTS
FROM TABLE_A
GROUP BY ID;
Full example with test data:
with table_a (id, contracts) AS (
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 2 FROM DUAL UNION ALL
SELECT 876, 2 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL
)
SELECT--+ NO_XML_QUERY_REWRITE
id,
xmlquery(
'string-join(distinct-values($R/R/X/text()),",")'
passing
Xmlelement(
R,
Xmlagg(
Xmlelement (X, CONTRACTS)
order by CONTRACTS
)) as R
RETURNING CONTENT
) AS CONTRACTS
FROM TABLE_A
GROUP BY ID;

How to join a count query with another table which has all counts?

I am selecting count of few tables and i have a table named table_counts which has two columns named table_name and table_count. I want to join the result of the query with the table_name column of table_counts table. Please see the example below.
select 'Table 1' as table_name, count(*) as table_count_from table_1
union
select 'Table 2' as table_name, count(*) as table_count_from table_2
union
select 'Table 3' as table_name, count(*) as table_count_from table_3
------------------
++table_counts++++
------------------
table_name table count
Table 1 10
Table 2 20
Table 3 30
I have to join the two things using the table_name. Could someone help me if iam missing few things?
you are searching something like this?
SELECT *
FROM table_counts cnt
LEFT OUTER JOIN (select 'Table 1' as table_name, count(*) as table_count_from table_1
union
select 'Table 2' as table_name, count(*) as table_count_from table_2
union
select 'Table 3' as table_name, count(*) as table_count_from table_3
) subcnt
ON cnt.table_name = subcnt.table_name

Create timeline from overlapping dates

Given this table:
CREATE TABLE positions
( "EMP_ID" CHAR(10 BYTE),
"GTYPE" NUMBER,
"AMT" NUMBER,
"START_DATE" DATE,
"STOP_DATE" DATE
)
and this data:
Insert Into positions (Emp_Id,Gtype,Amt,Start_Date,Stop_Date)
select 'XA0022',1,1000,'01-MAY-2010','08-MAY-2012' from dual union
Select 'XA0022',1,1000,'01-MAY-2010','31-DEC-2012' From Dual Union
Select 'XA0022',2,500,'03-APR-2012','15-JUL-2012' From Dual Union
Select 'XA0022',1,421,'01-MAY-2012','23-MAY-2012' From Dual Union
Select 'XA0022',1,1514,'09-MAY-2012','31-DEC-2012' From Dual union
select 'XA0022',1,600,'24-MAY-2012','24-MAY-2012' from dual;
How do I get to this:
from to type1 type2
01-May-2010 02-Apr-2012 2000 0
03-Apr-2012 30-Apr-2012 2000 500
01-May-2012 07-May-2012 2421 500
08-May-2012 08-May-2012 2421 500
09-May-2012 22-May-2012 2935 500
23-May-2012 23-May-2012 2935 500
24-May-2012 15-Jul-2012 3114 500
16-Jul-2012 31-Dec-2012 3014 0
Note: The amount is in effect on the start_date and is not in effect the day after the stop_date.
Any pointers gratefully received!
Use Oracle's pivot.
Oracle Pivot
select * from
(select emp_id, gtype, amt, start_date, stop_date
from positions)
pivot (sum(amt) as amt for (gtype) in (1 as "TYPE1", 2 as "TYPE2"))
order by emp_id, start_date;
With some more information (thanks), something like this?
select emp_id, gtype, start_date,
case when next_amt <> amt then next_start_date -1 end as to_date
from
(select emp_id, gtype, start_date, amt,
lead(start_date,1) over (order by emp_id, start_date) next_start_date,
lead(amt,1) over (order by emp_id, start_date) next_amt
from
positions)

Resources