Skip to content
튜닝 연습옵티마이저와 통계
OPT-019

카디널리티 점검표가 날마다 서른 줄을 올리는데 고칠 만한 줄은 한두 개뿐이다

중급SELECT버거609 / 700

상황 ​

조민우는 지난달 버거 본사 DB에 「추정 오차 점검표」를 만들었다. 매일 08:00에 V$SQL_PLAN_STATISTICS_ALL에서 E-Rows와 A-Rows가 10배 넘게 차이 나는 계획 줄을 뽑아 메일로 보낸다(실행 통계는 STATISTICS_LEVEL = ALL로 띄운 점검용 세션에서 모은다). 점검표에는 날마다 서른 줄 가까이 올라온다. 선배는 어제 점검표에서 SQL 세 개를 골라 계획을 펼쳐 보고 이렇게 말했다. "여섯 줄 가운데 진짜는 세 줄이고, 그 셋도 원인은 한 줄이야. 점검표부터 고치자."

테이블과 인덱스 ​

  • V$SQL_PLAN_STATISTICS_ALL: 계획 줄마다 한 행 · CARDINALITY = E-Rows(한 번 시작할 때의 추정) · LAST_STARTS = Starts · LAST_OUTPUT_ROWS = A-Rows(모든 Starts를 더한 값) · ID, PARENT_ID, DEPTH(계획의 줄과 부모 줄) · OPERATION, OPTIONS
  • ORD: 1,000,000행 · 22,422블록 · ORD_PK (ORD_NO) · ORD_X01 (ORD_DT, ORD_HMS): 날짜순 적재 · 하루 약 1,368건
  • ORD_ITEM: 2,472,717행 · 55,265블록 · ORD_ITEM_PK (ORD_NO, ITEM_ID) · 주문당 평균 2.47줄
  • 통계 최신 · 히스토그램 없음
  • SQL 셋: ① 「일 마감 메뉴 집계」(하루치 주문의 메뉴별 수량) ② 상황판 「최근 주문 20건」 ③ 「밸런타인데이 메뉴 비교」(ORD_DT LIKE '%0214')

원본 SQL ​

sql
-- 「추정 오차 점검표」 매일 08:00 (조민우)
SELECT p.SQL_ID, p.CHILD_NUMBER, p.ID,
       p.OPERATION || ' ' || p.OPTIONS AS op, p.OBJECT_NAME,
       p.CARDINALITY      AS e_rows,
       p.LAST_OUTPUT_ROWS AS a_rows
  FROM V$SQL_PLAN_STATISTICS_ALL p
 WHERE p.LAST_OUTPUT_ROWS > p.CARDINALITY * 10
    OR p.LAST_OUTPUT_ROWS * 10 < p.CARDINALITY
 ORDER BY p.SQL_ID, p.CHILD_NUMBER, p.ID

실행계획 학습용 예시 · 점검 방식은 교재 10화 추적 SOP ​

xplan
[점검표 · 10월 14일 08:00 (서른한 줄 가운데 SQL 세 개의 여섯 줄)]
 SQL   ID  OP                              OBJECT_NAME    E_ROWS    A_ROWS
 ①      6  INDEX RANGE SCAN                ORD_ITEM_PK         2     3,378
 ①      7  TABLE ACCESS BY INDEX ROWID     ORD_ITEM            1     3,378
 ②      3  TABLE ACCESS BY INDEX ROWID     ORD             1000K        20
 ③      1  HASH GROUP BY                                  67,252       184
 ③      2  HASH JOIN                                       124K     6,758
 ③      3  TABLE ACCESS FULL               ORD            50,000     2,736

[① 「일 마감 메뉴 집계」 · ORD_DT = '20131230']
-------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |             |      1 |        |     92 |00:00:00.02 |    4137 |
|   1 |  HASH GROUP BY                        |             |      1 |     92 |     92 |00:00:00.02 |    4137 |
|   2 |   NESTED LOOPS                        |             |      1 |   3383 |   3378 |00:00:00.02 |    4137 |
|   3 |    NESTED LOOPS                       |             |      1 |   3383 |   3378 |00:00:00.01 |    2771 |
|   4 |     TABLE ACCESS BY INDEX ROWID BATCHED| ORD        |      1 |   1368 |   1366 |00:00:00.01 |      39 |
|*  5 |      INDEX RANGE SCAN                 | ORD_X01     |      1 |   1368 |   1366 |00:00:00.01 |       8 |
|*  6 |     INDEX RANGE SCAN                  | ORD_ITEM_PK |   1366 |      2 |   3378 |00:00:00.01 |    2732 |
|   7 |    TABLE ACCESS BY INDEX ROWID        | ORD_ITEM    |   3378 |      1 |   3378 |00:00:00.01 |    1366 |
-------------------------------------------------------------------------------------------------------
   5 - access("O"."ORD_DT"='20131230')
   6 - access("OI"."ORD_NO"="O"."ORD_NO")

[② 「최근 주문 20건」]
--------------------------------------------------------------------------------------------
| Id  | Operation                     | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |         |      1 |        |     20 |00:00:00.01 |       5 |
|*  1 |  COUNT STOPKEY                |         |      1 |        |     20 |00:00:00.01 |       5 |
|   2 |   VIEW                        |         |      1 |     20 |     20 |00:00:00.01 |       5 |
|   3 |    TABLE ACCESS BY INDEX ROWID| ORD     |      1 |   1000K|     20 |00:00:00.01 |       5 |
|   4 |     INDEX FULL SCAN DESCENDING| ORD_X01 |      1 |     20 |     20 |00:00:00.01 |       3 |
--------------------------------------------------------------------------------------------
   1 - filter(ROWNUM<=20)

[③ 「밸런타인데이 메뉴 비교」]
-------------------------------------------------------------------------------------------------------
| Id  | Operation           | Name     | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |          |      1 |        |    184 |00:00:00.48 |   77687 |          |
|   1 |  HASH GROUP BY      |          |      1 |  67252 |    184 |00:00:00.48 |   77687 | 1250K (0)|
|*  2 |   HASH JOIN         |          |      1 |    124K|   6758 |00:00:00.47 |   77687 | 3712K (0)|
|*  3 |    TABLE ACCESS FULL| ORD      |      1 |  50000 |   2736 |00:00:00.14 |   22422 |          |
|   4 |    TABLE ACCESS FULL| ORD_ITEM |      1 |   2472K|   2472K|00:00:00.21 |   55265 |          |
-------------------------------------------------------------------------------------------------------
   2 - access("OI"."ORD_NO"="O"."ORD_NO")
   3 - filter("O"."ORD_DT" LIKE '%0214')

문제 ​

  1. 점검표의 여섯 줄 가운데 진짜 추정 오차는 어느 줄들이고 원인은 어느 한 줄인지 가리시오. 나머지 줄이 오탐인 이유를 Starts와 STOPKEY로 설명하시오.
  2. 점검 SQL을 고쳐 오탐을 걸러 내시오. 비교 기준과 빼야 할 줄을 SQL로 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자