ROW를 COLUMN으로 변환방법 정리

Updated:


1. 개요

ROW를 COLUMN으로 변환은 SQL을 만들때 많이 나오는 방법중 하나입니다. 레코드 형태로 수직으로 나열되어 있는 데이타들을 칼럼형태로 바꾼다는 이야기입니다. 오라클에서는 DECODE/CASE사용방법, 계층형쿼리, PIVOT, Listagg, Xmlagg, Wm_concat 등의 방법이 있습니다. 이중 DECODE/CASE, 계층형쿼리는 모든 DB버젼에서, 9i에서는 Xmlagg, 10g에서는 Wm_concat, 11g 이상에서는 PIVOT, Listagg 함수가 사용가능합니다.

가. 예제 테이블

1) EMP 테이블

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
7369 SMITH CLERK 7902 1980/12/17 00:00:00 800   20
7499 ALLEN SALESMAN 7698 1981/02/20 00:00:00 1600 300 30
7521 WARD SALESMAN 7698 1981/02/22 00:00:00 1250 500 30
7566 JONES MANAGER 7839 1981/04/02 00:00:00 2975   20
7654 MARTIN SALESMAN 7698 1981/09/28 00:00:00 1250 1400 30
7698 BLAKE MANAGER 7839 1981/05/01 00:00:00 2850   30
7782 CLARK MANAGER 7839 1981/06/09 00:00:00 2450   10
7788 SCOTT ANALYST 7566 1987/04/19 00:00:00 3000   20
7839 KING PRESIDENT   1981/11/17 00:00:00 5000   10
7844 TURNER SALESMAN 7698 1981/09/08 00:00:00 1500 0 30
7876 ADAMS CLERK 7788 1987/05/23 00:00:00 1100   20
7900 JAMES CLERK 7698 1981/12/03 00:00:00 950   30
7902 FORD ANALYST 7566 1981/12/03 00:00:00 3000   20
7934 MILLER CLERK 7782 1982/01/23 00:00:00 1300   10

2) DEPT 테이블

DEPTNO DNAME LOC
10 ACCOUNTING} NEW YORK
20 RESEARCH DALLAS
30 SALES CHICAGO
40 OPERATIONS BOSTON


2. 사용예제

가. DECODE/CASE 사용

이 방법은 모든 RDBMS에서 사용 가능한 방법입니다. 10G 이전 버젼에서는 이 방법을 많이 사용했는데 속도는 wm_concat, listagg, pivot보다는 느리다. 실제 어느곳에서 테스트해보니 1만건을 전환하는데 decode/case는 6분, wm_cocnate 6초 정도로 속도차이가 많이 납니다.

아래의 1) SQL에서는 테스트 테이블을 만들지 않고 UNION ALL을 사용하여 간단하게 만들어 보았습니다. 2)번에서는 DECODE/CASE절을 사용하여 조건에 맞게끔 NAME1, NAME2, NAME3를 만들고나서 최종적으로 그룹함수의 특징인 NULL을 자동적으로 없애주는 특징을 이용하여 처리하면 홍길동, 김길동, 이길동이 수평적으로 연결되어 나옵니다. 결과값을 참고하기 바란다. 이때 MIN을 사용했는데 MAX를 사용하여도 무방합니다. 단지 NULL값만 없애주는 용도로 사용한것입니다.

1) SQL
SELECT *
FROM  (SELECT '홍길동' RETURN# 
       FROM DUAL 
       UNION ALL 
       SELECT '김길동' RETURN# 
       FROM DUAL 
       UNION ALL 
       SELECT '이길동' RETURN# 
       FROM DUAL) TAB; 

2) SQL
SELECT RETURN#  ,
       DECODE(RETURN#, '홍길동', RETURN#) "NAME_1"  ,
       DECODE(RETURN#, '김길동', RETURN#) "NAME_2"  ,
       DECODE(RETURN#, '이길동', RETURN#) "NAME_3"
FROM  (SELECT '홍길동' RETURN# 
       FROM DUAL 
       UNION ALL 
       SELECT '김길동' RETURN# 
       FROM DUAL 
       UNION ALL 
       SELECT '이길동' RETURN# 
       FROM DUAL) TAB; 

3) 최종 SQL
SELECT  MIN(NAME_1) || ', ' || MIN(NAME_2) || ', ' || MIN(NAME_3) AS NAME
FROM (SELECT RETURN#,
             DECODE(RETURN#, '홍길동', RETURN#) "NAME_1",
             DECODE(RETURN#, '김길동', RETURN#) "NAME_2"
            ,DECODE(RETURN#, '이길동', RETURN#) "NAME_3" 
FROM (SELECT '홍길동' RETURN#
      FROM DUAL
      UNION ALL
      SELECT '김길동' RETURN#
      FROM DUAL
      UNION ALL
      SELECT '이길동' RETURN#
      FROM DUAL) TAB) ;
             
  • 2)번 결과값
RETRUN# NAME#1 NAME#2 NAME#3
홍길동 홍길동    
김길동   김길동  
이길동     이길동
  • 최종 결과값
NAME
홍길동, 김길동, 이길동


나. DECODE/CASE 사용

이번에는 다른 예를 들어보겠습니다. 각 부서별 JOB별로 몇명인지를 구하도록 하라. 최종값은 아래와 같습니다.

먼저 1)번 SQL을 사용하여 EMP와 DEPT테이블에서 원하는 칼럼들을 가져온다. 1)번 결과값처럼 데이타가 추출되는데 이 테이타를 2)번 SQL처럼 DECODE/CASE절을 사용하여 각 JOB별로 필요 데이타를 추출한다. 그리고 최종적으로 SUM 함수로 데이타를 카운트하여 원하는 최종값을 추출한다. 많이 사용하는 SQL방식이니 기억하기 바랍니다.

DNAME CLERK MANAGER PRESIDENT ANALYST SALESMAN
ACCOUNTING 1 1 1 0 0
RESEARCH 2 1 0 2 0
SALES 1 1 0 0 4
1) SQL 
 select d.dname, e.job,count(e.empno) member
 from emp e, dept d
 where e.deptno = d.deptno
 group by d.dname,e.job
 order by d.dname;

2) SQL
 select dname,  
        decode(job,'CLERK',member,0) clerk,
        decode(job,'MANAGER',member,0) manager,
        decode(job,'PRESIDENT',member,0) president,
        decode(job,'ANALYST',member,0) analyst,
        decode(job,'SALESMAN',member,0) salesman
   from (select e.job, d.dname,count(e.empno) member
         from emp e, dept d
         where e.deptno = d.deptno
         group by d.dname,e.job
         order by d.dname);

3) SQL
select dname,
     sum(decode(job,'CLERK',member,0)) clerk,
     sum(decode(job,'MANAGER',member,0)) manager,
     sum(decode(job,'PRESIDENT',member,0)) president,
     sum(decode(job,'ANALYST',member,0)) analyst,
     sum(decode(job,'SALESMAN',member,0)) salesman
from ( select e.job, d.dname,count(e.empno) member
       from emp e, dept d
       where e.deptno = d.deptno
       group by d.dname,e.job
       order by d.dname)
group by dname;
 

1) SQL결과값

DNAME JOB MEMBER
ACCOUNTING CLERK 1
ACCOUNTING MANAGER 1
ACCOUNTING PRESIDENT 1
RESEARCH ANALYST 2
RESEARCH CLERK 2
RESEARCH MANAGER 1
SALES CLERK 1
SALES MANAGER 1
SALES SALESMAN 4

2) SQL결과값

DNAME CLERK MANAGER PRESIDENT ANALYST SALESMAN
ACCOUNTING 1 0 0 0 0
ACCOUNTING 0 1 0 0 0
ACCOUNTING 0 0 1 0 0
RESEARCH 0 0 0 2 0
RESEARCH 2 0 0 0 0
RESEARCH 0 1 0 0 0
SALES 1 0 0 0 0
SALES 0 1 0 0 0
SALES 0 0 0 0 4


다. PIVOT 사용

이번에는 11G 부터 지원이 되는 PIVOT함수를 사용하여 처리해보겠습니다. 이 함수는 놀랍게도 위의 나항의 1)번 SQL처럼 SUM(DECODE) 방식으로 내부적으로 처리된다. 아래의 2)번과 3)번 SQL 형태를 보면 쉽게 이해할수 있으리라 생각한다. PIVOT은 반대 개념인 UNPIVOT도 지원하고 있으나 LISTAGG는 UNLISTAGG 함수가 없습니다.

10G에서는 사실상 속도가 나오는 방법은 vm_concat 함수밖에 없는데 이 함수는 오라클에서 인증해주는 함수가 아니다란걸 알아야 한다. 속도 관련해서 bug가 있으며 10.2.0.4 window 버젼에서 10.2.0.5 버젼으로 패치하면 속도저하가 일어나므로 주의하여야 합니다.

1) SQL
 select  * 
 from (select d.dname, e.job,  e.empno
       from emp e, dept d
       where e.deptno = d.deptno)
             pivot ( count(empno) for job in ('CLERK' clerk ,'MANAGER'  manager,
                     'PRESIDENT' president,  'ANALYST'  analyst, 'SALESMAN' saleman)
                    );

2) PIVOT 사용
Select  * from t1 pivot ( sum(c3) for c2 in (1,2,3,4,5) );  

3) PIVOT 내부
Select  c1, 
       sum(case when (c2 = 1) then c3 end) as "1"sum(case when (c2 = 2) then c3 end) as "2"sum(case when (c2 = 3) then c3 end) as "3"sum(case when (c2 = 4) then c3 end) as "4"sum(case when (c2 = 5) then c3 end) as "5" 
from t1
group by c1;

  • 결과값
DNAME CLERK MANAGER PRESIDENT ANALYST SALESMAN
ACCOUNTING 1 1 1 0 0
RESEARCH 2 1 0 2 0
SALES 1 1 0 0 4


라. Listagg 사용

Listagg 함수는 11G 이상에서 ROW를 COLUMN으로 변환할때 사용할수 있는 가장 편리한 함수중 하나입니다. Grouping 하고자하는 칼럼을 Group By 로 묶은후 가로로 나열하고자 하는 칼럼을 Listagg에 명시후 Within Group 에서 나열 순서를 Order BY로 지정한다. 1)번, 2)번 SQL을 비교해보겠습니다.

LISTAGG[(,]) WITHIN GROUP(ORDER BY) [OVER (PARTITION BY)]

1) SQL
SELECT DEPTNO, LISTAGG(ENAME, ‘,‘) WITHIN GROUP(ORDER BY HIREDATE) AS AGG_ENAME
FROM EMP
GROUP BY DEPTNO;

2) SQL
SELECT DEPTNO, ENAME, HIREDATE,
     LISTAGG(ENAME, ‘,‘) WITHIN GROUP(ORDER BY HIREDATE) OVER(PARTITION BY DEPTNO) AS AGG_ENAME
FROM EMP;

1) 결과값

DEPTNO AGG_ENAME
10 CLARK,KING,MILLER
20 SMITH,JONES,FORD,SCOTT,ADAMS
30 ALLEN,WARD,BLAKE,TURNER,MARTIN,JAMES

2) 결과값

DEPTNO ENAME HIREDATE AGG_ENAME  
10 CLARK 1981/06/09 00:00:00 CLARK, KING, MILLER
10 KING 1981/11/17 00:00:00 CLARK, KING, MILLER
10 MILLER 1982/01/23 00:00:00 CLARK, KING, MILLER
20 SMITH 1980/12/17 00:00:00 SMITH, JONES, FORD, SCOTT, ADAMS
20 JONES 1981/04/02 00:00:00 SMITH, JONES, FORD, SCOTT, ADAMS
20 FORD 1981/12/03 00:00:00 SMITH, JONES, FORD, SCOTT, ADAMS
20 SCOTT 1987/04/19 00:00:00 SMITH, JONES, FORD, SCOTT, ADAMS
20 ADAMS 1987/05/23 00:00:00 SMITH, JONES, FORD, SCOTT, ADAMS
30 ALLEN 1981/02/20 00:00:00 ALLEN, WARD, BLAKE, TURNER, MARTIN, JAMES
30 WARD 1981/02/22 00:00:00 ALLEN, WARD, BLAKE, TURNER, MARTIN, JAMES
30 BLAKE 1981/05/01 00:00:00 ALLEN, WARD, BLAKE, TURNER, MARTIN, JAMES
30 TURNER 1981/09/08 00:00:00 ALLEN, WARD, BLAKE, TURNER, MARTIN, JAMES
30 MARTIN 1981/09/28 00:00:00 ALLEN, WARD, BLAKE, TURNER, MARTIN, JAMES
30 JAMES 1981/12/03 00:00:00 ALLEN, WARD, BLAKE, TURNER, MARTIN, JAMES


3. 실전 사용예제

wm_concat, listagg, pivot 같은 함수로 인해 속도가 갑자기 저하되면 어떻게 하겠습니까? 실제 일어났던 상황이었는데. 위쪽에 기술한 10.2.0.4 버젼을 0.5로 패치하니 잘 수행되던 wm_concat가 속도가 갑자기 저하되어 1만건 처리하는데 6초정도 소요되는게 3분으로 급격히 화면속도가 떨어졌습니다. DBMS의 특징은 한번 패치하면 패치를 취소하는게 상당히 어려운법이죠. 물론 파라미터를 사용해서 그 패치만 무효화하는 방법도 있지만 DBMS을 더이상 변경 안할려고 하는 그 상황에서 응급으로 대응한 SQL이 2번 SQL입니다.

아래의 가번 TEST SQL은 EMP테이블을 이용하여 테스트가능 하도록 만든 SQL으로 나번 실전SQL에 사용한 개념과 동일한 SQL입니다.

원테이블에서 데이타를 점검해보니 칼럼으로 변경해야할 대상 레코드건수가 각각 최대 6건이 넘지 않는다는 업무 조사를 하고나서 숫자를 자동으로 부여하게끔 row_number 함수를 사용하였습니다. 이 ranking 펑션에서 나오는 숫자를 flag로 삼아 바깥단에서 lead 펑션을 사용하여 데이타를 추출하고 연결자를 사용하여 데이타를 연결하였습니다.

가. TEST SQL

1) listagg 사용
SELECT deptno,  listagg(ename,', ') within group( order by hiredate)
FROM   emp
GROUP BY  deptno;

2) lead over() sql
select DEPTNO, ENAME
from   (SELECT DEPTNO,  
               RTRIM(ENAME||','||
               lead(ENAME, 1) OVER(partition by DEPTNO ORDER BY HIREDATE )||','||
               lead(ENAME, 2) OVER(partition by DEPTNO ORDER BY HIREDATE )||','||
               lead(ENAME, 3) OVER(partition by DEPTNO ORDER BY HIREDATE )||','||
               lead(ENAME, 4) OVER(partition by DEPTNO ORDER BY HIREDATE )||','||
               lead(ENAME, 5) OVER(partition by DEPTNO ORDER BY HIREDATE )||','||
               lead(ENAME, 6) OVER(partition by DEPTNO ORDER BY HIREDATE ), ' ,')
               as ename, flag
         from (select deptno,  ename,  sal, hiredate,
                      row_number() over(partition by deptno order by hiredate) flag
               FROM (SELECT deptno,  ename,  sal, hiredate
                     FROM  emp
                     ) A ) )
where  flag = 1
order by 1, 2 ;

DEPTNO ENAME
10 KING, MILLER, CLARK
20 SCOTT, ADAMS, FORD, SMITH, JONES
30 JAMES, TURNER, BLAKE, ALLEN, WARD, MARTIN
나. 실전 SQL

1) 원 SQL
SELECT PJT_SEQ,  CKLT_REG_SEQ,  wm_concat(CL_ID_NM), wm_concat(CL_ID_ENG_NM)
FROM   QG_PJT_EXTR_ITEM
GROUP BY  PJT_SEQ,  CKLT_REG_SEQ ;

*12c에서는 wm_concat가 없어짐

2) 변형 SQL
select PJT_SEQ,   CKLT_REG_SEQ ,   CL_ID_NM as CL_ID_NM,   CL_ID_ENG_NM as CL_ID_ENG_NM
from   (SELECT PJT_SEQ,
               CKLT_REG_SEQ ,
               RTRIM(CL_ID_NM||','||
               lead(CL_ID_NM, 1) OVER(partition by PJT_SEQ, CKLT_REG_SEQ ORDER BY PJT_SEQ,CKLT_REG_SEQ, flag )||','||
               lead(CL_ID_NM, 2) OVER(partition by PJT_SEQ, CKLT_REG_SEQ ORDER BY PJT_SEQ, CKLT_REG_SEQ, flag )||','||
               lead(CL_ID_NM, 3) OVER(partition by PJT_SEQ, CKLT_REG_SEQ ORDER BY PJT_SEQ, CKLT_REG_SEQ, flag ),',') as CL_ID_NM,
               RTRIM(CL_ID_ENG_NM||','||
               lead(CL_ID_ENG_NM, 1) OVER(partition by PJT_SEQ, CKLT_REG_SEQ ORDER BY PJT_SEQ, CKLT_REG_SEQ, flag ) ||','||
               lead(CL_ID_ENG_NM, 2) OVER(partition by PJT_SEQ, CKLT_REG_SEQ   ORDER BY PJT_SEQ, CKLT_REG_SEQ, flag ) ||','||
               lead(CL_ID_ENG_NM, 3) OVER(partition by PJT_SEQ, CKLT_REG_SEQ   ORDER BY PJT_SEQ, CKLT_REG_SEQ, flag ),',')  as cL_ID_ENG_NM , flag
         from   (select PJT_SEQ,  CKLT_REG_SEQ,  CL_ID_NM, CL_ID_ENG_NM,
                        row_number() over(partition by PJT_SEQ, CKLT_REG_SEQ order by PJT_SEQ, CKLT_REG_SEQ, CL_ID_NM) flag
                 FROM   (SELECT PJT_SEQ,  CKLT_REG_SEQ,  CL_ID_NM,   CL_ID_ENG_NM
                         FROM   QG_PJT_EXTR_ITEM
                         order by PJT_SEQ, CKLT_REG_SEQ, CL_ID_NM, CL_ID_ENG_NM asc ) A ) )
where  flag = 1
order by 1, 2 ;

1) 결과값

PJT_SEQ CKLT_REG_SEQ CL_ID_NM CL_ID_ENG_NM
PJ200109.0012053 CR00304872 토목 Civil
PJ200109.0012053 CR00316367 공통가설,기타,전기(통신),토목 Common facilities,etc,Electrical,Civil
PJ200109.0012053 CR00317833 기타,전기(통신),토목 etc,Electrical,Civil
PJ200109.0012053 CR00318187 기타 Etc

2) 원테이블

PJT_SEQ CKLT_REG_SEQ CL_ID_NM CL_ID_ENG_NM flag
PJ200109.0012053 CR00304872 토목 Civil 1
PJ200109.0012053 CR00316367 공통가설 Common facilities 1
PJ200109.0012053 CR00316367 기타 etc 2
PJ200109.0012053 CR00316367 전기(통신) Electrical 3
PJ200109.0012053 CR00316367 토목 Civil 4
PJ200109.0012053 CR00317833 기타 etc 1
PJ200109.0012053 CR00317833 전기(통신) Electrical 2
PJ200109.0012053 CR00317833 토목 Civil 3
PJ200109.0012053 CR00318187 기타 Etc 1


4. 결언

이번에는 ROW를 COLUMN으로 변환방법을 정리해보았습니다. 워낙 많이 사용하는 기술이다 보니 이미 알고 있는 분들도 많을거라 생각합니다.