Tuning

동일 패턴의 스칼라 서브쿼리 형태 인라인 뷰로 통합으로 성능 향상 사례

돼지와함께춤을 2025. 5. 28. 17:03

운영 쿼리를 보다 보면 작성자들은 스칼라 서브쿼리를 참 좋아한다.

몰론 운영 쿼리는 초기 작성 상태가 현재까지 유지되는게 아니라

사용자 요구에 따라, 변화에 따라 몇년간 수정에 수정을 가해 지금 상태의 아주 길고 복잡한 쿼리가 된건 알지만..

 

WAS에 올린 쿼리가 아닌 SQL을 보관했다 정기적으로 실행하여 데이터를 추출 전달하는 1회성 쿼리를 보아도

지나치게 스칼라 서브쿼리를 좋아하는 경향이 있는것 같다.

이유는 딱 한가지, 스칼라 서브쿼리는 SELECT 절 밑에다 덧붙이기 쉬우니까.. 

 

하지만 이러한 SQL작성 습관은  추후 악성쿼리를 유발하기 쉽다.

그래서 처음부터 설계시 동일 패턴의 스칼라서브쿼리를 통한 중복 엑세스를 방지 하기 위해

인라인뷰를 활용해보는것도 좋을 것 같다.

 


아래는 한가지 사례를 들고왔다, 원래는 스칼라 서브쿼리가 7개 정도 더 있어 3배는 길었지만 가시성을 위해 짤랐다..

해당 업무 프로세스와 모델 아키텍처를 몰라도 , 실행계획을 까보기 전에도 비효율적인 부분을 볼 수 있을것이다.

  • 원본 SQL
select clbs.청구년월 
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and exists (select * 
                         from 마스터
                        where 기관코드 = d.기관코드
                          and 코드 = d.단일코드
                          and d.시행시작일자 between 코드시작일자 and 코드종료일자
                          and 코드 like 'D%'
                          and earncls1 = '05')) as inj
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and 품목코드 in ('V200','V210','V220','V300','V310','V320')) as er
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and substr(품목코드, 0 ,5) in ('A200','A010')) as homecare
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and exists (select * 
                         from 마스터
                        where 기관코드 = d.기관코드
                          and 코드 = d.단일코드
                          and d.시행시작일자 between 코드시작일자 and 코드종료일자
                          and 코드 like 'D%'
                          and earncls1 = '05'
                          and earncls2 = '09')) as inj_cancer
     , (select listagg(distinct p.특정내역, ',') within group (order by p.특정내역)
          from 사후심사 d, 사후심사특정 p
         where d.기관코드 = clbs.기관코드
           and d.청구년월 = clbs.청구년월
           and d.청구차수 = clbs.청구차수
           and d.사번 = clbs.사번
           and d.사번sn = clbs.사번sn
           and d.rowstat != 'D'
           and d.품목번호 > 0
           and d.기관코드 = p.기관코드
           and d.청구년월 = p.청구년월
           and d.청구차수 = p.청구차수
           and d.사번 = p.사번
           and d.사번sn = p.사번sn
           and d.순번 = p.clod순번
           and p.특정코드 = 'JS002'
           and p.특정내역 in ('41','45','55','99')
           and p.rowstat != 'D'
           and exists (select * 
                         from 마스터
                        where 기관코드 = d.기관코드
                          and 코드 = d.단일코드
                          and d.시행시작일자 between 코드시작일자 and 코드종료일자
                          and earncls1 = '05')) JS002_1
     , (select listagg(distinct p.특정내역, ',') within group (order by p.특정내역)
          from 사후심사 d, 사후심사특정 p
         where d.기관코드 = clbs.기관코드
           and d.청구년월 = clbs.청구년월
           and d.청구차수 = clbs.청구차수
           and d.사번 = clbs.사번
           and d.사번sn = clbs.사번sn
           and d.rowstat != 'D'
           and d.품목번호 > 0
           and d.기관코드 = p.기관코드
           and d.청구년월 = p.청구년월
           and d.청구차수 = p.청구차수
           and d.사번 = p.사번
           and d.사번sn = p.사번sn
           and d.순번 = p.clod순번
           and p.특정코드 = 'JS002'
           and p.특정내역 not in ('41','45','55','99')
           and p.rowstat != 'D'
           and exists (select * 
                         from 마스터
                        where 기관코드 = d.기관코드
                          and 코드 = d.단일코드
                          and d.시행시작일자 between 코드시작일자 and 코드종료일자
                          and earncls1 = '05')) JS002_2
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and 단일코드 in ('A', 'B')) as expdrug
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and exists (select * 
                         from 마스터
                        where 기관코드 = d.기관코드
                          and 코드 = d.단일코드
                          and d.시행시작일자 between 코드시작일자 and 코드종료일자
                          and earncls1 = '05'
                          and 코드 like 'D%')
           and 단일코드 not in ('C', 'D')) as expdrug_ex
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and exists (select * 
                         from 마스터
                        where 기관코드 = d.기관코드
                          and 코드 = d.단일코드
                          and d.시행시작일자 between 코드시작일자 and 코드종료일자
                          and earncls1 = '05'
                          and earncls2 = '11')) as inj_antibio
  from 사후심사명세서 clbs, 사후심사상병 cldi
 where clbs.기관코드 = '016'
   and substr(clbs.심사차수,1,6) in  ('202307')
   and clbs.ioflag = 'O'
   and clbs.종류 in ('11','21','22')
   and clbs.rowstat != 'D'
   and clbs.acptno != '-'
   and clbs.기관코드 = cldi.기관코드(+)
   and clbs.청구년월 = cldi.청구년월(+)
   and clbs.청구차수 = cldi.청구차수(+)
   and clbs.사번 = cldi.사번(+)
   and clbs.사번sn= cldi.사번sn(+)
   and cldi.diagseq(+) = 1
   and cldi.rowstat(+) != 'D';

  • 1번째 튜닝 포인트
    스칼라 서브쿼리 구성이 크게 "사후심사" 혹은 "사후심사" + "사후심사특정" 테이블에 동일한 조인 패턴으로 접근한다는 것
    그렇다면 스칼라 서브쿼리 마다 엑세스 하기보단 인라인뷰로 만들고 꺼내서 쓰자.
  • 조인 패턴은 같지만 추가적인 필터 조건은 다른데 어떻게 인라인뷰로 구성할것인가?
    인라인뷰에 공통적인 조인 패턴은 기술, 필터는 CASE문 조건으로 필터링하자.

  • 어떻게 인라인뷰를 묶어 구성해야 할까?
    답은 없다, 효율적인 묶음을 위해 먼저 각 스칼라 서브쿼리가 엑세스 하는 테이블을 정리해보면
    inj : 사후심사 -> 마스터(exists)
    er : 사후심사
    homecare : 사후심사
    inj_cancer : 사후심사 -> 마스터(exists)
    expdrug : 사후심사
    expdrug_ex : 사후심사 -> 마스터(exists)
    inj_antibio : 사후심사 -> 마스터(exists)
    JS002_1 : 사후심사, 심사특정 -> 마스터(exists)
    JS002_2 : 사후심사, 심사특정 -> 마스터(exists)

    나같은 경우 inj ~ inj_antibio 까지를 하나의 뷰, JS002_1 JS002_2를 하나의 뷰로 2가지 인라인뷰로 구성하였다
  • inj 와 er은 마스터를 exists조인하는 차이가 있는데 어떻게 하나의 뷰로 구성해? 
    위 쿼리의 경우는 가능하다

    왜냐하면 스칼라 서브쿼리에 집계 컬럼이 count 결과가 0보다 크냐 즉 만족하는 데이터가 있냐이기 때문에
    1:m구조는 신경쓰지 않아 exists절을 left outer join으로 변환할 것이다

    이때 1:M 구조는 그렇다 쳐도 마스터가 존재하지 않을시 메인집합이 사라지는 exists와 보존되는 outer join에 차이도
    이 경우엔 결과가 달라지지 않는다
    마스터 테이블과 join 후 count() 시 내부에 마스터 컬럼(필터조건)에 대한 case조건을 걸어서 counting 할것이다

    단 만약 마스터 테이블에 필터조건(and 코드 like 'D%' and earncls1 = '05'  )없이
    엑세스 조건( 기관코드 = d.기관코드 and 코드 = d.단일코드 and d.시행시작일자 between 코드시작일자 and 코드종료일자)
    로만 구성되있다면 마스터테이블의 not null컬럼 (pk도 좋고 아무거나) 에 is not null CASE조건을 기술하자.

  • 이해를 위한 inj, er 에 관해서만 (AS IS)
select clbs.청구년월 
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and exists (select * 
                         from 마스터
                        where 기관코드 = d.기관코드
                          and 코드 = d.단일코드
                          and d.시행시작일자 between 코드시작일자 and 코드종료일자
                          and 코드 like 'D%'
                          and earncls1 = '05')) as inj
     , (select case when count(*) > 0 then 'Y' else null end
          from 사후심사 d
         where 기관코드 = clbs.기관코드
           and 청구년월 = clbs.청구년월
           and 청구차수 = clbs.청구차수
           and 사번 = clbs.사번
           and 사번sn = clbs.사번sn
           and rowstat != 'D'
           and 품목번호 > 0
           and 품목코드 in ('V200','V210','V220','V300','V310','V320')) as er
  from 사후심사명세서 clbs, 사후심사상병 cldi
 where clbs.기관코드 = '016'
   and substr(clbs.심사차수,1,6) in  ('202307')
   and clbs.ioflag = 'O'
   and clbs.종류 in ('11','21','22')
   and clbs.rowstat != 'D'
   and clbs.acptno != '-'
   and clbs.기관코드 = cldi.기관코드(+)
   and clbs.청구년월 = cldi.청구년월(+)
   and clbs.청구차수 = cldi.청구차수(+)
   and clbs.사번 = cldi.사번(+)
   and clbs.사번sn= cldi.사번sn(+)
   and cldi.diagseq(+) = 1
   and cldi.rowstat(+) != 'D';

 

  • 이해를 위한 inj, er 에 관해서만 (TO BE)
SELECT clbs.청구년월
     , CASE WHEN clod_mech_view.inj_count > 0 THEN 'Y' ELSE NULL END            AS inj
     , CASE WHEN clod_mech_view.er_count > 0 THEN 'Y' ELSE NULL END             AS er
FROM 사후심사명세서 clbs
   , 사후심사상병 cldi
   , (
       SELECT d.기관코드
            , d.청구년월
            , d.청구차수
            , d.사번
            , d.사번sn
            , COUNT(CASE WHEN d.품목코드 IN ('V200', 'V210', 'V220', 'V300', 'V310', 'V320') THEN 1 END) AS er_count
            , COUNT(CASE WHEN m.코드 LIKE 'D%' 
                         AND m.earncls1 = '05' THEN 1 END) AS inj_count
       FROM 사후심사 d
          , 마스터 m
       WHERE d.rowstat != 'D'
         AND d.품목번호 > 0
         AND m.기관코드(+) = d.기관코드
         AND m.코드(+) = d.단일코드
         AND d.시행시작일자 BETWEEN m.코드시작일자(+) AND m.코드종료일자(+)
       GROUP BY d.기관코드, d.청구년월, d.청구차수, d.사번, d.사번sn
   ) clod_mech_view
WHERE clbs.기관코드 = '016'
  AND clbs.심사차수 LIKE '202307%'
  AND clbs.ioflag = 'O'
  AND clbs.종류 IN ('11','21','22')
  AND clbs.rowstat != 'D'
  AND clbs.acptno != '-'
  
  AND clbs.기관코드 = cldi.기관코드(+)
  AND clbs.청구년월 = cldi.청구년월(+)
  AND clbs.청구차수 = cldi.청구차수(+)
  AND clbs.사번 = cldi.사번(+)
  AND clbs.사번sn = cldi.사번sn(+)
  AND cldi.diagseq(+) = 1
  and cldi.rowstat(+) != 'D'
  
  AND clbs.기관코드 = clod_mech_view.기관코드(+)
  AND clbs.청구년월 = clod_mech_view.청구년월(+)
  AND clbs.청구차수 = clod_mech_view.청구차수(+)
  AND clbs.사번    = clod_mech_view.사번(+)
  AND clbs.사번sn  = clod_mech_view.사번sn(+)

 

 

  • 2번째 튜닝포인트
    번외로 인덱스 정보나 실행계획을 적진 않아서 찾지 못했겠지만 인덱스 컬럼 조건의 좌변컬럼은 가공하지 않는다가 기본이다

    메인 쿼리 조건에서  심사차수 조건을 가공하여 다른 비효율적인 인덱스를 사용하고 있길래
    담당 파트관리자에게 clbs.심사차수 LIKE '202307% 로 변환에도 결과집합이 동일한지 확인하여 변환하였다.

  • 최종 SQL
SELECT clbs.청구년월
     , CASE WHEN clod_mech_view.inj_count > 0 THEN 'Y' ELSE NULL END            AS inj
     , CASE WHEN clod_mech_view.er_count > 0 THEN 'Y' ELSE NULL END             AS er
     , CASE WHEN clod_mech_view.homecare_count > 0 THEN 'Y' ELSE NULL END       AS homecare
     , CASE WHEN clod_mech_view.inj_cancer_count > 0 THEN 'Y' ELSE NULL END     AS inj_cancer   
     , CASE WHEN clod_mech_view.expdrug_count > 0 THEN 'Y' ELSE NULL END        AS expdrug
     , CASE WHEN clod_mech_view.expdrug_ex_count > 0 THEN 'Y' ELSE NULL END     AS expdrug_ex
     , CASE WHEN clod_mech_view.inj_antibio_count > 0 THEN 'Y' ELSE NULL END    AS inj_antibio
     , JS002_view.JS002_1_list                                                  AS JS002_1 
     , JS002_view.JS002_2_list                                                  AS JS002_2  
FROM 사후심사명세서 clbs
   , 사후심사상병 cldi
   , (
       SELECT d.기관코드
            , d.청구년월
            , d.청구차수
            , d.사번
            , d.사번sn
            , COUNT(CASE WHEN d.품목코드 IN ('V200', 'V210', 'V220', 'V300', 'V310', 'V320') THEN 1 END) AS er_count
            , COUNT(CASE WHEN SUBSTR(d.품목코드, 1, 5) IN ('A200', 'A010') THEN 1 END) AS homecare_count
            , COUNT(CASE WHEN m.코드 LIKE 'D%' 
                         AND m.earncls1 = '05' 
                         AND m.earncls2 = '09' THEN 1 END) AS inj_cancer_count
            , COUNT(CASE WHEN m.코드 LIKE 'D%' 
                         AND m.earncls1 = '05' THEN 1 END) AS inj_count
            , COUNT(CASE WHEN d.단일코드 in ('A', 'B')  
                        THEN 1 END) AS expdrug_count
            , COUNT(CASE WHEN d.단일코드 not in ('C', 'D')
                        AND m.earncls1 = '05'
                        AND m.코드 like 'D%'
                        THEN 1 END ) AS expdrug_ex_count
            , COUNT(CASE WHEN m.earncls1 = '05'
                          AND m.earncls2 = '11'
                         THEN 1 END ) as inj_antibio_count
       FROM 사후심사 d
          , 마스터 m
       WHERE d.rowstat != 'D'
         AND d.품목번호 > 0
         AND m.기관코드(+) = d.기관코드
         AND m.코드(+) = d.단일코드
         AND d.시행시작일자 BETWEEN m.코드시작일자(+) AND m.코드종료일자(+)
       GROUP BY d.기관코드, d.청구년월, d.청구차수, d.사번, d.사번sn
   ) clod_mech_view
   , (
       SELECT d.기관코드
            , d.청구년월
            , d.청구차수
            , d.사번
            , d.사번sn
            , LISTAGG(DISTINCT CASE WHEN p.특정내역 IN ('41', '45', '55', '99') THEN p.특정내역 END, ',') WITHIN GROUP (ORDER BY p.특정내역) AS JS002_1_list
            , LISTAGG(DISTINCT CASE WHEN p.특정내역 NOT IN ('41', '45', '55', '99') THEN p.특정내역 END, ',') WITHIN GROUP (ORDER BY p.특정내역) AS JS002_2_list
       FROM 사후심사 d, 사후심사특정 p
       WHERE d.rowstat != 'D'
         AND d.품목번호 > 0
         and d.기관코드 = p.기관코드
         and d.청구년월 = p.청구년월
         and d.청구차수 = p.청구차수
         and d.사번 = p.사번
         and d.사번sn = p.사번sn
         and d.순번 = p.clod순번
         AND d.순번 = p.clod순번
         AND p.특정코드 = 'JS002'
         AND p.rowstat != 'D'
         AND EXISTS (select * 
                         from 마스터
                        where 기관코드 = d.기관코드
                          and 코드 = d.단일코드
                          and d.시행시작일자 between 코드시작일자 and 코드종료일자
                          and earncls1 = '05')
         
       GROUP BY d.기관코드, d.청구년월, d.청구차수, d.사번, d.사번sn
   ) JS002_view
WHERE clbs.기관코드 = '016'
  AND clbs.심사차수 LIKE '202307%'
  AND clbs.ioflag = 'O'
  AND clbs.종류 IN ('11','21','22')
  AND clbs.rowstat != 'D'
  AND clbs.acptno != '-'
  
  AND clbs.기관코드 = cldi.기관코드(+)
  AND clbs.청구년월 = cldi.청구년월(+)
  AND clbs.청구차수 = cldi.청구차수(+)
  AND clbs.사번 = cldi.사번(+)
  AND clbs.사번sn = cldi.사번sn(+)
  AND cldi.diagseq(+) = 1
  and cldi.rowstat(+) != 'D'
  
  AND clbs.기관코드 = clod_mech_view.기관코드(+)
  AND clbs.청구년월 = clod_mech_view.청구년월(+)
  AND clbs.청구차수 = clod_mech_view.청구차수(+)
  AND clbs.사번    = clod_mech_view.사번(+)
  AND clbs.사번sn  = clod_mech_view.사번sn(+)
  
  AND clbs.기관코드 = JS002_view.기관코드(+)
  AND clbs.청구년월 = JS002_view.청구년월(+)
  AND clbs.청구차수 = JS002_view.청구차수(+)
  AND clbs.사번    = JS002_view.사번(+)
  AND clbs.사번sn  = JS002_view.사번sn(+);

  • 성능비교
    글이 길어지니 실행계획까지 비교하진 않고 STATISTICS만 비교
    그래도 큰수치의 논리적 I/O 블럭수이지만 수치상 7배를 개선하였다
  AS IS TO BE
recursive calls 164717 164731
db block gets 2 2
consistent gets 37086964 5531764
physical reads 4155337 150128
redo size 228897108 11547780
bytes sent via SQL*Net to client 8691973 8690380
bytes received via SQL*Net from client 45827 43039
SQL*Net roundtrips to/from client 824 824
sorts (memory)  164454 328904
sorts (disk) 0 0
rows processed 82226 82226

 


  • 검증
    검증은 전 후, 후 전 결과를 MINUS 연산으로 0이면 어느정도 결과집합을 보장한다라고 말할 수 있다.
    몰론 100%는 아니지만 더 좋은 검증방법은 모르겠다.
    전 후 결과를 TEMP 테이블에 저장하고 비교했다
(SELECT DATA2	,DATA3	,DATA4	,DATA5	,DATA6	,DATA7	,DATA8	,DATA9	,DATA10	,DATA11	,DATA12	,DATA13	,DATA14	,DATA15	,DATA16	,DATA17	,DATA18	,DATA19	,DATA20	,DATA21	,DATA22	,DATA23	,DATA24	,DATA25	,DATA26	,DATA27	,DATA28	,DATA29	,DATA30	,DATA31	,DATA32
 FROM TMPLJG WHERE DATA1 = '20241114_튜닝전'
MINUS 
SELECT DATA2	,DATA3	,DATA4	,DATA5	,DATA6	,DATA7	,DATA8	,DATA9	,DATA10	,DATA11	,DATA12	,DATA13	,DATA14	,DATA15	,DATA16	,DATA17	,DATA18	,DATA19	,DATA20	,DATA21	,DATA22	,DATA23	,DATA24	,DATA25	,DATA26	,DATA27	,DATA28	,DATA29	,DATA30	,DATA31	,DATA32 
FROM TMPLJG WHERE DATA1 = '20241114_튜닝후'
)
UNION ALL
(SELECT DATA2	,DATA3	,DATA4	,DATA5	,DATA6	,DATA7	,DATA8	,DATA9	,DATA10	,DATA11	,DATA12	,DATA13	,DATA14	,DATA15	,DATA16	,DATA17	,DATA18	,DATA19	,DATA20	,DATA21	,DATA22	,DATA23	,DATA24	,DATA25	,DATA26	,DATA27	,DATA28	,DATA29	,DATA30	,DATA31	,DATA32 
FROM TMPLJG WHERE DATA1 = '20241114_튜닝후'
MINUS 
SELECT DATA2	,DATA3	,DATA4	,DATA5	,DATA6	,DATA7	,DATA8	,DATA9	,DATA10	,DATA11	,DATA12	,DATA13	,DATA14	,DATA15	,DATA16	,DATA17	,DATA18	,DATA19	,DATA20	,DATA21	,DATA22	,DATA23	,DATA24	,DATA25	,DATA26	,DATA27	,DATA28	,DATA29	,DATA30	,DATA31	,DATA32 
FROM TMPLJG WHERE DATA1 = '20241114_튜닝전';
)