Skip to content
튜닝 연습서브쿼리와 쿼리 변환
SUBQ-018

점주 앱 장애의 재발을 막겠다며 DBA가 오늘 밤 옵티마이저 파라미터 하나를 시스템 전체에 반영하겠다고 한다

실전SELECT버거253 / 700

상황 ​

가맹점주 앱 「최근 주문 20건」이 19c 업그레이드 뒤 스칼라 서브쿼리 Unnesting 때문에 느려진 일이 있었다(Buffers 104 → 60,310, SUBQ-011). DBA는 재발을 막겠다며 오늘 밤 ALTER SYSTEM SET "_optimizer_unnest_scalar_sq" = FALSE를 반영하겠다고 통보했다. 박팀장은 반영 전에 스칼라 서브쿼리를 쓰는 주요 SQL 셋을 보고 판단해 달라고 했다. 아래는 지금(기본값)과, 복사본 DB에서 파라미터를 끈 뒤 잰 숫자다.

테이블과 인덱스 ​

  • ORD: 1,000,000행 · 22,422블록 · 날짜순으로 쌓여 있다 · ORD_X01 (ORD_DT, ORD_HMS) · ORD_X02 (SHOP_NO, ORD_DT, ORD_HMS) · 한 달 약 41,700건 · 매장마다 약 5,000건(한 달 약 208건)
  • ORD_ITEM: 2,472,717행 · 55,265블록 · ORD_ITEM_PK (ORD_NO, ITEM_ID) · ORD_ITEM_X01 (ORD_DT, ORD_HMS) · 한 달 약 103,000줄 · 하루 약 3,400줄
  • SHOP: 200행 · 5블록 · ITEM: 92행 · 5블록 · 둘 다 PK
  • Q1 하루 1만 번, Q2 하루 20번(본사 월 리포트), Q3 하루 200번 · 통계 최신

원본 SQL ​

sql
-- Q1 가맹점주 앱 「최근 주문 20건」 (하루 1만 번)
SELECT *
  FROM (SELECT o.ORD_NO, o.ORD_DT, o.ORD_HMS,
               (SELECT SUM(i.ORD_ITEM_QTY) FROM ORD_ITEM i WHERE i.ORD_NO = o.ORD_NO) AS qty
          FROM ORD o
         WHERE o.SHOP_NO = :shop
         ORDER BY o.ORD_DT DESC, o.ORD_HMS DESC)
 WHERE ROWNUM <= 20;

-- Q2 본사 「매장별 월 판매 수량」 (하루 20 번)
SELECT s.SHOP_NO, s.SHOP_NM,
       (SELECT SUM(i.ORD_ITEM_QTY)
          FROM ORD o, ORD_ITEM i
         WHERE o.SHOP_NO = s.SHOP_NO
           AND o.ORD_DT BETWEEN :d1 AND :d2
           AND i.ORD_NO = o.ORD_NO) AS qty
  FROM SHOP s
 ORDER BY s.SHOP_NO;

-- Q3 「일 상품 판매 순위」 (하루 200 번)
SELECT t.ITEM_ID,
       (SELECT m.ITEM_NM FROM ITEM m WHERE m.ITEM_ID = t.ITEM_ID) AS item_nm,
       t.qty
  FROM (SELECT ITEM_ID, SUM(ORD_ITEM_QTY) AS qty
          FROM ORD_ITEM
         WHERE ORD_DT = :d
         GROUP BY ITEM_ID) t
 ORDER BY t.qty DESC;

실행계획 학습용 예시 · 파라미터를 끈 숫자는 복사본 DB에서 잰 것으로 둔다 ​

xplan
[세 SQL · 지금(기본값) ↔ 복사본에서 _optimizer_unnest_scalar_sq = FALSE]
            지금 Buffers    끈 뒤 Buffers    하루 실행    하루 블록 (지금 → 끈 뒤)
  Q1          60,310             104          10,000     약 6억 310만 → 104만
  Q2          56,341         209,165              20     약 113만 → 418만
  Q3             268             268             200     약 5만 4천 → 5만 4천 (상품명 서브쿼리 Starts 88 · 펼쳐지지 않는다)

[Q2 — 지금 (펼쳐짐)]
-------------------------------------------------------------------------------------------------------
| Id  | Operation                              | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                       |             |      1 |        |    200 |00:00:01.21 |   56341 |
|   1 |  SORT ORDER BY                         |             |      1 |    200 |    200 |00:00:01.21 |   56341 |
|*  2 |   HASH JOIN OUTER                      |             |      1 |    200 |    200 |00:00:01.21 |   56341 |
|   3 |    TABLE ACCESS FULL                   | SHOP        |      1 |    200 |    200 |00:00:00.01 |       5 |
|   4 |    VIEW                                | VW_SSQ_1    |      1 |    200 |    200 |00:00:01.20 |   56336 |
|   5 |     HASH GROUP BY                      |             |      1 |    200 |    200 |00:00:01.20 |   56336 |
|*  6 |      HASH JOIN                         |             |      1 |    104K|    103K|00:00:01.05 |   56336 |
|   7 |       TABLE ACCESS BY INDEX ROWID BATCHED| ORD       |      1 |  41700 |  41712 |00:00:00.02 |    1071 |
|*  8 |        INDEX RANGE SCAN                | ORD_X01     |      1 |  41700 |  41712 |00:00:00.01 |     139 |
|   9 |       TABLE ACCESS FULL                | ORD_ITEM    |      1 |   2472K|   2472K|00:00:00.52 |   55265 |
-------------------------------------------------------------------------------------------------------
   2 - access("ITEM_1"(+)="S"."SHOP_NO")
   6 - access("I"."ORD_NO"="O"."ORD_NO")
   8 - access("O"."ORD_DT">=:D1 AND "O"."ORD_DT"<=:D2)

[Q2 — 파라미터를 끈 뒤 (복사본)]
-------------------------------------------------------------------------------------------------------
| Id  | Operation                              | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                       |             |      1 |        |    200 |00:00:00.92 |     209K|
|   1 |  SORT AGGREGATE                        |             |    200 |      1 |    200 |00:00:00.91 |     209K|
|   2 |   NESTED LOOPS                         |             |    200 |    520 |    103K|00:00:00.88 |     209K|
|   3 |    NESTED LOOPS                        |             |    200 |    520 |    103K|00:00:00.47 |     167K|
|   4 |     TABLE ACCESS BY INDEX ROWID BATCHED| ORD         |    200 |    208 |  41712 |00:00:00.12 |   42312 |
|*  5 |      INDEX RANGE SCAN                  | ORD_X02     |    200 |    208 |  41712 |00:00:00.01 |     600 |
|*  6 |     INDEX RANGE SCAN                   | ORD_ITEM_PK |  41712 |      2 |    103K|00:00:00.21 |     125K|
|   7 |    TABLE ACCESS BY INDEX ROWID         | ORD_ITEM    |    103K|      1 |    103K|00:00:00.30 |   41712 |
|   8 |  SORT ORDER BY                         |             |      1 |    200 |    200 |00:00:00.92 |     209K|
|   9 |   TABLE ACCESS FULL                    | SHOP        |      1 |    200 |    200 |00:00:00.01 |       5 |
-------------------------------------------------------------------------------------------------------
   5 - access("O"."SHOP_NO"=:B1 AND "O"."ORD_DT">=:D1 AND "O"."ORD_DT"<=:D2)
   6 - access("I"."ORD_NO"="O"."ORD_NO")

(DBMS_XPLAN 이 줄여 쓴 값 — 125K = 125,136 · 167K = 167,448 · 209K = 209,160 / 209,165)

문제 ​

  1. 세 SQL이 파라미터를 끄면 각각 어떻게 되는지 숫자로 정리하고, 하루 실행 횟수를 곱해 시스템 전체에서 끄는 안을 평가하시오.
  2. 파라미터를 바꾸지 않고 Q1이 다시 느려지지 않도록 SQL을 고치시오. Q2도 변환에 기대지 않는 SQL로 고치시오.
  3. SQL 수정, SQL 하나에만 거는 힌트나 베이스라인, 시스템 파라미터의 대가를 비교해 서술하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자