Skip to content

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/FREEPDB1
bash: sqlplus: command not found

생각해 보면 당연한 결과인데 손이 먼저 나갔다. 오라클은 컨테이너 안에서 돌고 있고, sqlplus도 그 안에 있다. 내 PC에는 오라클 클라이언트를 깐 적이 없었다. 방법은 둘뿐이었다. 컨테이너 안의 sqlplus를 빌려 쓰거나, 내 PC에 클라이언트를 설치하거나.

빌려 쓰는 쪽이 빨랐다.

bash
docker exec -it ora-hanorder sqlplus sqlp/sqlp@localhost/FREEPDB1
Connected 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           37

169.

"맞혔네." 선배가 뒤에서 말했다. "이제 아침에 본 숫자가 다르게 보일 거야. 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          1

Q2 줄에서 눈이 멈췄다.

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를 펼친다.