STARRY PASS 12 — 정산 주방의 주문 순서
쓴 순서와 움직이는 순서는 다르다
이 이야기는 W16 개요와 D1–D6만 다룹니다. 정확한 범위는 p518 L7–24 + p519–540 + p541 L5–12이며, p518 L5의 W15 복구 꼬리와 p541 L14부터 시작하는 D7·p546 주간 통합·p547 W17은 제외합니다. 공통 코어 · 트랙 선택 시 증권 기본으로 표시하지만 증권 전용 기능 주차는 아닙니다. source audit은 PASS여도 testsExecutedByAudit=0이므로 현재 상태는 W16 D1–D6 LABS NOT_RUN입니다.
상가 축제 날, STARRY는 하루 동안 카레 가게가 되었다. 니지카는 앞치마를 두르고 주문을 받았고 키타는 접시를 꾸몄다. 료는 계산대 옆에서 감자 한 조각을 시식한 뒤 다섯 번째 조각도 검사라고 주장했다. 히토리는 주방에 숨으면 손님을 피할 수 있을 줄 알았지만 양파 산 앞에 배치되었다.
“사람 대신 양파를 만나니 눈물이 나는 이유가 더 분명해졌어요.” 히토리가 말했다.
“고토의 눈물도 재료가 되는 좋은 가게.” 료가 대답했다.
키타가 주문표 스무 장을 흔들었다. 손님은 완성 접시에 무엇을 보여 달라고 먼저 적었지만, 주방이 실제로 움직일 때는 냉장고에서 재료 상자를 꺼내는 일이 먼저였다. 니지카는 메뉴판의 글 순서와 주방 안의 처리 순서를 바닥에 따로 붙이자고 했다.

니지카가 첫 화살표를 FROM에 놓았다. 어떤 상자에서 행을 가져올지 정하는 단계였다. WHERE는 아직 낱개인 행을 걸렀고, GROUP BY는 남은 행을 같은 계좌끼리 묶었다. HAVING은 완성된 묶음을 다시 걸렀다. 그 뒤 SELECT가 보여 줄 열과 gross 같은 별칭을 만들었고, ORDER BY가 마지막 줄을 세웠다.
키타는 주문표에 SELECT가 먼저 쓰여 있으니 별칭도 처음부터 존재한다고 생각했다. 히토리는 WHERE gross > 1000이라는 쪽지를 냉장고 문에 붙였다가 아무도 gross를 모른다는 답을 들었다. 같은 query level의 WHERE 시점에는 SELECT 별칭이 아직 태어나지 않았다. 반면 마지막 ORDER BY에 도착했을 때는 완성된 별칭을 사용할 수 있었다.
“미래의 접시 이름으로 과거의 양파를 부르면 양파가 대답하지 않는군요.” 히토리가 말했다.
“그리고 낱개 재료를 거르는 WHERE와 묶음 합계를 거르는 HAVING도 자리를 바꾸면 안 돼.” 니지카가 덧붙였다.
그 순서는 SQL 언어를 이해하기 위한 logical processing order였다. 실제 PostgreSQL이 디스크와 메모리에서 물리 연산자를 어떤 순서로 골랐는지는 optimizer와 실행 계획의 문제였다. 바닥 화살표를 외웠다고 물리 plan이나 성능까지 증명한 것은 아니었다.
두 번째 주문은 계좌별 영수증 수와 검사 기록 수를 한 접시에 보여 달라는 것이었다. 계좌 한 줄 옆에는 ledger 영수증 두 장, audit 검사표 세 장이 달려 있었다. 둘을 detail 상태로 동시에 붙이자 두 장과 세 장의 모든 조합이 생겨 한 계좌가 여섯 줄로 불어났다.
히토리는 원래 한 계좌였는데 접시가 여섯 개인 것을 보고 의자를 더 가져왔다. 료는 SQL 오류가 난 것이 아니라 각 결과 행의 뜻이 계좌 한 건에서 계좌·ledger·audit 한 조합으로 바뀐 것이라고 설명했다. 그 상태에서 SUM을 하면 ledger 값은 audit 수만큼, audit 값은 ledger 수만큼 반복될 수 있었다.
“DISTINCT로 지우면 되지 않을까요?” 키타가 물었다.
“같은 금액의 서로 다른 사건까지 지울 수 있어. 중복의 원인이 grain인데 값만 보고 지우면 다른 문제를 만든다.” 료가 말했다.
니지카는 두 자식 상자를 계좌별 한 줄로 먼저 접었다. ledger는 account_id별 count, audit도 account_id별 count를 만든 뒤 account에 붙였다. 이제 부모 한 줄이라는 결과 grain이 유지되었다. 다만 D2 PDF의 개념 fixture는 ledger와 audit 두 자식을 말하지만, 함께 제시된 pre-aggregation SQL의 두 번째 집계는 별도 audit table이 아니라 ledger_entry.reversal_of IS NOT NULL을 센다. 비유의 이름과 실제 코드의 source를 같은 것처럼 바꾸어 부르면 안 됐다.
빈 집계행에는 값이 없었다. COALESCE를 쓰면 보고서에서 그 빈자리를 0으로 표시할 수 있었다. 그러나 잘못된 join key나 이미 여섯 줄로 불어난 grain을 COALESCE가 고쳐 주지는 않았다.

세 번째 주문은 “영수증이 하나도 없는 계좌를 찾아 주세요”였다. 료는 각 account를 바깥에서 한 줄씩 들고 ledger 상자에 같은 account_id가 존재하는지 물었다. 하나라도 있으면 제외하고, 전혀 없을 때만 남기는 NOT EXISTS였다. 고정 예에서 1번과 2번에는 ledger가 있고 3번에는 없으므로 결과는 account 3이었다.
키타는 account를 왼쪽에 둔 LEFT JOIN 뒤 e.id IS NULL을 고르는 방법도 적었다. exact correlation과 결과 grain이 같다면 두 방식은 같은 세 번째 계좌를 찾을 수 있었다. 하지만 상관 조건 e.account_id = a.id를 빠뜨리면 다른 질문이 되었다.
히토리는 세 번째 방법으로 NOT IN을 썼다. ledger 목록에 account_id가 NULL인 찢어진 영수증 한 장을 넣자 3번 계좌까지 사라졌다. 3 <> NULL은 TRUE가 아니라 UNKNOWN이고, WHERE는 UNKNOWN인 행을 남기지 않았다. PDF는 이 함정을 설명하지만 canonical D7 SQL이 직접 그 반례 query까지 실행했다는 뜻은 아니다.
“빈 봉투와 0원 영수증 한 장은 같은가요?” 히토리가 물었다.
니지카는 3번 계좌에 0원 ledger 한 장을 넣어 보였다. 합계는 여전히 0이지만 이제 EXISTS는 참이었다. 행 없음과 합계 0은 합계만 보면 같아도 존재를 묻는 질문에서는 달랐다.
네 번째 주문은 계좌마다 들어온 돈, 나간 돈, 거래 수를 보여 달라는 것이었다. account를 왼쪽에 두고 ledger를 LEFT JOIN해야 거래가 없는 계좌도 남았다. 그 계좌에서 COUNT(*)는 NULL 확장행을 한 줄로 셀 수 있지만 COUNT(e.id)는 실제 ledger id만 세므로 0을 만들었다.
양수 signed_amount는 credits, 음수는 debits 바구니에 넣었다. PDF 예시는 debit을 양의 표시 금액으로 만들기 위해 음수에 마이너스를 한 번 더 붙였다. CASE ... ELSE 0은 각 입력 행이 어느 바구니에도 맞지 않을 때 0을 내고, 바깥 COALESCE(SUM(...), 0)는 집계 자체가 NULL일 때 표시값을 0으로 바꿨다. 두 0은 같은 역할이 아니었다.
니지카는 전체 큰 저울과 계좌별 작은 저울을 함께 꺼냈다. 고정 project fixture의 account balance는 100, 50, 0이고 ledger signed_amount는 +100, +70, -20이라 전역 합은 150이었다. 그러나 전체 150이 맞아도 다른 계좌의 +20과 -20이 우연히 상쇄될 수 있었다. account별로 100=100, 50=70-20, 0=0인지 비교해야 분배 오류가 드러났다.
“합계가 맞다는 말과 배치가 맞다는 말은 다른 질문이네요.” 키타가 말했다.
“맞아. 큰 저울 하나의 일치는 작은 저울 전부의 일치를 증명하지 않아.” 니지카가 답했다.
D3의 필수 통합 Q23은 최근 7일 거래를 고객별로 합치는 문제였다. 료는 오늘이라는 말을 벽시계에서 떼어 fixture_clock.as_of라는 고정 시각에 박았다. 시작은 as_of에서 7일을 뺀 순간을 포함하고, 끝 as_of는 포함하지 않는 반열린 구간으로 정했다. 다음 구간과 경계 거래를 두 번 세지 않기 위한 선택이었다.
첫 CTE는 bounds, 둘째는 recent transactions, 마지막 결과는 customer 한 줄이라는 grain을 가졌다. CTE는 이름 붙인 query 단계이지 항상 물리 materialization된다는 약속은 아니었다. 거래가 없는 customer까지 0으로 보존하려면 시작 table과 join shape를 별도로 설계해야 했다.
별도 source audit이 제공한 Q23 SQL은 all status를 포함하고 recent row가 있는 고객만 남기는 학습용 예시다. PDF가 완성 SQL이나 그 정책을 정본 답안으로 제공한 것은 아니다. 따라서 FAILED·PROCESSING을 포함하는 선택과 zero-customer가 사라지는 경계를 예시의 가정으로 표시했다.
D5의 Q24는 금액을 SMALL, MEDIUM, LARGE로 분류했다. CASE는 위에서부터 조건을 읽고 처음 TRUE인 branch에서 멈췄다. 넓은 조건을 앞에 두면 뒤의 좁은 조건에 도달하지 못할 수 있었다.
PDF 문제는 threshold 숫자를 주지 않는다. 별도 audit-authored Q24 학습용 예시는 account.balance < 100000을 SMALL, account.balance < 500000을 MEDIUM, 나머지를 LARGE로 둔 명시적 가정이다. 그 가정에서는 100000이 MEDIUM, 500000이 LARGE다. 이 숫자와 4·2·2 결과를 업무 정본 정책으로 확대하지 않았다.
같은 날 축제 본부는 OPENING 사건과 DEPOSIT 사건을 한 목록으로 달라고 했다. 둘을 UNION ALL로 이어 붙이면 같은 금액이어도 id와 account_id가 다른 사건을 모두 보존했다. UNION은 projection 전체가 같은 중복 row를 제거하며 sort나 hash 비용이 들 수 있었다.
두 명단에 모두 있는 row는 INTERSECT, 왼쪽에만 있는 row는 EXCEPT였다. EXCEPT는 방향이 있으므로 순서를 바꾸면 다른 초승달이 남았다. 각 query는 열 수와 호환 가능한 type을 맞춰야 했고, 최종 ORDER BY는 결합 결과 뒤에서 한 번 적용했다. output에 entry_type을 projection하지 않았다면 downstream이 OPENING과 DEPOSIT을 그 열로 구분할 수 없었다.

토요일에는 SQLD 기본 SQL 모의 두 회분을 각각 50분에 풀었다. 한 문항에서 3분이 지나도 길이 보이지 않으면 표시하고 first pass를 이어 갔다. 오답은 logical order, JOIN grain, NULL, subquery 네 상자 중 하나에 넣었다. 맞혔지만 근거가 없는 추측은 정답 더미가 아니라 lucky 더미로 옮겼다.
PDF의 CSV에서 score와 elapsed_min이 0이고 worksheet의 first_answer와 recheck 칸이 비어 있는 것은 실제 성적이 아니라 작성할 TEMPLATE였다. 빈 표를 열었다는 사실을 두 회 통과로 해석하지 않았다. 오답은 3–6행 최소 fixture로 재현하고 project schema SQL로 바꾸어 보아야 원인을 자기 말로 설명할 수 있었다.
마지막으로 니지카는 D1–D6의 learner artifact validator를 계산대에 붙였다. validator는 evidence 파일이 존재하는지, 40 bytes 이상인지, TODO 같은 placeholder가 없는지, 비공백 줄이 세 줄 이상인지, SHA-256을 계산할 수 있는지만 확인했다. 표시되는 LOCAL_ARTIFACT_VALIDATED는 그 좁은 구조 검사 결과였다.
그 검사는 SQL을 parse하거나 PostgreSQL에서 실행하지 않았고, 예상 행과 비교하거나 설명 의미를 채점하지도 않았다. 파일이 존재하고 process exit가 0이어도 SQL Green이나 사람 의미 검토가 끝났다는 뜻은 아니었다. 그래서 증거 옆에 HUMAN_SEMANTIC_REVIEW_REQUIRED를 남겼다.
카레가 모두 팔리고 주방 수레가 멈췄다. 벽에는 logical order, 2×3 fan-out, account 3, 거래 0건, 최근 7일, CASE 경계, UNION ALL, 50분 time box가 남았다. p541 L14부터의 D7 project runner와 48시간 누적 재풀이는 오늘 이야기 밖이라 문을 닫아 두었다.
손님이 “그럼 이번 주 SQL이 전부 Green인가요?”라고 물었다.
히토리는 고개를 저었다. “오늘 만든 건 미리보기예요. source audit PASS는 PDF와 source가 맞물리는지 확인한 것이고, 이번 감사에서 실제 실행한 test는 0개예요. 각 evidence의 내용과 실행 결과는 따로 확인해야 해요.”
키타가 덧붙였다. “대신 무엇을 확인했고 무엇을 아직 확인하지 않았는지는 한눈에 볼 수 있어요!”
료는 마지막 감자 조각을 집어 들었다. “쓴 순서와 움직이는 순서가 다르다는 것도.”
니지카는 바닥 화살표를 떼지 않았다. 내일 다른 주문이 와도 한 줄의 뜻, 경계, NULL, 증거의 범위를 먼저 묻게 하기 위해서였다.
이미지 출처와 이용 안내: 공식 장면컷은 TV 애니메이션 《봇치 더 록!》 공식 사이트 자료입니다. ©はまじあき/芳文社・アニプレックス. 이 문서는 개인 학습용 비공식 구성으로, 원작의 공식 설정과 별개의 학습 비유입니다. 개념도는 원문 감사 내용에 맞춰 새로 만든 교육용 SVG입니다.