Oracle Hierarchical query: how to include top-level parent - oracle

I have a hierarchical query to track a reporting structure. This almost works, except that it's not reporting the very top level node, probably because the top-level people "report" to themselves.
The query is:
select
level,
empid,
parentid
from usertable
connect by nocycle prior parentid= empid
start with empid = 50
This produces:
LEVEL EMPID PARENTID
------ ----- --------
1 50 258
2 258 9555
3 9555 17839
I'm not getting a level 4, since it would look like:
4 17839 17839
Without changing data, is there a way to modify my query so that all 4 levels are returned? The goal is to get the empids, so I can do a check for
id in (hierarchical subquery)
BTW, if I remove the nocycle from the query I get an error.

Chris,
You only get 3 rows because your top level row is not set the way it should to handle hierarchical queries. Typically the top level row, or president KING in Oracle's well known EMP table, has no manager. In your case you should not set the parentid of 17389 to 17389 itself, but to NULL. Either update the table accordingly, or use a view to accomodate for this situation.
An example:
SQL> select empno
2 , mgr
3 from emp
4 where empno in (7876,7788,7566,7839)
5 /
EMPNO MGR
---------- ----------
7566 7839
7788 7566
7839 7839
7876 7788
4 rijen zijn geselecteerd.
This part of the EMP table has four levels with its top level row (7839) set to itself. The same as your empid 17839. And this leads to only three rows using your query:
SQL> select level
2 , empno
3 , mgr
4 from emp
5 connect by nocycle prior mgr = empno
6 start with empno = 7876
7 /
LEVEL EMPNO MGR
---------- ---------- ----------
1 7876 7788
2 7788 7566
3 7566 7839
3 rijen zijn geselecteerd.
Either use a (inline) view to set the mgr/parentid column to null for the top level:
SQL> select level
2 , empno
3 , mgr
4 from ( select empno
5 , nullif(mgr,empno) mgr
6 from emp
7 )
8 connect by nocycle prior mgr = empno
9 start with empno = 7876
10 /
LEVEL EMPNO MGR
---------- ---------- ----------
1 7876 7788
2 7788 7566
3 7566 7839
4 7839
4 rijen zijn geselecteerd.
Or fix your data with an UPDATE statement:
SQL> update emp
2 set mgr = null
3 where empno = 7839
4 /
1 rij is bijgewerkt.
SQL> select level
2 , empno
3 , mgr
4 from emp
5 connect by nocycle prior mgr = empno
6 start with empno = 7876
7 /
LEVEL EMPNO MGR
---------- ---------- ----------
1 7876 7788
2 7788 7566
3 7566 7839
4 7839
4 rijen zijn geselecteerd.
And you can leave out the NOCYCLE keyword as well, after you are done fixing.
Regards,
Rob.

You need to do the hierarchy the other way around, from the root to the leaves.
select
level,
empid,
parentid
from usertable
start with empid = 17839
connect by empid != 17839 and prior empid = parentid
LEVEL EMPID PARENTID
---------------------- ---------------------- ----------------------
1 17839 17839
2 9555 17839
3 258 9555
4 50 258
4 rows selected

You don't have to change your structure.
just use the following query
select
level,
empid,
parentid
from usertable
connect by prior parentid = empid
AND parentid <> empid -- This line prohibits cycling and ALLOWS a row where parentid = empid
start with empid = 50

Van Heddegem Roeland's answer doesn't work for me, I had already tried that, but I have managed to do it without an inline view, in the connect clause, by adding:-
and prior empid <> parentid
The following post explains why that works - if you can get your head round it! Although it does make sound logical sense once you do 'get it'. (It's to do with the order of evaluation of each side of the <> operator.)
Oracle: Connect By Loop in user data
The inline view will work but without research on your particular dataset, I don't know what impact an inline view might have on the query path. Adding the extra clause is probably the 'correct' way to do it in most situations, IMHO.

seems like you have a cycle in data. Without "nocycle" it will not work straight away. If you know that all your data has maximum nesting level 4, then you could add condition "and level <= 4" and remove nocycle. Should work.

Related

How to align database output in text file using sql query

I have an issue while opening the text file of database output. The null value columns are replaced with the columns which have data. So the data is not properly formatting in the text file. I generated the text file from oracle sql developer. And after opening the text file I can see the alignment of columns is not proper(misplacing of values between null columns and columns which have data ). Can anyone please help me to solve this issue
Just guessing here, but - what you said reminds me of this. Example is based on Scott's EMP table where some employees have NULL in the COMM column:
SQL> select ename, job, comm, deptno
2 from emp
3 where deptno = 30;
ENAME JOB COMM DEPTNO
---------- --------- ---------- ----------
ALLEN SALESMAN 300 30
WARD SALESMAN 500 30
MARTIN SALESMAN 1400 30
BLAKE MANAGER 30
TURNER SALESMAN 30
JAMES CLERK 30
6 rows selected.
If you're spooling such data by concatenating columns, you get mess which is difficult to read:
SQL> select ename||'-'||job||'-'||comm||'-'||deptno
2 from emp
3 where deptno = 30;
ENAME||'-'||JOB||'-'||COMM||'-'||DEPTNO
----------------------------------------------------------
ALLEN-SALESMAN-300-30
WARD-SALESMAN-500-30
MARTIN-SALESMAN-1400-30
BLAKE-MANAGER--30
TURNER-SALESMAN--30
JAMES-CLERK--30
6 rows selected.
But, if you right-pad strings to certain length (RPAD function) and use NVL for missing data, then the result is somewhat prettier:
SQL> select rpad(ename, 10, ' ') ||'-'||
2 rpad(job , 10, ' ') ||'-'||
3 nvl(to_char(comm, '9990D00'), ' ') ||'-'||
4 deptno as result
5 from emp
6 where deptno = 30;
RESULT
---------------------------------------------------------------------
ALLEN -SALESMAN - 300,00-30
WARD -SALESMAN - 500,00-30
MARTIN -SALESMAN - 1400,00-30
BLAKE -MANAGER - -30
TURNER -SALESMAN - -30
JAMES -CLERK - -30
6 rows selected.
SQL>

HOW can I add a sort row column?

Im using view on oracle plsql. In the table showing my sales,
I want to have a column showing the sequence number next to the sales of top 50 products.
The best selling products should be listed and followed by the sequence number in the row.
how can i do that?
Thanks.
This is my related query
NVL (
(SELECT ROUND (
SUM (
CASE DOCUMENT_TYPE
WHEN 2
THEN
(CASE TRANSACTION_TYPE
WHEN 0 THEN 0 - AMOUNT
ELSE AMOUNT
END)
ELSE
(CASE TRANSACTION_TYPE
WHEN 1 THEN 0 - AMOUNT
ELSE AMOUNT
END)
END),
8)
FROM TBL_TRANSACTION_LINES
WHERE STORE_NO = tbl_location.locationno
AND (TRANSACTION_TYPE NOT IN (10, 30))
AND TRANSACTION_DATE >
TO_DATE ('2020-09-27 0:0:0',
'yyyy-mm-dd HH24:MI:SS')
AND TRANSACTION_DATE <=
TO_DATE ('2020-10-04 0:0:0',
'yyyy-mm-dd HH24:MI:SS')
AND (URUNID = TBL_URUNLER.URUNID)),
0)
AS RESULTS
I don't have your tables nor data, so - here's an example based on Scott's sample schema; ranking employees by salary within their departments. Apply that to your case.
SQL> with temp as
2 (select deptno, ename, sal,
3 rank() over (partition by deptno order by sal desc) rnk
4 from emp
5 )
6 select deptno, ename, sal, rnk
7 from temp
8 order by deptno, rnk;
DEPTNO ENAME SAL RNK
---------- ---------- ---------- ----------
10 KING 10000 1
10 CLARK 2450 2
10 MILLER 1300 3
20 SCOTT 3000 1
20 FORD 3000 1
20 JONES 2975 3
20 ADAMS 1100 4
20 SMITH 920 5
30 BLAKE 2850 1
30 ALLEN 1600 2
30 TURNER 1500 3
30 MARTIN 1250 4
30 WARD 1250 4
30 JAMES 950 6
14 rows selected.
SQL>

SQL Top 5 and the rest as others

I want to select the top 5 rows and additional a 6th row named Others with rest aggregated.
with
Anzahl as
(SELECT
NVL (parse_listener_log_line (connect_string, 'HOST'), 'n/a') host, COUNT(*) cnt
FROM listener_log
WHERE ID_MANDANT = :P100_MANDANT
AND ID_SERVER = :P100_SERVER
GROUP BY parse_listener_log_line (connect_string, 'HOST')
ORDER BY cnt DESC),
client as
(select
case
when rownum > 4 then 'Others'
else host
end as client, cnt
FROM Anzahl)
SELECT client, cnt
FROM client;
CLIENT CNT
jdbc 118553
server2 106170
server1 101710
server4 13370
Others 8734
Others 1760
Others 1365
Others 1058
A little bit of analytic functions (row_number) along with set operations (union) might do what you're looking for.
Scott's EMP table contains these data:
SQL> select ename, sal from emp order by sal desc;
ENAME SAL
---------- ----------
KING 5000
FORD 3000
SCOTT 3000
JONES 2975
BLAKE 2850
CLARK 2450
ALLEN 1600
TURNER 1500
MILLER 1300
WARD 1250
MARTIN 1250
ADAMS 1100
JAMES 950
SMITH 800
14 rows selected.
Now: find each row's ordinal number (using row_number) and union the first 5 rows (take each of them as is) with the sixth one that contains aggregated salaries:
SQL> with temp as
2 (select ename,
3 sal,
4 row_number() over (order by sal desc) rn
5 from emp
6 )
7 select rn,
8 ename,
9 sal
10 from temp
11 where rn <= 5
12 union all
13 select 6,
14 'Other',
15 sum(sal)
16 from temp
17 where rn > 5
18 order by rn;
RN ENAME SAL
---------- ---------- ----------
1 KING 5000
2 SCOTT 3000
3 FORD 3000
4 JONES 2975
5 BLAKE 2850
6 Other 12200
6 rows selected.
SQL>

Rank the managers based on how many employees they have

[Update]
Thanks for all the comments!
Appreciated!
I solved this by the code below, after referring to all of your posts.
I did not put into account of hierarchy but I will for the later works.
Thank you!
SELECT m.first_name
, m.last_name
, RANK() over (partition by (select COUNT(e.employee_id)
from DB1_employee e)
ORDER BY e.employee_id DESC) AS RANK from DB2_manager m LEFT JOIN RITDB_employee e ON m.employee_id=e.manager
I am having difficulty to troubleshoot my code to rank the managers
based on the number of employees they have. The error is "missing right parenthesis". Any helps would be highly appreciated. Thanks!
SELECT m.first_name
, m.last_name
, RANK() over (partition by (select COUNT(e.employee_id)
from DB1_employee e) AS NUM_EMP
ORDER BY NUM_EMP DESC) AS RANK
from DB2_manager m
, DB1_employee e
group by m.first_name, m.last_name
ORDER BY RANK
An example based on Scott's schema (as I don't have your tables nor data), so that it is easier to see what should be returned.
SQL> select mgr, empno, ename, job from emp order by mgr;
MGR EMPNO ENAME JOB
---------- ---------- ---------- ---------
7566 7902 FORD ANALYST --> MGR 7566 (Jones) has 2 employees
7566 7788 SCOTT ANALYST
7698 7900 JAMES CLERK --> 7698 (Blake) has 5 employees
7698 7499 ALLEN SALESMAN
7698 7521 WARD SALESMAN
7698 7844 TURNER SALESMAN
7698 7654 MARTIN SALESMAN
7782 7934 MILLER CLERK --> 7782 (Clark) has 1 employee
7788 7876 ADAMS CLERK --> 7788 (Scott) has 1 employee
7839 7698 BLAKE MANAGER --> 7839 (King) has 3 employees
7839 7566 JONES MANAGER
7839 7782 CLARK MANAGER
7902 7369 SMITH CLERK --> 7902 (Ford) has 1 employee
7839 KING PRESIDENT
14 rows selected.
SQL>
So:
SQL> select m.ename mgrname,
2 count(*) cnt,
3 dense_rank() over (order by count(*) desc) rnk
4 from emp e join emp m on e.mgr = m.empno
5 where e.mgr is not null
6 group by e.mgr, m.ename
7 order by rnk;
MGRNAME CNT RNK
---------- ---------- ----------
BLAKE 5 1
KING 3 2
JONES 2 3
SCOTT 1 4
FORD 1 4
CLARK 1 4
6 rows selected.
SQL>
However:
That's not correct. Why? Because of hierarchy. Not all managers share the same level. Have a look: King is the president; he is the big boss and manages them all; how can he be ranked as #2? The same goes for the rest of them.
SQL> select lpad(' ', 2 * level - 2) || e.empno ||' '|| e.ename val
2 from emp e
3 connect by prior e.empno = e.mgr
4 start with e.mgr is null;
VAL
--------------------------------------------------------------------
7839 KING
7566 JONES --> Jones has 4 employees!!! (Scott, Adams, Ford and Smith)
7788 SCOTT --> Scott has 1 employee (that's Adams)
7876 ADAMS
7902 FORD --> Ford has 1 employee (that's Smith)
7369 SMITH
7698 BLAKE
7499 ALLEN
7521 WARD
7654 MARTIN
7844 TURNER
7900 JAMES
7782 CLARK
7934 MILLER
14 rows selected.
SQL>
Therefore, we need another approach. Begin with a simple query which, basically, returns "root" for each of them:
SQL> select connect_by_root(ename) manager
2 from emp
3 connect by prior empno = mgr;
MANAGER
----------
SCOTT
SCOTT
FORD
FORD
ALLEN
JAMES
<snip>
KING
KING
39 rows selected.
SQL>
It is further used as a source for
SQL> select x.mgrname,
2 count(*) - 1 cnt
3 from (select connect_by_root(e.ename) mgrname
4 from emp e
5 connect by prior e.empno = e.mgr
6 ) x
7 group by x.mgrname;
MGRNAME CNT
---------- ----------
ALLEN 0
JONES 4
FORD 1
MILLER 0
CLARK 1
WARD 0
SMITH 0
SCOTT 1
TURNER 0
MARTIN 0
ADAMS 0
JAMES 0
BLAKE 5
KING 13
14 rows selected.
SQL>
Finally, remove those that don't have any employees and rank them; the result is quite different than the first (the most obvious, but probably wrong) approach:
SQL> select r.mgrname,
2 r.cnt,
3 dense_rank() over (order by cnt desc) rnk
4 from (select x.mgrname,
5 count(*) - 1 cnt
6 from (select connect_by_root(e.ename) mgrname
7 from emp e
8 connect by prior e.empno = e.mgr
9 ) x
10 group by x.mgrname
11 ) r
12 where r.cnt > 0
13 order by rnk;
MGRNAME CNT RNK
---------- ---------- ----------
KING 13 1
BLAKE 5 2
JONES 4 3
SCOTT 1 4
FORD 1 4
CLARK 1 4
6 rows selected.
SQL>
First of all
Your query is doing a cross join, and then grouping by manager name. There can be an issue if two managers have the same name. Better to use PK and FK.
Don't use old join syntax
NUM_EMP. has no reference in your query and still used in the ORDER BY.
I am considering that DB1_employee table must have some column referring to its manager(let's say, MANAGER_FK ). So writing the query accordingly as following:
SELECT
M.FIRST_NAME,
M.LAST_NAME,
RANK() OVER(
ORDER BY
E.EMPLOYEE_CNT DESC
) AS RNK
FROM
DB2_MANAGER M
JOIN (
SELECT
MANAGER_FK,
COUNT(1) AS EMPLOYEE_CNT
FROM
DB1_EMPLOYEE
GROUP BY
MANAGER_FK
) E ON ( M.MANAGER_ID = E.MANAGER_FK )
ORDER BY
RNK;
Cheers!!
try:
SELECT m.first_name
, m.last_name
, RANK() over (partition by (select COUNT(e.employee_id)
from DB1_employee e)
ORDER BY NUM_EMP DESC) AS RANK
from DB2_manager m
, DB1_employee e
group by m.first_name, m.last_name
ORDER BY RANK
;

Oracle Conditional where clause

is there any way to write query with following functionality, add where clause as a conditional way,
select e.emp_id, emp.admin_user from employees e
if emp.admin != 'Y'
then
query run with where clause
else
query run without where clause ?
Using a CASE expression in the WHERE clause should do the trick. When you say you don't need the where clause if condition is not met, then all you want is a condition like WHERE 1 = 1, i.e. when condition is not met then return all rows. So, you need to make the not met condition as always TRUE.
For example,
I have an employee table,
SQL> SELECT empno, ename, deptno
2 FROM emp;
EMPNO ENAME DEPTNO
---------- ---------- ----------
7369 SMITH 20
7499 ALLEN 30
7521 WARD 30
7566 JONES 20
7654 MARTIN 30
7698 BLAKE 30
7782 CLARK 10
7788 SCOTT 20
7839 KING 10
7844 TURNER 30
7876 ADAMS 20
7900 JAMES 30
7902 FORD 20
7934 MILLER 10
14 rows selected.
SQL>
I want to select the employee details, if department is 20 then use the where clause else return all the employee details, but filter the department which meets the where condition.
SQL> SELECT empno, ename, deptno
2 FROM emp
3 WHERE ename =
4 CASE
5 WHEN deptno = 20
6 THEN 'SCOTT'
7 ELSE ename
8 END
9 /
EMPNO ENAME DEPTNO
---------- ---------- ----------
7499 ALLEN 30
7521 WARD 30
7654 MARTIN 30
7698 BLAKE 30
7782 CLARK 10
7788 SCOTT 20
7839 KING 10
7844 TURNER 30
7900 JAMES 30
7934 MILLER 10
10 rows selected.
SQL>
So, for department 20, the filter is applied by where clause, and I get only the row for ename SCOTT, for others it returns all the rows.
To keep it simple I would go for union clause in this case, so you can have your where clause as complex as you need. I tried to guess your table structure from above comment, let's see this example:
SQL> create table employees (emp_id number, admin_user number, project_id number);
Table created.
SQL> create table project_accessible_to_user (emp_id number, project_id number);
Table created.
Now make simple union all of two queries one with where condition anoother without it
SQL> select * from employees e where e.admin_user!='Y' and project_id in
(select project_id from project_accessible_to_user where emp_id=e.emp_id)
union all
select * from employees e where (e.admin_user is null or
e.admin_user='Y');
UNION ALL is better from performance point of view as UNION because it means that it is not checking for intersect values so if there are any it will return duplicates. However in this case it is filtered already by condition on admin_user, so these duplicates will not occure.

Resources