운영 쿼리를 보다 보면 작성자들은 스칼라 서브쿼리를 참 좋아한다.
몰론 운영 쿼리는 초기 작성 상태가 현재까지 유지되는게 아니라
사용자 요구에 따라, 변화에 따라 몇년간 수정에 수정을 가해 지금 상태의 아주 길고 복잡한 쿼리가 된건 알지만..
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_튜닝전';
)'Tuning' 카테고리의 다른 글
| 생각보다 자주 놓치는 간단한 집계 쿼리 튜닝 요소 (0) | 2025.06.09 |
|---|---|
| Oracle 실행 계획 (1) | 2025.06.04 |
| 셀프조인 없이 최종일자 중 최종번호 구하기 (2) | 2025.05.27 |