테마
01화 · 인수인계 없는 인수인계
3월 4일 월요일, 입사 사흘째다. 오늘은 아무것도 고치지 않는다. 대신 잴 수 있게 해 둔다.
오전 9시 12분
월요일 아침 사무실은 아직 반쯤 비어 있었다. 출근한 지 사흘째였고, 책상에 뭘 올려 둘지도 아직 정하지 못했다. 내게 있는 거라곤 모니터 두 대와 사내 메신저, 그리고 아무도 쓰는 법을 알려 주지 않은 VPN 계정 하나뿐이었다.
9시 12분에 티켓이 하나 배정됐다.
─────────────────────────────────────────────────────────────
[운영지원] #4417 야간정산 화면 지연
등록: 박팀장 (운영1팀) 우선순위: 높음
─────────────────────────────────────────────────────────────
정산 조회 화면이 30초 넘게 걸립니다.
예전엔 안 그랬어요. 데이터가 늘어난 것 말고는 바뀐 게 없습니다.
야간 배치도 요즘 04시를 넘긴다고 오과장님이 그러시더군요.
담당자가 없어서 신입분께 배정합니다.
─────────────────────────────────────────────────────────────담당자 칸에 내 이름이 적혀 있었다.
"담당자가 없어서"라는 문장을 세 번 읽었다. 이 시스템은 본 적도 없었다. 테이블이 몇 개인지도 몰랐다. 어제까지 한 일이라고는 사내 위키에서 조직도를 찾아 읽은 게 전부였다.
시스템을 만든 사람은 강도현 선배인데, 2년 전에 퇴사했다고 했다. 인수인계 문서를 찾아봤지만 위키에도, 공유 드라이브에도, 팀 채널 고정 메시지에도 없었다. 소스를 열어 보니 맨 위에 주석 한 줄만 달려 있었다.
sql
-- 건드리지 말 것 (2019.03 강도현)건드리지 말라고 적힌 코드를 고치라는 티켓을 받았다. 입사 사흘째 아침은 그렇게 시작됐다.
오전 9시 40분
옆자리에 누가 앉았다. 짐은 박스 하나뿐이었다.
"오늘부터 같은 팀이에요. 윤세라입니다."
경력이 12년이라고 했다. 반갑기보다 안도감이 먼저 들었다. 적어도 이 티켓을 혼자 붙들고 있지 않아도 됐으니까.
티켓을 보여 줬다. 선배는 화면을 5초쯤 보더니 물었다.
"30초라는 건 누가 잰 거야?"
잠깐 멈칫했다. 무슨 뜻으로 묻는지 몰라서였다. "…박팀장님이요."
"그럼 그건 측정이 아니라 증언이야."
선배는 커피를 한 모금 마셨다. 탓하는 말투는 아니었다. 오히려 당연한 사실을 일러 주는 쪽에 가까웠다.
"'30초 걸린다'는 말에는 세 가지가 빠져 있어. 첫째, 무엇을 쟀는지. 화면 전체야, 쿼리 하나야, 배치까지 다 포함한 거야? 둘째, 어떤 조건에서 쟀는지. 혼자 썼을 때야, 야간 배치랑 겹쳤을 때야? 셋째, 어떤 단위로 쟀는지. 초는 장비랑 그날 부하에 따라 달라지거든."
그제야 티켓을 다시 봤다. 셋 다 없었다.
"그럼 뭐부터 해야 하죠?"
소스 파일을 열려고 마우스에 손을 올리자 선배가 손을 들었다.
"잠깐. SQL 열기 전에 할 게 있어." 선배는 모니터가 아니라 나를 보고 있었다. "이 시스템에 테이블이 몇 개야?"
몰랐다. 사흘째였으니까.
"모르는 시스템에서 SQL부터 고치는 건 도면도 없이 남의 집 배관을 고치는 거랑 같아. 운 좋으면 물이 나오고, 운 나쁘면 아랫집까지 젖어." 선배는 커피잔을 내려놨다. "오전은 시스템 파악하는 데 써. 튜너가 제일 먼저 할 일이 그거야."
오전 10시 05분
접속부터 해야 했다. 터미널을 열고 습관처럼 명령을 쳤다.
bash
sqlplus sqlp/sqlp@localhost:1521/FREEPDB1bash: sqlplus: command not found생각해 보면 당연한 결과인데 손이 먼저 나갔다. 오라클은 컨테이너 안에서 돌고 있고, sqlplus도 그 안에 있다. 내 PC에는 오라클 클라이언트를 깐 적이 없었다. 방법은 둘뿐이었다. 컨테이너 안의 sqlplus를 빌려 쓰거나, 내 PC에 클라이언트를 설치하거나.
빌려 쓰는 쪽이 빨랐다.
bash
docker exec -it ora-hanorder sqlplus sqlp/sqlp@localhost/FREEPDB1Connected to:
Oracle AI Database 26ai Free Release 23.26.2.0.0
SQL>프롬프트가 떴다. 들어가자마자 한 줄부터 쳤다. 표가 잘려 나오면 읽을 수가 없어서였다.
sql
SET LINESIZE 150 PAGESIZE 100 SERVEROUTPUT OFF📌 접속이 안 되거나 GUI(SQL Developer)로 붙고 싶다면
00_자료준비/02_접속과_실행_방법.md를 본다.
그리고 맨 먼저 물어야 할 것부터 물었다. 무엇이 얼마나 있나.
sql
SELECT t.table_name, t.num_rows, t.blocks, t.avg_row_len
FROM user_tables t
ORDER BY t.num_rows DESC NULLS LAST;테이블 16개가 나왔다.
TABLE_NAME NUM_ROWS BLOCKS AVG_ROW_LEN
---------------- ---------- --------- ------------
T_ORD_BIG 1000000 5923 37
T_ORD_DET 200000 828 24
ORDERS 200000 1232 38
T_ORD 100000 849 54
T_ORD_STLM 100000 622 33
CUSTOMERS 20000 138 37
T_ITM_EVL 5000 43 47
PRODUCTS 2000 21 29
M_CUS 2000 28 90
M_ITM 100 5 29
M_ITM_PRC_HIS 100 5 32
EMP 14 5 38
M_RGN 8 5 16
SALGRADE 5 5 10
DEPT 4 5 20
T_STLM_NUM 1 5 7한참 목록을 들여다보다가 이상한 점을 하나 발견했다.
이름 규칙이 두 가지였다.
한쪽은 T_ORD_BIG, T_ORD_DET, M_CUS처럼 접두어를 붙인 줄임말이었고, 다른 한쪽은 ORDERS, CUSTOMERS, PRODUCTS, EMP, DEPT처럼 접두어 없는 영어 단어였다. 한 시스템 안에 규칙 두 개가 섞여 있는 건 이상했다.
선배에게 물었다.
"아, 그거." 선배는 화면을 흘끗 보고 바로 답했다. "접두어 없는 게 2015년에 만든 구 주문시스템이고, M_/T_ 붙은 게 2019년에 강도현 씨가 새로 만든 거야. 구 시스템은 지금 안 쓰는데 못 지웠어. 정산 대사할 때 가끔 들여다본대."
"안 쓰는데 안 지운다고요?"
"현장에선 흔한 일이야. 새 시스템을 만들고도 옛것을 못 버리는 거지. 지우려면 '정말 아무도 안 쓰나'를 증명해야 하는데, 그걸 증명할 사람이 퇴사했으니까." 선배가 어깨를 으쓱했다. "접두어는 외워 둬. M_은 Master, 잘 안 바뀌는 기준 정보. T_는 Transaction, 계속 쌓이는 거래 데이터. 강도현 씨가 쓰던 규칙이야."
그러니까 한 계정 안에 시스템 두 개가 섞여 있었다. 오늘 문제가 된 쪽은 신 시스템이었다. 정산 배치가 읽는다는 T_ORD_BIG이 100만 행으로 단연 컸다.
⛳ 오늘은 여기까지(중단 지점 1) 시간이 없다면 여기서 끊어도 된다. 오전에 한 일은 "무엇이 얼마나 있나"를 눈으로 확인한 것뿐이다. 이어서 읽으면 관계(ERD)와 인덱스를 본다.
오전 11시 20분
테이블 목록만으로는 관계를 알 수 없었다. 주문과 고객이 어떻게 이어지는지 봐야 했다. 그래서 제약조건을 조회했다.
sql
SELECT c.table_name, c.constraint_name,
DECODE(c.constraint_type,'P','PK','R','FK','U','UK') AS tp
FROM user_constraints c
WHERE c.constraint_type IN ('P','R','U')
ORDER BY c.table_name, tp;결과를 보다가 같은 줄을 두 번 읽었다.
TABLE_NAME CONSTRAINT_NAME TP
---------------- -------------------- ---
CUSTOMERS PK_CUSTOMERS PK
DEPT PK_DEPT PK
EMP FK_EMP_DEPT FK ← 유일한 FK
EMP PK_EMP PK
M_CUS PK_M_CUS PK
M_ITM PK_M_ITM PK
M_ITM_PRC_HIS PK_M_ITM_PRC_HIS PK
M_RGN PK_M_RGN PK
ORDERS PK_ORDERS PK
PRODUCTS PK_PRODUCTS PK
T_ITM_EVL PK_T_ITM_EVL PK
T_ORD PK_T_ORD PK
T_ORD_DET PK_T_ORD_DET PK
T_STLM_NUM SYS_C0011988 PK ← 이름을 안 지어 준 PK마지막 줄의 SYS_C0011988은 이름 없이 만든 제약조건에 오라클이 자동으로 붙인 번호다. PK_T_STLM_NUM처럼 이름을 주지 않고 PRIMARY KEY만 쓰면 이렇게 된다. 번호는 설치할 때마다 달라지므로 화면에 다른 번호가 나와도 정상이다.
그보다 눈에 띄는 게 따로 있었다.
외래키가 딱 하나뿐이었다. 그나마도 EMP → DEPT로, 구 시스템에 딸려 온 교과서 예제가 남긴 흔적이었다.
주문 시스템 전체에 FK가 0개였다.
T_ORD_BIG은 아예 목록에 없었다. PK조차 없다는 뜻이었다.
"선배님, 이거 FK가 하나도 없는데요."
선배는 자기 모니터에서 눈을 떼고 내 쪽을 봤다. 조금 흥미롭다는 표정이었다.
"그래서?"
"그러면… T_ORD.CUS_ID가 M_CUS.CUS_ID를 가리킨다는 걸 데이터베이스는 모르는 거잖아요."
"맞아. 관계가 데이터에만 있고 스키마에는 없는 거야. 레거시에서는 아주 흔해." 선배가 의자를 돌렸다. "왜 뺐을 것 같아?"
잠깐 생각했다. "…귀찮아서요?"
"그런 사람도 있지. 근데 대부분은 이유가 있어. 대량으로 적재할 때 FK 체크가 느리거든. 1억 건을 밀어 넣는데 건마다 부모가 있는지 확인하면 배치가 안 끝나. 그래서 빼. 그리고 다시는 안 넣지. 급할 게 없으니까."
"그럼 괜찮은 거 아닌가요? 어차피 애플리케이션이 맞춰서 넣을 텐데."
"세 가지가 깨져." 선배가 손가락을 폈다. "첫째, 고아 데이터를 못 막아. 없는 고객의 주문이 들어가도 DB는 그냥 통과시켜. 둘째, 옵티마이저가 관계를 몰라. 조인하면 몇 건이 나올지 추정이 부정확해져. 셋째가 재밌는데, 그건 조금 있다가 직접 보게 될 거야."
오전 11시 50분
세 번째가 뭔지는 10분 뒤에 알았다.
구조를 그림으로 보고 싶어서 SQL Developer를 열었다. 오라클이 무료로 배포하는 도구인데, 안에 든 Data Modeler로 기존 DB에서 ERD를 거꾸로 그려 낼 수 있다고 했다.
파일 > Data Modeler > 가져오기 > 데이터 딕셔너리
→ 접속 선택 (HANORDER) → 스키마 SQLP 체크 → 테이블 전체 선택 → 완료다이어그램이 그려졌다. 상자가 16개 나타났다.
그런데 상자 사이에 선이 하나도 없었다.
잘못 누른 줄 알고 다시 해 봤다. 그대로였다. 옵션을 바꿔 봤다. 역시 그대로였다. 도구가 고장 난 줄 알고 선배를 불렀다.
선배는 화면을 보더니 웃었다.
"고장 안 났어. Data Modeler는 FK를 읽어서 선을 긋거든. 이 시스템엔 FK가 없으니까 그을 선이 없는 거야."
아까 SQL로 확인한 "FK 0개"를 그림으로 한 번 더 확인한 셈이었다. 한참 그 빈 화면을 봤다. 상자 16개가 서로 아무 관계 없이 떠 있었다. 실제로는 전부 이어져 있는데도.
"그럼 이 관계는 누가 알고 있는 거예요?"
"지금은 아무도." 선배가 덤덤하게 말했다. "강도현 씨 머릿속에 있었겠지. 이제는 네가 알아내야 해. 컬럼 이름 보고 추론해서, 그림 그려서, 위키에 올려 두는 것까지가 인수인계야. 그게 네 첫 산출물이 될 거고."
그날 오후, 컬럼 이름을 하나씩 맞춰 보며 관계를 추론해 그렸다. T_ORD.CUS_ID는 M_CUS.CUS_ID로, T_ORD_DET.ORD_SEQ는 T_ORD.ORD_SEQ로 이었다. 모두 이름이 같다는 것 말고는 근거가 없는 연결이었다.
📄 그렇게 그린 ERD와 테이블별 컬럼 설명은
2_파고들기.md에 있다.
오후 12시 40분
점심을 먹고 돌아와서 인덱스를 봤다. 조회가 느리다는 얘기가 나왔으니 당연한 순서였다.
sql
SELECT i.table_name, i.index_name, i.uniqueness
FROM user_indexes i ORDER BY i.table_name;
-- 그리고 반대로, 인덱스가 하나도 없는 테이블
SELECT table_name FROM user_tables
WHERE table_name NOT IN (SELECT table_name FROM user_indexes) ORDER BY 1;두 번째 쿼리 결과에 T_ORD_BIG이 있었다.
100만 행짜리 테이블에 인덱스가 0개였다.
비교해 보니 더 눈에 띄었다. 구 시스템의 ORDERS는 20만 행인데 인덱스가 3개(PK, 고객ID, 주문일자) 있었다. 신 시스템의 T_ORD_BIG은 그 5배 크기인데 0개였다.
이쯤 되자 답을 찾은 것 같았다.
"선배님, 알았어요. T_ORD_BIG이 100만 건인데 인덱스가 0개예요. 인덱스를 만들면 될 것 같습니다."
선배는 고개를 끄덕이지도, 젓지도 않았다. 그냥 물었다.
"답일 수도 있어. 그런데 지금 그걸 어떻게 알아?"
입을 열었다가 도로 닫았다.
"'100만 건이라 느리다'가 맞으려면, 100만 건을 다 읽고 있다는 걸 봤어야지. 봤어?"
못 봤다. 행 수만 봤지, 그 쿼리가 실제로 몇 건을 읽는지는 본 적이 없었다.
"인덱스가 없는 건 사실이야. 근데 그게 원인인지는 아직 아무도 안 봤어." 선배가 덧붙였다. "지금 네 머릿속에 있는 문장을 적어 둬. 지우지 말고. 오늘 오후에 네 손으로 그 문장을 반박하게 될 거야."
메모장에 적었다.
3/4 오후 12:50 — 내 첫 가설
"T_ORD_BIG이 100만 건인데 인덱스가 0개라서 느리다. 인덱스를 만들면 해결된다."✍️ 지금 같이 적어 보자. 더 읽기 전에 왜 느릴 것 같은지 한 문장으로 적는다. 틀려도 된다. 오히려 틀려야 기억에 남는다. 실행계획을 보고 나면 누구나 "나도 그럴 것 같았어"라고 생각하게 되는데, 이를 사후 확신 편향이라고 한다. 이 편향 때문에 무엇을 몰랐는지가 기록에 남지 않는다. 적어 둘 곳은
3_실습과_기록.md에 있다.
오후 2시 20분
이제 코드를 열 차례였다.
야간 정산 패키지 PKG_ORD_STLM을 열어 보니 생각보다 작았다. 조각이 6개뿐이었다.
| 조각 | 하는 일 |
|---|---|
| Q1 | 기간별 주문 조회 |
| Q2 | 고객등급별 매출 집계 |
| Q3 | 상품평가 조인 |
| Q4 | 정산 목록 페이징 |
| P1 | 정산 적재 배치 |
| N1 | 채번 |
선배가 지나가다 화면을 봤다.
"그 여섯 개, 잘 봐 둬. 앞으로 1년 동안 네가 계속 고칠 물건이야. 매주 새 예제를 보는 게 아니라 같은 여섯 조각을 계속 고쳐. 그래야 '지난주에 한 것' 위에 '이번 주에 배운 것'이 쌓여."
Q1부터 열었다.
sql
SELECT COUNT(*), SUM(ORD_AMT)
FROM T_ORD_BIG
WHERE TO_CHAR(ORD_DT, 'YYYYMMDD') BETWEEN '20240601' AND '20240630';날짜 컬럼을 문자로 바꿔서 비교하고 있었다. 왜 거슬리는지 정확히 설명할 수는 없었지만, 어쩐지 먼 길을 돌아가는 것처럼 보였다.
커밋 로그를 찾아봤다.
2019-03-11 hd.kang
날짜 타입 비교에서 시분초 때문에 하루가 누락되는 버그 수정. 문자로 통일.그 한 줄을 두 번 읽었다. 버그를 고친 거였다. ORD_DT에 시분초가 들어 있으면 BETWEEN으로 6월 30일까지 잡을 때 30일 오전 0시 이후 데이터가 빠진다. 실제로 터진 문제였고, 강도현은 그 문제를 고쳤다. 방법이 나빴을 뿐이다.
"그 사람 욕하고 싶어졌어?" 선배가 물었다.
"아니요. 오히려 좀…." 적당한 말이 떠오르지 않았다.
"그게 맞아. 미워해야 할 건 사람이 아니라 근거를 안 남긴 결정이야. 저 커밋 로그엔 '왜 문자로 통일했는지'는 있는데 '그때 얼마나 빨랐는지'가 없어. 그래서 지금 우리가 '이걸 바꿔도 되나'를 판단할 수가 없는 거야."
📄 6조각의 원문 전체와 조각별 커밋 로그는
2_파고들기.md에 있다.
오후 3시 10분
선배가 커피를 두 잔 들고 왔다. 한 잔을 내 책상에 놓더니, 화이트보드 쪽이 아니라 탕비실 쪽으로 턱짓을 했다.
"따라와 봐."
탕비실 캐비닛에 네 잔짜리 종이 캐리어가 쌓여 있었다.
"우리 층 커피 주문 규칙 알지."
"…한 잔만 마셔도 캐리어째 가져오는 거요."
"왜 그렇게 할 것 같아?"
생각해 본 적이 없었다. 원래 그렇게 하는 거였다. "…들고 오기 편해서요?"
"그래. 한 잔씩 네 번 왔다 갔다 하는 것보다 네 잔을 한 번에 드는 게 싸거든. 비싼 건 왕복이지 커피가 아니야." 선배가 자기 잔을 들었다. "오라클도 똑같은 결론을 내렸어. 오라클에는 행을 읽는 기능이 없어."
잘못 들은 줄 알았다. 행을 못 읽는 데이터베이스라니. 그럼 SELECT는 뭘 한다는 건가.
"…그럼 뭘 읽는데요?"
"블록. 8킬로바이트짜리 덩어리. 주문 한 건만 필요해도 그 주문이 든 8KB를 통째로 들고 와. 캐리어처럼." 선배가 내 자리 쪽을 가리켰다. "아까 본 표에서 T_ORD_BIG 평균 행 길이가 얼마였지?"
아침에 본 숫자였다. 37바이트.
"그럼 캐리어 하나에 몇 잔 들어가?"
머릿속으로 계산했다. 8192 나누기 37이면 221이다. 헤더와 여유 공간이 있을 테니 그보다는 조금 적을 것이다.
"한 170건쯤이요."
"재 봐."
자리로 돌아와 쿼리를 쳤다.
sql
SELECT blocks, num_rows, ROUND(num_rows/blocks) AS rows_per_block, avg_row_len
FROM user_tables WHERE table_name = 'T_ORD_BIG'; BLOCKS NUM_ROWS ROWS_PER_BLOCK AVG_ROW_LEN
--------- ---------- --------------- ------------
5923 1000000 169 37169.
"맞혔네." 선배가 뒤에서 말했다. "이제 아침에 본 숫자가 다르게 보일 거야. T_ORD_BIG 전체가 5,923블록이지? 그게 무슨 뜻이야?"
잠깐 생각했다. "…이 테이블을 처음부터 끝까지 다 읽으면 캐리어를 5,923번 든다는 뜻이요."
"그래. 그게 이 테이블을 통째로 읽는 비용이야. 5,923. 초가 아니라." 선배가 커피를 마셨다. "오늘부터 규칙 하나. 초로 말하지 마. 블록으로 말해."
"왜요? 사용자는 초로 느끼잖아요."
"느끼지. 근데 초는 네가 어쩔 수 있는 숫자가 아니야." 선배가 손가락으로 책상을 짚었다. "같은 SQL을 점심시간에 재면 빠르고, 배치 도는 새벽에 재면 느려. 장비를 바꾸면 또 달라져. 네가 고친 게 효과가 있었는지, 그냥 서버가 한가했던 건지 구분이 안 돼."
"블록은요?"
"같은 SQL, 같은 데이터면 몇 번을 재도 같은 숫자가 나와. 서버가 바빠도, 새벽이어도, 장비를 바꿔도 같아. 그러니까 네가 한 일을 증명할 수 있는 숫자는 이것뿐이야." 선배가 빈 캐리어를 쓰레기통에 넣었다. "그리고 이 캐리어, 기억해 둬. 나중에 다시 꺼낼 거야. 캐리어 하나에 몇 잔이 들어 있느냐보다 더 고약한 문제가 있거든."
"그게 뭔데요?"
"아직. 지금 말하면 네가 엉뚱한 걸 고치려 들 거야."
📐 블록 안에 정확히 무엇이 들어 있는지(헤더, 행 디렉터리,
PCTFREE), 그리고BLOCKS와 실제로 읽는 블록 수가 왜 다른지는2_파고들기.md에서 다룬다.
오후 4시 00분
이제 잴 차례였다. 그런데 무엇으로 재야 하는지 몰랐다.
"계기판부터 켜야지." 선배가 말했다. "실행계획이라는 게 있어. 오라클이 '이 SQL을 이렇게 처리하겠다'고 세운 계획표야. 근데 두 종류가 있으니까 헷갈리면 안 돼."
선배가 내 키보드를 잠깐 빌렸다.
sql
EXPLAIN PLAN FOR
SELECT COUNT(*) FROM T_ORD_BIG WHERE ORD_ST = 'CANCEL';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY());"이건 예상이야. 실행은 안 해. '이렇게 할 거야'라는 계획만 보여 줘." 선배가 이어서 다음 쿼리를 쳤다.
sql
SET SERVEROUTPUT OFF
SELECT /*+ GATHER_PLAN_STATISTICS */ COUNT(*)
FROM T_ORD_BIG WHERE ORD_ST = 'CANCEL';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST'));"이건 실제야. 진짜로 돌리고, 돌리면서 센 걸 보여 줘. 우리가 쓸 건 이쪽이야."
결과가 나왔다.
xplan
------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 5826 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 5826 |
|* 2 | TABLE ACCESS FULL | T_ORD_BIG | 1 | 333K| 20000 |00:00:00.01 | 5826 |
------------------------------------------------------------------------------------------"읽어 봐." 선배가 말했다. "Buffers부터."
"5,826이요. 아까 그 캐리어 수네요."
"그래. Buffers가 논리적 읽기야. 블록을 몇 번 만졌는지. 오늘부터 네가 보고서에 쓸 숫자가 이거야. 그다음, E-Rows랑 A-Rows는?"
두 숫자를 보고, 다시 봤다.
E-Rows = 333K ← 오라클이 예상한 건수
A-Rows = 20000 ← 실제로 나온 건수16배 차이가 났다.
"…오라클이 틀렸는데요?"
"틀렸지. 왜 틀렸을 것 같아?"
알 수 없었다. 선배가 힌트를 줬다. "ORD_ST에 값이 몇 종류 있지?"
sql
SELECT ORD_ST, COUNT(*),
ROUND(RATIO_TO_REPORT(COUNT(*)) OVER ()*100, 2) AS pct
FROM T_ORD_BIG GROUP BY ORD_ST ORDER BY 2 DESC;ORD_ST COUNT(*) PCT
---------- --------- ------
COMPLETE 900000 90.00
PENDING 80000 8.00
CANCEL 20000 2.00세 종류였고, 90 대 8 대 2로 심하게 치우쳐 있었다.
"오라클은 종류가 세 개라는 건 알아. 근데 어떻게 나뉘어 있는지는 몰라. 그럼 어떻게 추정하겠어?"
"…3분의 1씩이라고 보겠네요. 100만 나누기 3이면 33만."
"그래서 E-Rows가 333K야. 실제 CANCEL은 2%인데."
한참 그 숫자를 봤다. 데이터베이스가 자기 데이터를 잘못 알고 있고, 잘못 안 그대로 계획을 세운다는 게 그제야 실감 났다.
"이거 고칠 수 있어요?"
"고칠 수 있어. 근데 오늘은 아니야." 선배가 말했다. "그건 10화에서 할 거야. 지금은 이것만 알아 둬. 오라클도 틀릴 수 있어. 그리고 틀렸는지는 E-Rows와 A-Rows를 나란히 놓아야만 보여."
오후 4시 30분 — 측정
이제 6조각을 전부 쟀다. 스크립트는 3_실습과_기록.md에 있고, 하는 일은 단순했다. 조각마다 한 번씩 돌리고, V$SQL에서 태그로 찾아 블록 수를 뽑는다.
약 3초 뒤에 표가 나왔다.
PIECE SQL_ID EXECS BUFFERS READS MS ROWS_OUT
------ -------------- ------ ---------- ---------- ---------- ----------
Q1 gcn22v46wnzy3 1 5827 0 112 1
Q2 5agpn6jmgm8rf 1 1026849 0 774 3
Q3 ff93fhdf13hwz 1 841 0 14 5
Q4 9dhfhrd7cqvsf 1 5826 0 42 10
Q4c 8zwr294pmr8zn 1 5826 0 9 1Q2 줄에서 눈이 멈췄다.
1,026,849.
아침에 본 숫자를 떠올렸다. T_ORD_BIG 전체가 5,923블록이었다. 그런데 Q2 하나가 백만 블록을 읽고 있었다.
나눠 봤다. 1,026,849 ÷ 5,826 = 176.
결과 3줄을 만들려고 테이블 전체를 176번 읽고 있었다.
"선배님."
"봤어?"
"Q2가… 테이블을 176번 읽는데요. 결과는 세 줄이에요."
"그게 오늘 건진 거야." 선배가 의자를 끌고 왔다. "자, 이제 아침에 적은 가설 꺼내 봐."
메모장을 열었다. "T_ORD_BIG이 100만 건인데 인덱스가 0개라서 느리다."
"뭐가 틀렸는지 말해 봐."
표를 다시 보고 천천히 말했다.
"…Q3는 841블록이에요. 같은 데이터베이스인데요. '100만 건이라서 느리다'가 맞으면 Q3도 느려야 하는데 안 느려요. 크기로는 설명이 안 되네요."
"또?"
"Q1도 5,827이에요. Q2랑 같은 테이블을 읽는데 176배 차이가 나요. 테이블이 문제가 아니라… 쿼리가 테이블을 어떻게 읽느냐가 문제인 것 같아요."
"그게 오늘 네가 얻은 거야." 선배가 말했다. "하루 만에 '인덱스 만들면 되겠네'에서 거기까지 왔으면 괜찮아."
⛳ 오늘은 여기까지(중단 지점 2) 측정이 끝났다. 원장에 숫자가 들어갔고, 아침에 세운 가설도 반박했다. 여기까지만 해도 오늘 할 일은 다 했다. 이어지는 저녁 대화에서는 오늘 내린 결론을 뒤집어 본다. 18화의 마지막 조항이 여기서 나온다.
오후 6시 10분
사무실이 비어 갈 무렵, 선배가 커피를 또 들고 왔다. 오늘만 세 잔째였다.
"하나만 더 묻고 보내 줄게." 선배가 내 화면에 띄운 원장을 봤다. "Q2가 전체의 98%야. 그럼 Q2부터 고치면 되겠네?"
당연한 것 같았다. "네."
"Q3 봐. 841블록. 제일 작지?"
"네."
"그거 답이 틀려."
나는 선배를 쳐다봤다.
"상품평가 테이블이랑 주문상세를 바로 조인하고 있거든. 상품 하나에 평가가 10개 있으면, 그 상품 매출이 10번 더해져. 지금 조민우 씨가 정산 숫자가 회계랑 안 맞는다고 몇 달째 얘기하고 있는데, 아마 저게 원인일 거야."
"그럼… 성능 순위로 줄 세우면 Q3는 맨 마지막인데요."
"그래서 묻는 거야." 선배가 커피잔을 내려놨다. "제일 느린 거랑 제일 급한 건 달라. 느린 건 사람이 기다리면 되는데, 틀린 건 기다려도 안 고쳐져. 오히려 매일 더 쌓여."
원장을 다시 봤다. 숫자만 보면 Q3는 무시해도 되는 조각이었다. 그런데 바로 그 조각이 가장 위험했다.
"그럼 뭘 기준으로 정하죠?"
"그걸 오늘 네가 만들어." 선배가 일어섰다. "무엇부터 볼지 정하는 기준이랑, 무엇을 증거로 인정할지 정하는 규칙. 두 장이면 돼. 내일부터 1년 동안 그 두 장이 네 판단 근거가 될 거야."
뭔가 더 물어보려다 그만뒀다. 선배가 마지막으로 한마디 했다.
"그리고 오늘 아무것도 안 고쳤다고 신경 쓰지 마. 잴 수 없던 걸 잴 수 있게 만들었잖아. 그게 오늘 일이야."
오늘 남은 것
고친 SQL은 없다. 대신 숫자 한 장과 규칙 두 개가 남았다.
| 무엇 | 어디로 간다 |
|---|---|
| W01 베이스라인 원장 (6조각의 블록 수) | 02~15화의 개선 근거 · 18화 졸업 판정의 유일한 기준 |
| T0 측정규약 | 1년 내내 바꾸지 않는다. 이 규약을 어긴 측정은 증거로 치지 않는다 |
| T9 대상 선정·중단 기준 | 화마다 "무엇부터 볼지"를 정한다 |
그리고 모르는 채로 넘어간 것들이 있다. 선배가 "나중에"라며 미뤄 둔 것들이다.
- 왜
E-Rows가 틀리는지, 고칠 수 있는지 → 10화 - 인덱스를 만들면 정말 빨라지는지 → 02화. 그리고 04화에서 뒤집힌다
- FK가 없으면 무슨 일이 생기는지 → 12화
- Q3의 틀린 답을 어떻게 고치는지 → 08화
다음 →
3_실습과_기록.md에서 오늘 한 측정을 직접 해 보고, 보고서 두 장을 쓴다. 오늘 나온 개념을 더 파고들고 싶으면2_파고들기.md를 펼친다.