Skip to content

08화 · 실습과 기록 ​

여기서 하는 일: 빠르고 틀린 쿼리를 원장과 대조하고, 고치는 법 여섯 가지를 두 축으로 재고, 옵티마이저가 고쳐 쓴 흔적을 읽는다.1_사건.md를 읽은 것은 "선배가 하는 걸 지켜본 것"이고, 이 문서를 끝내야 직접 해 본 것이 된다. 개념이 막히면 2_파고들기.md를 펼친다.

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


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

bash
docker start ora-hanorder

Q3에는 직전 리비전이 없다. 01화 이후 한 번도 고치지 않았기 때문이다. 그래서 오늘 처음 할 일은 rev00을 만드는 것이다.

bash
cd 10_본편_튜너의_1년/08화_금액이_50배로_찍힌다
cat 리비전/Q3_rev00.sql                              # 강도현 원본. 오늘 만든 rev00
cat ../06화_100만_번의_왕복/리비전/Q2_rev01.sql        # Q2 의 현재판. 오늘 3차로 다시 연다

01화 원장에 적힌 Q3는 Buffers 841, 결과 5행이었다. 그 옆에 「정합성 · 별도 트랙 최우선」이라고 적어 두었다.

전제를 확인한다.

sql
-- ① 07화를 마친 상태인가 (T_ORD_BIG 인덱스 둘 · 08화 임시 객체 없음)
SELECT index_name FROM user_indexes WHERE table_name = 'T_ORD_BIG' ORDER BY 1;
-- 기대: IX_ORD_BIG_DT, IX_ORD_BIG_DT_AMT
SELECT table_name FROM user_tables WHERE table_name LIKE '%\_C08' ESCAPE '\';
-- 기대: 0 행 (스크립트가 만들었다 지우는 복사본)

-- ② 데이터 지문
-- 기대 255,398,623,700
SELECT TO_CHAR(SUM(ORD_AMT), '999,999,999,999') FROM T_ORD_BIG;

PowerShell에서 돌린다면 먼저 한 줄 ​

powershell
$OutputEncoding = New-Object System.Text.UTF8Encoding $false

PowerShell 5.1은 파이프로 넘기는 한글을 ?로 바꾼다. 이 줄이 없으면 스크립트 안내문(PROMPT)의 한글이 깨진다. SQL에는 한글을 넣지 않았으므로 결과 숫자는 영향을 받지 않는다.


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

Q3 는 주문상세(T_ORD_DET)에 상품평가(T_ITM_EVL)를 상품번호로 붙여 상품별 매출 TOP 5 를 낸다.
화면 1 위 ITM00071 22,216,620,000 · 재무팀 원장 444,332,400.

Q1. 화면 금액은 원장의 몇 배인가? 상품마다 배수가 같을까, 다를까?
    ______________

Q2. 화면의 TOP 5 '순위'는 원장과 같을까?
    ______________

Q3. 중복을 지우려고 SUM(DISTINCT 금액) 으로 고치면 1 위 금액은 원장보다 큰가, 같은가, 작은가?
    ______________

Q4. 고치는 방법 여섯(EXISTS · IN · 평가를 한 줄로 묶어 조인 · 조인 삭제 · SUM(DISTINCT) · 먼저 집계 후 조인)의
    Buffers 는 서로 얼마나 다를까?
    ______________
▶ 적고 나서 펼치기

Q1: 정확히 50배이고, 상품 100개 모두 같다. 상품마다 평가가 정확히 50건이기 때문이다. 대부분 "두세 배"라고 쓴다.

Q2: 같다. 모든 상품에 같은 수(50)가 곱해졌기 때문이다. 그래서 몇 달 동안 아무도 몰랐다. 곱하는 수가 상품마다 다르면 순위도 바뀐다. 원장 6위가 1위로 올라온다(STEP 2 [F]).

Q3: 작다. 게다가 순위가 바뀐다. 1위가 ITM00006 286,527,900(원장 12위)이 되고, 원장 1위 ITM00071은 5위로 내려간다. 같은 '값'을 하나로 치기 때문에 금액이 서로 같은 주문상세가 사라진다. 그런데 COUNT(DISTINCT 주문키)는 2,000으로 원장과 맞는다. 그래서 고쳤다고 착각하기 쉽다.

Q4: 거의 같다(807~841). 맞는 것 넷, 틀린 것 셋이 모두 그 사이에 있다. Buffers로는 정답이 갈리지 않는다.


STEP 2 — 원장과 대조한다 (5분) ★ 오늘의 본체 ​

bash
cd 실습
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 00_원장과_대조.sql
powershell
Set-Location 실습
Get-Content 00_원장과_대조.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

원본 테이블에는 쓰지 않는다. [E]에서 평가 테이블의 복사본 T_ITM_EVL_C08을 만들고 맨 끝에서 지운다.

단계무엇기대☐
[A]rev00 그대로Buffers 839~841 · ITM00071 22,216,620,000 · CNT 100,000☐
[B]원장 (평가 조인 없이)ITM00071 444,332,400 · CNT 2,000 · 상품 100개 배율 모두 50☐
[C]왜 50인가평가 50건인 상품 100개 · 조인 행 10,000,000 (원래 200,000)☐
[D]TOP 5 합계화면 110,484,340,000 · 원장 2,209,686,800 · 회사 전체 30,272,557,700☐
[E]그때 (상품당 평가 1건, 복사본)MINUS 0 / 0 · 100행 · 합계 30,272,557,700. 그때는 맞았다☐
[F]12월 평가만 (3~5건)조인 1위 ITM00041 (원장 6위) · 2위 ITM00036 (원장 18위) · EXISTS 1위 ITM00071. 순위가 바뀐다☐

눈으로 짚을 것 ​

xplan
[A] — 계획의 Name 열
    ☐ VW_GBF_8 · VW_GBC_7 가 보이는가?    → 내가 쓴 적 없는 뷰. 오라클이 만든 것이다
    ☐ HASH JOIN 의 A-Rows 가 100 인가?    → 1,000 만 줄을 만들지 않았다 (STEP 4 에서 이유를 본다)
    ☐ Outline 에 PLACE_GROUP_BY 가 있는가?
    ☐ Plan hash value 가 119122671 인가?  → 01화 W01 원장에 적힌 계획 해시와 같다

[B] — CNT 열
    ☐ 100,000 대 2,000 인가?              → 건수 열이 처음부터 50 배를 말하고 있었다

[D] — 세 숫자
    ☐ 다섯 개 상품의 '화면 매출'이 회사 전체 매출의 3.6 배인가?

[F] — 두 표의 1 위
    ☐ 원장 6 위(TOP 5 밖) ITM00041 이 조인에서는 1 위인가?   → 틀린 답은 '다른 상품'을 1 위에 올린다
    ☐ 원장 2 위 ITM00081 이 조인 쪽 TOP 5 에서 사라졌는가?    → 12월 평가가 3 건뿐이다

STEP 3 — 고치는 법 여섯을 두 축으로 잰다 (10분) ★ ​

bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 01_고치는_법_여섯.sql
powershell
Get-Content 01_고치는_법_여섯.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

채점표: 돌리기 전에 '정답' 열을 먼저 ○/✗로 적어 보자.

#방법내 예상실측 정답실측 Buffers계획 해시
0(rev00) 평가 그대로 조인✗ 50배839119122671
1SUM(DISTINCT) + COUNT(DISTINCT 주문키)✗ 작고 순위가 바뀜8392231864090
2주문상세만 먼저 묶고 평가 원본에 조인✗ 같은 줄 50번839466241876
3EXISTS ← rev01○8392231864090
4IN○8392231864090
5평가를 상품별 한 줄로 묶어 조인○8393732459948
6평가 조인 삭제○ (지금만)8072031405164

[C] 정답 대조 표에서 확인할 것 ​

VARIANT        ROWS_OUT  SUM_AMT               V_MINUS_L  L_MINUS_V
0_REV00             100  1,513,627,885,000          100        100
1_DISTINCT          100     19,421,974,500          100        100
2_AGG_RAW          5000  1,513,627,885,000            0          0    ← MINUS 는 통과한다
3_EXISTS            100     30,272,557,700            0          0
...
LEDGER              100     30,272,557,700

☐ 2_AGG_RAW 의 MINUS 가 0 / 0 인데 ROWS_OUT 이 5,000 인가?   → MINUS 는 중복을 못 본다. 건수·합계를 같이 본다
☐ 1 번과 3 · 4 번의 계획 해시가 같은가?                       → 같은 계획에서 다른 답이 나온다

[D] 조인을 지운 안이 갈리는 날 ​

복사본에서 ITM00071의 평가를 뺀다(평가가 아직 없는 신상품이라고 친다).

☐ 조인 삭제(6) 의 1 위가 여전히 ITM00071 인가?       → '평가가 있는 상품만' 이라는 요구가 사라졌다
☐ EXISTS(3) 의 1 위가 ITM00081 인가?
☐ MINUS 양방향이 1 / 0 인가?
=> 지금 데이터에서 같다는 것은 '맞다'가 아니다 (02화 BETWEEN 과 같은 함정)

STEP 4 — 변환을 읽는다 (15분) ​

bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 02_변환을_읽는다.sql
powershell
Get-Content 02_변환을_읽는다.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

[D]에서 상품과 주문상세의 복사본(M_ITM_C08 · T_ORD_DET_C08)을 만들고 같은 단계 안에서 지운다.

단계변환기대☐
[A]서브쿼리 UnnestingHASH JOIN RIGHT SEMI · UNNEST(@"SEL$2") · 839. NO_UNNEST면 FILTER · Starts 5,097 · 11,001☐
[B]Group-by Placement기본 VW_GBC_/VW_GBF_ · 해시 조인 100행. 끄면 10M 행 · 약 0.33초 · 답은 똑같이 50배☐
[C]View Merging · JPPD기본 VM_NWVW_1 약 931 · NO_MERGE → VIEW PUSHED PREDICATE 약 930 · 둘 다 막으면 1,635☐
[D]조인 제거FK 없음 806 · FK 있음 ELIMINATE_JOIN 802 · 상품명을 쓰면 조인이 남는다☐
[E]NOT IN과 NULLNOT IN 0명 (HASH JOIN ANTI NA) · NOT EXISTS 8명☐
[F]OR-ExpansionNVL · IS NULL OR · DECODE 전부 VW_ORE_ 약 12 · 컬럼을 NVL로 감싸면 FULL 1,205☐

읽는 순서 — 이름표 하나에서 거꾸로 ​

xplan
1. Name 열에서 내가 안 쓴 이름을 찾는다      VW_GBC_7 · VW_ORE_9F22E1CA · VM_NWVW_1 · VIEW PUSHED PREDICATE
2. Outline Data 에서 그 변환의 지시문을 찾는다  PLACE_GROUP_BY · OR_EXPAND · MERGE · PUSH_PRED · UNNEST · ELIMINATE_JOIN
3. 지시문의 별칭(@"SEL$1" "D"@"SEL$1")으로     원래 SQL 의 어느 블록·어느 테이블인지 찾는다
4. Predicate Information 에서 변환이 붙인 조건을 본다   :C IS NULL · LNNVL(:C IS NULL) · "CUST_ID" IS NOT NULL

☐ [B] 두 계획의 결과가 둘 다 22,216,620,000 인가?   → 변환은 답을 바꾸지 않는다. 틀린 답도
☐ [F] NVL 갈래의 전체 스캔 쪽에 "CUST_ID" IS NOT NULL 이 붙었는가?  → 원래 조건의 뜻(NULL 고객 제외)을 지킨다
☐ [A] NO_UNNEST 의 Starts 5,097 이 07화의 식(× 2,000)으로는 왜 안 맞는가?
      → 같은 상품이 연달아 오면 다시 열지 않는다. 5,097 = 97 × 1 + (1,666 + 1,667 + 1,667)  (2_파고들기 §4)

ℹ️ Note 섹션에는 변환이 나오지 않는다. 실행 환경에 관한 사실(this is an adaptive plan 등)만 나온다. 변환의 흔적은 Name 열의 이름표와 Outline에서 읽는다.


STEP 5 — Q2 3차: 스칼라는 왜 조인으로 안 바뀌었나 (10분) ★ 07화가 남긴 질문 ​

bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 03_Q2_스칼라는_왜.sql
powershell
Get-Content 03_Q2_스칼라는_왜.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

약 8초 걸린다. [C]에서는 고객과 지역 코드의 복사본(M_CUS_C08 · M_RGN_C08)을, [E]에서는 T_ORD_BIG에 인덱스 IX_ORD_BIG_C08을 잠깐 만들었다가 지운다. 맨 끝의 [정리 확인]에서 T_ORD_BIG 인덱스가 원래대로 둘인지 확인한다.

단계무엇기대☐
[A]rev01 · UNNEST 힌트5,868~5,869 · Starts 8 · 힌트를 줘도 계획 해시 2292855265 그대로☐
[B]MAX()로 감싸면07화 고객명 779,832~779,833 → 5,856 (HASH JOIN RIGHT OUTER) · Q2는 그대로☐
[C1]지역 코드에 없는 고객 20명스칼라 = 외부조인 (MINUS 0 / 0) · 내부조인만 주문 10,000건 감소 (3 / 3)☐
[C2]지역 코드 두 번 적재외부조인 A 5,128,644,200 (정확히 두 배) · 스칼라 ORA-01427 · 먼저 묶기도 ORA-01427☐
[D]CPU 10회rev01 114~118 · 외부조인 56~61 · 먼저 묶기 30~34 · 바닥 21~24 · Buffers는 5,826~5,868☐
[E](선택) 커버링 인덱스리프 3,624 · Q2 3,689 · 지운 뒤 인덱스 둘☐
[C2] — 오늘의 두 번째 클라이맥스
    B_OUTER_DUP  A   20000   5,128,644,200      ← 원래 10,000 건 / 2,564,322,100
    A_SCALAR_DUP -> ORA-01427: single-row subquery returns more than one row
    ☐ 조인은 조용히 두 배, 스칼라는 멈추는가?
    => 오라클이 '한 행' 증명 없이는 스칼라를 조인으로 바꾸지 않는 이유

[D] — Buffers 와 CPU 를 따로
    ☐ Buffers 가 바닥(5,826)에서 42 블록 안쪽인가?    → 요구를 그대로 두면 블록은 더 못 줄인다
    ☐ 먼저 묶기의 CPU 가 rev01 의 3 분의 1 아래인가?  → 줄일 수 있는 것은 행마다 하는 일뿐
    ☐ 그래도 rev02 를 안 만드는 이유를 T0 로 말할 수 있는가?

📌 Q2는 rev01 그대로 둔다. 외부조인은 7블록을 얻는 대가로 '틀리면 멈춤'을 내준다. 먼저 묶기는 Buffers가 같고, 지금 CPU가 문제라는 증거도 없다. 리포트에 보류안과 재검토 조건(16화)으로 남긴다.


STEP 6 — 정합성 회귀 체크리스트를 채운다 (10분) ★ 새 표준 문서 ​

양식: 정합성_회귀_체크리스트.md

오늘부터 모든 리포트의 T1 8번 칸에 이 표를 붙인다. Q3 rev00 → rev01을 놓고 여덟 항목 A~H를 채운다.

#항목오늘 채울 내용 (힌트)
A기준원장은 평가를 붙이지 않은 상품별 합계다. 직전 리비전(rev00)은 기준이 될 수 없다. 왜일까?
B건수 · 합계 · MINUS · 중복 키STEP 3 [C]의 3_EXISTS 줄
C잘라 내기 전 전체FETCH FIRST 5를 뗀 100행
D조인마다 1쪽T_ITM_EVL은 ITM_ID 하나에 몇 줄인가. 그래서 조인을 무엇으로 바꿨나
E경계 데이터STEP 3 [D] (평가 없는 상품) · STEP 2 [F] (고르지 않은 분포)
F계획 해시틀린 안과 rev01의 계획 해시가 같았다
G변환rev00의 PLACE_GROUP_BY를 '정합성 근거'로 쓰지 않았다
H영향 분석T_ITM_EVL을 조인하는 SQL은 조각 6개 가운데 몇 개인가

소급 — 지난 리포트에도 붙인다 ​

선배는 "1호부터 5호까지. 적어도 B 칸은 채워."라고 했다. 양식 아래쪽의 「소급」 표에 08화에서 잰 두 줄(Q1, Q2)이 있다. 나머지 리포트도 같은 SQL 틀로 대조해 본다.

⚠️ 표가 '통과'로 채워졌다고 끝이 아니다 ​

B 칸이 0 / 0이어도 E 칸이 비어 있으면 '이 데이터에서 같다'는 뜻일 뿐이다. 오늘 하루가 그 말에 대한 반례 모음이었다.


STEP 7 — 10053 트레이스를 꺼내 읽는다 (선택 · 10분) ​

SYSDBA는 필요 없다. 시간이 없으면 건너뛰어도 된다. 이 화의 결론은 STEP 4와 5의 실행계획만으로도 나온다.

bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 90_10053_변환로그.sql
powershell
Get-Content 90_10053_변환로그.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

스크립트가 트레이스 파일 경로 다섯 줄을 찍는다. 파일은 컨테이너 안에 있다. 꺼내서 읽는다.

bash
# Git Bash — MSYS_NO_PATHCONV=1 을 꼭 붙인다 (없으면 /opt/... 가 C:/Program Files/Git/opt/... 로 바뀐다)
T=/opt/oracle/diag/rdbms/free/FREE/trace
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "grep -n '^SU:' $T/FREE_ora_NNN_CH08_9A.trc"
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "grep -n '^GBP:' $T/FREE_ora_NNN_CH08_9D.trc"
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "grep -n -A1 '^Final query after' $T/FREE_ora_NNN_CH08_9D.trc"

# 파일째 꺼내려면
MSYS_NO_PATHCONV=1 docker cp ora-hanorder:$T/FREE_ora_NNN_CH08_9A.trc .
powershell
# PowerShell — 경로 변환이 없으므로 그대로
docker exec ora-hanorder sh -c "grep -n '^SU:' /opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_NNN_CH08_9A.trc"
docker cp "ora-hanorder:/opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_NNN_CH08_9A.trc" .
Select-String -Path .\FREE_ora_NNN_CH08_9A.trc -Pattern '^SU:'

NNN 자리는 스크립트가 찍은 경로의 숫자로 바꾼다.

파일찾을 줄☐
9A Q2 rev01SU: bypassed: Scalar subquery may return more than one row. · GBP: Bypassed: Unsupported argument to aggregate function.☐
9B Q2 + MAX()SU: bypassed: Outer qurery has illegal group-by. (철자 원문 그대로)☐
9C 07화 고객명 + MAX()SU: Unnesting scalar subquery … · Transforming subquery block into inline view.☐
9D Q3 rev00GBP: Starting iteration 1 … 4 · Cost = 760.914613 / 243.190352 / 247.366202 / 243.113503 · "VW_GBC_7"."ITEM_3"*"VW_GBF_8"."ITEM_3"☐
9E Q3 rev01SU: Performing unnesting that does not require costing. · Transforming EXISTS subquery to a join.☐

9D와 9E를 나란히 놓고 보자. EXISTS를 펼칠 때는 비용을 매기지 않고(휴리스틱), 집계 자리를 옮길 때는 네 가지 모양에 비용을 매겨 고른다(비용 기반). 변환이 먼저, 비용이 나중이라는 순서가 파일 안에 그대로 보인다. 트레이스 파일은 실행할 때마다 새로 생긴다. 연습 컨테이너라 지우지 않아도 되지만, 지우려면 docker exec ora-hanorder sh -c "rm $T/*_CH08_9?.trc"를 실행한다(Git Bash에서는 MSYS_NO_PATHCONV=1을 붙인다).


STEP 8 — T1 리포트 6호 ★ 오늘의 산출물 ​

양식은 T1_단건SQL_튜닝리포트.md를 따른다. 이 교재의 첫 정합성 리포트다. Buffers는 839 → 839로 그대로이고, 고친 것은 답이다.

┌──────────────────────────────────────────────────────────────────┐
│ T1 단건 SQL 튜닝 리포트 — 6호                                     │
│ 제목: 빠르고 틀린 쿼리 — 상품별 매출 TOP 5 의 50 배               │
├──────────────────────────────────────────────────────────────────┤
│ 1. 대상      : Q3 (상품별 매출 TOP 5) · rev00 -> rev01 · diff 첨부 │
│ 2. 측정 환경 : ora-hanorder · 26ai Free 23.26.2.0.0 · cpu_count 2    │
│                T_ORD_DET 200,000 / T_ITM_EVL 5,000                │
│ 3. 부하 조건 : ______________________________________________     │
│ 4. 현상      : ______________________________________________     │
│ 5. 원인      : ______________________________________________     │
│                [성능고도화 II권 Ch4 p.462 · 466] [tgsql Query      │
│                 Transformations]                                  │
│ 6. 개선안    : 채택 ______________________________________        │
│                기각 ______________________________________        │
│ 7. 효과      : Buffers ______ -> ______  금액 ______ -> ______      │
│ 8. 정합성    : ★ 정합성 회귀 체크리스트 A ~ H 를 붙인다            │
│ 9. 환경 의존성 : ____________________________________________     │
│ 10. 리스크   : ______________________________________________     │
│ 11. 롤백 절차: ______________________________________________     │
│ 12. 재측정   : ______________________________________________     │
│ 부록. Q2 3차 진단 (리비전 없음) : ____________________________      │
└──────────────────────────────────────────────────────────────────┘

칸마다 들어가야 할 것 ​

4 「현상」: 숫자로 쓴다. "화면 1위 ITM00071 22,216,620,000(원장 444,332,400). 상품 100개 모두 정확히 50배. 순위는 원장과 같다. Buffers 839(01화 841)."

5 「원인」(한 문장): "상품 하나에 주문상세(2,000줄)와 평가(50줄)가 함께 매달린 M:1:M 조인이라 매출이 평가 수만큼 곱해졌다. 2020-02 평가 다건 허용(PK 변경) 때 이 SQL이 영향 분석에서 빠졌다."

6 「개선안」

  • 채택: EXISTS (rev01). 요구('평가가 있는 상품만')를 그대로 두고 곱하기만 없앤다
  • 기각(모두 실측):
    • SUM(DISTINCT): 건수는 맞지만 금액이 작고 순위가 바뀐다. 계획 해시가 rev01과 같다
    • 주문상세만 먼저 묶고 평가 원본에 조인: 같은 줄이 50번 나온다. MINUS로는 안 잡힌다
    • 평가 조인 삭제(807): 요구 조건까지 지운다. 평가 없는 상품이 생기면 MINUS 1 / 0
    • 평가를 한 줄로 묶어 조인(839): 정답이다. 평가 수를 화면에 보여 줘야 할 때 쓴다

7 「효과」: 성능 효과는 없다고 쓴다. Buffers 839 → 839. 정합성 효과는 22,216,620,000 → 444,332,400, 상품 100개 MINUS 0 / 0이다.

T0는 지켰다. '빨라졌다'도 '느려졌다'도 쓸 일이 없는 리포트다.

8 「정합성」 ★: 체크리스트를 통째로 붙인다(STEP 6). 이 칸이 오늘 리포트의 본문이다.

9 「환경 의존성」

  • rev00의 839는 옵티마이저의 Group-by Placement 덕분이다. 변환이 꺼지면 같은 SQL이 1,000만 행을 만든다(0.01 → 약 0.33초). rev01은 이 변환에 기대지 않는다
  • 변환 설정은 버전(optimizer_features_enable 23.1.0)에 묶여 있다

10 「리스크」

  • 과거 자료의 정정 범위: 평가 다건 허용(2020-02) 이후 이 화면 캡처를 쓴 자료. 판단은 재무팀이 한다
  • 평가 없는 상품이 생기면 rev01은 그 상품을 뺀다. 요구대로이긴 하지만, 마케팅팀이 이 요구를 아직 원하는지 확인해야 한다
  • T_ITM_EVL의 카디널리티를 바꾸는 변경이 또 있으면 같은 일이 생긴다(체크리스트 H)

11 「롤백 절차」: "rev00으로 되돌리면 틀린 금액으로 돌아가므로 롤백 대상이 아니다. rev01에 문제가 생기면 화면을 비노출로 내리고 원인을 잰다." '되돌리지 않는다'도 절차다. 칸을 비워 두지 않는다.

12 「재측정」: "T_ITM_EVL의 PK·UK가 바뀌면 즉시. 그리고 18화에서 조각 6개를 원장과 대조할 때."

부록 「Q2 3차 진단」: 리비전 없음.

  • 스칼라 Unnesting은 이 버전에 있으나, Q2에서는 자물쇠 둘(may return more than one row · illegal group-by)에 막혀 일어나지 않음(10053)
  • 손으로 외부조인 전환: Buffers 7블록 감소, 지역 코드가 중복되면 조용히 두 배(스칼라는 ORA-01427). 기각
  • 먼저 묶고 붙이기: Buffers 5,868 그대로 · CPU 114~118 → 30~34(10회) · 결과 같음. 보류. 재검토 조건: 16화에서 이 SQL이 CPU 상위에 오를 때
  • 100만 행 → 3행: 요구를 그대로 두면 블록은 바닥(5,826). 나머지는 요구를 바꿔야 하는 일이라 화면 주인과 협의한다(09화)

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

조민우 씨(정산팀)와 재무팀에 보낼 회신을 쓴다. 질문은 하나였다. "화면 금액이 원장과 안 맞는다."

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

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

① "수정했습니다. 이제 일치합니다"는 증거가 아니다.무엇으로 일치를 확인했는지 쓴다. "상품 100개 전체에서 건수와 합계가 같고, 양쪽으로 대조해도 다른 줄이 없다."

② 재무팀이 제일 먼저 물을 것, 곧 '언제부터 틀렸나'를 먼저 쓴다. 고치는 건 우리 일이지만 지난 숫자를 바로잡는 건 재무팀 일이다. 그 판단에 필요한 정보(평가 다건 허용 이후)를 준다.

③ "순위는 맞았다"를 빼면 안 된다. 마케팅팀이 이 화면을 믿고 써 온 이유이고, 앞으로 평가 수가 고르지 않으면 순위도 틀어진다는 경고이기도 하다.

④ 현업이 알아듣는 말로 쓴다. "M:1:M 조인"이라고 하면 뜻이 전달되지 않는다. "매출에 상품평가를 붙이면서, 평가 한 건마다 매출을 한 번씩 다시 더했다"라고 옮겨야 한다.

⑤ 성능 이야기는 하지 않는다. 이 리포트에는 Buffers 개선이 없다. 성능 이야기를 쓰면 초점이 흐려진다.


STEP 10 — 파인만 자가설명 ​

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

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

태그전형적인 문장
#인과"Group-by Placement로 1,000만 행을 안 만들었는데 왜 답은 그대로 50배인지 설명하다가 막혔다" → 2_파고들기.md §3의 고쳐 쓴 SQL(ITEM_3 * ITEM_3)을 본다
#용어"세미 조인, 안티 조인, Null-Aware 안티 조인의 차이를 말로 못 하겠다" → §4 · §7
#수치"NO_UNNEST의 Starts 5,097이 어디서 나왔는지 계산이 안 된다" → §4의 식을 따라간다(07화의 메모판)

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

  1. 조건 반전: "평가 수가 상품마다 1~100건으로 들쭉날쭉했다면 Q3의 오류는 몇 달이나 숨을 수 있었을까?"
  2. 반례 요구: "양방향 MINUS가 0 / 0인데 답이 틀린 경우를 하나 든다면? 건수와 합계가 같은데 틀린 경우는?"
  3. 측정 지목: "'오라클이 이 스칼라 서브쿼리를 조인으로 바꾸지 않은 이유'를 추측 없이 증명하려면 무엇을 봐야 하나?"

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


완료 체크리스트 ​

  • [ ] STEP 0: Q3_rev00.sql을 만들어(열어) 봤고, Q2의 현재판이 rev01인 것을 확인했다
  • [ ] STEP 1: 보기 전에 예측 4개를 적었다
  • [ ] STEP 2: 화면 금액이 정확히 50배이고 TOP 5 합계가 회사 전체 매출보다 크다는 것을 봤다
  • [ ] STEP 2: 12월 평가로 순위가 뒤집히는 것을 봤다
  • [ ] STEP 3: 여섯 가지 방법의 Buffers로는 정답을 못 가린다는 것과 계획 해시가 같아도 답이 다를 수 있다는 것을 확인했다
  • [ ] STEP 3: MINUS는 0 / 0인데 건수는 5,000인 안을 봤다
  • [ ] STEP 4: rev00의 PLACE_GROUP_BY를 끄면 1,000만 행을 만들고 답은 같다는 것을 봤다
  • [ ] STEP 5: 지역 코드가 중복될 때 조인은 두 배가 되고 스칼라는 ORA-01427로 멈추는 것을 봤다
  • [ ] STEP 6: 「정합성 회귀 체크리스트」 A~H를 채웠고, 소급 표를 확인했다
  • [ ] STEP 8: T1 6호를 썼고, 7 「효과」에는 '성능 효과 없음'을 적고 11 「롤백」도 비우지 않았다
  • [ ] STEP 9: 조민우 씨와 재무팀에 보낼 회신을 문장으로 풀어 썼다
  • [ ] STEP 10: 막힘 태그를 최소 1개 _막힘로그.md에 옮겼다

완결성 게이트: STEP 6의 체크리스트와 STEP 10의 태그가 비어 있으면 이 화는 미완성이다. 이 화의 진짜 산출물은 숫자가 아니라, 빠르다고 해서 맞는 것은 아님을 확인하는 손버릇이다.


다음 화로 넘기는 상태 ​

무엇값
T_ORD_BIG 인덱스2개(IX_ORD_BIG_DT, IX_ORD_BIG_DT_AMT). 08화는 아무것도 남기지 않았다(IX_ORD_BIG_C08은 만들었다가 지웠다)
Q1 현재 리비전rev02 · Buffers 91
Q2 현재 리비전rev01 유지 · Buffers 5,869. 08화는 리비전을 만들지 않았다(보류안 1건: 먼저 묶고 붙이기)
Q3 현재 리비전rev01 · Buffers 839 · 원장과 일치
임시 객체없음 (*_C08 복사본과 인덱스는 스크립트가 지운다) · 10053 트레이스 파일만 컨테이너에 남는다
새 표준 문서「정합성 회귀 체크리스트」. 09화 리포트부터 8번 칸에 붙인다
남긴 빚인덱스 2개의 적재 비용 → 14화
열어 둔 질문100만 행 → 3행의 요구를 바꿔야 하는 반쪽 → 09화 · Q2 보류안 → 16화

다음 → 09화 「100페이지째부터 화면이 멈춘다」: 01화에서 재 놓고 한 번도 열지 않은 Q4를 연다. 오늘 만든 체크리스트를 처음으로 붙이는 리포트가 된다.