STARRY PASS 13 — 공동 1등과 되감기 표식
같은 점수의 순위와 어긋난 장부의 복구를 구분한다
이 이야기는 W17 개요와 D1–D6만 다룹니다. 정확한 범위는 p547 개요 + p548–568 + p569 혼합 경계 + p570–572입니다. p569 상단의 D5 최종 Gate 뒤에 D6 토 모듈이 시작되며, p573부터의 D7·p579 주간 통합·p580의 W17 복구 문장과 W18은 제외합니다. 공통 코어 · 트랙 선택 시 증권 기본으로 표시하지만 증권 전용 기능 주차는 아닙니다. source audit은 PASS여도 testsExecutedByAudit=0이므로 현재 상태는 W17 D1–D6 LABS NOT_RUN입니다.
STARRY의 객석이 하룻밤 동안 별 모양 공 던지기 경기장으로 바뀌었다. 키타는 사회자 띠를 둘렀고, 니지카는 점수와 식권 잔액을 함께 관리하는 계산대를 맡았다. 료는 우승 상품인 카레 세트를 노렸고, 히토리는 참가 신청서 뒤에 숨어 조용히 관전하려 했다.
경기 시작 전 계산대에서는 이상한 일이 먼저 벌어졌다. 카드에 적힌 잔액과 영수증 원장의 합이 모두 맞아 불일치 수가 0이었는데, 료가 한 카드 잔액에 7을 더하자 불일치가 정확히 1이 되었다. 니지카는 장난 직전에 작은 별 표식을 꽂아 두었고, 그 표식까지 되감자 다시 0이 되었다.

“처음 0, 장난 뒤 1, 되감은 뒤 0이에요.” 키타가 표시창을 손가락으로 짚었다.
“별 앞의 기록까지 사라지는 건 아니죠?” 히토리가 물었다.
니지카는 SAVEPOINT가 transaction 중간의 복귀점을 표시하고, ROLLBACK TO SAVEPOINT는 그 뒤의 변경만 취소한다고 설명했다. 전체 transaction은 여전히 열려 있으므로 이후 COMMIT이나 바깥 ROLLBACK으로 끝을 정해야 했다. 표식까지 돌아온 것과 밤 전체를 없앤 것은 달랐다.
D1의 목표는 이 0→1→0을 실제 PostgreSQL 대사와 transcript로 확인하는 것이었다. 예상 marker가 문서에 적혀 있다는 사실은 현재 실행 성공과 다르다. source audit은 파일을 읽었지만 runner를 실행하지 않았고, reconcile-transcript.txt 존재만으로도 최신 Green을 증명할 수 없었다.
료가 “복구했으니 처음 seed도 사라졌겠네”라고 말하자 니지카가 고개를 저었다. canonical drill의 account·business_tx·ledger_entry seed는 BEGIN보다 앞에서 실행되어 기본 autocommit 아래 남을 수 있다. 뒤의 최종 ROLLBACK은 post-BEGIN 구간을 닫을 뿐, 이미 commit된 seed까지 지우지 않는다.
경기 첫 네 점수는 100, 100, 80, 70이었다. 시작부터 공동 1등이었다. 키타가 번호표를 나눠 주려 하자 같은 점수 둘에게 어떤 번호를 붙일지를 두고 객석이 웅성거렸다.
ROW_NUMBER는 모든 행에 서로 다른 번호를 붙여 1, 2, 3, 4가 되었다. RANK는 공동 1등을 함께 1로 두고 다음 자리를 건너뛰어 1, 1, 3, 4가 되었다. DENSE_RANK는 빈 번호 없이 1, 1, 2, 3이 되었다.
“80점 선수가 3등이기도 하고 2등이기도 하네요.” 히토리가 말했다.
“함수가 답을 바꾼 게 아니라, 우리가 동률 다음 자리를 어떻게 정의할지 골랐기 때문이야.” 료가 답했다.
고정 예의 exact row는 1|100|1|1|1, 2|100|2|1|1, 3|80|3|3|2, 4|70|4|4|3이었다. ROW_NUMBER에서 두 100점의 순서를 안정시키려면 점수 외에 account_id 같은 결정적 tie-breaker가 필요했다. 마지막 화면의 ORDER BY는 표시 순서를 정하지만, window 함수 안의 번호 약속을 대신하지 않았다.

가족부·학생부·상인부가 동시에 점수를 내자 키타는 통로에 색 테이프를 붙였다. PARTITION BY는 부문마다 번호를 새로 시작하게 했고, window 안의 ORDER BY score DESC, account_id는 각 차선 안의 순서를 결정했다. 부문별 최신 한 건처럼 반드시 한 줄만 골라야 하는 문제에는 ROW_NUMBER와 안정적인 두 번째 정렬 기준이 맞았다. 공동 최고 점수를 모두 남겨야 한다면 RANK의 의미가 더 맞을 수 있었다.
휴식 시간, 관리인이 여러 계좌의 저장 잔액과 원장 합을 비교해 달라고 했다. 한 문장에 전부 넣자 히토리의 눈에는 괄호와 별칭만 보였다. 니지카는 질문을 세 개의 이름 붙은 판으로 나눴다.
첫 판 ledger_sum은 account_id별 signed_amount 합을 만들었다. 둘째 compared는 account의 stored balance와 ledger 합을 같은 행에 놓았다. 셋째 mismatch는 두 값이 다른 계좌만 남겼다. 각 CTE의 한 행이 무엇을 뜻하는지, 즉 query grain을 account로 유지하는 것이 핵심이었다.
원장이 하나도 없는 계좌도 비교 대상에 남겨야 했다. account에서 시작해 ledger_sum을 LEFT JOIN하고, 없는 합을 COALESCE(..., 0)으로 표시했다. 이것은 NULL 표시 계약이지 잘못된 join이나 빠진 원장을 자동으로 고치는 주문은 아니었다.
검증 장면은 한 계좌에만 맞춘 요령을 막기 위해 두 반례를 사용했다. baseline은 EMPTY, 202번의 저장 잔액을 7 높이면 202|82|75|7, 복구하면 다시 EMPTY였다. 이어 101번을 11 높이면 101|111|100|11, 다시 복구하면 EMPTY가 되었다.
“결과가 한 줄이라는 것만 보면 어느 계좌가 얼마나 틀렸는지 모르겠어요.” 키타가 말했다.
“그래서 account_id, stored_balance, ledger_balance, difference 네 열과 순서를 함께 확인해.” 니지카가 답했다.
D3의 Q25는 성공률을 소수 둘째 자리까지 계산하는 문제였다. 별도 audit-authored 학습용 예시는 customer를 보존하고 전체 상태를 분모, SUCCESS만 분자로 둔다. 11/12는 91.67, 2/7은 28.57이며 거래 0건은 NULLIF(total, 0) 때문에 NULL로 남는다. customer grain·분모 정책·0건 NULL은 prompt가 고정한 정본 답안이 아니라 공개한 예시 가정이었다.
목요일에는 되감기 별을 transaction 수업용으로 다시 꺼냈다. 니지카는 BEGIN 뒤 baseline을 확인하고 SAVEPOINT before_fault를 만들었다. 그 다음 일부러 잔액을 7 높여 mismatch 1을 관찰하고, ROLLBACK TO SAVEPOINT before_fault로 돌아가 mismatch 0을 확인했다.
constraint 오류가 발생한 PostgreSQL transaction은 자동으로 정상 상태로 돌아오지 않을 수 있었다. savepoint가 있다면 그 지점으로 rollback해 실패 상태를 걷어내고 계속할 수 있고, 없다면 transaction 전체를 rollback해야 했다. 연습은 disposable schema에서 해야 하며 production DB에 고의 오류를 주입하면 안 됐다.
“별까지 돌아온 뒤 바로 집에 가도 되나요?” 히토리가 물었다.
“아니. transaction 문은 아직 열려 있어.” 료가 말했다. “COMMIT할지, 마지막 ROLLBACK으로 닫을지 정해야 해.”
D1의 물리 drill과 D4의 SQLD DML·constraint·transaction 설명은 같은 SAVEPOINT를 보지만 질문의 초점이 달랐다. 하나는 대사 불일치 0→1→0을 재현하고, 다른 하나는 오류 격리와 transaction 종료 범위를 해석했다.
금요일, 료는 경기장 동선을 짠 계획표를 내밀었다. 종이에 가장 가까운 문이 표시되어 있어도 실제 관객이 반드시 그 문을 쓰는 것은 아니었다. PostgreSQL optimizer도 index가 존재한다는 이유만으로 index scan을 강제하지 않았다. 예상 행 수, 선택도, table 크기와 비용을 바탕으로 seq scan·index scan·join·sort를 골랐다.
니지카는 D5의 일곱 열을 표에 썼다. estimated_rows, actual_rows, filter_rows, index_name, buffer_hit, buffer_read, execution_time이었다. estimate는 planner의 예측이고 actual은 실행 관찰값이다. nested loop의 안쪽 node는 loops만큼 반복될 수 있으므로 rows 한 숫자만 떼어 전체 일을 단정하면 안 됐다.
W13의 before-plan.txt, after-plan.txt, perf-manifest.json을 연결할 때는 filename과 hash를 맞춰야 했다. 하지만 실제 field를 읽지 않고 “index가 있으니 빨라졌다”라고 쓰거나 절대 latency를 약속하면 안 됐다. cache가 따뜻한지, data 규모와 통계가 어떤지에 따라 관찰값은 달라진다.
D5의 Q26은 correlation_id를 GROUP BY하고 HAVING COUNT(*) > 1인 중복 후보를 찾았다. illustrative workbook fixture에서는 CORR-DUP|2 한 줄이다. 이 결과는 두 행이 같은 logical request이거나 사기라는 판정이 아니다. 더구나 application V001에는 correlation_id UNIQUE가 있지만 workbook schema는 중복 연습을 허용하므로 두 schema의 계약을 섞으면 안 됐다.

토요일에는 75분 압축 누적 세트를 풀었다. 제목에 75분이 있어도 50문항·90분 full mock은 아니었다. CSV의 full_exam=false와 실제 question_count를 그대로 기록해야 했다. window·NULL·subquery·transaction 문제가 섞여 있었지만, 시간 상한을 넘겨 빈 문항을 억지로 채우지 않았다.
정답 여부 옆에는 confidence를 남겼다. 자신 있게 틀린 high-wrong은 개념 오해 가능성이 커서 가장 먼저 복구했고, 낮은 확신으로 맞힌 low-right는 추측인지 근거를 다시 확인했다. 점수만 보면 둘 다 놓치기 쉬운 정보였다.
니지카는 마지막 벽에 source 경계를 붙였다. 코드 뒤풀이는 canonical 5개와 illustrative 4개를 분리한다. ranking·reconcile CTE·Q25·Q26 SQL은 audit-authored 학습용 예시이며 shipped learner 정본 답안이 아니다. LOCAL_ARTIFACT_VALIDATED 같은 marker도 정적 파일 계약이나 좁은 sentinel을 나타낼 뿐 모든 SQL 의미를 대신하지 않으므로 HUMAN_SEMANTIC_REVIEW_REQUIRED가 남는다.
W17 D1–D6의 재계산 학습 시간은 2시간 40분–5시간이다. p547의 2시간 50분–5시간 20분은 D7까지 포함한 전체 주차 합계이므로 strict 미리보기의 합계로 가져오지 않았다.
손님이 “그럼 이번 주가 전부 Green인가요?”라고 묻자 히토리가 고개를 저었다.
“source audit PASS는 원문과 source bytes가 닫혔다는 뜻이고, testsExecutedByAudit=0이에요. 예상 marker와 실제 최신 실행 증거는 달라요.”
키타가 덧붙였다. “대신 무엇을 확인했고 무엇을 아직 확인하지 않았는지는 한눈에 볼 수 있어요!”
료는 남은 카레 감자를 집어 들었다. “공동 1등의 번호는 규칙에 따라 달라져도, 어긋난 장부는 되돌려 맞춰야 한다는 것도.”
니지카는 p573의 D7 문을 닫아 두었다. 오늘은 p572의 D6 최종 Gate까지만이었다. 점수판에는 1·1·3과 1·1·2가 나란히 남았고, 계산대에는 0→1→0 별 표식이 빛났다.
이미지 출처와 이용 안내: 공식 장면컷은 TV 애니메이션 《봇치 더 록!》 공식 사이트 자료입니다. ©はまじあき/芳文社・アニプレックス. 이 문서는 개인 학습용 비공식 구성으로, 원작의 공식 설정과 별개의 학습 비유입니다. 개념도는 원문 감사 내용에 맞춰 새로 만든 교육용 SVG입니다.