STARRY PASS 14 — 공연 전날의 다섯 장 복구 카드
새것을 늘리지 않고 핵심을 빈 종이에서 꺼내기
이 이야기는 p580 W18 제목 아래 + p581–601 + p602 상단 D6 최종 Gate만 다룹니다. p580 상단의 W17 잔여 문장, p602 하단 D7 시작점, p606 주간 통합·요일별 해설, p607 official reference와 그 이후는 제외합니다. 공통 코어 · 트랙 선택 시 증권 기본이며 증권 전용 기능 주차는 아닙니다. source audit은 원문과 source의 정적 폐쇄를 확인했지만 실행하지 않았으므로 현재 상태는 W18 D1–D6 LABS NOT_RUN, testsExecutedByAudit=0입니다.
공연을 엿새 앞둔 월요일, STARRY의 예약 단말기가 갑자기 모든 메뉴를 지웠다. 화면에는 새 기능을 설치하라는 말 대신 “이미 배운 길을 빈 종이에서 다시 찾을 것”이라는 문장만 남았다. 마지막 날의 공식 시험까지 무사히 가려면 여섯 문을 차례로 지나야 했고, 넷째 문 안에는 다섯 장의 SQL 복구 카드가 들어 있었다.
키타는 새 문제집 세 권을 안고 달려왔다. 히토리는 책 높이를 보고 기타 가방 뒤로 반쯤 숨었다. 료는 맨 위 책을 집었다가 가격표를 보고 조용히 내려놓았다. 니지카는 태블릿을 켜고 네 사람 앞에 빈 종이 한 장씩만 놓았다.
“이번 주의 규칙은 늘리는 게 아니라 되찾는 거야.” 니지카가 말했다. “확신하며 틀린 것, 작은 표 없이 외운 것, 시간 안에 놓치던 것만 꺼내자.”

첫 문에는 W15부터 W17까지 남은 오답표가 걸려 있었다. 단순히 틀린 문제보다 높은 확신으로 틀린 high-wrong이 위로 떠올랐다. 맞다고 굳게 믿었던 설명일수록 같은 실수를 다시 만들 가능성이 컸기 때문이다. 니지카는 점수 영향이 큰 열 장만 남기고 나머지 종이를 서랍에 넣었다.
빈 종이의 왼쪽에는 entity와 candidate key, 가운데에는 cardinality와 정규형, 오른쪽에는 프로젝트 table 예시를 썼다. NULL = NULL이 참이라고 믿었던 카드에는 UNKNOWN이 되는 한 행 반례를, detail 두 묶음을 바로 JOIN해도 행 수가 그대로라고 믿었던 카드에는 2×3으로 여섯 행이 되는 작은 표를 붙였다. 한 문제마다 틀린 믿음과 그 믿음을 깨는 최소 반례가 하나씩 생겼다.
키타가 “열 장을 다 맞혀야 다음 문이 열리나요?”라고 묻자 니지카는 고개를 저었다. 원문의 Gate는 원오답 열 개 중 resolved=true가 8개 이상인지를 보고, 아직 풀리지 않은 것은 다음날 목록에 최대 5개만 남긴다. 불안하다고 새 문제 40개를 여는 행동은 복구 범위를 늘리는 실패였다.
화면에는 LOCAL_ARTIFACT_VALIDATED W18D1 bytes=<actual> sha256=<64-hex actual hash>가 예상 형식으로 적혀 있었다. 그러나 그 문자열은 high-wrong.csv의 존재·최소 크기·placeholder 부재·hash를 확인하는 좁은 표지일 뿐, 열 개 반례가 의미상 옳다는 판정은 아니었다. 파일조차 지금 실행해 검사하지 않았으므로 네 사람은 초록 도장을 찍지 않았다.
화요일의 둘째 문은 열두 개의 함정 발판이었다. 첫 네 칸에는 NULL 비교, NULL이 든 NOT IN, COUNT(*)와 COUNT(column), LEFT JOIN 뒤 우측 조건을 WHERE에 둬 unmatched 행을 지우는 함정이 놓였다. 다음 세 칸은 window의 기본 frame, 동률 순위, pagination tie였다. 마지막 다섯 칸은 DML rollback, commit, SAVEPOINT, CHECK, FK constraint였다.
히토리가 문장을 외워 발판을 뛰려 하자 료가 2~3행짜리 카드를 발밑에 밀어 넣었다. NOT IN (1, NULL)은 원하는 값이 아니라고 곧장 TRUE가 되지 않았고, LEFT JOIN으로 남겨 둔 빈 우편함도 WHERE right.status='OK'를 통과하지 못하면 사라졌다. COUNT(column)은 NULL을 세지 않지만 COUNT(*)는 행을 셌다.
동률 timestamp 두 건에는 ID를 두 번째 정렬 기준으로 붙였다. 순서가 완성되지 않으면 window 중간값과 다음 페이지 경계는 반복 실행마다 흔들릴 수 있었다. PostgreSQL transaction에서 constraint 오류가 난 뒤에는 오류 메시지만 무시한다고 정상으로 돌아오지 않았다. 준비한 SAVEPOINT로 되돌리거나 transaction 전체를 ROLLBACK해야 했다.
“정답 문장 한 줄이면 빨리 끝나는데요.” 키타가 말했다.
“빨리 외운 문장은 빨리 뒤집혀.” 료가 답했다. “열두 칸마다 입력 2~3행, 예상 truth value나 결과 행, 실제와 다른 이유가 있어야 해.”
D2의 LOCAL_ARTIFACT_VALIDATED도 파일 bytes와 hash를 대신 계산할 뿐, SQL parser도 아니고 열두 반례의 사람 검토도 아니었다. 그래서 HUMAN_SEMANTIC_REVIEW_REQUIRED 표찰은 문 옆에 그대로 남았다.
수요일에는 객석 시계를 실제 시험처럼 맞췄다. 이번에는 줄인 세트가 아니었다. 단말기에 question_count=50, full_exam=true, timer_target_min=90 세 값이 모두 켜져야 했다. 45분짜리 연습이나 문항 수가 비어 있는 기록을 full mock이라고 부를 수 없었다.

확실한 문제를 먼저 확보하고, 3분 넘게 막힌 문제에는 표시를 남겨 두 번째 pass로 넘겼다. first_pass_min, second_pass_min, elapsed_min을 실제 값으로 기록하고 마지막 10분 review 규칙도 미리 정했다. 답을 바꾼 문항은 첫 답을 지우지 않아 판단 경로를 나중에 확인할 수 있게 했다.
90분 타이머가 울린 뒤에야 별도의 30분 채점이 시작됐다. correct·wrong·blank의 합은 각 section 문항 수와 같아야 했고 total은 정확히 50이어야 했다. 복구 목록은 오답 전부가 아니라 최대 5개였다. 점수가 불안하다고 새 교재를 여는 대신 가장 큰 오해 다섯 개만 다음 카드로 옮겼다.
같은 날의 Q27 문에는 여섯 고객의 첫 거래일과 마지막 거래일을 구하는 문제가 있었다. 키타는 transaction에서 시작하려 했지만, 니지카는 거래가 없는 고객도 결과에 남길 정책이라면 customer에서 시작해 LEFT JOIN해야 한다고 말했다. 각 고객 한 행에서 MIN(business_date)와 MAX(business_date)를 계산하고, 거래가 없는 고객의 두 날짜는 NULL로 남겼다.
이 Q27 SQL은 prompt가 status나 tx_type의 포함 범위를 전부 고정하지 않았기 때문에 “모든 행 포함”이라는 가정을 주석으로 공개한 학습용 예시다. W18-SQL-Q27.sql은 shipped 정본 답안이 아니며, 실제 workbook schema와 정책에 맞춰 검토해야 했다.
목요일, 넷째 문이 열리자 제목의 다섯 복구 카드가 나타났다. 자료는 account 세 행과 ledger 세 행뿐이었다. account는 (1,70), (2,0), (3,5)였고, ledger는 1번 account에 +100과 -30, 3번 account에 +5가 있었다. 2번 account에는 ledger가 없었다. 시각과 ID는 1번의 +100이 먼저, -30이 다음이 되도록 고정돼 있었다.
첫 카드는 저장 잔액과 원장 합을 대사했다. 1번은 100-30=70, 2번은 ledger 없음의 합을 0으로 읽어 0=0, 3번은 5=5라 mismatch는 0건이었다. marker는 W18_Q1 rows=0이었다. 둘째 카드는 NOT EXISTS로 ledger 없는 account를 찾아 W18_Q2 rows=1 id=2를 기대했다.
셋째 카드는 account 1의 양수를 credit, 음수의 크기를 debit으로 모아 W18_Q3 rows=1 credits=100 debits=30을 썼다. 넷째 카드는 created_at,id 순서 window sum으로 100에서 70으로 내려가 W18_Q4 rows=2 final=70을 기대했다. 다섯째 카드는 cursor (2026-01-03Z,99)보다 작은 행을 최신순 두 건 골라 W18_Q5 rows=2 ids=2|1을 표시했다.
“다섯 marker가 출력되면 모든 게 증명된 거죠?” 키타가 물었다.
니지카는 runner가 W18_Q1 부터 W18_Q5 까지의 substring과 native exit를 확인하지만, marker 뒤 숫자를 실제 query 결과에서 다시 계산하거나 행 수·순서·유일성을 전부 인증하지는 않는다고 설명했다. SQL 안의 marker는 hardcoded literal이었다. 특히 anti join의 array_agg, running의 x, keyset의 ids가 NULL일 때 IF value <> expected 자체가 UNKNOWN이 되어 fail-open할 수 있는 경계도 남았다.
runner의 최종 문구는 정확히 W18_PROJECT_SQL5_GREEN files=5 native_exits=0 rows=5 hashes=6 cleanup=1이었다. 하지만 지금 화면에 적힌 것은 기대 문자열이지 이 미리보기 감사가 새로 실행한 결과가 아니었다. cleanup=1도 manifest의 literal이며, supplied Container mode의 외부 container까지 제거했다는 뜻이 아니었다.
canonical teaching unit은 shared Compose 1개, W18 runner 1개, fixture 1개, invariant SQL 5개로 모두 8개였다. 여기에 Q27·Q28 illustrative SQL 2개가 따로 붙어 코드 뒤풀이의 열 장을 이뤘다. “project SQL 5개”는 다섯 query의 수이고 “canonical 8개”는 runtime과 지원 source를 포함한 teaching unit 수라 서로 다른 셈이었다.
금요일의 문에는 커다란 종료 버튼이 있었다. 시험 전날 준비는 장소·시간·신분증·이동·알람을 확인하고, admission and ID, route and arrival buffer, alarm and sleep time, one-page notes only, stop after 60 minutes의 다섯 체크를 채우는 일이었다.

복습 대상은 D1부터 D4까지 아직 unresolved인 항목 최대 5개뿐이었다. 완료 기록은 actual_minutes<=60, 새 문제 0, 새 project 작업 0이어야 했다. 료가 밤늦게 project SQL을 한 번 더 돌리자고 하자 니지카는 project 작업은 전날 이미 종료됐으며 수면이 우선이라고 선을 그었다.
Q28은 actor별로 연속 실패가 3회 이상인 후보를 찾는 문제였다. 학습용 예시는 occurred_at,tx_id로 안정 순서를 만들고, FAILED가 아닌 행의 누적 개수를 reset group으로 삼아 실패 island를 묶었다. 고정 fixture의 후보는 USER-FAIL, streak 3, 10:00부터 10:02까지였다.
그러나 연속의 순서 기준은 prompt를 보강한 가정이고, +09 출력 문자열은 session TimeZone에 따라 달라질 수 있었다. fixture contract는 실패가 세 건 이상임을 확인할 뿐 연속성을 직접 판정하는 정본 답안이 아니었다. W18-SQL-Q28.sql 역시 shipped 정본 답안이 아닌 illustrative 학습용 예시였다.
토요일에는 마지막 문 앞에 두 개의 시계가 놓였다. 첫 시계는 공식 응시 90분, 둘째 시계는 시험 종료 후 30분 이내에 시작해 정확히 30분만 쓰는 기억 기반 복기였다. 두 블록은 섞거나 하나로 줄이면 안 됐다.
복기 CSV에는 modeling·normalization·join-cardinality·null-logic·window-function·transaction 같은 영역, 시간 배분, confidence, 바꾼 답, 기억난 조건, 불확실한 이유, 다음 drill만 남겼다. 공식 정답 공개 전 correct를 추측해 쓰지 않았고 문항 원문이나 선택지를 상세히 복원하지 않았다. 이동과 대기시간도 학습 합계에 새로 더하지 않았다.
여기에는 시간 표기의 충돌이 있었다. D6 제목과 수행 절차는 응시 90분 뒤 복기 30분을 분명히 요구하지만, 같은 모듈의 표시 예상시간은 전체 1h 30m로 적혀 복기 30분을 포함하지 않는다. 원문의 일별 표시값만 더한 strict D1–D6 합계는 305–390분, 즉 5시간 05분–6시간 30분이고, 개요의 310–400분은 D7 5–10분까지 포함한 값이다. 하지만 두 합계 모두 D6의 필수 복기 30분을 담지 못한다. 그래서 네 사람은 원문 표기 합계를 고쳐 쓴 척하지 않고, 실제 계획에서는 필수 블록을 D3 90+30, D6 90+30으로 각각 분리했다. 모든 필수 블록을 단순 합산한 335–420분은 충돌을 드러내기 위한 파생 계획값이지 원문의 인쇄 합계가 아니다.
D6의 최종 Gate는 p602 상단까지 이어졌다. exam-retro.csv 존재, 90분과 30분 분리, 문항 원문 미복제, 이동·대기시간 미가산을 확인한 바로 뒤에서 D7 제목이 시작됐다. 니지카는 그 아래 결과 분기 문을 닫았다. p606 통합 해설과 p607 official reference도 오늘 미리보기에는 들이지 않았다.
예약 단말기의 여섯 불이 모두 켜졌지만 니지카는 실행 Green 스티커 대신 경계표를 붙였다. LOCAL_ARTIFACT_VALIDATED는 파일의 좁은 구조와 hash를 확인하는 marker이고, HUMAN_SEMANTIC_REVIEW_REQUIRED는 반례·grain·업무 가정과 실제 결과를 사람이 봐야 한다는 뜻이었다. source audit PASS와 current runtime Green은 같지 않았다.
“그러면 오늘 얻은 건 초록불이 아닌가요?” 히토리가 물었다.
“정확히 말할 수 있는 경계가 생겼지.” 니지카가 답했다. “무엇을 다시 실행해야 하는지, 무엇이 예시이고 무엇이 정본인지, 어느 페이지에서 멈춰야 하는지.”
료는 다섯 복구 카드를 뒤집어 순서 없이 놓았다. 히토리는 작은 fixture부터 다시 그려 reconciliation 0건, ledger 없는 2번, 100과 30, 누적 70, 2와 1을 차례로 꺼냈다. 키타는 full mock 시계와 전날 종료 버튼을 함께 챙겼다. 새것은 하나도 늘지 않았지만 손에 남은 길은 전보다 또렷했다.
이미지 출처와 이용 안내: 공식 장면컷은 TV 애니메이션 《봇치 더 록!》 공식 사이트 자료입니다. ©はまじあき/芳文社・アニプレックス. 이 문서는 개인 학습용 비공식 구성으로, 원작의 공식 설정과 별개의 학습 비유입니다. 개념도는 원문 감사 내용에 맞춰 새로 만든 교육용 SVG입니다.