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

고객 두 명을 찾는 「고객 360」 한 화면이 콜센터 하루 논리 읽기의 절반을 차지한다

실전SELECT한빛유통292 / 700

상황 ​

콜센터의 「고객 360」은 상담원이 고객명 앞부분을 넣으면 고객 정보와 주문, 정산, 평가 요약을 한 줄씩 보여 준다. 동명이인이 있어 보통 두 명쯤 나온다. 상담원 40명이 통화마다 열어 하루 6,000번 돌고, 한 번은 0.04초지만 하루 논리 읽기는 약 490만 블록으로 콜센터 전체의 절반이다. 박팀장은 "인덱스는 꼭 필요한 것만 만든다. 정산 테이블은 매일 밤 비우고 다시 싣는 테이블이니 그 비용까지 적어 와라"라고 했다.

테이블과 인덱스 ​

  • M_CUS 고객: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · CUS_NM에는 인덱스가 없다
  • T_ORD 주문: 100,000행 · 896블록 · IX_ORD_CUS (CUS_ID) (다른 콜센터 화면도 쓴다) · 고객당 약 50건 · 한 고객의 주문은 흩어져 있다
  • T_ORD_STLM 정산: 100,000행 · 640블록 · 인덱스 없음 · 고객당 약 50건 · 매일 밤 TRUNCATE 뒤 INSERT /*+ APPEND */로 10만 건을 다시 싣는다(지금 약 0.1초)
  • T_ITM_EVL 상품평가: 5,000행 · 48블록 · PK_T_ITM_EVL (ITM_ID, EVL_LST_NO)만 있다 · 고객당 2 ~ 3건 · 리뷰가 올라올 때마다 한 건씩 들어온다
  • 주문, 정산, 평가가 없는 고객도 화면에는 나와야 한다 · 통계 최신

원본 SQL ​

sql
SELECT c.CUS_ID, c.CUS_NM, c.CUS_GD,
       o.ORD_CNT, o.ORD_AMT, o.LAST_ORD_DT,
       s.STLM_AMT,
       e.EVL_CNT, e.AVG_PT
  FROM M_CUS c,
       (SELECT CUS_ID, COUNT(*) AS ORD_CNT, SUM(ORD_AMT) AS ORD_AMT, MAX(ORD_DT) AS LAST_ORD_DT
          FROM T_ORD GROUP BY CUS_ID) o,
       (SELECT CUS_ID, SUM(STLM_AMT) AS STLM_AMT
          FROM T_ORD_STLM GROUP BY CUS_ID) s,
       (SELECT CUS_ID, COUNT(*) AS EVL_CNT, ROUND(AVG(EVL_PT), 2) AS AVG_PT
          FROM T_ITM_EVL GROUP BY CUS_ID) e
 WHERE c.CUS_NM LIKE :nm || '%'
   AND o.CUS_ID(+) = c.CUS_ID
   AND s.CUS_ID(+) = c.CUS_ID
   AND e.CUS_ID(+) = c.CUS_ID

실행계획 학습용 예시 ​

xplan
------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                               | Name       | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                        |            |      1 |        |      2 |00:00:00.04 |     822 |          |
|*  1 |  HASH JOIN OUTER                        |            |      1 |      2 |      2 |00:00:00.04 |     822 | 1123K (0)|
|*  2 |   HASH JOIN OUTER                       |            |      1 |      2 |      2 |00:00:00.03 |     774 | 1036K (0)|
|   3 |    NESTED LOOPS OUTER                   |            |      1 |      2 |      2 |00:00:00.01 |     134 |          |
|*  4 |     TABLE ACCESS FULL                   | M_CUS      |      1 |      2 |      2 |00:00:00.01 |      30 |          |
|   5 |     VIEW PUSHED PREDICATE               |            |      2 |      1 |      2 |00:00:00.01 |     104 |          |
|   6 |      SORT GROUP BY                      |            |      2 |      1 |      2 |00:00:00.01 |     104 |          |
|   7 |       TABLE ACCESS BY INDEX ROWID BATCHED| T_ORD     |      2 |     50 |    100 |00:00:00.01 |     104 |          |
|*  8 |        INDEX RANGE SCAN                 | IX_ORD_CUS |      2 |     50 |    100 |00:00:00.01 |       4 |          |
|   9 |    VIEW                                 |            |      1 |   2000 |   2000 |00:00:00.03 |     640 |          |
|  10 |     HASH GROUP BY                       |            |      1 |   2000 |   2000 |00:00:00.03 |     640 | 1405K (0)|
|  11 |      TABLE ACCESS FULL                  | T_ORD_STLM |      1 |   100K |   100K |00:00:00.01 |     640 |          |
|  12 |   VIEW                                  |            |      1 |   1990 |   1990 |00:00:00.01 |      48 |          |
|  13 |    HASH GROUP BY                        |            |      1 |   1990 |   1990 |00:00:00.01 |      48 | 1289K (0)|
|  14 |     TABLE ACCESS FULL                   | T_ITM_EVL  |      1 |   5000 |   5000 |00:00:00.01 |      48 |          |
------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - access("E"."CUS_ID"(+)="C"."CUS_ID")
   2 - access("S"."CUS_ID"(+)="C"."CUS_ID")
   4 - filter("C"."CUS_NM" LIKE :NM||'%')
   8 - access("CUS_ID"="C"."CUS_ID")

문제 ​

  1. 세 집계 뷰가 각각 어떻게 처리되었는지(조인 조건이 뷰 안으로 들어갔는지, 뷰를 통째로 만들었는지) 줄 번호와 Buffers로 설명하시오. 고객 조건이 이름이라는 점이 왜 중요한지도 쓰시오.
  2. 세 뷰가 모두 고객 두 명의 행만 읽도록 인덱스를 설계하고, 고친 뒤의 Buffers와 하루 읽기량을 쓰시오.
  3. 만드는 인덱스마다 DML 쪽 대가(정산 야간 적재, 리뷰 입력, 주문 입력)를 쓰고 우선순위를 정하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자