Skip to Main Content
  • Questions
  • Query to display Master - Detail Output in Separate Lines

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Amit.

Asked: January 26, 2017 - 5:27 am UTC

Last updated: March 14, 2023 - 1:13 am UTC

Version: 11.2.0.4.0 Enterprise Edition

Viewed 10K+ times! This question is

You Asked

Hi,

Good Day !
In my current requirement, I have to display the master details relation in separate rows. The Header should show only the master record while detail should show the detail record only.

I written a query using join & union all and the output displayed correctly. However, the code looks very clumsy & bit complicated as i've many levels like master, details, sub-details. So in this case I will have 3 union all to use.
Is there any other way to write a query for this.
Sample query considering Employee & Department Table using Scott Schema is as below:

Note: TestScript available in :
https://livesql.oracle.com/apex/livesql/s/ehjst59eahrc0i4cw7h1le9bi

SELECT LEVELS ,
(CASE WHEN LEVELS = 'H' THEN DEPTNO ELSE NULL END) DEPTNO,
(CASE WHEN LEVELS = 'H' THEN DNAME ELSE NULL END) DNAME,
ENAME,
EMP_DEPTNO,
SAL
FROM (
SELECT 'H' LEVELS,DEPTNO,DNAME,NULL ENAME,NULL EMP_DEPTNO,NULL SAL,DEPTNO REF_DEPTNO
FROM DEPT
UNION ALL
SELECT 'D' LEVELS, B.DEPTNO,B.DNAME,A.ENAME,A.DEPTNO,A.SAL,B.DEPTNO REF_DEPTNO
FROM EMP A, DEPT B
WHERE A.DEPTNO = B.DEPTNO
)
ORDER BY REF_DEPTNO,DECODE(LEVELS,'H',1,'D',2);

Output Is:

LEVEL DEPTNO DNAME ENAME EMP_DEPTNO SAL
H 10 ACCOUNTING
D CLARK 10 2450
D MILLER 10 1300
D KING 10 5000
H 20 RESEARCH
D SMITH 20 800
D FORD 20 3000
D ADAMS 20 1100
D SCOTT 20 3000
D JONES 20 2975
H 30 SALES
D ALLEN 30 1600
D JAMES 30 950
D TURNER 30 1500
D BLAKE 30 2850
D MARTIN 30 1250
D WARD 30 1250
H 40 OPERATIONS


Also can it be done using any analytical function?

Thank You,
Regards,
Amit

with LiveSQL Test Case:

and Chris said...

Why do you need to do this in your query? I'd have thought you could just join the tables:

select * from scott.emp e
join   scott.dept d
on     d.deptno = e.deptno;


Then define in your app which are the header rows and which the detail. Or if you wanted to get fancy, use XML, JSON or object types to return one header row with the details:

select xmlforest(
         d.dname as "deptName",
         xmlagg(
           xmlelement("employee",
             xmlforest(
               e.empno as "empno",
               e.ename as "empname",
               e.job as "job"
             )
           ) 
         ) as "employees"
       ) as dept_emps
from   scott.emp e
right join scott.dept d
on     d.deptno = e.deptno
group  by d.deptno, d.dname;

<deptName>ACCOUNTING</deptName>
<employees>
  <employee>
    <works_number>7782</works_number>
    <name>CLARK</name>
    <job>MANAGER</job>
  </employee>
  <employee>
    <works_number>7934</works_number>
    <name>MILLER</name>
    <job>CLERK</job>
  </employee>
  <employee>
    <works_number>7839</works_number>
    <name>KING</name>
    <job>PRESIDENT</job>
  </employee>
</employees>

<deptName>RESEARCH</deptName>
<employees>
  <employee>
    <works_number>7369</works_number>
    <name>SMITH</name>
    <job>CLERK</job>
  </employee>
  <employee>
    <works_number>7902</works_number>
    <name>FORD</name>
    <job>ANALYST</job>
  </employee>
  <employee>
    <works_number>7876</works_number>
    <name>ADAMS</name>
    <job>CLERK</job>
  </employee>
  <employee>
    <works_number>7788</works_number>
    <name>SCOTT</name>
    <job>ANALYST</job>
  </employee>
  <employee>
    <works_number>7566</works_number>
    <name>JONES</name>
    <job>MANAGER</job>
  </employee>
</employees>

<deptName>SALES</deptName>
<employees>
  <employee>
    <works_number>7499</works_number>
    <name>ALLEN</name>
    <job>SALESMAN</job>
  </employee>
  <employee>
    <works_number>7900</works_number>
    <name>JAMES</name>
    <job>CLERK</job>
  </employee>
  <employee>
    <works_number>7844</works_number>
    <name>TURNER</name>
    <job>SALESMAN</job>
  </employee>
  <employee>
    <works_number>7698</works_number>
    <name>BLAKE</name>
    <job>MANAGER</job>
  </employee>
  <employee>
    <works_number>7654</works_number>
    <name>MARTIN</name>
    <job>SALESMAN</job>
  </employee>
  <employee>
    <works_number>7521</works_number>
    <name>WARD</name>
    <job>SALESMAN</job>
  </employee>
</employees>

<deptName>OPERATIONS</deptName>
<employees>
  <employee/>
</employees>


Anyway, assuming you must to this in SQL, here's another way that only accesses each table once:

- Union all the two together as you've done. But make the "levels" column a number. 0 for departments, 1 for employees
- Then use a hierarchical query starting at lev = 0 to link the departments to employees.

select * from (
select 0 lev,deptno,dname,null ename,null sal
from   scott.dept
union all
select 1 lev, deptno, null dname,a.ename,a.sal
from   scott.emp a
)
start with lev = 0
connect by prior lev = lev - 1
and     prior deptno = deptno;

LEV  DEPTNO  DNAME       ENAME   SAL    
0    10      ACCOUNTING                 
1    10                  CLARK   2,450  
1    10                  MILLER  1,300  
1    10                  KING    5,000  
0    20      RESEARCH                   
1    20                  SMITH   800    
1    20                  FORD    3,000  
1    20                  ADAMS   1,100  
1    20                  SCOTT   3,000  
1    20                  JONES   2,975  
0    30      SALES                      
1    30                  ALLEN   1,600  
1    30                  WARD    1,250  
1    30                  MARTIN  1,250  
1    30                  BLAKE   2,850  
1    30                  TURNER  1,500  
1    30                  JAMES   950    
0    40      OPERATIONS 


PS - the scott schema already exists in LiveSQL! So there's no need to create it again.

Rating

  (9 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

SQL

Amit Chaudhary, January 31, 2017 - 7:30 am UTC

Hi,
Thanks for the suggestion provided. It is suitable for my requirement. However, I was just curious to know if the expected result is achievable within a single query rather than using multiple unions. Anyways many thanks for your prompt response. Keep Going.
Best Regards,
Amit
Chris Saxon
January 31, 2017 - 12:01 pm UTC

It's still one query even if you have unions!

You can do it with joins instead, but it depends upon the format you want the output in.

column ambiguously defined error

Leon, March 03, 2023 - 6:43 pm UTC

When I try to run the same code in Toad, I get column ambiguously defined error:
select * from (
select 0 lev,deptno,dname,null ename,null sal
from scott.dept
union all
select 1 lev, deptno, null dname,a.ename,a.sal
from scott.emp a
)
start with lev = 0
connect by prior lev = lev - 1
and prior deptno = deptno;
Chris Saxon
March 06, 2023 - 3:29 pm UTC

I'm unable to reproduce this.

Which version of Oracle Database are you using? What happens if you run this in another client (e.g. SQL Developer)?

Just for fun

Chuck Jolley, March 06, 2023 - 10:03 pm UTC

with dta as
(select grouping(d.deptno) g1,
       d.deptno,
       grouping(d.dname) g2, 
       d.dname,
       grouping(e.ename) g3, 
       e.ename, 
       grouping(e.sal) g4,
       sum(e.sal) sal
  from scott.dept d,
       scott.emp e
 where e.deptno = d.deptno
 group by rollup(d.deptno, d.dname, e.ename, e.sal))
 select decode(g3, 1,0,1) lev,
        deptno,
        decode(g3, 1, dname, '') dname,
        ename,
        decode(g3, 0, sal, to_number(null)) sal
   from dta
  where g2 = 0
    and g4 = 1       
 order by deptno, g3 desc, ename   
>/

       LEV     DEPTNO DNAME          ENAME             SAL
---------- ---------- -------------- ---------- ----------
         0         10 ACCOUNTING
         1         10                CLARK            4900
         1         10                KING            10000
         1         10                MILLER           2600
         0         20 RESEARCH
         1         20                ADAMS            2200
         1         20                FORD             6000
         1         20                JONES            5950
         1         20                SCOTT            6000
         1         20                SMITH            1600
         0         30 SALES
         1         30                ALLEN            3200
         1         30                BLAKE            5700
         1         30                JAMES            1900
         1         30                MARTIN           2500
         1         30                TURNER           3000
         1         30                WARD             2500

17 rows selected.

Chuck Jolley, March 06, 2023 - 10:33 pm UTC

oops, I forgot about that pesky empty Operations dept so:

sselect decode(grouping(e.ename), 1, 0, 1) lev,
       d.deptno,
       d.dname,
       e.ename, 
       decode(grouping(ename), 0, sum(e.sal), to_number(null)) sal
  from scott.dept d,
       scott.emp e
 where e.deptno(+) = d.deptno
 group by rollup(d.deptno, d.dname, e.ename, e.sal) 
 having grouping(e.sal) = 1
    and grouping(d.dname) +  grouping(e.ename) < 2
    and not( grouping(e.ename) = 0 and ename is null)
  order by dname, grouping(e.ename) desc  

       LEV     DEPTNO DNAME          ENAME             SAL
---------- ---------- -------------- ---------- ----------
         0         10 ACCOUNTING
         1         10 ACCOUNTING     MILLER           2600
         1         10 ACCOUNTING     KING            10000
         1         10 ACCOUNTING     CLARK            4900
         0         40 OPERATIONS
         0         20 RESEARCH
         1         20 RESEARCH       SCOTT            6000
         1         20 RESEARCH       JONES            5950
         1         20 RESEARCH       ADAMS            2200
         1         20 RESEARCH       FORD             6000
         1         20 RESEARCH       SMITH            1600
         0         30 SALES
         1         30 SALES          ALLEN            3200
         1         30 SALES          BLAKE            5700
         1         30 SALES          JAMES            1900
         1         30 SALES          MARTIN           2500
         1         30 SALES          TURNER           3000
         1         30 SALES          WARD             2500

18 rows selected.



Connor McDonald
March 07, 2023 - 1:48 am UTC

Nice :-)

Union join

Stew Ashton, March 07, 2023 - 7:27 am UTC

The original request seems to describe the result of what is called a "union join", which basically does a UNION ALL but puts the data from each table in different columns. Oracle does not have UNION JOIN in its syntax, but we can get the same result using FULL JOIN ON 1=0. To make sense of the result, just COALESCE on DEPTNO and sort.
select coalesce(e.DEPTNO, d.DEPTNO) deptno, DNAME, LOC,
  EMPNO, ENAME, JOB, MGR, SAL, COMM
from dept d
full join emp e on 1=0
order by deptno, empno nulls first;

    DEPTNO DNAME          LOC                EMPNO ENAME      JOB              MGR        SAL       COMM
---------- -------------- ------------- ---------- ---------- --------- ---------- ---------- ----------
        10 ACCOUNTING     NEW YORK                                                                      
        10                                    7782 CLARK      MANAGER         7839       2450           
        10                                    7839 KING       PRESIDENT                  5000           
        10                                    7934 MILLER     CLERK           7782       1300           
        20 RESEARCH       DALLAS                                                                        
        20                                    7369 SMITH      CLERK           7902        800           
        20                                    7566 JONES      MANAGER         7839       2975           
        20                                    7788 SCOTT      ANALYST         7566       3000           
        20                                    7876 ADAMS      CLERK           7788       1100           
        20                                    7902 FORD       ANALYST         7566       3000           
        30 SALES          CHICAGO                                                                       
        30                                    7499 ALLEN      SALESMAN        7698       1600        300
        30                                    7521 WARD       SALESMAN        7698       1250        500
        30                                    7654 MARTIN     SALESMAN        7698       1250       1400
        30                                    7698 BLAKE      MANAGER         7839       2850           
        30                                    7844 TURNER     SALESMAN        7698       1500          0
        30                                    7900 JAMES      CLERK           7698        950           
        40 OPERATIONS     BOSTON

Chris Saxon
March 07, 2023 - 3:08 pm UTC

Thanks for sharing Stew

Chuck Jolley, March 07, 2023 - 2:56 pm UTC

Cleaned it up a bit when I had more time.
The trick is to produce every row you could possibly need
and then use group by rollup to produce grouping numbers
you can use to filter out what you don't need or want.
  1  select decode(grouping(e.ename), 0, 'D', 'H') lev,
  2         decode(grouping(e.ename), 1, d.deptno, '') deptno,
  3         decode( grouping(e.ename), 1, d.dname, '') dname,
  4         e.ename,
  5         decode(grouping(e.ename), 0, e.deptno, '') emp_deptno,
  6         decode(grouping(ename), 0, sum(e.sal), to_number(null)) sal
  7    from scott.dept d,
  8         scott.emp e
  9   where e.deptno(+) = d.deptno
 10   group by rollup(d.deptno, d.dname, e.deptno, e.ename, e.sal)
 11   having grouping(e.sal) = 1
 12      and grouping(d.dname) +  grouping(e.ename) < 2
 13      and not( grouping(e.ename) = 0 and ename is null)
 14      and grouping(e.deptno) <> 1
 15*   order by d.deptno, e.ename nulls first
>/

L     DEPTNO DNAME          ENAME      EMP_DEPTNO        SAL
- ---------- -------------- ---------- ---------- ----------
H         10 ACCOUNTING
D                           CLARK              10       4900
D                           KING               10      10000
D                           MILLER             10       2600
H         20 RESEARCH
D                           ADAMS              20       2200
D                           FORD               20       6000
D                           JONES              20       5950
D                           SCOTT              20       6000
D                           SMITH              20       1600
H         30 SALES
D                           ALLEN              30       3200
D                           BLAKE              30       5700
D                           JAMES              30       1900
D                           MARTIN             30       2500
D                           TURNER             30       3000
D                           WARD               30       2500
H         40 OPERATIONS

Chris Saxon
March 07, 2023 - 3:05 pm UTC

Great work

Grouping sets

Rajeshwaran Jeyabal, March 10, 2023 - 5:44 am UTC

Sorry for late in the game, with "grouping sets" in place.
demo@PDB1> select decode(grouping_id(e.ename),0,'D','H') as display_level,
  2      d.deptno as deptno, d.dname,e.ename,e.deptno emp_deptno,e.sal
  3  from scott.dept d,
  4     scott.emp e
  5  where d.deptno = e.deptno (+)
  6  group by grouping sets( (d.deptno, d.dname,e.ename,e.deptno,e.sal) ,(d.deptno) )
  7  having( grouping_id(d.deptno, d.dname,e.ename,e.deptno,e.sal)  > 0
  8          or count(e.empno) > 0 )
  9  order by nvl(d.deptno,e.deptno) , d.dname nulls first
 10  /

D     DEPTNO DNAME          ENAME      EMP_DEPTNO        SAL
- ---------- -------------- ---------- ---------- ----------
H         10
D         10 ACCOUNTING     CLARK              10       2450
D         10 ACCOUNTING     MILLER             10       1300
D         10 ACCOUNTING     KING               10       5000
H         20
D         20 RESEARCH       SCOTT              20       3000
D         20 RESEARCH       SMITH              20        800
D         20 RESEARCH       ADAMS              20       1100
D         20 RESEARCH       JONES              20       2975
D         20 RESEARCH       FORD               20       3000
H         30
D         30 SALES          JAMES              30        950
D         30 SALES          BLAKE              30       2850
D         30 SALES          MARTIN             30       1250
D         30 SALES          TURNER             30       1500
D         30 SALES          WARD               30       1250
D         30 SALES          ALLEN              30       1600
H         40

18 rows selected.

Connor McDonald
March 14, 2023 - 1:13 am UTC

grouping_sets and grouping_id are underrated features


Chuck Jolley, March 10, 2023 - 8:49 pm UTC

Run the with clause alone to see what the final select does:
select d.deptno,
           grouping(d.deptno) deptnogrp,
           d.dname,
           grouping(d.dname) dnamegrp,       
           e.ename,
           grouping(e.ename) enamegrp,        
           sum(e.sal) sal,
           grouping(e.sal) salgrp       
      from scott.dept d,
           scott.emp e
     where e.deptno(+) = d.deptno
     group by rollup(d.deptno, d.dname, e.ename, e.sal))
select decode(enamegrp, 1, 'H', 'D') lev,
       decode(enamegrp, 1, deptno, '') deptno,
       decode(enamegrp, 1, dname, '') dname,
       ename,
       decode(enamegrp, 0, deptno, '') emp_deptno,
       decode(enamegrp, 0, sal, to_number(null)) sal
  from dta 
 where salgrp = 1 
   and not (dnamegrp = 1 and enamegrp = 1)
   and not (enamegrp = 0 and ename is null)
 order by dta.deptno, dta.ename nulls first

Connor McDonald
March 14, 2023 - 1:13 am UTC

nifty

Sorry, left part out

Chuck Jolley, March 10, 2023 - 8:52 pm UTC

with dta
as (select d.deptno,
           grouping(d.deptno) deptnogrp,
           d.dname,
           grouping(d.dname) dnamegrp,       
           e.ename,
           grouping(e.ename) enamegrp,        
           sum(e.sal) sal,
           grouping(e.sal) salgrp       
      from scott.dept d,
           scott.emp e
     where e.deptno(+) = d.deptno
     group by rollup(d.deptno, d.dname, e.ename, e.sal))
select decode(enamegrp, 1, 'H', 'D') lev,
       decode(enamegrp, 1, deptno, '') deptno,
       decode(enamegrp, 1, dname, '') dname,
       ename,
       decode(enamegrp, 0, deptno, '') emp_deptno,
       decode(enamegrp, 0, sal, to_number(null)) sal
  from dta 
 where salgrp = 1 
   and not (dnamegrp = 1 and enamegrp = 1)
   and not (enamegrp = 0 and ename is null)
 order by dta.deptno, dta.ename nulls first;