Skip to content

02화 · 실습과 기록 ​

여기서 하는 일: 두 번 틀리고, 한 번 이긴다.1_사건.md를 읽은 것은 "선배가 하는 걸 지켜본 것"이고, 이 문서를 끝내야 직접 해 본 것이 된다. 개념이 막히면 2_파고들기.md를 펼친다.

시간이 없는 주에는 STEP 0 → 1 → 2 → 5만 한다(약 40분).


STEP 0 — 직전 리비전을 연다 (2분) ★ 매주 처음 하는 일 ​

bash
docker start ora-hanorder
bash
cat 리비전/Q1_rev00.sql

오늘 고칠 SQL부터 눈으로 확인한다. 지난주에 잰 값은 Buffers 5,827이었다.

sql
-- 전제 확인: T_ORD_BIG 에 인덱스가 없어야 한다 (01화 종료 상태)
SELECT COUNT(*) AS idx_on_ord_big FROM user_indexes WHERE table_name='T_ORD_BIG';   -- 기대 0

0이 아니어도 실습 스크립트가 알아서 지우고 시작한다. 그래도 그 인덱스가 왜 있는지는 꼭 확인해야 한다. 누가 만들었는지 모르는 인덱스가 운영에 있다면 그 자체로 사건이다.


STEP 1 — 예측을 봉인한다 (3분) ​

오늘 ORD_DT 에 인덱스를 만들 것이다. rev00 은 TO_CHAR 로 날짜를 가공한다.

Q1. 인덱스를 만들면 Buffers 가 얼마가 될 것 같은가?   ______
Q2. 인덱스를 만들었는데 안 타면, 그 이유는 무엇일 것 같은가?
    ________________________________________________________
Q3. SQL 에서 TO_CHAR 를 없애면 Buffers 가 얼마가 될 것 같은가?   ______
▶ 적고 나서 펼치기

대부분 Q1에는 "크게 줄어든다", Q3에는 "더 줄어든다"라고 쓴다.

실측값은 Q1이 5,826으로 그대로, Q3도 5,827로 그대로다. 두 번 틀리는 것까지가 이 화의 설계다. 64배는 세 번째 시도에서 나온다.


STEP 2 — 실험 6단계를 돌린다 (5분) ★ 오늘의 본체 ​

bash
cd 10_본편_튜너의_1년/02화_인덱스는_있는데_안_탄다/실습
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 00_인덱스_실험.sql
powershell
Set-Location 10_본편_튜너의_1년\02화_인덱스는_있는데_안_탄다\실습
Get-Content 00_인덱스_실험.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

스크립트는 맨 앞에서 인덱스를 전부 지우고 시작한다. 그래서 몇 번을 돌려도 결과가 같다.

⚠️ 왜 초기화가 필요한가 — 실측으로 걸린 함정 ​

[F]에서 만든 커버링 인덱스가 남아 있으면, 다음에 돌릴 때 [D]가 TABLE ACCESS FULL이 아니라 곧장 91로 나온다. "SQL을 고쳐도 안 탄다"는 이 화의 핵심 장면이 통째로 사라진다. 인덱스가 없는 상태에서 출발해야 이 이야기가 성립한다.

여섯 단계에서 확인할 것 ​

단계무엇기대☐
[A]인덱스 없이 rev00TABLE ACCESS FULL · 5,826 · E-Rows 2,500☐
[B]인덱스 만들고 rev00똑같다. 달라지는 게 없다☐
[C]rev00 + INDEX 힌트여전히 FULL. 힌트가 조용히 무시된다☐
[D]rev01 (가공 제거)여전히 FULL. 하지만 E-Rows는 28,336☐
[E]rev01 + INDEX 힌트 강제INDEX RANGE SCAN · 27,466 (4.7배 악화)☐
[F]rev02 (커버링)INDEX RANGE SCAN만 · 91☐

각 단계에서 눈으로 짚을 것 ​

[A][B][C] — Predicate 를 보라
    filter((TO_CHAR(INTERNAL_FUNCTION("ORD_DT"),'YYYYMMDD')>='20240601' AND ...))
    ☐ filter 인가 access 인가?         → filter. 다 읽고 버렸다
    ☐ INTERNAL_FUNCTION 이 보이는가?   → 컬럼이 함수에 감싸였다는 신호

[D] — 같은 자리에서 함수가 사라진 것을 확인
    filter(("ORD_DT">=TO_DATE(...) AND "ORD_DT"<TO_DATE(...)))
    ☐ E-Rows 가 2,500 -> 28,336 으로 바뀌었는가? (실제 27,390)

[E] — 두 줄의 Buffers 를 비교
    TABLE ACCESS BY INDEX ROWID BATCHED ... 27,466   ← 여기서 다 붙는다
      INDEX RANGE SCAN ..................... 76      ← 인덱스 자체는 싸다
    ☐ 인덱스가 비싼 게 아니라 '테이블로 가는 것'이 비싸다는 걸 확인

[F] — TABLE ACCESS 줄이 아예 없는 것을 확인
    ☐ access(...) 로 바뀌었는가?
    ☐ 91 인가?

STEP 1의 예측을 꺼낸다 ​

Q1(인덱스를 만들면)을 맞혔나?
"힌트를 줘도 안 탄다"를 예상했나?
"고쳤는데도 FULL"을 예상했나?
"억지로 태우면 4.7배 느려진다"는?

STEP 3 — 무력화 6종을 손으로 확인한다 (15분) ​

bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 01_무력화_6종.sql
powershell
Get-Content 01_무력화_6종.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

무대는 구 시스템의 CUSTOMERS(20,000행 · CUST_X_CITY)다. CITY에는 6가지 값이 고르게 퍼져 있다.

채점표: 돌리기 전에 각 줄의 Operation을 먼저 적어 보자.

#조건내 예상실측 Operation실측 Buffers
①UPPER(city) = 'SEOUL'TABLE ACCESS FULL126
①city = 'SEOUL'INDEX RANGE SCAN10
②order_date = DATE '2024-06-01'INDEX RANGE SCAN4
③city LIKE '%OUL'INDEX FAST FULL SCAN56
③city LIKE 'SE%'INDEX RANGE SCAN10
④city != 'SEOUL'INDEX FAST FULL SCAN56
⑤city IS NULLTABLE ACCESS FULL126
⑥ORD_AMT = 50000 (선두 누락)INDEX FAST FULL SCAN3,227

📌 여기서 교과서가 틀리는 지점 ​

③④⑥은 흔히 "인덱스를 못 탄다"고 설명하지만, 실측 결과는 INDEX FAST FULL SCAN이다. 인덱스를 쓰기는 쓴다. 다만 찾아가지(RANGE SCAN) 못하고 전부 훑는다. 인덱스가 테이블보다 작으니 테이블 전체 스캔보다는 쌀 수 있다. 정확하게 말하면 이렇다. "찾아갈 수 없어서 훑는다."

①의 숨은 두 번째 손해 ​

UPPER(city)의 E-Rows는 200인데 실제는 3,333이다. 17배나 틀렸다. rev00의 TO_CHAR에서 본 것과 같은 현상이다. 컬럼을 가공하면 인덱스와 통계를 한꺼번에 잃는다.

② 묵시적 형변환 — 어느 쪽이 변환되는가 ​

sql
WHERE order_date = DATE '2024-06-01'   -- 리터럴이 변환된다 → 멀쩡하다 (4 블록)
WHERE varchar_col = 12345              -- 컬럼이 변환된다 → TO_NUMBER(varchar_col)

스크립트에는 M_CUS.CUS_ID = 123이 주석으로 들어 있다. 주석을 풀어서 돌려 보면 느려지는 게 아니라 ORA-01722로 터진다. 묵시적 형변환에서 가장 고약한 경우로, 성능 문제가 아니라 장애다.


STEP 4 — 교정이 불가능할 때 (5분) ​

UPPER(city)를 없앨 수 없는 화면이 있다고 하자. 그때는 가공된 값 자체를 인덱싱한다.

sql
CREATE INDEX IX_CUST_CITY_UP ON customers(UPPER(city));
-- SQL 을 한 글자도 안 고쳤는데 RANGE SCAN 을 탄다 (실측 10 블록)

대가는 세 가지다. ⓐ 그 함수 형태와 정확히 일치하는 조건에서만 탄다(LOWER로는 안 탄다). ⓑ 인덱스가 하나 늘어 DML이 느려진다. ⓒ 함수가 DETERMINISTIC이어야 한다.

SQL을 고칠 수 있으면 그쪽이 먼저다. FBI는 최후의 수단이다.


STEP 5 — T1 리포트 1호 ★ 오늘의 산출물 ​

이 교재에서 처음 쓰는 단건 SQL 튜닝 리포트다. 양식은 T1_단건SQL_튜닝리포트.md를 따른다.

┌──────────────────────────────────────────────────────────────────┐
│ T1 단건 SQL 튜닝 리포트 — 1호                                     │
├──────────────────────────────────────────────────────────────────┤
│ 대상        : PKG_ORD_STLM.Q1 (기간별 주문 조회)                  │
│ 측정 일시   : ____________    측정 조건 : 단독 세션 · 1회 실행     │
│ 리비전      : rev00 → rev02                                       │
├──────────────────────────────────────────────────────────────────┤
│ 1. 현상                                                           │
│    ______________________________________________________________ │
│ 2. 원인                                                           │
│    ______________________________________________________________ │
│ 3. 조치 (단계별로)                                                │
│    rev01: ______________________________________________________  │
│    rev02: ______________________________________________________  │
│ 4. 효과 (Buffers 로)                                              │
│    ______________________________________________________________ │
│ 5. 리스크  ★ 비우면 반려된다                                      │
│    ______________________________________________________________ │
│ 6. 회귀 검증                                                      │
│    ______________________________________________________________ │
└──────────────────────────────────────────────────────────────────┘

5번 「리스크」에 반드시 들어가야 할 것 ​

지난주에 이 칸을 비웠다가 반려당했다. 오늘은 쓸 거리가 분명히 있다.

  • T_ORD_BIG의 인덱스가 0개 → 2개로 늘었다. 이제 INSERT/UPDATE마다 인덱스 2개를 갱신해야 한다
  • 야간 배치(P1)가 100만 건을 적재할 때 이 비용이 드러난다. 청구서는 14화에서 온다
  • 오늘 잰 범위는 6월 한 달(2.74%)이다. 범위가 넓어지면 커버링 인덱스도 불리해질 수 있다(04화)
  • 운영에 적용할 때는 INVISIBLE로 먼저 만들고 세션 단위로 검증한다

6번 「회귀 검증」 — 결과가 같은가 ​

성능을 고쳤으면 답이 바뀌지 않았음을 증명해야 한다. 검증용 스크립트가 따로 있다.

bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/02_경계값_검증.sql

확인할 것 두 가지

sql
-- ① rev00 과 rev02 의 결과가 같은가 (T_ORD_BIG)
--    기대: 양쪽 다 27,390건 / 6,982,065,200

-- ② 날짜에 BETWEEN 을 쓰면 어떻게 되는가
--    T_ORD_BIG : 27,390 으로 '똑같다'   <- 전부 자정이라 버그가 숨는다
--    T_ORD     : 4,110 vs 3,973         <- 137건 34,159,700원 누락

☠️ 오늘 리포트에서 가장 중요한 줄 ​

T_ORD_BIG에서는 BETWEEN을 써도 결과가 똑같이 나온다. 테스트도 통과한다. 그런데 같은 코드를 T_ORD에 쓰면 3,416만 원이 사라진다.

반면 강도현의 TO_CHAR 원본은 T_ORD에서도 4,110건으로 정확하다.강도현의 코드는 느릴 뿐 답은 맞다. BETWEEN으로 고친 코드는 빠르지만 틀린다.

느린 코드를 빠르게 만드는 것이 튜닝이고, 맞는 코드를 틀리게 만드는 것은 장애다. 느린 건 사람이 기다리면 되지만, 답이 틀리면 정산이 틀어진다.


STEP 6 — 서술 답안 (400~600자) ​

박팀장에게 보낼 회신을 쓴다. 지난주에는 "아직 안 고쳤습니다"라고 보냈지만, 오늘은 보고할 게 있다.

4단계 프레임(현상/진단 → 원인 → 개선안 → 근거/효과)은 01화 3_실습과_기록.md와 같다.

▶ 선배의 첨삭 포인트 (쓰고 나서)

① "인덱스를 만들어서 빨라졌습니다"는 틀린 보고다. 인덱스만 만들었을 때는 아무것도 바뀌지 않았다. 빨라진 건 SQL을 고치고 커버링 인덱스까지 만든 뒤다. 둘 중 하나만 했다면 효과가 없었다. 무엇이 효과를 냈는지 정확히 쓰는 것이 이 리포트의 가치다.

② 64배를 자랑하기 전에 조건을 쓴다. "6월 한 달, 전체의 2.74% 범위에서"가 빠지면 과장이다. 1년치를 조회하면 이 숫자는 안 나온다.

③ 박팀장이 알아듣는 말로 쓴다. "Buffers 5,827 → 91"로는 뜻이 전달되지 않는다. "조회 한 번에 읽는 데이터가 64분의 1로 줄었습니다"라고 옮겨야 한다.

④ 대가를 숨기지 않는다. 인덱스를 2개 만들었으니 야간 배치가 느려질 수 있다. 지금 적어 두지 않으면 14화에서 그 책임을 떠안게 된다.


STEP 7 — 파인만 자가설명 ​

절차는 01화와 같다. 1단 백지 설명 → 2단 막힘 태그(최소 1개) → 3단 원천 복귀 → 4단 재설명 순서로 진행한다.

오늘 자주 나오는 태그 세 가지

태그전형적인 문장
#인과"왜 TO_CHAR를 쓰면 인덱스를 못 쓰는지 설명하다가 막혔다" → 2_파고들기.md §1
#용어"FAST FULL SCAN과 FULL SCAN의 차이를 말로 못 하겠다" → §2
#수치"CF가 100만이면 왜 27,466이 되는지 계산이 안 된다" → §5의 근사식에 직접 대입해 본다

4단 되묻기 3문 (오늘 버전) ​

  1. 조건 반전: "6월 한 달이 아니라 1년치를 조회하면 rev02는 어떻게 되나?"
  2. 반례 요구: "커버링 인덱스가 오히려 해가 되는 경우를 하나 든다면?"
  3. 측정 지목: "'인덱스 때문에 배치가 느려졌다'를 증명하려면 무엇을 재야 하나?"

태그는 _막힘로그.md로 옮긴다.


완료 체크리스트 ​

  • [ ] STEP 0: Q1_rev00.sql을 열어 봤다
  • [ ] STEP 1: 보기 전에 예측 3개를 적었다
  • [ ] STEP 2: [A]~[F] 여섯 단계를 돌리고 91을 확인했다
  • [ ] STEP 2: filter가 access로 바뀌고 TABLE ACCESS 줄이 사라진 것을 눈으로 짚었다
  • [ ] STEP 3: 무력화 6종의 예상을 먼저 적고 실측과 대조했다
  • [ ] STEP 5: T1 리포트 1호를 썼고, 리스크 칸과 경계값 검증을 채웠다
  • [ ] STEP 6: 박팀장 회신을 문장으로 풀어 썼다
  • [ ] STEP 7: 막힘 태그를 최소 1개 _막힘로그.md에 옮겼다

완결성 게이트: STEP 5의 리스크 칸과 STEP 7의 태그가 비어 있으면 이 화는 미완성이다.


다음 화로 넘기는 상태 ​

무엇값
T_ORD_BIG 인덱스2개(IX_ORD_BIG_DT, IX_ORD_BIG_DT_AMT). 지우지 않는다
Q1 현재 리비전rev02 · Buffers 91
남긴 빚인덱스 2개의 적재 비용 → 14화
열어 둔 질문손익분기점은 몇 %인가 → 04화

다음 → 03화 「날짜가 문자로 박혀 있다」: 오늘은 TO_CHAR를 지울 수 있었다. 03화에서는 컬럼 자체가 VARCHAR2(8)이라 지울 수가 없다.