Skip to content
튜닝 연습인덱스 스캔 효율과 테이블 액세스
ACC-073

가맹점 포털 오픈 점검에서 점주 앱 SQL과 본사 순위 SQL이 모두 주문 테이블을 1,300블록 넘게 읽는다

실전SELECT버거148 / 700

상황 ​

버거원이 가맹점 포털을 연다. 오픈 전 점검에서 SQL 넷이 걸렸다. 넷 다 운영 주문 ORD_RANDOM을 읽는데, 쓸 수 있는 인덱스가 날짜 인덱스 X01 하나뿐이다. 점주 앱(P1 ~ P3)은 매장 200곳이 수시로 열고, 본사 순위 화면(P4)은 10분마다 새로 고친다. 박팀장은 이렇게 주문했다. "인덱스는 최소로. 넷이 각각 무엇을 읽는지, 하루 일량으로 보면 무엇이 급한지, 만들면 POS 적재에 무엇이 붙는지까지 적어 와라."

테이블과 인덱스 ​

  • ORD_RANDOM(운영본): 1,000,000행 · 22,414블록(블록당 약 45행) · 서버 교체 때 무작위 순서로 다시 적재했다 · 하루 약 1,370건 · 매장 200곳(매장 하나의 하루 주문은 평균 6.9건, 한 달 약 208건) · POS가 판매마다 INSERT하고, 매월 초 정정분 약 4만 건을 다시 넣는다
  • ORD_DT VARCHAR2(8) · ORD_HMS VARCHAR2(6) · SHOP_NO VARCHAR2(10) 'SH0001' · ORD_NO NUMBER(10)
  • 인덱스 ORD_RANDOM_PK (ORD_NO) · ORD_RANDOM_X01 (ORD_DT, ORD_HMS): BLEVEL 2 · 리프 3,774블록 · CLUSTERING_FACTOR 999,958(교재 04화 실측) · 「일별 주문 조회」(ORD_DT = :d ORDER BY ORD_HMS, 첫 화면 30줄)도 X01을 쓴다
  • 같은 조건을 FULL로 읽으면 22,234블록이다 · 인덱스 하나는 정정분 4만 건 적재에 redo 약 9.4MB를 더한다(교재 04화 실측 비례)
  • 어림: 항목 하나는 '컬럼 값 + 컬럼마다 1바이트 + ROWID 7바이트 + 머리 약 4바이트' · 리프 한 블록에 약 7,200바이트 · SHOP_NO 6바이트, ORD_DT 8바이트, ORD_HMS 6바이트

원본 SQL ​

sql
-- P1 「시간대별 주문 수」 · 점주 앱 · 하루 약 8,000번
SELECT SUBSTR(ORD_HMS, 1, 2) AS hh, COUNT(*) AS cnt
  FROM ORD_RANDOM
 WHERE SHOP_NO = :v_shop AND ORD_DT = :v_dt
 GROUP BY SUBSTR(ORD_HMS, 1, 2);

-- P2 「매장 주문 목록」 · 점주 앱 · 하루 약 4,000번
SELECT ORD_NO, ORD_HMS
  FROM ORD_RANDOM
 WHERE SHOP_NO = :v_shop AND ORD_DT = :v_dt
 ORDER BY ORD_HMS;

-- P3 「월 주문 추이」 · 점주 앱 · 하루 약 2,000번 · 한 달
SELECT ORD_DT, COUNT(*) AS cnt
  FROM ORD_RANDOM
 WHERE SHOP_NO = :v_shop AND ORD_DT BETWEEN :v_from AND :v_to
 GROUP BY ORD_DT;

-- P4 「오늘 매장 순위」 · 본사 · 하루 약 150번
SELECT SHOP_NO, COUNT(*) AS cnt
  FROM ORD_RANDOM
 WHERE ORD_DT = :v_dt
 GROUP BY SHOP_NO;

실행계획 학습용 예시 · 인덱스 규모와 CF는 교재 04화 실측 ​

xplan
[P3 · SH0042 · 2013년 11월]
--------------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                |      1 |        |     30 |00:00:00.06 |   41838 |
|   1 |  HASH GROUP BY                        |                |      1 |     20 |     30 |00:00:00.06 |   41838 |
|*  2 |   FILTER                              |                |      1 |        |    208 |00:00:00.06 |   41838 |
|*  3 |    TABLE ACCESS BY INDEX ROWID BATCHED| ORD_RANDOM     |      1 |     20 |    208 |00:00:00.06 |   41838 |
|*  4 |     INDEX RANGE SCAN                  | ORD_RANDOM_X01 |      1 |   4065 |  41666 |00:00:00.01 |     160 |
--------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter(:V_FROM<=:V_TO)
   3 - filter("SHOP_NO"=:V_SHOP)
   4 - access("ORD_DT">=:V_FROM AND "ORD_DT"<=:V_TO)

[P1 · SH0042 · 2013-11-15]
--------------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                |      1 |        |      6 |00:00:00.01 |    1378 |
|   1 |  HASH GROUP BY                        |                |      1 |      7 |      6 |00:00:00.01 |    1378 |
|*  2 |   TABLE ACCESS BY INDEX ROWID BATCHED | ORD_RANDOM     |      1 |      7 |      7 |00:00:00.01 |    1378 |
|*  3 |    INDEX RANGE SCAN                   | ORD_RANDOM_X01 |      1 |   1370 |   1371 |00:00:00.01 |       8 |
--------------------------------------------------------------------------------------------------------------------
   2 - filter("SHOP_NO"=:V_SHOP)
   3 - access("ORD_DT"=:V_DT)

[P2 · P4]  P1과 같은 모양 · Buffers 약 1,378 (P2는 SORT ORDER BY가 붙는다)

문제 ​

  1. 지금 P1 ~ P4가 각각 몇 블록을 읽는지 실행계획과 CLUSTERING_FACTOR로 설명하시오. 하루 실행 횟수를 곱해 어느 SQL이 가장 급한지 쓰시오.
  2. 최소 인덱스 세트를 설계해 DDL을 작성하시오. 세트로 P1 ~ P4가 어떤 스캔으로 몇 블록을 읽을지 어림하고, 다른 후보 하나와 비교하시오.
  3. 고른 세트의 대가, X01을 쓰는 다른 SQL에 주는 영향, 검증 절차를 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자