13주차 미리보기
W13 미리보기 · 웹소설 본편

STARRY PASS 9 — 영수증 산과 세 칸짜리 책갈피

“빨라졌다”는 말보다 같은 산·같은 길·같은 횟수를 먼저 남기는 법

범위D1–D6 · 월–토p426–450 전체 + p451 D6 마무리
문서 상태PREVIEW QA PASS정적·브라우저 검증 완료
실행 상태BENCHMARK NOT_RUN현재 full Green 미주장

이 이야기는 원문의 월∼토 수행 계약을 이해하기 위한 모형극입니다. 현재 소스로 여섯 단계를 모두 재실행해 Green을 얻었다는 성능 보고가 아니며, 현재·과거 증거의 이음새는 뒤의 질문 노트에서 따로 검사합니다.

STARRY의 낮 공연이 끝나자 계산대 아래에서 하얀 종이 한 장이 미끄러져 나왔다. 히토리가 그것을 주우려는 순간, 뒤이어 십 장과 백 장이 쏟아졌다. 창고 문까지 열리자 바닥에 쌓인 영수증은 작은 산처럼 보였다. 니지카는 가장 위의 한 장을 들어 보지도 않고, 먼저 문을 닫았다.

“왜 닫아요? 손님이 최근 내역 스물… 아니, 오십 장을 달라고 했어요.” 키타가 물었다.

“지금 손대면 우리가 조건을 바꿨는지, 창고가 원래 그랬는지 나중에 구분할 수 없어. 우선 어떤 도구와 어떤 작업 지시서를 쓸지부터 봉인하자.”

료는 다섯 장의 작업 지시서를 투명 봉투에 넣었다. 하나는 전체 작업을 진행하는 지시서였고, 나머지 네 장은 산을 만드는 법, 최근 영수증을 찾는 질문, 세 칸짜리 책갈피를 만드는 법, 산의 숫자가 맞는지 확인하는 법이었다. 각 봉투 겉면에는 긴 손가락 무늬가 하나씩 적혔다.

“다섯 장이 동일한지를 어떻게 기억해요?” 히토리가 봉투를 빛에 비추며 물었다.

“기억하지 않아. 손가락 무늬를 기록해. 한 글자라도 바뀌면 무늬가 달라져야 하고, 측정 후에도 현재 봉투와 대조할 수 있어야 해.” 니지카가 답했다.

니지카와 료가 측정 전에 작업 지시서와 출발 조건을 함께 확인하는 공식 장면
그림 한눈에: 공식 장면컷은 분위기 전환용이며, 학습 사건은 비공식 구성입니다.

창고 입구의 검사대는 그 다섯 무늬와 도구 상자 하나만을 확인했다. 아직 영수증 산을 만들지도, 길을 재지도, 책갈피를 꽂지도 않았다. 히토리가 빈 결과 함을 보고 실망하자 료가 고개를 저었다.

“비어 있어야 성공이야. 준비 단계에서 뒷단계의 성적표가 있으면, 예전 결과를 오늘 것처럼 착각할 수 있어.”

“그럼 지금 확인한 건 도구 상자 하나, 동일한 지시서 다섯 장, 그리고 미래 결과 영 개네요.” 키타가 메모했다.

니지카는 메모 아래에 더 진하게 선을 그었다. ‘준비 성공’은 ‘빨라졌다’는 뜻이 아니었다. 아직 산은 없었고, 질문도 실행되지 않았다. 준비가 확인한 것은 출발선과 재료뿐이었다.

준비표에는 조금 더 까다로운 조건이 있었다. 도구 상자의 호흡 소리가 실제로 들려야 했고, 상자 안의 서비스 이름은 단 하나여야 했다. 다른 서비스가 하나라도 끄나풀처럼 붙으면 멈추고 왜 섞였는지 확인해야 했다. 평소에 쓰던 비밀번호가 없으면 준비 단계에서만 쓸 가짜 표식을 붙여 설정 모양만 확인했다. 실제 영수증 산을 올리기 위한 비밀을 만든 것은 아니었다.

히토리는 결과 함 안의 열 칸을 하나씩 열어 보았다. 책갈피 전 지도, 책갈피 후 지도, 분포 표, 전후 시간표, 마지막 보고서가 모두 없었다. “없다”는 것은 허전했지만, 오늘은 그 허전함이 과거의 종이를 오늘 증거로 잘못 꺼내지 않았다는 표식이었다. 준비표에는 성능 숫자 대신 소스 다섯 장의 이름과 무늬만 남았다.

SHA-256으로 묶는 정본 파일 다섯 개와 hash 밖 Compose 의존성을 구분한 그림
그림 한눈에: 준비는 소스 다섯 개와 빈 미래 결과를 고정하는 단계입니다.

검사대 옆에는 여섯 칸짜리 바닥 지도가 펼쳐졌다. 첫 칸은 도구와 지시서만 확인했고, 둘째 칸은 책갈피를 꽂기 전의 길, 셋째 칸은 꽂은 뒤의 길을 담당했다. 넷째 칸은 산의 모양, 다섯째 칸은 아직 걸지 않은 길의 경계, 여섯째 칸은 전후 시간표를 맡았다. 각 칸은 자기 일을 끝내고 창고를 통째로 비웠다. 앞 칸의 가구를 다음 칸이 몰래 쓰지 못하게 하려는 규칙이었다.

“매번 산을 다시 만든다고요?” 히토리의 눈이 어두워졌다.

“그래야 셋째 칸이 둘째 칸의 남은 책갈피를 재사용하지 않아. 마지막에는 산과 부속품도 모두 치워야 하고.”

“정리까지 성공해야 결과 표를 남기는 거군요.”

D1 Prepare부터 D6 Benchmark까지 여섯 소유 경계와 범위 밖 D7을 구분한 그림
그림 한눈에: 월∼토 여섯 작업은 자기 증거만 소유하고, 일요일 보고서는 범위 밖입니다.

둘째 칸에서는 드디어 영수증 산을 만들었다. 손님 표식은 천 개, 거래 종이는 십만 장, 원장 영수증도 십만 장이었다. 모든 거래는 이미 완료된 것으로 표시되었고, 영수증의 금액은 모두 일이었다. 시각은 정해진 시작점에서 한 초씩 늘어났다. 무작위로 섞지 않았기 때문에 어제 다시 만들어도 오늘과 같은 산이 되었다.

하지만 산은 고르지 않았다. 일 번 손님의 영수증이 구만 장이었다. 나머지 천의 영십구 명이 나머지 일만 장을 나눠 가졌다. 이 규칙은 평균적인 손님을 만들지 않았다. 오히려 한쪽에 압도적으로 쏠린 편향을 고정했다.

“일 번 손님이 전체의 구십 퍼센트예요.” 키타가 손가락으로 산의 절벽을 가리켰다.

“그런데 이걸 보고 인기 손님과 드문 손님의 길을 모두 재 봤다고 말하면 안 돼.” 니지카가 말했다. “이 칸이 직접 확인한 건 정확한 네 숫자와 구십 퍼센트라는 비율이야. 여러 손님을 찾아다니며 길지기의 선택이 달라지는 실험은 하지 않았어.”

계좌 천 개와 거래·원장 각 십만 행, hot 계좌 구만 행과 비율 영점구를 나타낸 그림
그림 한눈에: 계좌 천 개·거래 십만 건·원장 십만 행 중 일 번 계좌에 구만 행이 몰립니다.

공식 영업 준비를 하던 네 사람이 계산대 앞에 다시 모였다. 니지카는 손님의 요청을 한 문장으로 고정했다. 일 번 손님의 영수증을 최신 시각부터 보되, 같은 시각이면 번호가 큰 쪽을 먼저 놓고, 오십 장만 가져온다. 손님 번호, 시각, 영수증 번호, 오십이 한 문장에 모두 남았다.

“왜 시각 뒤에 번호까지 붙여야 해요?” 히토리가 물었다.

“같은 초에 여러 장이 생길 수 있으니까요. 시각만 보면 오십 번째 경계가 흔들려요.” 키타가 대답했다.

“좋아. 우리의 측정 질문은 깊은 몇 번째 묶음으로 이동하는 질문이 아니야. 항상 일 번 손님의 첫 오십 장을 찾는 질문이야.”

키타는 질문 카드의 열 순서를 바꾸어 보며 같은 질문인지 확인했다. 손님 번호를 먼저 거르지 않고 시각만 정렬하면 모든 손님의 영수증이 섞였다. 시각을 오름차순으로 바꾸면 가장 오래된 영수증이 먼저 나왔다. 번호를 빼면 같은 시각에서의 순서 계약이 사라졌다. 오십을 빼면 결과 크기가 한정되지 않았다. 한 줄의 질문은 짧았지만, 그 안의 네 계약 중 하나라도 바뀌면 다른 실험이 됐다.

“결과 영수증이 오십 장이면 길지기가 오십 장만 봤다는 뜻인가요?” 히토리가 물었다.

“꼭 그렇진 않아. 오십 장은 마지막에 나온 결과의 크기야. 그것을 찾기 위해 걸은 길은 길 메모안에 따로 적혀 있어.” 니지카가 답했다. 이 구분 덕분에 결과 행 수와 중간 노드가 읽은 행 수를 혼동하지 않았다.

account_id 1 조건과 created_at id 내림차순, LIMIT 50인 top-N query 모양
그림 한눈에: 현재 측정 질문은 일 번 계좌의 최신 오십 행뿐입니다.

그들은 책갈피를 꽂기 전에 창고지기에게 길 찾기 메모를 부탁했다. 메모는 단순히 ‘몇 초’만 적지 않았다. 창고지기가 어떤 묶음을 먼저 보았는지, 몇 장을 나올 것으로 예상했는지, 실제로 몇 장을 건넸는지, 준비와 실행에 얼마가 걸렸는지, 이미 손에 들고 있던 종이와 새로 가져온 종이가 얼마인지를 구조화해 남겼다.

료는 예상한 장수와 실제 장수가 같은 말이라고 생각했다. 니지카는 두 칸을 따로 그리게 했다. 예상은 창고지기가 길을 선택하기 전에 생각한 값이고, 실제는 길을 걸은 뒤 나온 값이었다. 둘의 차이는 창고지기가 산의 분포를 얼마나 잘 알았는지를 생각할 단서였다.

료는 길 메모의 맨 위 줄만 보고 “여기에 책갈피라고 써 있지 않으니 책갈피를 안 쓴 거야”라고 말했다. 창고지기는 메모 안의 자식 항목을 펼쳐 보여 주었다. 길의 맨 위에는 ‘오십 장에서 멈춤’이 있었지만, 그 안쪽에 책갈피 이름이 있을 수 있었다. 그때부터 네 사람은 맨 위의 이름 하나로 결론을 내지 않고 tree 안의 길을 따라가 보았다.

히토리는 시간을 비용과 같은 말로 써도 되는지 물었다. 니지카는 창고지기의 예상 비용은 초가 아니라 여러 길을 상대적으로 비교하기 위한 숫자라고 설명했다. 실제 시간은 걸은 뒤에 나오지만, 그 시간도 하나의 순간값만으로 산의 품질을 다 설명하지 못했다. 이 대화가 나중에 서른 번씩 재는 이유가 되었다.

PostgreSQL plan JSON에서 추정값, 실제값, 시간, buffer를 구분한 그림
그림 한눈에: 길 메모의 예상·실제·읽기 흔적을 각각 보아야 합니다.
네 사람이 같은 질문과 측정 조건을 다시 확인하는 공식 장면
그림 한눈에: 공식 장면컷은 분위기 전환용이며, 계획 표와 수치는 원문 코드에서 검증했습니다.

셋째 칸에서 니지카는 창고 본관이 아니라 매번 없애는 실험용 별관에만 세 칸짜리 책갈피를 꽂았다. 첫 칸은 손님 번호였고, 둘째는 시각을 최신순으로 가리켰고, 셋째는 같은 시각일 때 영수증 번호가 큰 쪽을 가리켰다. 질문의 낱말과 책갈피의 낱말이 같은 순서였다.

“본관에 박아 두면 손님도 바로 빠른 길을 쓸 수 있지 않아?” 료가 물었다.

“오늘은 성능 비교의 원인을 하나로 잡아야 해. 공개 창고의 규칙을 바꾸는 날이 아니야. 실험용 별관을 새로 만들고, 같은 산과 같은 질문에서 책갈피 하나만 달라지게 할 거야.”

책갈피를 꽂은 뒤 창고지기의 메모를 다시 받았다. 이번에는 메모 안의 어느 노드인가에 정확한 책갈피 이름이 있어야 했다. 길의 모양은 두 가지만 허용했다. 책갈피 자체를 따라 걸었거나, 책갈피의 정보만으로 요청한 칸을 완성했거나 둘 중 하나였다. 결과가 빨랐다는 사실만으로는 통과할 수 없었다.

account_id, created_at DESC, id DESC 세 칸의 복합 인덱스 순서
그림 한눈에: 일치 조건 한 칸을 먼저 두고, 정렬 두 칸의 방향을 질문과 맞춥니다.

네 번째 칸은 복잡한 길 비교를 하지 않았다. 대신 산의 형태를 다시 세었다. 손님 표식 천 개, 거래 종이 십만 장, 원장 영수증 십만 장, 일 번 손님 구만 장이 하나라도 다르면 멈췄다. 모든 계좌의 잔액을 그 계좌 영수증 금액의 합과 비교했고, 다른 것이 하나도 없어야 했다. 책갈피가 정확히 하나 있는지도 보았다.

“그럼 구십 퍼센트가 자연의 법칙이네요?” 키타가 물었다.

“아니. 우리가 만든 이 산의 법칙일 뿐이야. 실제 영업 자료의 분포나 다른 손님 질문의 길은 이 칸에서 재지 않았어.”

쉬는 시간에 니지카는 세 장의 작은 문제 카드를 꺼냈다. 첫 카드는 각 계좌로 들어온 돈과 나간 돈을 따로 합치라고 했다. 두 번째 카드는 고객별로 묶은 뒤 총 거래액이 백만 이상인 묶음을 고르라고 했다. 셋째 카드는 들어온 기록은 있지만 나간 기록은 하나도 없는 계좌를 찾으라고 했다.

“정답은 뒤에 있나요?” 키타가 카드를 뒤집었다.

“없어. 오늘 주어진 건 문제와 통과 계약이야. 어떤 표의 한 행을 입력으로 보는지, 결과의 한 행은 무엇인지, 기록이 영 건인 계좌를 어떻게 다룰지를 먼저 쓰고 풀어야 해.”

히토리는 가장 짧아 보이는 셋째 문제를 골랐다가 쉽지 않다는 걸 깨달았다. 나간 기록이 없는 걸 알아보려면 ‘나간 계좌 목록에 없음’을 보는 대신, 해당 계좌와 맞는 나간 기록이 존재하지 않는지를 직접 물어야 했다. 빈 칸이 섞인 목록은 ‘없다’는 판단을 애매하게 만들 수 있었다. 세 문제는 오늘 할 일이었지만, 정답 종이는 작업 봉투 다섯 장에 포함되지 않았다.

Limit 아래 plan tree에서 정확한 index 이름과 허용 node를 찾는 그림
그림 한눈에: 책갈피 이름·허용 길·정확한 산의 숫자를 따로 검증합니다.

다섯 번째 칸 앞에는 깊은 쪽으로 통하는 두 문이 있었다. 한 문에는 ‘앞의 오만 장을 건너뛰고 시작’이라고 써 있었고, 다른 문에는 ‘마지막으로 본 시각과 번호 뒤에서 시작’이라고 써 있었다. 히토리가 첫 문의 손잡이를 잡자 니지카가 멈춰 세웠다.

“오늘은 두 문을 비교하는 날이 아니야.”

“그럼 왜 문이 있어요?”

“보였다고 걸은 것은 아니라는 걸 기록하려고. 현재 질문에는 앞의 행을 버리라는 표식도, 마지막 위치 뒤를 찾으라는 표식도 없어. 일 번 손님의 최신 오십 장을 바로 가져오는 질문뿐이야.”

료가 두 문 사이에 빨간 끈을 매달았다. 키타는 ‘건너뛰기 비교 안 함’, ‘이어보기 비교 안 함’이라고 두 줄로 적었다. 좋은 설명은 어떤 길이 더 빨라서 바로 추천하는 말이 아니었다. 아직 걸지 않은 길을 지도에서 길 처럼 색칠하지 않는 말이었다.

키타는 얼핏 보면 두 문에 대한 정보가 충분하다고 생각했다. 하지만 직접 비교하려면 두 문은 각각 자기 질문과 시간표를 가져야 했다. 첫 묶음·깊은 묶음·새 영수증이 끼어든 상태를 어떻게 처리할지, 마지막 시각이 같을 때 번호를 어떻게 기억할지, 전후 각 몇 번을 재고 어떤 흔적을 보존할지가 필요했다. 지금 자료에는 그 세트가 없었다.

“이론으로 두 문의 장단점을 설명할 수는 있어.” 료가 말했다.

“맞아. 하지만 이론 설명과 우리가 직접 걸어 얻은 결과를 다른 칸에 써야 해. 오늘 결과 칸은 비어 있어.” 니지카가 두 문에 붙은 빨간 끈을 다시 확인했다.

실측한 top-N과 측정하지 않은 OFFSET 및 keyset을 분리한 그림
그림 한눈에: 오늘 측정은 최신 오십 행 한 질문이며, 두 pagination 길은 미검증입니다.

마지막 칸에서는 같은 산을 한 번 만들고, 책갈피를 꽂기 전 질문을 서른 번, 꽂은 뒤 같은 질문을 서른 번 실행했다. 각 실행은 영수증 오십 장을 실제로 찾아야 했고, 창고지기가 말한 실행 시간을 표에 남겼다. 앞줄 서른 개와 뒷줄 서른 개, 정확히 예순 개의 시간이었다. 예시 시간을 미리 채우지 않았고, 실제 환경의 값을 써야 했다.

검사 규칙은 의외로 조심스러웠다. 가운데 지표와 느린 꼬리 지표가 둘 다 나빠지거나 같으면 멈췄다. 다시 말해, 둘 중 적어도 하나가 좋아지면 현재 규칙은 통과시켰다. 하나가 나아졌더라도 다른 하나가 나빠졌다면 그 나빠짐을 숨기면 안 됐다.

“그럼 ‘모두 더 빨라졌다’고 쓰면 안 되겠네요.” 히토리가 말했다.

“응. 이 산, 이 손님, 이 질문, 이 환경에서 두 지표의 실제 값을 보여 줘야 해. 그것도 절대 시간 약속은 아니고.”

히토리는 개선 규칙을 다시 읽었다. 두 지표가 모두 좋아져야만 통과하는 규칙이 아니었다. 둘 다 같거나 나빠지면 실패였고, 적어도 하나가 좋아지면 현재 규칙은 통과시켰다. 그런데 느린 꼬리가 악화되고 가운데만 좋아져도 통과할 수 있었다. 반대도 가능했다. 성공 벨 하나로 두 지표의 모양을 추측하면 안 됐다.

“성공했다는 표식은 실제 네 숫자를 대신하지 못하네요.” 키타가 전·후 두 지표를 각각 적었다. “둘 중 하나가 나빠졌다면 나빠졌다고 그대로 쓸게요.”

현재 계산기에는 연습용 예열 버튼도 없었다. 첫 번째 측정부터 바로 서른 개 표에 들어갔다. 니지카는 이 사실도 쓰라고 했다. “열 번 연습한 뒤 재서 cache 영향을 통제했다”고 만들어 쓰면, 서른 개의 숫자가 정확해도 실험 설명은 틀리게 되었다.

같은 query의 before 30회와 after 30회 raw sample을 보존하는 그림
그림 한눈에: 전 서른 회·후 서른 회의 실제 값과 두 지표를 함께 보존합니다.

그런데 초시계를 일부러 일부터 삼십까지 놓고 계산 규칙을 점검하던 중, 히토리가 이상한 것을 발견했다. 현재 계산기는 가운데 지표를 구할 때 열다섯 번째 값 하나를 골랐다. 그런데 벽에 붙은 작업 약속은 짝수 개의 가운데라면 열다섯 번째와 열여섯 번째의 평균을 쓰라고 했다. 그 약속대로면 일부터 삼십의 가운데는 십오점오였다. 현재 계산기가 내는 값 십오와 달랐다.

느린 쪽 구십오 퍼센트 경계는 구체적인 순위 규칙에 따라 이십구가 맞았다. 문제는 가운데였다. 료가 “작은 차이니 그냥 넘어가자”고 하자 니지카는 계산기에 빨간 표식을 붙였다.

“작은 차이가 아니야. 이름과 계산법의 계약이 다른 거야. 지금 자료에서는 십오점오가 나온다고 쓰면 안 돼. 계산기를 고치기 전에는 현재 구현이 십오를 고른다고 적고, 약속과 다르다는 사실도 함께 남겨야 해.”

료는 계산기 설명서에서 구십오 퍼센트 규칙을 따로 찾았다. 서른의 구십오 퍼센트는 이십팔점오 위를 올림한 이십구 번째였고, 살짝 더운 순위의 경계를 그대로 고르는 방식이었다. 이 부분은 벽의 약속과 계산기가 같았다. 니지카는 문제를 ‘통계가 전부 틀렸다’고 크게 부풀리지도, ‘아무 문제 없다’고 줄이지도 않았다. 그녀는 중앙 값 계약 하나가 정확히 충돌한다고 적었다.

1부터 30에서 PDF median 15.5와 현재 runner 15의 충돌, p95 29 일치를 보인 그림
그림 한눈에: 현재 계산식은 일∼삼십에서 가운데 십오, 느린 경계 이십구를 고르며 교재의 십오점오 계약과 충돌합니다.
공연 부스에서 여섯 단계의 증거 경계를 정리하는 공식 장면
그림 한눈에: 공식 장면컷은 마무리 분위기용입니다. 여섯 일의 직접 증거만 보고합니다.

밤이 되자 네 사람은 여섯 칸의 계약을 한 장의 보고서로 합치는 장면을 연습하려 했다. 니지카는 그 순간 손을 들어 멈춘다. 하나로 봉인하는 마지막 작업은 다음 칸에 있었고, 그곳은 이야기의 범위가 아니었다. 계약상 토요일이 만들어야 할 마지막 종이는 전후 서른 회, 책갈피 이름, 허용된 길 유형, 정리 완료를 담는 여섯 번째 칸이었다. 이는 현재 같은 무늬로 토요일 종이가 새로 생성됐다는 보고가 아니었다.

그들은 출석부를 현재 봉투들과 대조했다. 첫 번째 준비 봉투의 무늬는 현재 지시서와 같았다. 그런데 둘째부터 여섯째 칸까지 남은 예전 출석부는 오늘 봉투와 다른 손가락 무늬를 가리켰다. 다섯 번째 칸의 ‘두 문은 안 걸었음’ 종이는 현재 출석부에서 아예 찾을 수 없었다. 모형극은 여섯 칸의 약속을 모두 보여 주었지만, 출석부는 오늘 시점의 연속된 완료를 증명하지 못했다.

“연극에서 모두 성공했으니 출석부에도 모두 성공으로 써도 될까요?” 히토리가 조심스럽게 물었다.

“안 돼. 연극은 무슨 순서로 무엇을 확인해야 하는지 보여 준 거야. 실제 출석은 같은 지시서 무늬로 다시 확인해야 해. 그 전에는 현재 연속 Green이라고 쓰지 않아.”

“그럼 최종 보고서가 없으니 이번 주는 실패인가요?” 키타가 물었다.

“아니. 이야기의 범위가 토요일 계약에서 닫히는 거야. 현재 출석부는 첫 칸만 최신 무늬와 맞고, 뒷 칸은 낡은 무늬이며 다섯 번째 종이도 없어. 재실행 전에는 월부터 토까지 현재 완료라고 말하지 않아. 일요일에 할 일을 앞당겨 했다고도 말하지 않고.”

히토리는 손님에게 최신 영수증 오십 장을 건네며 작은 메모를 함께 붙였다. 일 번 손님에게 구만 장이 몰린 실험용 산, 일 번 손님의 최신 오십 장 질문, 책갈피 전후 각 서른 회라는 조건을 적었다. 깊은 페이지 두 방식은 재지 않았고, 실제 영업 부하의 절대 시간을 약속하지 않으며, 중앙 값 계산식은 교재 약속과 다른 상태라는 문장도 빼지 않았다.

“여전히 산은 무서워요.” 히토리가 창고 불을 끄며 말했다.

“그런데 오늘은 도망안 갔네.” 료가 말했다.

“어디까지 걸었는지 표식이 있었으니까요. 안 걸은 문까지 걸었다고 말하지 않으면, 다음에 어디서 시작할지도 알 수 있고요.”

니지카는 세 칸짜리 책갈피를 실험용 별관에서 뽑아 봉투에 넣었다. 봉투 앞면에는 ‘같은 산, 같은 질문, 같은 횟수’라고 적혔다. 뒷면에는 더 작게 ‘걸지 않은 길은 결과로 그리지 말 것’이라고 적혔다.

W13 미리보기 · 히토리 질문 노트

히토리의 질문 노트 — W13 성능 증거의 경계

본편의 영수증 산·길지기 메모·세 칸짜리 책갈피를 실제 PowerShell·PostgreSQL·SQL workbook 계약으로 다시 연결합니다. 커리큘럼이 요구하는 성공 계약, 현재 소스가 직접 검사하는 것, 오늘 새로 실행하지 않아 보장할 수 없는 것을 구분합니다.

1. 이번 미리보기의 정확한 PDF 범위는 어디까지인가요?

원문 PDF 426쪽 W13 overview부터 451쪽 중간의 일 모듈 | W13 tuning 보고서와 반례 표제 직전까지입니다. 완전한 페이지로는 426∼450쪽이고, 451쪽 상단의 토요일 진도·달력 규칙과 최종 Gate 잔여 13행은 포함합니다. 같은 451쪽 중간에서 일요일이 시작하므로 페이지 숫자만으로 범위를 잘라서는 안 됩니다.

451쪽의 일요일 Report, 453∼455쪽의 runner 전문 재게시, 458쪽 주간 통합·요일별 해설, 459쪽 미달 시 복구는 제외합니다. 다만 월요일에 완전한 runner 파일 전문이 열리기 때문에, 파일 안의 Report 분기 원문은 소스 보존 차원에서 보이지만 여섯 날의 실행 증거로 세지 않습니다.

실제 기술 이름strict W13 scope = p426 through the p451 Sunday-heading boundary, D1–D6 only

2. 여섯 날은 각각 어떤 검사를 소유하나요?

D1은 Prepare, D2는 BeforePlan, D3은 AfterPlan, D4는 Selectivity, D5는 별도 inline pagination-scope 검사, D6은 Benchmark입니다. 하루 명령은 총 6개이고, 공통 runner phase 호출이 5개, D5 inline PowerShell body가 1개입니다. DB를 실제로 올리는 runner phase는 D2·D3·D4·D6 네 개입니다. D1은 Docker server와 Compose config·source hash만 보며 DB seed를 실행하지 않습니다.

오늘 정보를 읽었다고 해서 이 여섯 명령이 현재 소스로 전부 Green이었다고 선언하지 않습니다. 현재 D1 준비 증거와 과거 D2∼D6 증거가 서로 다른 runner hash를 가리키고, D5 현재 pagination-scope 증거도 없어 현재 full-Green 연속성이 닫히지 않았습니다.

실제 기술 이름D1 Prepare → D2 BeforePlan → D3 AfterPlan → D4 Selectivity → D5 scope-only → D6 Benchmark

D1 Prepare부터 D6 Benchmark까지 여섯 소유 경계와 범위 밖 D7을 구분한 그림
그림 한눈에: 여섯 일의 소유 경계와 범위 밖 Report를 나눕니다.

3. D1 Prepare가 직접 확인하는 것은 무엇인가요?

compose.yaml이 파일로 존재하고 Docker engine의 server version이 비어 있지 않은지 봅니다. docker compose ... config --services의 결과가 정확히 db 하나인지도 봅니다. 그 뒤 packaged source 다섯 파일의 경로와 SHA-256을 prepare.json에 적습니다.

실행 전에 before-day.json, plan text, raw CSV, benchmark manifest, perf-manifest.json 등 미래 산출물 10개 중 하나라도 존재하면 stale artifact로 멈춥니다. 성공한 D1은 future_artifacts_created=0을 적습니다. 이는 DB·schema·seed·plan·index·latency를 확인했다는 뜻이 아닙니다.

실제 기술 이름reproducible environment, config-only Prepare, stale future-artifact rejection

4. 손가락 무늬를 남기는 다섯 파일은 정확히 무엇인가요?

scripts/run-w13-perf.ps1, sql/w13/seed.sql, measure.sql, create-index.sql, assert.sql입니다. 현재 정본은 순서대로 10,313·1,253·173·136·845 bytes이고, 합계 12,720 bytes·173 trim-normalized lines입니다. SHA-256은 각각 d7298bc7..., cec3786f..., 4ffb0003..., bdb52b53..., f7601add...로 시작합니다.

compose.yaml은 Prepare와 DB lifecycle의 직접 실행 의존성이지만 SourceHashes() 다섯 행에는 포함되지 않습니다. 따라서 “완전한 설명 unit 7개”는 compose 의존성 1 + packaged file 5 + D5 inline gate 1이고, “SHA-256 closure”는 5파일입니다. 두 숫자를 섞지 않습니다.

실제 기술 이름five-file SHA-256 source integrity closure plus one unhashed Compose dependency

SHA-256으로 묶는 정본 파일 다섯 개와 hash 밖 Compose 의존성을 구분한 그림
그림 한눈에: hash closure 다섯 파일과 실행 의존성을 구분합니다.

5. 정본 소스 몸체를 ‘5개’, ‘6개’, ‘7개’라고 다르게 세는 이유는 무엇인가요?

canonical file body는 앞의 packaged file 5개입니다. D5의 pagination-scope PowerShell은 별도 파일이 아니라 PDF와 커리큘럼 내에서 제시된 inline body이므로, 중복 제거한 executable/source body는 6개입니다. 여기에 전문이 실려 있지는 않지만 직접 필요한 compose.yaml을 포함하면 완전한 설명 unit는 7개입니다.

PDF에서 완전한 파일 모양으로 반복 제시된 논리적 occurrence는 runner 1, seed 2, measure 3, index 2, assert 1로 9회입니다. D5 inline gate는 실행·재검증 카드에 같은 몸체가 2회 나와 전체 logical body occurrence는 11회입니다. 중복 제거 후 개수와 PDF 배치 횟수는 다른 물음입니다.

실제 기술 이름canonical files 5 / unique executable bodies 6 / complete explanatory units 7 / PDF occurrences 11

6. seed.sql이 만드는 정확한 표와 행 수는 어떻게 되나요?

disposable perf_w13 schema를 지우고 다시 만든 뒤 account, business_tx, ledger_entry 세 표를 만듭니다. account 1,000행, business_tx 100,000행, ledger_entry 100,000행을 generate_series 기반으로 만듭니다. 거래 ID는 md5('perf-' || g)::uuid, status는 모두 COMPLETED, ledger entry_type은 모두 PERF_CREDIT, signed_amount는 모두 1입니다.

이 fixture는 실제 금융 거래의 복잡한 입·출금 의미를 재현하는 운영 데이터가 아닙니다. 정렬·분포·인덱스 실험을 위한 결정적 synthetic fixture입니다. 행 수가 크다는 사실만으로 운영 현실성을 주장할 수 없습니다.

실제 기술 이름deterministic synthetic perf_w13 fixture

7. 구만 행이 한 계좌에 몰리는 규칙은 어떻게 만들어지나요?

ledger ID g<=90000이면 account_id=1이고, 그 뒤 10,000행은 2 + ((g-90001) % 999)로 account 2∼1000에 순환 배치됩니다. 그래서 account 1은 정확히 90,000행, 나머지 999계좌는 합계 10,000행을 가집니다. hot_ratio=.9는 이 fixture에서 계산된 값입니다.

created_at은 2026-01-01 00:00:00 UTC에서 g 초씩 더해져 모든 행이 서로 다른 시각을 갖습니다. 따라서 현재 fixture에서 id DESC는 시각 동률을 실제로 재현하기보다, query와 index의 안정적 정렬 계약을 만드는 tie-breaker입니다.

실제 기술 이름skewed distribution, hot account ratio 0.9, deterministic timestamp sequence

계좌 천 개와 거래·원장 각 십만 행, hot 계좌 구만 행과 비율 영점구를 나타낸 그림
그림 한눈에: 정확한 네 숫자와 그 숫자가 보장하지 않는 범위를 봅니다.

8. 성능을 재는 질문은 정확히 어떤 SQL인가요?

perf_w13.ledger_entry에서 account_id=1인 행을 created_at DESC, id DESC로 정렬하고 LIMIT 50으로 최신 50행을 가져옵니다. 선택 열은 id, entry_type, signed_amount, created_at입니다. 질문 앞에 EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)이 붙어 실제 query를 수행하고 plan JSON을 내놓습니다.

이 SQL에는 OFFSET이 없고, (created_at,id) < (...) 같은 cursor predicate도 없습니다. 따라서 현재 측정 단위는 pagination 두 방식이 아니라 하나의 account-scoped top-N query입니다.

실제 기술 이름account-scoped top-N query ordered by created_at DESC, id DESC with LIMIT 50

account_id 1 조건과 created_at id 내림차순, LIMIT 50인 top-N query 모양
그림 한눈에: 조건·정렬·경계를 하나의 query shape로 고정합니다.

9. EXPLAIN, ANALYZE, BUFFERS, FORMAT JSON은 각각 무엇을 더하나요?

EXPLAIN은 planner가 선택한 연산 tree와 cost·예상 행 수를 보여 줍니다. ANALYZE 옵션은 해당 읽기 query를 실제로 실행해 actual rows·actual time·execution time을 더합니다. 이때 ANALYZE table 명령으로 통계를 갱신하는 행위와 EXPLAIN ANALYZE의 실제 실행 옵션을 혼동하지 않습니다.

BUFFERS는 shared buffer hit/read 등을 plan node에 더하고, FORMAT JSON은 PowerShell이 안정적으로 field를 읽을 수 있게 구조화합니다. plan의 Total Cost는 ms가 아니고, Plan Rows는 실제 행 수가 아닙니다. Actual Rows도 loops가 여러 번이면 node의 전체 처리량과 단순히 같다고 보면 안 됩니다.

실제 기술 이름PostgreSQL EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) evidence

PostgreSQL plan JSON에서 추정값, 실제값, 시간, buffer를 구분한 그림
그림 한눈에: 추정과 실측, 시간과 buffer를 각각 봅니다.

10. D2 BeforePlan이 직접 assertion하는 것과 그냥 기록하는 것은 다른가요?

D2는 새 disposable DB를 시작하고 seed를 넣은 뒤 index 생성 전 measure.sql의 JSON을 before-plan.txt에 저장합니다. JSON parse와 native exit가 성공하면 plan의 맨 위 Node Type, Actual Rows, plan file SHA-256, cleanup=1before-day.json에 적습니다. 그런 뒤 자신이 올린 Compose project와 volume을 제거합니다.

현재 runner는 D2에서 top node가 특정 이름인지, actual_rows=50인지, Seq Scan이 나왔는지를 fail condition으로 비교하지 않습니다. 이 값들을 기록한다는 사실과, 특정 값을 강제한다는 주장은 다릅니다. 과거 plan 결과를 현재 소스 실행값으로 대체해서도 안 됩니다.

실제 기술 이름BeforePlan evidence capture versus invariant enforcement boundary

11. 세 칸짜리 책갈피의 정확한 정의는 무엇인가요?

CREATE INDEX idx_ledger_account_created_id ON perf_w13.ledger_entry(account_id, created_at DESC, id DESC)입니다. equality 조건인 account_id를 선두에 두고, 그 안에서 query의 내림차순 정렬 created_at DESC, id DESC를 이어 놓습니다. DDL 후 ANALYZE perf_w13.ledger_entry로 통계를 갱신합니다.

이는 public V001 migration을 수정하거나 운영 schema에 index를 배포하는 작업이 아닙니다. 매 phase마다 새로 만들고 제거하는 perf_w13 treatment만이 대상입니다. 그러므로 index 쓰기 비용·용량·운영 rollback 전략이 검증됐다고 넘겨짚지 않습니다.

실제 기술 이름isolated treatment composite index idx_ledger_account_created_id

account_id, created_at DESC, id DESC 세 칸의 복합 인덱스 순서
그림 한눈에: query shape과 index 열 순서를 맞추되 적용 schema를 한정합니다.

12. D3 AfterPlan의 성공 조건은 어떤 plan node를 허용하나요?

D3은 같은 seed와 query를 새 disposable DB에서 만든 뒤 treatment index를 추가하고 plan JSON을 읽습니다. FindIndex 함수는 plan tree를 재귀하며 첫 Index Name을 찾습니다. 이름이 정확히 idx_ledger_account_created_id이고, 해당 node의 Node TypeIndex Scan 또는 Index Only Scan이어야 통과합니다.

plan의 맨 위 node가 반드시 Index Scan이어야 한다는 규칙은 아닙니다. top-N query에서 상위 Limit 아래에 index node가 있을 수 있으므로 tree 안을 찾습니다. 또한 “index를 선택했다”는 사실은 “어떤 분포와 query에서도 index가 최적”이라는 일반화가 아닙니다.

실제 기술 이름recursive plan search allowing Index Scan or Index Only Scan

Limit 아래 plan tree에서 정확한 index 이름과 허용 node를 찾는 그림
그림 한눈에: 이름·node type·schema를 정확히 한정합니다.

13. assert.sql은 인덱스 외에 무엇을 확인하나요?

account=1000, business_tx=100000, ledger_entry=100000, account_id=1 ledger=90000을 확인합니다. 또 account의 balance와 해당 account ledger signed_amount 합을 left join해 다른 account의 수 m=0을 확인합니다. 마지막으로 pg_indexes에 정확한 schema·index 이름이 1개인지 봅니다.

성공 stdout은 W13_ASSERT 1000|100000|100000|90000|0입니다. 이 reconciliation은 signed amount가 모두 1인 synthetic fixture의 balance 합을 보는 것이지, 실제 회계 불변식·복식부기·대상 상태·역분개 규칙을 폭넓게 확인하는 것은 아닙니다.

실제 기술 이름exact row-count, hot-distribution, balance-reconciliation, and index-existence invariants

14. 왜 각 DB phase는 자기 schema를 만들고 다시 지우나요?

D2·D3·D4·D6은 각각 Compose mode에서 고유 project 이름으로 db를 올리고, seed.sql이 기존 perf_w13을 drop한 뒤 새로 만듭니다. phase 작업이 성공하거나 중간에 예외가 나도 finally에서 docker compose down -v를 시도합니다. cleanup이 실패하면 증거 JSON을 쓰는 뒤가 아니라 쓰기 전에 예외로 멈춥니다.

이러한 소유 경계는 before에 이전 after index가 남아 있거나, selectivity phase가 benchmark raw data를 재사용하는 것을 막습니다. Container mode는 사용자가 주어진 container를 소유하므로 runner가 down하지 않습니다. 따라서 Compose mode와 Container mode의 cleanup 계약도 다릅니다.

실제 기술 이름phase-owned disposable Compose lifecycle with fail-closed cleanup

15. D4 Selectivity는 어떤 선택도 실험을 직접 했나요?

D4의 native count query는 세 표의 전체 행 수와 account_id=1 ledger 행 수, 총 네 정수를 |로 받습니다. 값이 1000|100000|100000|90000와 정확히 같아야 하고, selectivity-day.jsonhot_ratio=.9, native_exit=0, cleanup=1을 남깁니다.

하지만 rare account·hot account·전체 범위 세 query를 각각 EXPLAIN하지 않습니다. 여러 선택도에서 planner가 Seq Scan과 Index Scan을 다르게 골랐다는 사실도 직접 검사하지 않습니다. D4 Green 계약은 분포 숫자의 정확성이지 선택도별 plan comparison이 아닙니다.

실제 기술 이름exact distribution verification, not multi-selectivity planner evidence

16. Seq Scan이 보이면 무조건 튜닝 실패인가요?

아닙니다. 전체 표의 많은 페이지를 읽어야 하는 낮은 선택도 query에서는 index를 오가며 random access하는 것보다 sequential scan이 싸게 예상될 수 있습니다. planner node의 이름은 실패·성공 배지가 아니라 query shape·통계·선택 행 수·I/O 비용의 결과입니다.

다만 이 W13 여섯 날의 D4는 그 반례를 실제 여러 query로 실행해 수치로 보여 주지 않습니다. “Seq Scan도 올바를 수 있다”는 올바른 개념이지만, 이 특정 packaged runner가 그 반례를 재현했다는 실행 주장으로 바꾸지 않습니다.

실제 기술 이름selectivity-aware planner choice and Seq Scan counterexample boundary

17. D5의 pagination-scope 검사는 정확히 어떤 token을 확인하나요?

inline PowerShell body는 measure.sql을 읽고 ORDER BY created_at DESC, id DESC, LIMIT 50 세 token이 있는지 확인합니다. 또 대소문자를 무시한 \bOFFSET\b regex가 맞으면 예외를 냅니다. 통과하면 verified='top-N only', offset_benchmark=false, cursor_benchmark=false, source SHA-256을 pagination-scope.json에 쓰는 계약입니다.

현재 gate는 account_id=1 또는 WHERE token을 직접 require하지 않습니다. cursor predicate가 없는지를 검색하는 regex도 없고, JSON의 cursor_benchmark=false를 상수로 쓸 뿐입니다. 실제 measure.sqlaccount_id=1과 cursor 부재가 있는 것은 소스 직접 독해 사실이고, gate의 직접 assertion coverage는 그보다 좁습니다.

실제 기술 이름D5 static token gate and its under-validation boundary

18. OFFSET과 keyset pagination을 비교했다고 말할 수 있나요?

말할 수 없습니다. 현재 packaged measure.sql에는 OFFSET query도 keyset cursor query도 없습니다. 각 방식의 raw sample 30개, plan, buffers, deep-page scanned rows, 동시 insert 중 중복·누락 fixture도 없습니다. D5 증거의 올바른 문장은 “현재 실측은 top-N only이며 OFFSET=false, cursor=false”입니다.

기존 W13 초안에서 앞의 50,000행을 직접 넘기고 cursor 뒤에서 시작하는 두 길을 걸었다고 서술한 부분은 현재 원문·소스 계약과 다릅니다. 이번 미리보기에서는 두 방식을 후속 실험으로만 남기고 측정 결과로 그리지 않습니다.

실제 기술 이름W13_PAGINATION_SCOPE top-N-only offset=false cursor=false

실측한 top-N과 측정하지 않은 OFFSET 및 keyset을 분리한 그림
그림 한눈에: 실측한 top-N과 미검증 pagination 두 방식을 나눕니다.

19. Q15 ‘계좌별 총 입금액·총 출금액’은 어디까지가 문제 계약인가요?

Task ID는 W13-SQL-Q15, 종류는 SQL workbook/집계, 증거 경로는 evidence/w13/sql-q15.sql입니다. query 전에 시작 table의 input grain, 출력 한 행의 grain, 예상 cardinality를 주석으로 적고 CASE 조건부 집계를 사용하는 것이 핵심입니다. 원장 방향을 한 번만 계산했는지 반례로 확인합니다.

정본 정답 SQL과 정복 output은 제공되지 않습니다. ledger를 시작 표로 삼아 0건 계좌를 제외할지, account left join으로 0을 보일지, opening·reversal 유형을 어떻게 분류할지는 학습자가 계약을 선언해야 합니다. 결정하지 않은 정책을 미리보기가 정답처럼 만들지 않습니다.

실제 기술 이름W13-SQL-Q15, conditional aggregation with declared ledger-direction policy

20. Q17 ‘총 거래액 100만원 이상 고객’에서 WHEREHAVING은 어떻게 나뉘나요?

Task ID는 W13-SQL-Q17, 증거는 evidence/w13/sql-q17.sql입니다. 문제의 핵심 도구는 HAVING이고, 필수 설명은 WHEREHAVING의 역할 차이입니다. WHERE는 grouping 전 입력 행을 거르고, HAVING은 group을 만든 뒤 집계 결과를 거릅니다. 임계값 1,000,000을 포함하는지도 경계 반례로 적어야 합니다.

하지만 ‘거래액’이 gross amount인지 signed net인지, 성공 거래만 포함하는지, customer를 어떤 key로 묶는지는 입력 fixture와 학습자 주석을 통해 분명히 해야 합니다. 이에 대한 canonical answer나 고정 output은 shipped source에 없습니다.

실제 기술 이름W13-SQL-Q17, grouped aggregate filtering with HAVING

21. Q18 ‘입금은 있으나 출금은 없는 계좌’에서 NOT IN을 조심하는 이유는 무엇인가요?

Task ID는 W13-SQL-Q18, 증거는 evidence/w13/sql-q18.sql입니다. 핵심 도구는 “입금 행이 존재함”을 보는 EXISTS와 “출금 행이 존재하지 않음”을 보는 NOT EXISTS입니다. 각 subquery는 밖의 account와 상관된 조건으로 연결되어야 합니다.

NOT IN (subquery) 결과에 NULL이 하나라도 있으면 SQL의 three-valued logic 때문에 비교가 UNKNOWN이 되어 예상과 다른 결과가 나올 수 있습니다. NOT EXISTS는 해당 account에 맞는 행의 존재 자체를 보므로 이 함정을 피하기 쉽습니다. 역시 정본 정답 SQL·output은 제공되지 않습니다.

실제 기술 이름W13-SQL-Q18, correlated EXISTS / NOT EXISTS and NULL-safe anti-semi join reasoning

22. 이번 주차의 JUnit test와 selector는 몇 개인가요?

0개입니다. strict D1∼D6의 실행 owner는 PowerShell native runner, Docker/Compose, PostgreSQL psql, SQL assertion, inline static scope gate입니다. @Test method, JUnit selector class, intentional Red JUnit phase를 수량에 넣지 않습니다.

PDF에 계좌·거래·원장 모델이 나오고 reference project에 Java test가 많다고 해서 인접 주차의 JUnit을 W13 수량으로 가져오면 안 됩니다. 이번 주차의 “실행 6개”는 하루 명령 수이지 test method 수가 아닙니다.

실제 기술 이름zero JUnit selectors; native PowerShell/PostgreSQL evidence ownership

23. D6 Benchmark는 정확히 몇 번 측정하고 어떤 파일을 남기나요?

하나의 disposable DB에 seed를 넣고 index 생성 전 measure.sql을 30번 실행해 before-raw.csv를 만듭니다. 그 뒤 treatment index를 생성하고 benchmark-after-plan.txt를 한 번 저장한 후, 같은 query를 30번 더 실행해 after-raw.csv를 만듭니다. 각 row에 variant, 1∼30 sample, plan JSON의 Execution Time ms를 적습니다.

그 뒤 assert.sql을 실행하고 chosen index를 찾으며, before/after 각 30개의 요약과 index 정보를 benchmark-manifest.json에 적습니다. D6은 perf-manifest.json을 만들지 않습니다. 그 최종 파일은 제외된 D7 Report의 소유입니다.

실제 기술 이름30 before + 30 after raw EXPLAIN execution-time samples in one Benchmark lifecycle

같은 query의 before 30회와 after 30회 raw sample을 보존하는 그림
그림 한눈에: 각 variant 30개의 raw와 D6 manifest를 보존합니다.

24. Benchmark는 median과 p95가 둘 다 좋아져야 통과하나요?

현재 코드는 after_median >= before_median 이고 동시에 after_p95 >= before_p95일 때만 no relative improvement로 실패합니다. 즉 두 지표 중 적어도 하나가 엄격히 작아지면 이 Gate는 통과할 수 있습니다. 한 지표가 좋아지고 다른 하나가 악화된 경우도 성공 가능합니다.

그러므로 통과 marker를 보고 “median·p95가 둘 다 개선됐다”고 쓰면 안 됩니다. manifest의 실제 네 값을 그대로 보여 주고, 하나가 악화되었다면 tradeoff로 명시해야 합니다. 또 한 synthetic fixture의 상대 비교이지 운영 절대 latency SLA가 아닙니다.

실제 기술 이름disjunctive relative-improvement gate with mandatory tradeoff disclosure

25. 고정 값 1∼30의 median은 15.5인가요, 15인가요?

일반적으로 짝수 표본의 median을 중앙 두 값의 평균으로 정의하면 (15+16)/2=15.5입니다. PDF D6 완료 체크와 재검증 계약도 median=15.5를 요구합니다. p95는 nearest-rank ceil(30×0.95)-1=28 인덱스, 즉 29번째 값 29를 요구합니다.

하지만 현재 Quantile($Rows,.5) 코드는 모든 p에 ceil(30×p)-1을 쓰므로 .5에서 인덱스 14, 값 15를 반환합니다. 필드 이름을 median이라고 쓰지만 교재 계약과 구현이 충돌합니다. 현재 코드를 고치고 재실행하기 전에는 “1∼30 fixture가 median=15.5를 통과했다”고 주장하지 않습니다.

실제 기술 이름even-sample median contract defect — expected 15.5, current runner returns 15; p95 returns 29

1부터 30에서 PDF median 15.5와 현재 runner 15의 충돌, p95 29 일치를 보인 그림
그림 한눈에: 교재 정의와 현재 함수의 서로 다른 값을 숨기지 않습니다.

26. runner가 warm-up·cache·운영 부하까지 고정했다고 말할 수 있나요?

현재 MeasurePlan은 별도 warm-up loop 없이 sample 1부터 30까지를 바로 raw CSV에 넣습니다. before 30회가 먼저 실행되고 index·ANALYZE 후 after plan 1회와 after 30회가 뒤따릅니다. 따라서 첫 실행과 후속 실행의 cache 상태가 완전히 같다고 보장하지 않습니다.

고정된 것은 packaged query·seed·index DDL, 하나의 D6 disposable lifecycle, variant별 30회, PostgreSQL이 보고한 Execution Time을 읽는 방식입니다. CPU contention·Docker host·DB version·cold cache·I/O·운영 traffic의 절대 재현성, p95 SLA, production speedup은 이 source만으로 보장하지 않습니다.

실제 기술 이름repeated deterministic-lab measurement without an explicit warm-up protocol

27. 현재 증거 상태로 여섯 날이 모두 Green이라고 말할 수 있나요?

아닙니다. 이 미리보기는 current source·PDF·과거 evidence를 정적으로 감사했지만 Docker performance phase를 새로 실행하지 않았습니다. 현재 D1은 release_staging/retrieval_contract_patch_20260720/qa/w13-prepare-runtime-v3/prepare.json이 runner hash d7298bc7734177f5fe66362711d09c46e5f9e1bde4fc720a8a4c01ff78b8c223를 가리킵니다. 반면 과거 D2∼D6은 release_staging/mastery_rebuild_20260717/qa/w13_w24_fresh_packaged_regression_20260718/learner/evidence/w13에 있고, 그 증거의 runner hash는 stale 06f252ec3c3f5d94a71e273223975cffdc4dc5d15b17c950bbf255b5697b982e입니다. workspace 전체에서 current D5 pagination-scope.json과 marker를 찾은 결과도 0건입니다.

그러므로 앞의 숫자와 marker는 curriculum contract·current code path·expected output을 설명하는 것이지, 현재 다섯 runner phase와 D5 inline gate가 하나의 current-hash evidence chain으로 완료됐다는 보고가 아닙니다. 신뢰할 수 있는 current full Green을 만들려면 median 계약 불일치와 D5 검사 경계를 먼저 수정한 뒤, 동일 source hash로 D1∼D6을 재실행해야 합니다.

실제 기술 이름current-evidence continuity failure — D1 current hash, D2–D6 stale hash, D5 artifact absent

28. 월∼토 범위가 최종적으로 보장하는 것과 보장하지 않는 것은 무엇인가요?

소스 계약상 D1은 환경·hash 준비, D2는 before plan capture, D3은 지정 treatment index 선택, D4는 exact row distribution, D5는 top-N-only 주장 경계, D6은 before/after 각 30 sample과 상대 개선 Gate를 담당합니다. SQL workbook prompt Q15·Q17·Q18도 포함하지만 canonical answer는 0개입니다. 정본 hash file 5, unique executable body 6, JUnit 0으로 수량 계층을 나눕니다.

하지만 current full Green, D7 Reportperf-manifest.json, predecessor index, query-tuning report, OFFSET/keyset 비교, 선택도별 planner 실측, warm-up 통제, index 쓰기·용량 비용, 운영 절대 latency, 모든 query에서의 우위는 보장하지 않습니다. 특히 current median 함수는 교재의 15.5 계약과 달라 수정·재실행 전에 통과를 선언할 수 없습니다.

실제 기술 이름evidence-bounded W13 D1–D6 contract and explicit non-guarantees