Skip to content

08화 · 금액이 50배로 찍힌다 ​

🗓️ 4월 22일 (월) · 07화에서 일주일 뒤 · 무대 ora-hanorder오늘 여는 것: Q3. 01화에서 "가장 작은데 가장 위험하다"라고 적어 두고 7주 동안 미뤄 둔 조각이다. 오늘은 네 번 틀린다. 그리고 "오라클이 알아서 고쳐 준다"는 믿음이 깨진다.


파일 세 개 ​

파일무엇을 하나시간
읽는다1_사건.md재무팀 반려를 받고 50배를 재 보고, 회식비 정산표로 곱셈을 몸소 겪고, 두 번 잘못 고친 끝에 오라클이 이미 SQL을 고쳐 쓰고 있었다는 걸 알게 되고, Q2의 스칼라 서브쿼리가 왜 그대로인지 트레이스로 읽어 내는 하루40분
한다3_실습과_기록.md원장 대조 · 고치는 법 여섯 · 변환 읽기 · Q2 3차 · 정합성 회귀 체크리스트 · (선택) 10053 · T1 6호60~90분
찾는다2_파고들기.md조인 카디널리티 · 변환이 먼저 · 변환 도감(Unnesting · View Merging · JPPD · GBP · 조인 제거 · OR-Expansion) · 10053 · NOT IN · 스칼라 Unnesting필요할 때

1을 읽고 3을 따라 한다. 2는 막힐 때 펼친다. 시간이 없는 주에는 1_사건을 중단 지점 1까지 읽고 3_실습은 STEP 0, 1, 2, 3, 8만 한다(약 45분).


지난 화에서 들고 오는 것 ​

Q3 = 841블록01화 원장에 적힌 값이다. 결과는 5행이다. 옆에 「정합성 · 별도 트랙 최우선」이라고 적어 두고 그 뒤로 한 번도 열지 않았다
Q2 = 5,869블록 (rev01)07화가 넘긴 질문: "오라클은 왜 M_RGN 스칼라를 조인으로 안 바꿨나"(CPU의 절반이 스칼라 캐시 탐색)
06화가 넘긴 질문"100만 행을 읽어 3행을 만드는 게 맞는 요구인가"
07화가 잰 것스칼라 서브쿼리 캐싱은 입력 NDV에 달려 있다. NDV가 2,000이면 Starts가 752,995

그리고 정산팀 조민우 씨에게서 메시지가 하나 와 있다.

"'상품별 매출 TOP 5' 화면 캡처를 분기 자료에 붙였는데 재무팀에서 반려됐습니다. 1위만 봐도 화면은 22,216,620,000, 원장은 444,332,400입니다. 순위는 원장이랑 똑같은데 금액만 이상합니다."


오늘 여는 코드 ​

sql
SELECT d.ITM_ID, SUM(d.ORD_QTY * d.UNT_PRC) AS amt, COUNT(*) AS cnt
  FROM T_ORD_DET d, T_ITM_EVL e
 WHERE e.ITM_ID = d.ITM_ID
 GROUP BY d.ITM_ID ORDER BY amt DESC FETCH FIRST 5 ROWS ONLY;

【그때의 근거】 2019-05-14 hd.kang"상품별 매출 TOP5 : 평가가 등록된 상품만 노출 (마케팅 요청 - 판매 개시 전 테스트 상품 제외)"

그때 상품평가는 상품당 한 건(PK ITM_ID)이었다. 조인은 '평가가 있는 상품만 남기는' 필터 역할을 했고, 답도 맞았다(복사본으로 재현해 보면 원장과 MINUS 0 / 0). 그 뒤 2020-02-03에 "앱 리뷰 오픈. T_ITM_EVL 다건 허용" 커밋이 들어갔는데, 이 커밋에는 '이 테이블을 조인하는 SQL 목록'이 없었다.


오늘의 전개 — 네 번 틀리고, 답을 고친다 ​

내가 믿은 것측정
1차"재무팀 원장 기준이 다른 거 아닐까"평가를 빼고 더하니 444,332,400. 원장이 맞다. 화면 금액은 상품 100개 모두 정확히 50배이고, TOP 5 합계가 회사 전체 매출의 3.6배다
—왜 몇 달 동안 아무도 몰랐나상품마다 평가가 똑같이 50건이라 순위가 원장과 같았다. 12월 평가(3~5건)로 재면 원장 6위가 1위로, 18위가 2위로 올라온다
2차"중복이니까 DISTINCT"건수는 2,000으로 맞는데 금액은 작아지고 순위가 바뀐다(1위 ITM00006 286,527,900, 원장으로는 12위)
3차"먼저 더하고 붙이면 곱할 게 없다"금액은 맞는데 같은 줄이 50번 나온다. MINUS로는 안 잡힌다
—EXISTS로 고쳤다(rev01)Buffers 839 → 839. 처음으로 답을 고친 리비전이다
—그런데 rev00 실행계획에 1,000만 행이 없다오라클이 이미 PLACE_GROUP_BY 변환으로 '먼저 집계하고 나중에 곱하기'를 하고 있었다. 답은 그대로 50배
4차"M_RGN은 PK가 있으니 오라클이 바꿨어야 맞다"10053: may return more than one row · illegal group-by. 코드 테이블이 두 번 적재되면 조인은 금액이 두 배가 되고, 스칼라는 ORA-01427로 멈춘다

이 화는 왜 성능을 다루지 않나? 고친 방법 여섯 가지가 모두 Buffers 807~841이다. Buffers로는 정답이 갈리지 않는다. 01화의 T9에서 정한 순서대로 오늘은 정합성이 먼저다. 오늘 만든 결과물은 앞으로 모든 리포트에 붙는다.


오늘 배우는 한 문장 ​

옵티마이저는 SQL을 빠르게 고쳐 쓸 수는 있어도, 뜻을 고쳐 쓰지는 않는다. 변환은 답을 지킨다. 틀린 답도 그대로 지킨다. 그래서 빠르다는 것이 맞다는 증거가 되지는 않는다.

그리고 하나 더.

조인이 안전한 것은 '1쪽'이 정말 하나일 때뿐이다. 하나가 아니게 되는 순간 스칼라 서브쿼리는 에러를 내며 멈추고, 조인은 조용히 곱한다.


🔑 오늘의 비유 — 회식비 정산표 ​

지난 금요일에 회식이 있었다. 영수증은 세 장(184,000, 62,000, 62,000)이고 참석자는 네 명이다. 총무팀 엑셀 양식대로 참석자마다 그날 영수증을 전부 옮겨 적으면 열두 줄에 1,232,000이 된다. 영수증은 한 장도 늘지 않았는데 회식비는 네 배가 됐다.

회식비 정산표Q3
회식 한 건 (가운데 있는 하나)상품 ITM_ID
영수증 (한쪽에 여럿)주문상세: 상품당 2,000줄, 곧 매출
참석자 서명 (다른 쪽에 여럿)상품평가: 상품당 50줄
참석자마다 영수증을 전부 옮겨 적는다M:1:M 조인
3장 × 4명 = 12줄2,000 × 50 = 100,000줄(상품 하나)
'같은 금액 영수증은 한 장만': 62,000 하나가 사라진다SUM(DISTINCT)
'이 회식에 참석자가 있었나'만 확인한다EXISTS (세미 조인)
'영수증 합계 × 참석 인원'으로 계산하는 영리한 엑셀옵티마이저의 Group-by Placement: 빠르지만 여전히 네 배
회식마다 참석 인원이 다르면 1등이 바뀐다평가 수가 고르지 않으면 순위가 뒤집힌다

(01화는 커피 캐리어(블록), 02화는 방문객 출입대장(인덱스), 06화는 4층 거래처 카드함(조인), 07화는 탕비실 픽업 선반(버퍼 캐시)이었다.)


☠️ 오늘의 함정 — 같은 계획, 다른 답 ​

문장답Buffers계획 해시
SUM(DISTINCT 금액) + COUNT(DISTINCT 주문키)✗ 틀림 (1위 ITM00006 286,527,900)8392231864090
EXISTS (rev01)○ (1위 ITM00071 444,332,400)8392231864090
IN○8392231864090

계획 해시가 같다. DISTINCT 앞에서는 중복이 의미가 없다는 걸 알고 옵티마이저가 조인을 세미 조인으로 바꿨기 때문이다(PARTIAL_JOIN). 계획 해시는 연산의 모양만 본다. 「정합성 회귀 체크리스트」의 F 항목이 여기서 나왔다. 계획이 같아도 결과를 대조한다.


실습 준비 ​

bash
docker start ora-hanorder

07화를 마친 상태(T_ORD_BIG에 인덱스 2개, 임시 객체 없음)에서 시작한다. 원본 테이블에는 쓰지 않는다. 경계 데이터는 전부 복사본(*_C08)에 만들고 스크립트 끝에서 지운다. 몇 번을 돌려도 같은 결과가 나온다.

PowerShell에서는 먼저 $OutputEncoding = New-Object System.Text.UTF8Encoding $false 한 줄을 실행해 둔다. 그래야 안내문의 한글이 ?로 깨지지 않는다.

스크립트무엇소요
실습/00_원장과_대조.sql[A]~[F]. 오늘 실습의 본체: 50배 · 그때는 맞았다 · 순위가 뒤집히는 날약 1초
실습/01_고치는_법_여섯.sql여섯 가지 고친 문장 × 정답 · Buffers · 계획 해시 · 경계 데이터약 1초
실습/02_변환을_읽는다.sqlUnnesting · GBP · View Merging/JPPD · 조인 제거 · NOT IN · OR-Expansion약 1초
실습/03_Q2_스칼라는_왜.sqlQ2 3차: 스칼라 Unnesting의 자물쇠 · 코드 중복/고아 · CPU · (선택) 커버링 인덱스약 8초
실습/90_10053_변환로그.sql선택. 10053 트레이스 다섯 개. 꺼내는 법은 3_실습 STEP 7에 있다(Git Bash에서는 MSYS_NO_PATHCONV=1)약 1초

리비전은 리비전/에 Q3_rev00 → Q3_rev01 순서로 남는다. Q2는 리비전을 만들지 않는다.


이 화의 산출물 ​

무엇어디
Q3_rev01: 금액 22,216,620,000 → 444,332,400 · Buffers 839 → 839리비전/Q3_rev01.sql
T1 리포트 6호: 이 교재의 첫 정합성 리포트(성능 효과: 없음)3_실습과_기록.md STEP 8
「정합성 회귀 체크리스트」 ★ 새 표준 문서. 이후 모든 리포트에 붙는다40_보고서와_표준/템플릿/정합성_회귀_체크리스트.md

이 화가 다음으로 넘기는 것 ​

무엇어디로
100만 행 → 3행의 나머지 반: 요구(마감 기준, 기간)를 바꾸자는 이야기를 어떻게 꺼내고 어떻게 적나09화
「정합성 회귀 체크리스트」를 붙이는 첫 리포트09화
조인 결과의 E-Rows는 어떻게 추정되나10화
OR-Expansion의 :C IS NULL 갈래: 바인드 값에 따라 갈리는 실행계획11화
기각한 커버링 인덱스 (CUS_ID, ORD_AMT)의 적재 비용14화
같은 3행을 실행할 때마다 다시 계산하지 않는 법15b화
Q2 보류안('주문을 고객별로 먼저 묶고 붙이기', CPU 114~118 → 30~34): 이 SQL이 정말 CPU 상위인가16화
조각 6개 모두 원장과 대조18화

Q2는 왜 전진하지 않나 ​

07화에서는 잴 게 없어서 Q2를 고치지 않았다. 08화에서는 바꾸면 잃는 것이 있어서 고치지 않는다. 스칼라 서브쿼리를 외부조인으로 바꾸면 Buffers는 7블록 아끼지만, 지역 코드가 하나가 아니게 되는 날 '에러로 멈춤'이 '조용히 두 배'로 바뀐다. '먼저 묶고 붙이기'는 답과 멈춤 동작을 그대로 둔 채 CPU를 3분의 1 아래로 줄이지만, Buffers는 5,869 → 5,868이다. T0 기준으로는 리비전이 아니다. 그래서 리포트에 보류안으로만 남긴다.


← 07화 「히트율 99퍼센트인데 서버가 탄다」 · 교재 전체 안내 · 다음 → 09화 「100페이지째부터 화면이 멈춘다」