INSERT INTO TB_OPEN_REQ
SELECT LPAD(TO_CHAR(ROWNUM), 10, '0'),
RANDOM_DT,
TO_CHAR(TO_DATE(RANDOM_DT, 'YYYYMMDD') + DBMS_RANDOM.VALUE(1, 30), 'YYYYMMDD'),
LPAD(TO_CHAR(TRUNC(DBMS_RANDOM.VALUE(1, 10000))), 10, '0'),
SYSDATE,
'DBMSEXPERT'
FROM (
SELECT TO_CHAR(SYSDATE - DBMS_RANDOM.VALUE(1, 3650), 'YYYYMMDD') RANDOM_DT
FROM DUAL
) A,
DUAL
CONNECT BY LEVEL <= 1000000;
COMMIT;
TB_DISABLE_REQ 테이블 더미 데이터 입력
INSERT INTO TB_DISABLE_REQ
SELECT LPAD(TO_CHAR(ROWNUM), 10, '0'),
RANDOM_DT,
TO_DATE(RANDOM_DT || TO_CHAR(SYSDATE - 864000 / 24 / 60 / 60, 'HH24MISS'), 'YYYYMMDDHH24MISS'),
LPAD(TO_CHAR(TRUNC(DBMS_RANDOM.VALUE(1, 10000))), 10, '0'),
SYSDATE,
'DBMSEXPERT'
FROM (
SELECT TO_CHAR(SYSDATE - DBMS_RANDOM.VALUE(1, 3650), 'YYYYMMDD') RANDOM_DT
FROM DUAL
) A,
DUAL
CONNECT BY LEVEL <= 1000000;
COMMIT;
TB_JOB_ORDER 테이블 더미 데이터 입력
INSERT /*+ APPEND */ INTO TB_JOB_ORDER
SELECT LPAD(TO_CHAR(ROWNUM), 10, '0'),
TO_CHAR(SYSDATE - DBMS_RANDOM.VALUE(1, 3650), 'YYYYMMDD'),
LPAD(TO_CHAR(TRUNC(DBMS_RANDOM.VALUE(1, 10000))), 10, '0'),
LPAD(TO_CHAR(TRUNC(MOD(DBMS_RANDOM.VALUE(1, 10000), 10))), 2, '0'),
JOB_GUBUN,
REQ_NO,
SYSDATE,
'DBMSEXPERT'
FROM DUAL,
(SELECT '1' JOB_GUBUN, OPEN_REQ_NO REQ_NO
FROM TB_OPEN_REQ
UNION ALL
SELECT '2' JOB_GUBUN,
DISABLE_REQ_NO REQ_NO
FROM TB_DISABLE_REQ
) A;
COMMIT;
테이블 제약 조건 추가
ALTER TABLE TB_JOB_ORDER
ADD CONSTRAINT TB_JOB_ORDER_PK
PRIMARY KEY (JOB_NO);
ALTER TABLE TB_OPEN_REQ
ADD CONSTRAINT TB_OPEN_REQ_PK
PRIMARY KEY (OPEN_REQ_NO);
ALTER TABLE TB_DISABLE_REQ
ADD CONSTRAINT TB_DISABLE_REQ_PK
PRIMARY KEY (DISABLE_REQ_NO);
통계 정보 생성
ANALYZE TABLE TB_JOB_ORDER COMPUTE STATISTICS
FOR TABLE FOR ALL INDEXES FOR ALL INDEXED COLUMNS SIZE 254;
ANALYZE TABLE TB_OPEN_REQ COMPUTE STATISTICS FOR TABLE
FOR ALL INDEXES FOR ALL INDEXED COLUMNS SIZE 254;
ANALYZE TABLE TB_DISABLE_REQ COMPUTE STATISTICS FOR TABLE
FOR ALL INDEXES FOR ALL INDEXED COLUMNS SIZE 254;
아우터 조인으로 테이블 스캔을 최소화하여 성능 개선하기
튜닝 전
SELECT A.JOB_NO, A.WORKER_ID, A.JOB_STATUS_CD, A.REQ_NO
FROM TB_JOB_ORDER A,
TB_OPEN_REQ B
WHERE A.VISIT_PRE_DT = '20151102'
AND A.JOB_GUBUN = '1'
AND A.REQ_NO = B.OPEN_REQ_NO
UNION ALL
SELECT A.JOB_NO, A.WORKER_ID, A.JOB_STATUS_CD, A.REQ_NO
FROM TB_JOB_ORDER A,
TB_DISABLE_REQ B
WHERE A.VISIT_PRE_DT = '20151102'
AND A.JOB_GUBUN = '2'
AND A.REQ_NO = B.DISABLE_REQ_NO;
-- [2021-07-29 17:39:05] 239 rows retrieved starting from 1 in 173 ms (execution: 30 ms, fetching: 143 ms)
INSERT INTO TB_EXPORTER
SELECT LPAD(TO_CHAR(ROWNUM), 10, '0'),
DBMS_RANDOM.STRING('U', 50)
FROM DUAL
CONNECT BY LEVEL <= 100000;
COMMIT;
ALTER TABLE TB_TRD_DAY
NOLOGGING;
INSERT /*+ APPEND */ INTO TB_TRD_DAY A -- APPEND 힌트 사용
SELECT TO_CHAR(SYSDATE - TRUNC(DBMS_RANDOM.VALUE(0, 3650)), 'YYYYMMDD'),
LPAD(TO_CHAR(TRUNC(DBMS_RANDOM.VALUE(1, 9999))), 4, '0'),
LPAD(TO_CHAR(ROWNUM), 6, '0'),
TRUNC(DBMS_RANDOM.VALUE(1, 999)),
TRUNC(DBMS_RANDOM.VALUE(1, 999)),
B.EXPORTER_NO
FROM TB_EXPORTER B,
(SELECT LEVEL LV FROM DUAL CONNECT BY LEVEL <= 10);
COMMIT;
테이블 제약 조건 추가
ALTER TABLE TB_TRD_DAY
ADD CONSTRAINT TB_TRD_DAY_PK
PRIMARY KEY (TRD_DT, INSU_CD, INSU_DETAIL_CD);
ALTER TABLE TB_EXPORTER
ADD CONSTRAINT TB_EXPORTER_PK
PRIMARY KEY (EXPORTER_NO);
통계 정보 생성
ANALYZE TABLE TB_EXPORTER COMPUTE STATISTICS
FOR TABLE FOR ALL INDEXES FOR ALL INDEXED COLUMNS SIZE 254;
ANALYZE TABLE TB_TRD_DAY COMPUTE STATISTICS
FOR TABLE FOR ALL INDEXES FOR ALL INDEXED COLUMNS SIZE 254;
튜닝 전
SELECT /*+ LEADING(A) USE_NL(B) INDEX(A TB_TRD_DAY_PK) */
A.EXPORTER_NO,
B.EXPORTER_NM,
SUM(A.TRD_CNT - A.CNCL_CNT) TOT_CNT
FROM TB_TRD_DAY A
LEFT OUTER JOIN TB_EXPORTER B
ON (A.EXPORTER_NO = B.EXPORTER_NO)
AND A.TRD_DT BETWEEN '20100101' AND '20151231'
GROUP BY A.EXPORTER_NO, B.EXPORTER_NM
ORDER BY A.EXPORTER_NO;
-- [2021-07-29 18:18:15] 199,678 rows retrieved starting from 1 in 12 s 522 ms (execution: 7 s 29 ms, fetching: 5 s 493 ms)
SQL 분석
LEADING 힌트를 이용하여 TB_TRD_DAY 테이블 Outer 테이블로 지정, USE_NL 힌트를 이용하여 TB_EXPORTER 테이블을 Inner 테이블로 지정하고 중첩 루프 조인 처리가 되도록 유도
INDEX 힌트를 이용하여 TB_DRD_DAY_PK 인덱스를 사용
TB_TRD_DAY 테이블을 기준으로 LEFT 아우터 조인 처리
TB_EXPORTER 테이블은 Outer 테이블로 설정
SQL 문제점
Outer 테이블인 TB_EXPORTER 테이블 내에 EXPORTER_NM 컬럼만 SELECT절에 있다.
이런 경우 TB_EXPORTER 테이블을 반드시 아우터 조인할 필요가 없으므로 스칼라 서브쿼리로 변환하면 성능 향상을 기대할 수 있다.
실행 계획
TB_TRD_DAY_PK 인덱스를 인덱스 풀 스캔(TABLE ACCESS FULL)
위 작업을 통해 나온 ROWID를 이용하여 TB_TRD_DAY 테이블을 테이블 랜덤 액세스(TABLE ACCESS BY INDEX ROWID)
TB_EXPORTER_PK 인덱스를 인덱스 유일 스캔(INDEX UNIQUE SCAN)
위 작업의 ROWID를 이용하여 TB_EXPORTER 테이블을 테이블 랜덤 액세스(TABLE ACCESS BY INDEX ROWID)
테이블 랜덤 액세스한 두 작업을 중첩 루프 아우터 조인(NESTED LOOPS OUTER)
SORT와 GROUP BY 연산을 수행
SELECT 연산을 수행
튜닝 후
SELECT X.EXPORTER_NO,
(SELECT B.EXPORTER_NM
FROM TB_EXPORTER B
WHERE B.EXPORTER_NO = X.EXPORTER_NO),
TOT_CNT
FROM (SELECT A.EXPORTER_NO,
SUM(A.TRD_CNT - A.CNCL_CNT) TOT_CNT
FROM TB_TRD_DAY A
WHERE A.TRD_DT BETWEEN '20100101' AND '20151231'
GROUP BY A.EXPORTER_NO
ORDER BY A.EXPORTER_NO
) X;
-- [2021-07-29 18:17:43] 99,704 rows retrieved starting from 1 in 4 s 313 ms (execution: 392 ms, fetching: 3 s 921 ms)
SQL 분석
TB_TRD_DAY 테이블을 인라인 뷰를 이용하여 원하는 데이터를 조회
스칼라 서브쿼리를 이용하여 TB_EXPORTER 테이블을 스캔하여 EXPORTER_NM을 조회, 스칼라 서브쿼리의 캐싱 효과로 인하여 성능 향상
실행 계획
TB_TRD_DAY 테이블을 테이블 풀 스캔(옵티마이저의 판단)
SORT와 GROUP BY 연산을 수행
해당 결과를 인라인 뷰(View)로 생성
TB_EXPORTER_PK 인덱스를 인덱스 유일 스캔(INDEX UNIQUE SCAN)
EXPORTER_NM 컬럼의 값을 가져오기 위해 테이블 랜덤 액세스(TABLE ACCESS BY INDEX ROWID) 수행