STARRY PASS 코드 뒤풀이 · W16 Ver2 전체

16주차 코드 — SQL 논리 순서·JOIN·NULL·집계

고정 fixture와 W16 실행기의 실제 보장 범위를 읽고, 논리 순서·anti-join·집계 대사와 Q23/Q24의 명시적 가정을 초보자 눈높이로 확인합니다.

정본 PostgreSQL Compose runtime 1개정본 W16 파괴적 회귀 fixture 1개정본 W16 query invariant 3개정본 W16 project SQL 실행기 1개fixture 기반 학습용 SQL 예시 · 정본 답안 아님 2개항목마다 13단계연결 83줄번역 83줄
01

compose.yaml — PostgreSQL db service·healthcheck·disposable volume 선언

runtime/compose.yaml

정본 PostgreSQL Compose runtime · 정본 · W16-F01
18줄 연결18줄 번역5 chunks
01

STEP 01 / 13

오늘 이 코드에서 해결할 문제

무엇을 이해해야 하는지 질문부터 잡습니다.

오늘의 한 문장

W16 runner가 올릴 PostgreSQL service의 image·port·환경값·health·storage 계약을 source line에서 읽는다.

  1. 두 variable interpolation form은 unset과 empty를 어떻게 다르게 처리할까?
  2. pg_isready가 직접 증명하는 범위는 어디까지일까?
  3. tag와 digest는 왜 다른가?
  4. named volume은 runner cleanup에서 어떻게 사라질까?
  5. host port 5432 fallback이 곧 bind 성공일까?
이 파일에서 끝까지 다시 쓰는 값service=dbimage=postgres:17.10-alpinehost fallback=5432database=financial_coreuser=apphealth=2s/2s/30volume=financial-core-dbmount=/var/lib/postgresql/data
02

STEP 02 / 13

아주 짧게: 이 코드는 왜 필요할까?

웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.

STARRY가 db 상자에 문 번호·환경표·건강 검진표·저장 보관함을 붙인다.

PostgreSQL 연습 무대의 준비표

이 파일은 postgres 17.10-alpine db service와 port·database·user·password 환경값을 선언한다.

pg_isready healthcheck와 named volume을 더해 Compose가 만들 runtime model을 완성한다.

딱 여기까지만 준비표는 app 인증·schema·query Green이나 immutable image를 증명하지 않고 runner의 down -v에서는 보관함도 제거된다.

03

STEP 03 / 13

초등학생도 이해하는 설명

생활 비유와 실제 코드의 경계를 함께 확인합니다.

들여쓰기는 소속표

services 아래 db, db 아래 image·ports·environment가 들어간다.

코드 연결
1~16줄
비유
큰 서류철 안에 무대 카드와 세부 설정표를 차례로 끼운다.
비유의 끝
공백 수가 맞아도 Docker runtime 의미가 자동 검증되는 것은 아니다.

`:-`와 `:?`는 다른 문턱

port는 빈 변수도 default를 쓰고 password는 빈 변수면 error다.

코드 연결
5·9줄
비유
빈 문 번호는 5432를 대신 쓰지만 빈 열쇠표는 입장을 막는다.
비유의 끝
nonempty password가 강하거나 안전하게 보관됐다는 뜻은 아니다.

healthcheck는 좁은 신호

pg_isready exit로 server가 연결 요청을 받을 상태인지 본다.

코드 연결
10~14줄
비유
매표창구 불이 켜졌는지 보는 검사다.
비유의 끝
실제 app 표로 입장하거나 원하는 좌석이 준비됐는지는 확인하지 않는다.

named volume은 별도 저장 상자

container path와 project volume을 연결한다.

코드 연결
15~19줄
비유
무대 상자 밖의 창고를 안쪽 data 자리와 연결한다.
비유의 끝
이 workflow의 Compose owner는 down -v로 그 창고를 제거한다.
services opener의 범위히토리 → 니지카 → 료 → 키타
  1. 히토리

    1줄을 읽자마자 db container가 생긴 건가요?

  2. 니지카

    services 표지만 열렸고 아직 어떤 service 이름도 나오지 않았어.

  3. YAML opener의 downstream state는 nested key를 받을 scope까지다.

  4. 키타

    1줄 설명에서 db·image 값을 빼고 현재 scope만 확인하겠습니다.

두 interpolation form히토리 → 니지카 → 료 → 키타
  1. 히토리

    port와 password 모두 변수가 비면 default가 들어가죠?

  2. 니지카

    5줄의 `:-`만 fallback이고 9줄의 `:?`는 message와 함께 멈춰.

  3. unset뿐 아니라 empty도 두 form의 branch 조건에 포함된다.

  4. 키타

    빈 문자열을 넣은 config 결과를 두 줄별로 대조할게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

감사 규칙상 연결 대상인 원본 18줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.정본 PostgreSQL Compose runtime에서 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.18 / 18 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F01-L01 services: STARRY가 여러 무대를 적을 운영표의 최상단 표지를 펼친다. Compose 문서에서 top-level `services` mapping을 연다.
입력
YAML parser가 들여쓰기 없는 `services:` key를 읽는다.
결과·효과
하위 service 이름을 받을 mapping scope가 활성화된다.
비유의 한계
이 opener만으로 구체적인 service나 container는 하나도 선언되지 않는다.
2줄F01-L02 db: 운영표 안에 데이터베이스 무대용 `db` 칸을 새로 만든다. services 아래 `db` service의 설정 mapping을 시작한다.
입력
열린 services scope가 두 칸 들여쓴 `db:` key를 입력받는다.
결과·효과
db라는 service node가 생겨 자신의 설정 항목을 기다린다.
비유의 한계
현재 줄에는 image·port·credential·health 조건이 아직 없다.
3줄F01-L03 image: postgres:17.10-alpine db 무대에 PostgreSQL 17.10 alpine 포장 상자를 올린다. db service가 사용할 image tag를 `postgres:17.10-alpine`으로 지정한다.
입력
Compose model의 db node에 image 문자열이 전달된다.
결과·효과
engine이 service 생성 때 해당 tag의 image를 resolve할 정보가 채워진다.
비유의 한계
tag는 immutable digest가 아니므로 나중에도 같은 bytes라고 보장하지 않는다.
4줄F01-L04 ports: 외부 입구 번호표를 달기 위한 빈 목록 걸이를 설치한다. db service의 port publication list를 여는 `ports` key다.
입력
YAML parser가 db mapping 안의 `ports:` collection opener를 받는다.
결과·효과
다음 list item을 port binding으로 수집할 상태가 된다.
비유의 한계
아직 host·container port 숫자나 binding 성공 여부는 정해지지 않았다.
5줄F01-L05 - "${FCL_DB_PORT:-5432}:5432" 밖의 문은 사용자가 고르되 빈 표찰이면 5432, 안쪽 문은 항상 5432로 단다. host port는 FCL_DB_PORT의 unset·empty fallback 5432를 쓰고 container port 5432에 publish한다.
입력
Compose interpolation이 `${FCL_DB_PORT:-5432}`와 literal `5432`를 평가한다.
결과·효과
config가 유효하면 하나의 host-to-container TCP port binding이 model에 기록된다.
비유의 한계
선택된 host port가 이미 사용 중인지, 외부 공개가 안전한지는 이 문자열이 확인하지 않는다.
6줄F01-L06 environment: PostgreSQL 상자 안에 넣을 환경표의 빈 봉투를 연다. db container environment mapping의 시작을 선언한다.
입력
parser가 db service 아래 `environment:` key와 새 nested mapping을 읽는다.
결과·효과
이후 환경변수 이름과 값을 받을 scope가 준비된다.
비유의 한계
현재 줄만으로 어떤 변수도 설정되지 않고 secret 보호 방식도 생기지 않는다.
7줄F01-L07 POSTGRES_DB: financial_core 첫 환경표에 새로 만들 기본 database 이름을 `financial_core`로 적는다. PostgreSQL image의 POSTGRES_DB 값을 financial_core로 설정한다.
입력
container environment model에 database-name pair가 추가된다.
결과·효과
초기화가 필요한 새 data directory라면 image entrypoint가 이 이름의 DB를 만들 입력을 얻는다.
비유의 한계
기존 volume 재사용 때 이미 있는 database를 변경하거나 schema migration을 수행하지 않는다.
8줄F01-L08 POSTGRES_USER: app 두 번째 환경표에는 database 주 사용자 이름 `app`을 적어 둔다. POSTGRES_USER environment value를 app으로 지정한다.
입력
Compose가 db container에 넘길 user-name pair를 구성한다.
결과·효과
새 PostgreSQL cluster 초기화 시 app role을 만들 수 있는 entrypoint 입력이 마련된다.
비유의 한계
application이 이 role로 실제 인증되는지와 최소 권한 여부는 보이지 않는다.
9줄F01-L09 POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal} 비밀번호 표가 비었으면 무대를 열지 않고 정확한 준비 문구를 보여 준다. FCL_DB_PASSWORD가 unset 또는 empty이면 지정 message로 interpolation을 실패시키고 아니면 그 값을 전달한다.
입력
Compose variable expansion이 `${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}`을 평가한다.
결과·효과
nonempty 값일 때만 POSTGRES_PASSWORD pair가 완성되어 config 처리를 계속할 수 있다.
비유의 한계
whitespace-only도 nonempty라 direct Compose interpolation은 통과하며 값의 강도·노출·회전·실제 DB 인증은 검사하지 않는다.
10줄F01-L10 healthcheck: db 무대에 반복 건강 검진표를 붙일 빈 칸을 연다. service healthcheck configuration mapping을 시작한다.
입력
YAML parser가 db node 아래 `healthcheck:` opener를 입력받는다.
결과·효과
probe command와 주기·timeout·retry 조건을 받을 health scope가 생긴다.
비유의 한계
현재 줄에는 검사 명령이나 healthy 판정 기준이 아직 없다.
11줄F01-L11 test: ["CMD-SHELL", "pg_isready -U app -d financial_core"] 검진표가 shell에게 app·financial_core 주소로 접수창이 열렸는지 물어본다. CMD-SHELL 방식으로 `pg_isready -U app -d financial_core`를 health probe로 실행한다.
입력
container health subsystem이 배열의 command type과 shell text를 받는다.
결과·효과
각 probe 때 pg_isready process exit가 Docker health 상태 판정에 제공된다.
비유의 한계
연결 요청 수락 여부일 뿐 supplied password 인증·schema·migration·query 결과를 검증하지 않는다.
12줄F01-L12 interval: 2s 건강 질문을 한 번 한 뒤 다음 질문까지 2초 간격을 둔다. healthcheck의 반복 interval을 2초로 설정한다.
입력
Docker health scheduler가 duration `2s`를 읽는다.
결과·효과
probe 완료 뒤 다음 scheduled probe까지 사용할 interval 값이 저장된다.
비유의 한계
전체 startup 시간이나 정확히 2초마다 wall-clock 실행된다는 보장은 아니다.
13줄F01-L13 timeout: 2s 한 번의 건강 답변을 기다리는 모래시계도 2초에서 멈춘다. 각 health probe의 timeout 한도를 2초로 둔다.
입력
health runner가 단일 command 실행에 적용할 duration을 입력받는다.
결과·효과
probe가 이 한도를 넘으면 그 시도가 실패로 처리될 수 있다.
비유의 한계
network·server recovery 전체에 주는 timeout이나 SQL statement timeout은 아니다.
14줄F01-L14 retries: 30 검진 실패 도장이 연속 30번 쌓이면 unhealthy 표찰을 붙이게 한다. healthcheck retry threshold를 30으로 지정한다.
입력
Docker가 consecutive failure 판정에 쓸 정수 30을 받는다.
결과·효과
startup 이후 실패가 threshold에 도달하면 service health가 unhealthy로 바뀔 수 있다.
비유의 한계
총 probe 횟수·절대 60초 제한·성공 후 영구 건강을 뜻하지 않는다.
15줄F01-L15 volumes: db 상자에 붙일 저장 공간 목록용 고리를 연다. db service의 volume mount list를 시작한다.
입력
parser가 service-level `volumes:` collection opener를 읽는다.
결과·효과
다음 list item을 mount specification으로 받을 상태가 된다.
비유의 한계
아직 volume 이름·container path·read/write mode는 제시되지 않았다.
16줄F01-L16 - financial-core-db:/var/lib/postgresql/data 이름 있는 보관함을 PostgreSQL data directory 자리에 연결한다. named volume `financial-core-db`를 `/var/lib/postgresql/data`에 mount한다.
입력
Compose model이 source volume name과 target container path를 한 pair로 받는다.
결과·효과
container lifecycle과 분리된 storage mount가 db service definition에 추가된다.
비유의 한계
backup·encryption·external lifecycle은 없고 owner runner의 `down -v`면 이 volume은 제거된다.
18줄F01-L18 volumes: service 표 바깥에 이름 있는 보관함 목록의 최상단 표지를 다시 연다. top-level named volumes mapping을 시작한다.
입력
YAML parser가 들여쓰기 없는 `volumes:` key를 document root에서 읽는다.
결과·효과
service mount가 참조할 volume definition들을 받을 root scope가 준비된다.
비유의 한계
이 opener만으로 특정 volume이나 driver 설정은 생성되지 않는다.
19줄F01-L19 financial-core-db: 마지막으로 `financial-core-db` 보관함 이름을 기본 설정으로 등록한다. top-level named volume `financial-core-db`를 빈 option mapping으로 선언한다.
입력
Compose project model이 volume identifier와 default configuration을 입력받는다.
결과·효과
service mount source와 연결 가능한 project-scoped volume definition이 완성된다.
비유의 한계
external volume이 아니며 project name과 `down -v` 동작에 따라 실제 이름·수명이 달라진다.
healthy의 좁은 뜻히토리 → 니지카 → 료 → 키타
  1. 히토리

    pg_isready가 성공하면 W16 query도 통과했다고 써도 되나요?

  2. 니지카

    창구가 연결을 받을 수 있다는 신호와 답안 SQL 결과는 다른 표야.

  3. credential·schema·marker predicate는 healthcheck source에 없다.

  4. 키타

    11줄에는 server acceptance만 남기고 별도 psql gate를 적겠습니다.

05

STEP 05 / 13

원본 코드 조각

원본을 5개 의미 조각으로 나누어 그대로 확인합니다.

파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.

F01-C01 · database service and image1–3줄
1–3줄 원본
services:
  db:
    image: postgres:17.10-alpine
F01-C02 · host port mapping4–5줄
4–5줄 원본
    ports:
      - "${FCL_DB_PORT:-5432}:5432"
F01-C03 · database initialization environment6–9줄
6–9줄 원본
    environment:
      POSTGRES_DB: financial_core
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}
F01-C04 · readiness probe10–14줄
10–14줄 원본
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]
      interval: 2s
      timeout: 2s
      retries: 30
F01-C05 · named data volume15–19줄
15–19줄 원본
    volumes:
      - financial-core-db:/var/lib/postgresql/data

volumes:
  financial-core-db:
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

비어 있지 않은 18줄을 모두 한국어로 옮깁니다.

전체 번역 18 / 18

비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.

원본한국어 번역
1services:Compose 문서에서 top-level `services` mapping을 연다.
2 db:services 아래 `db` service의 설정 mapping을 시작한다.
3 image: postgres:17.10-alpinedb service가 사용할 image tag를 `postgres:17.10-alpine`으로 지정한다.
4 ports:db service의 port publication list를 여는 `ports` key다.
5 - "${FCL_DB_PORT:-5432}:5432"host port는 FCL_DB_PORT의 unset·empty fallback 5432를 쓰고 container port 5432에 publish한다.
6 environment:db container environment mapping의 시작을 선언한다.
7 POSTGRES_DB: financial_corePostgreSQL image의 POSTGRES_DB 값을 financial_core로 설정한다.
8 POSTGRES_USER: appPOSTGRES_USER environment value를 app으로 지정한다.
9 POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}FCL_DB_PASSWORD가 unset 또는 empty이면 지정 message로 interpolation을 실패시키고 아니면 그 값을 전달한다.
10 healthcheck:service healthcheck configuration mapping을 시작한다.
11 test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]CMD-SHELL 방식으로 `pg_isready -U app -d financial_core`를 health probe로 실행한다.
12 interval: 2shealthcheck의 반복 interval을 2초로 설정한다.
13 timeout: 2s각 health probe의 timeout 한도를 2초로 둔다.
14 retries: 30healthcheck retry threshold를 30으로 지정한다.
15 volumes:db service의 volume mount list를 시작한다.
16 - financial-core-db:/var/lib/postgresql/datanamed volume `financial-core-db`를 `/var/lib/postgresql/data`에 mount한다.
18volumes:top-level named volumes mapping을 시작한다.
19 financial-core-db:top-level named volume `financial-core-db`를 빈 option mapping으로 선언한다.
07

STEP 07 / 13

기존 수준의 한 줄 읽기·문법 해부

쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.

한 줄로 읽기

Compose YAML은 runtime model 입력이며 image provenance·application readiness·data durability의 완전한 proof가 아니다.

문법 해부

  • YAML indentation은 mapping과 sequence nesting을 정한다.
  • `${VAR:-x}`는 unset·empty에 x를 쓰고 `${VAR:?m}`은 unset·empty에서 error다.
  • direct Compose의 `:?`는 whitespace-only를 empty로 보지 않지만 runner의 IsNullOrWhiteSpace pre-processing은 이를 disposable 값으로 바꾼다.
  • CMD-SHELL healthcheck는 container 안 shell command의 exit를 사용한다.
  • service mount source는 top-level named volume definition을 참조한다.

실행 순서

  1. Compose가 YAML과 variable interpolation을 해석한다.
  2. engine이 image와 service configuration을 resolve한다.
  3. container가 새 data directory를 초기화할 수 있다.
  4. health scheduler가 pg_isready를 반복한다.
  5. runner가 owned Compose project를 내릴 때 down -v를 호출한다.

원래 W6 수준의 조각별 정밀 해설

F01-C01 · database service and image
문법 해부
top-level services mapping 안에 db service node를 열고 image tag를 postgres:17.10-alpine으로 지정한다.
실제 값 추적
Compose model에는 db라는 service와 pull·create 때 resolve할 한 image tag 문자열이 기록된다.
정상 예
YAML 들여쓰기를 유지한 채 `services → db → image` 구조를 config parser에 주는 것은 이 범위의 정상 입력이다.
틀린 예·반례
db를 services와 같은 깊이에 두거나 image 값을 mapping으로 바꾸면 의도한 service model을 만들지 못한다.
착각 방지
17.10-alpine은 사람이 읽는 tag이지 content-addressed digest가 아니므로 같은 bytes 영구 고정으로 읽지 않는다.
하지 않는 일
host port·database credential·health·storage와 image provenance 검증은 이 세 줄의 책임이 아니다.
다음 연결
다음 ‘host port mapping’ 범위가 외부 host port와 container 5432를 연결한다.
F01-C02 · host port mapping
문법 해부
ports sequence에 `${FCL_DB_PORT:-5432}:5432` 한 항목을 넣어 host 측 interpolation과 container port를 한 문자열로 선언한다.
실제 값 추적
FCL_DB_PORT가 unset 또는 empty면 host 5432를 택하고 nonempty 값이면 그것을 써 container 5432로 publish할 model이 생긴다.
정상 예
FCL_DB_PORT=55432라면 resolved binding text는 `55432:5432`가 되는 것이 이 form의 유효 예다.
틀린 예·반례
whitespace-only 값을 empty fallback으로 간주하거나 host 5432가 사용 중이어도 config 문자열만으로 bind 성공을 선언하면 틀리다.
착각 방지
Compose `:-`는 unset과 empty에는 fallback하지만 whitespace-only는 nonempty로 취급한다.
하지 않는 일
실제 socket 점유·firewall·외부 노출 안전성·runtime publish 성공은 port 선언만으로 증명되지 않는다.
다음 연결
다음 ‘database initialization environment’ 범위가 database·user·password 초기화 입력을 선언한다.
F01-C03 · database initialization environment
문법 해부
environment mapping에 POSTGRES_DB=financial_core, POSTGRES_USER=app, POSTGRES_PASSWORD=`${FCL_DB_PASSWORD:?...}`를 둔다.
실제 값 추적
password variable이 nonempty면 세 environment pair가 완성되고 unset 또는 empty면 지정 message를 내며 Compose interpolation이 실패한다.
정상 예
FCL_DB_PASSWORD에 `lab-secret`을 넣으면 financial_core/app/lab-secret 세 값이 container environment model로 전달된다.
틀린 예·반례
값을 empty로 둔 채 container가 임의 password를 만들 것이라 기대하거나 whitespace-only가 `:?`에서 거부된다고 단정하면 맞지 않는다.
착각 방지
Compose `:?`는 unset·empty를 error로 보지만 whitespace-only는 nonempty라 직접 interpolation을 통과시킨다.
하지 않는 일
secret strength·노출·rotation, 기존 cluster 재초기화, application 실제 인증과 schema migration은 여기서 확인하지 않는다.
다음 연결
다음 ‘readiness probe’ 범위가 pg_isready command와 2s/2s/30 health 조건을 붙인다.
F01-C04 · readiness probe
문법 해부
healthcheck mapping이 CMD-SHELL `pg_isready -U app -d financial_core`와 interval 2s, timeout 2s, retries 30을 선언한다.
실제 값 추적
Docker health scheduler는 각 probe exit를 최대 2초 기다리고 완료 사이 2초 interval과 연속 실패 threshold 30을 상태 판정에 사용한다.
정상 예
server가 app·financial_core 이름으로 연결 요청을 받을 상태여서 pg_isready가 성공하면 health success input이 생긴다.
틀린 예·반례
2s×30을 정확한 60초 startup deadline으로 계산하거나 healthy를 application query Green으로 바꾸면 과장이다.
착각 방지
pg_isready는 server acceptance 신호이며 supplied password 인증·table 존재·migration·업무 SELECT 결과를 실행하지 않는다.
하지 않는 일
probe scheduling 지연, 전체 recovery deadline, production SLO와 credential correctness는 이 범위가 책임지지 않는다.
다음 연결
다음 ‘named data volume’ 범위가 PostgreSQL data path와 project volume definition을 연결한다.
F01-C05 · named data volume
문법 해부
service-level mount `financial-core-db:/var/lib/postgresql/data`와 top-level empty volume definition을 같은 이름으로 결속한다.
실제 값 추적
Compose project model에는 db data directory를 project-scoped named volume에 연결할 mount와 default driver definition이 남는다.
정상 예
동일 project에서 volume을 만든 뒤 db container를 재생성할 때 같은 named store를 mount하는 것은 선언 모양과 맞는다.
틀린 예·반례
빈 option mapping을 external·backup-enabled volume으로 해석하거나 named라는 이유만으로 영구 보존을 보장하면 틀리다.
착각 방지
service mount와 document-root volume declaration은 역할이 다르며 둘 다 있어야 이 이름의 project resource를 명시한다.
하지 않는 일
삭제 시점·backup·encryption·복구·다른 project와의 공유 여부는 이 YAML 끝 범위에서 고정하지 않는다.
다음 연결
다음 dependency인 ‘fail-fast and destructive reset’은 disposable W16 schema 상태를 다시 만든다.
tag와 digest히토리 → 니지카 → 료 → 키타
  1. 히토리

    버전까지 적힌 tag면 image hash와 같지 않나요?

  2. 니지카

    사람이 읽는 tag와 content-addressed digest는 식별 방식이 달라.

  3. 현재 source에는 `@sha256:` digest가 없어서 immutable-byte claim은 금지다.

  4. 키타

    3줄 exact image 문자열과 미증명 provenance를 함께 기록하겠습니다.

08

STEP 08 / 13

실제 값 따라가기

같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.

순서들어온 값코드가 하는 일나온 값·상태경계
blank passwordFCL_DB_PORT unset, FCL_DB_PASSWORD emptyCompose interpolation을 평가한다.port는 5432를 고르지만 password `:?`에서 config가 실패한다.runner는 Compose mode에서 blank password를 disposable 값으로 먼저 바꿀 수 있다.
resolved serviceFCL_DB_PORT=55432, nonempty passworddb service model을 만든다.host 55432→container 5432, financial_core/app 환경값과 image tag가 잡힌다.host 55432가 사용 가능하거나 firewall policy가 적절한지는 아직 모른다.
health probecontainer가 실행 중이나 schema가 비어 있음pg_isready를 app·financial_core 이름으로 호출한다.server가 연결을 받을 상태면 healthy가 될 수 있다.실제 password 로그인과 W16 table/query는 검사하지 않는다.
owned teardownrunner가 만든 Compose project와 named volumerunner finally의 down -v가 실행된다.그 project의 container·network·volume이 제거 대상이 된다.Compose file 단독 실행이나 external backup의 lifecycle까지 말하지 않는다.
volume이 사라지는 때히토리 → 니지카 → 료 → 키타
  1. 히토리

    named volume이면 연습 결과가 계속 남겠네요?

  2. 니지카

    container와 분리돼도 owner가 down -v를 부르면 volume도 지워져.

  3. lifecycle claim은 compose 선언과 runner finally를 함께 읽어야 한다.

  4. 키타

    16·19줄 mount와 runner cleanup 경계를 원문에서 다시 찾겠습니다.

09

STEP 09 / 13

PowerShell·SQL·DB 내부에서 벌어지는 일

PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.

Compose parser

YAML nesting과 environment interpolation을 runtime model로 변환한다.

engine이 image를 pull하거나 port를 bind하기 전 단계다.
Docker engine

image tag를 resolve하고 port·environment·mount configuration으로 container를 만든다.

tag bytes와 host resource availability는 실행 시점에 달라질 수 있다.
health subsystem

interval·timeout·retries 조건으로 pg_isready command exit를 누적한다.

application-level correctness와 credential 검증은 범위 밖이다.
volume driver

project-scoped named volume을 PostgreSQL data path에 mount한다.

backup·encryption·retention이나 down -v 뒤 복구를 제공하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

그럴듯하지만 틀린 해석을 반례로 고칩니다.

❌ 17.10-alpine tag면 image bytes가 영구 고정된다.

왜 틀리나 tag는 digest 문자열이 아니다.

바르게 읽기 supply-chain 고정이 필요하면 audited digest와 provenance를 사용한다.

반례 registry의 같은 tag가 다른 manifest를 가리킬 수 있다.

❌ pg_isready가 healthy면 app query도 Green이다.

왜 틀리나 probe는 연결 요청 수락 상태를 좁게 본다.

바르게 읽기 credential login·migration·sentinel query를 별도 실행한다.

반례 server는 ready지만 password가 틀리거나 w16_project가 없을 수 있다.

❌ `${FCL_DB_PORT:-5432}`이면 5432 bind가 항상 성공한다.

왜 틀리나 interpolation fallback과 host socket availability는 다른 검사다.

바르게 읽기 compose up native exit와 실제 port conflict를 관찰한다.

반례 다른 process가 host 5432를 이미 점유할 수 있다.

❌ password `:?`가 안전한 secret을 자동 만든다.

왜 틀리나 이 form은 nonempty 여부만 요구한다.

바르게 읽기 secret storage·strength·rotation·log exposure를 따로 관리한다.

반례 값 ` `는 direct Compose interpolation을 통과하지만 runner pre-processing에서는 whitespace로 판정돼 disposable 값으로 대체된다.

❌ named volume이라 W16 실행 뒤에도 data가 보존된다.

왜 틀리나 runner의 Compose finally가 down -v를 호출한다.

바르게 읽기 이 lab volume은 disposable이라고 표시하고 필요한 증거만 밖에 보존한다.

반례 정상 실행 종료 뒤 project volume이 제거된다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

이 코드가 책임지지 않는 일을 분리합니다.

image provenance

같은 tag가 항상 같은 image bytes임

이 책임을 맡는 곳: digest pinning, signed provenance, registry policy
application readiness

app credential·schema·sentinel query가 성공함

이 책임을 맡는 곳: authenticated psql/application smoke and migration gate
network safety

fallback host port가 free이고 공개 정책이 안전함

이 책임을 맡는 곳: host runtime, firewall, port ownership checks
secret management

password strength·confidentiality·rotation

이 책임을 맡는 곳: secret manager and operational credential policy
durability

runner 종료 뒤 database volume 보존·복구

이 책임을 맡는 곳: explicit backup/restore outside disposable Compose project
12

STEP 12 / 13

직접 다시 써보기

뜻 → 조각 → 전체 코드 순서로 다시 씁니다.

1단계 · 뜻부터 복원

각 YAML key 옆에 parse-time·container-time·health-time 중 어느 단계인지 표시한다.

2단계 · 코드 조각 재조립

  1. `:-` → unset/empty fallback
  2. `:?` → unset/empty error
  3. pg_isready → server acceptance only
  4. named volume → down -v 제거 가능

3단계 · 파일 전체 다시 쓰기

빈 파일에서 같은 nesting을 다시 쓰고 `docker compose config` 결과와 source를 대조한다.

자가 점검
  • service-level volumes와 top-level volumes를 구분했는가?
  • health와 app Green을 분리했는가?
  • tag·digest, nonempty·secure password를 혼동하지 않았는가?
  • runner cleanup과 단독 Compose lifecycle을 구분했는가?
13

STEP 13 / 13

전체 원본 정답

감사로 고정한 전체 source를 가감 없이 확인합니다.

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourceruntime/compose.yamlSHA-256 3ef6ca229dfa8d54120fb66f2215b906ba8155784d32791d7f93f3be879ade8d
compose.yaml — PostgreSQL db service·healthcheck·disposable volume 선언 전체
services:
  db:
    image: postgres:17.10-alpine
    ports:
      - "${FCL_DB_PORT:-5432}:5432"
    environment:
      POSTGRES_DB: financial_core
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]
      interval: 2s
      timeout: 2s
      retries: 30
    volumes:
      - financial-core-db:/var/lib/postgresql/data

volumes:
  financial-core-db:
02

fixture.sql — w16_project reset과 세 계좌·세 원장 row

sql/w16/fixture.sql

정본 W16 파괴적 회귀 fixture · 정본 · W16-F02
7줄 연결7줄 번역5 chunks
01

STEP 01 / 13

오늘 이 코드에서 해결할 문제

무엇을 이해해야 하는지 질문부터 잡습니다.

오늘의 한 문장

Q1·Q2·Q3가 공유하는 schema reset, table 최소 모양, account·ledger exact fixture를 읽는다.

  1. 기존 w16_project가 있으면 어떤 범위가 지워질까?
  2. account_id FK가 NULL까지 막을까?
  3. 세 account의 positive·zero 경계는 무엇일까?
  4. 계좌별 signed 합과 global 합은 얼마일까?
  5. runner manifest가 fixture SHA도 기록할까?
이 파일에서 끝까지 다시 쓰는 값DROP SCHEMA w16_project CASCADEaccounts=(1,100)/(2,50)/(3,0)ledger=(1,1,100)/(2,2,70)/(3,2,-20)account sums=100/50/0global signed sum=150
02

STEP 02 / 13

아주 짧게: 이 코드는 왜 필요할까?

웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.

STARRY가 낡은 W16 연습장을 지운 뒤 두 표와 여섯 고정 row를 다시 놓는다.

같은 답을 내기 위한 작은 연습장 초기화

fixture는 기존 w16_project를 CASCADE로 제거하고 account·ledger_entry를 최소 column으로 다시 만든다.

세 계좌와 세 원장행이 Q1의 positive top, Q2의 zero-ledger, Q3의 signed 합 경계를 함께 만든다.

딱 여기까지만 이 reset은 supplied database도 파괴하며 fixture SHA는 runner manifest에 없고 운영 schema·회계 완전성을 나타내지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

생활 비유와 실제 코드의 경계를 함께 확인합니다.

CASCADE는 연결된 것도 지움

schema가 있으면 그 안 dependent object까지 제거한다.

코드 연결
2줄
비유
연습장 표지를 떼는 것이 아니라 안의 표까지 통째로 버린다.
비유의 끝
같은 이름의 보호할 data를 자동 구분하지 않는다.

최소 account table

id와 balance만 있고 balance는 null만 금지한다.

코드 연결
4줄
비유
번호와 숫자 두 칸짜리 연습표다.
비유의 끝
음수·통화·고객·상태 규칙은 없다.

FK와 NOT NULL은 별개

account_id는 값이 있으면 parent를 찾아야 하지만 null 자체는 허용될 수 있다.

코드 연결
5줄
비유
끈을 달았다면 실제 표에 연결해야 하지만 끈을 안 다는 선택은 남아 있다.
비유의 끝
업무상 ledger가 항상 account를 가져야 한다는 invariant는 표현되지 않았다.

세 query를 위한 한 fixture

positive 둘, ledger 없는 하나, signed 합 일치 셋을 동시에 만든다.

코드 연결
6~7줄
비유
한 세트의 표본으로 세 종류 저울을 시험한다.
비유의 끝
작은 고정 표본을 arbitrary data proof로 넓히면 안 된다.
어느 database를 지우나히토리 → 니지카 → 료 → 키타
  1. 히토리

    Container mode면 기존 schema를 보존하겠죠?

  2. 니지카

    mode와 무관하게 같은 fixture 2줄이 w16_project를 CASCADE로 지워.

  3. cleanup 차이는 끝날 때이고 destructive reset은 시작에 공통이다.

  4. 키타

    2줄 target과 supplied Container 경고를 함께 적겠습니다.

FK가 null을 막나히토리 → 니지카 → 료 → 키타
  1. 히토리

    account_id가 REFERENCES라서 빈 값은 못 들어가죠?

  2. 니지카

    부모 id가 있는지 보는 끈과 반드시 끈을 달라는 규칙은 달라.

  3. 5줄 account_id clause에는 NOT NULL이 없다.

  4. 키타

    정확한 token을 보고 존재 O·필수 X를 확인할게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

감사 규칙상 연결 대상인 원본 7줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.정본 W16 파괴적 회귀 fixture에서 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.7 / 7 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F02-L01 \set ON_ERROR_STOP on STARRY가 SQL 묶음 중 한 장이 실패하면 뒤 장을 넘기지 말라는 빨간 표지를 세운다. psql variable ON_ERROR_STOP을 on으로 설정한다.
입력
psql client가 backslash meta-command와 boolean-like value를 읽는다.
결과·효과
이후 SQL error가 발생하면 남은 input 처리를 멈추는 client 정책이 활성화된다.
비유의 한계
여러 statement를 하나의 transaction으로 묶거나 이미 commit된 변경을 rollback하지 않는다.
2줄F02-L02 DO $ddl$ BEGIN IF EXISTS(SELECT 1 FROM pg_namespace WHERE nspname='w16_project') THEN EXECUTE 'DROP SCHEMA w16_project CASCADE'; END IF; END $ddl$; 기존 W16 연습장이 있으면 안의 물건까지 함께 철거하는 조건부 지우개를 실행한다. pg_namespace에 w16_project가 있으면 dynamic SQL로 `DROP SCHEMA w16_project CASCADE`를 실행하는 DO block이다.
입력
PostgreSQL이 현재 catalog의 schema name 존재 여부와 anonymous block을 입력받는다.
결과·효과
schema가 있으면 dependent objects까지 제거되고 없으면 drop 없이 block이 끝난다.
비유의 한계
Compose·Container를 구분하지 않는 파괴적 reset이며 이름이 겹친 비실습 object도 보호하지 않는다.
3줄F02-L03 CREATE SCHEMA w16_project; 철거가 끝난 자리에 `w16_project`라는 새 빈 연습 구역을 만든다. w16_project schema를 생성한다.
입력
database catalog가 CREATE SCHEMA statement와 identifier를 받는다.
결과·효과
성공하면 뒤 qualified table name을 담을 빈 namespace가 생긴다.
비유의 한계
owner·privilege·cleanup policy를 명시하지 않고 기존 schema가 남아 있으면 실패할 수 있다.
4줄F02-L04 CREATE TABLE w16_project.account(id bigint PRIMARY KEY,balance bigint NOT NULL); 연습 구역에 고유 번호와 필수 잔액만 적는 아주 작은 계좌표를 만든다. account table을 id BIGINT primary key와 balance BIGINT not-null 두 column으로 생성한다.
입력
PostgreSQL parser가 한 줄짜리 qualified CREATE TABLE definition을 읽는다.
결과·효과
w16_project.account relation과 id PK·balance nullability constraint가 catalog에 기록된다.
비유의 한계
balance 음수 금지·통화·상태·고객 소유권·운영 account schema는 포함하지 않는다.
5줄F02-L05 CREATE TABLE w16_project.ledger_entry(id bigint PRIMARY KEY,account_id bigint REFERENCES w16_project.account(id),signed_amount bigint NOT NULL,created_at timestamptz NOT NULL); 원장표에는 고유 번호, 선택 가능한 계좌 끈, 필수 signed 값과 시각 칸을 단다. ledger_entry를 만들고 account_id FK, signed_amount·created_at NOT NULL, id PK를 선언한다.
입력
parser가 네 column과 w16_project.account(id) reference를 하나의 table definition으로 받는다.
결과·효과
성공하면 ledger relation과 parent-existence FK·필수 값 규칙이 활성화된다.
비유의 한계
account_id에는 NOT NULL이 없어 NULL 참조는 가능하며 금액 부호·대사·event type은 제한하지 않는다.
6줄F02-L06 INSERT INTO w16_project.account VALUES (1,100),(2,50),(3,0); 계좌표에 1번 100, 2번 50, 3번 0이라는 세 고정 표본을 차례로 놓는다. account에 `(1,100)`, `(2,50)`, `(3,0)` 세 positional row를 insert한다.
입력
INSERT가 table column order에 맞춘 세 tuple을 입력받는다.
결과·효과
성공 후 exact account fixture는 세 행이고 id 1·2만 positive balance를 가진다.
비유의 한계
column list가 없어 schema order 변경에 취약하고 이 세 값은 production 분포가 아니다.
7줄F02-L07 INSERT INTO w16_project.ledger_entry VALUES (1,1,100,'2026-01-01Z'),(2,2,70,'2026-01-01Z'),(3,2,-20,'2026-01-02Z'); 원장표에는 1번 계좌 +100 한 장, 2번 계좌 +70·-20 두 장을 시간표와 함께 놓는다. ledger_entry에 id 1~3, account 1/2/2, signed 100/70/-20, 두 UTC timestamp를 insert한다.
입력
positional INSERT가 FK 대상 account와 signed amount·created_at tuple 세 개를 받는다.
결과·효과
계좌별 signed 합은 100·50이고 계좌 3은 0건이며 global sum은 150인 fixture가 된다.
비유의 한계
원장 completeness·거래 상태·double entry·잔액 갱신 과정과 fixture source SHA 실행은 이 row들만으로 증명되지 않는다.
세 계좌의 역할히토리 → 니지카 → 료 → 키타
  1. 히토리

    account 3의 balance가 0이라 ledger도 0건이라고 자동인가요?

  2. 니지카

    balance row와 child row 수는 따로 심고 따로 관찰해야 해.

  3. 6줄은 zero balance, 7줄의 account_id 목록 부재가 no-ledger fixture를 만든다.

  4. 키타

    두 줄을 분리해 account3 경계를 검산하겠습니다.

05

STEP 05 / 13

원본 코드 조각

원본을 5개 의미 조각으로 나누어 그대로 확인합니다.

파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.

F02-C01 · fail-fast and destructive reset1–2줄
1–2줄 원본
\set ON_ERROR_STOP on
DO $ddl$ BEGIN IF EXISTS(SELECT 1 FROM pg_namespace WHERE nspname='w16_project') THEN EXECUTE 'DROP SCHEMA w16_project CASCADE'; END IF; END $ddl$;
F02-C02 · schema recreation3–3줄
3–3줄 원본
CREATE SCHEMA w16_project;
F02-C03 · account and ledger tables4–5줄
4–5줄 원본
CREATE TABLE w16_project.account(id bigint PRIMARY KEY,balance bigint NOT NULL);
CREATE TABLE w16_project.ledger_entry(id bigint PRIMARY KEY,account_id bigint REFERENCES w16_project.account(id),signed_amount bigint NOT NULL,created_at timestamptz NOT NULL);
F02-C04 · three accounts6–6줄
6–6줄 원본
INSERT INTO w16_project.account VALUES (1,100),(2,50),(3,0);
F02-C05 · three ledger rows7–7줄
7–7줄 원본
INSERT INTO w16_project.ledger_entry VALUES (1,1,100,'2026-01-01Z'),(2,2,70,'2026-01-01Z'),(3,2,-20,'2026-01-02Z');
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

비어 있지 않은 7줄을 모두 한국어로 옮깁니다.

전체 번역 7 / 7

비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.

원본한국어 번역
1\set ON_ERROR_STOP onpsql variable ON_ERROR_STOP을 on으로 설정한다.
2DO $ddl$ BEGIN IF EXISTS(SELECT 1 FROM pg_namespace WHERE nspname='w16_project') THEN EXECUTE 'DROP SCHEMA w16_project CASCADE'; END IF; END $ddl$;pg_namespace에 w16_project가 있으면 dynamic SQL로 `DROP SCHEMA w16_project CASCADE`를 실행하는 DO block이다.
3CREATE SCHEMA w16_project;w16_project schema를 생성한다.
4CREATE TABLE w16_project.account(id bigint PRIMARY KEY,balance bigint NOT NULL);account table을 id BIGINT primary key와 balance BIGINT not-null 두 column으로 생성한다.
5CREATE TABLE w16_project.ledger_entry(id bigint PRIMARY KEY,account_id bigint REFERENCES w16_project.account(id),signed_amount bigint NOT NULL,created_at timestamptz NOT NULL);ledger_entry를 만들고 account_id FK, signed_amount·created_at NOT NULL, id PK를 선언한다.
6INSERT INTO w16_project.account VALUES (1,100),(2,50),(3,0);account에 `(1,100)`, `(2,50)`, `(3,0)` 세 positional row를 insert한다.
7INSERT INTO w16_project.ledger_entry VALUES (1,1,100,'2026-01-01Z'),(2,2,70,'2026-01-01Z'),(3,2,-20,'2026-01-02Z');ledger_entry에 id 1~3, account 1/2/2, signed 100/70/-20, 두 UTC timestamp를 insert한다.
07

STEP 07 / 13

기존 수준의 한 줄 읽기·문법 해부

쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.

한 줄로 읽기

fixture.sql은 destructive reset 뒤 fixed oracle rows를 구성하지만 manifest provenance와 production invariant를 완성하지 않는다.

문법 해부

  • DO block 안 IF EXISTS는 catalog query 결과에 따라 dynamic DDL을 실행한다.
  • qualified name은 search_path와 무관하게 w16_project object를 지정한다.
  • inline REFERENCES는 nullable column에 자동 NOT NULL을 더하지 않는다.
  • column list 없는 INSERT는 physical column order에 의존한다.

실행 순서

  1. psql error-stop 정책을 켠다.
  2. 기존 w16_project가 있으면 CASCADE drop한다.
  3. 새 schema와 account·ledger_entry를 만든다.
  4. account 세 행을 먼저 넣어 FK parent를 준비한다.
  5. ledger 세 행을 넣어 Q1~Q3 oracle을 완성한다.

원래 W6 수준의 조각별 정밀 해설

F02-C01 · fail-fast and destructive reset
문법 해부
psql ON_ERROR_STOP을 켠 뒤 pg_namespace에서 w16_project 존재를 검사해 있을 때만 dynamic DROP SCHEMA ... CASCADE를 실행하는 DO block이다.
실제 값 추적
schema가 있으면 그 namespace와 dependent objects가 제거되고, 없으면 drop 없이 anonymous block이 끝난다.
정상 예
격리된 lab database에서 기존 disposable w16_project를 지우고 새 fixture를 준비하는 사용은 이 파괴 범위와 맞는다.
틀린 예·반례
caller 소유 production database에서 같은 schema name을 쓰면서 보존될 것이라 기대하면 dependent data까지 잃을 수 있다.
착각 방지
ON_ERROR_STOP은 뒤 error에서 input을 멈추는 client 설정이지 이 DROP을 자동 rollback하는 transaction 선언이 아니다.
하지 않는 일
schema ownership 승인·backup·복구·mode별 안전 격리는 두 줄이 제공하지 않는다.
다음 연결
다음 ‘schema recreation’ 범위가 제거된 이름으로 빈 w16_project namespace를 만든다.
F02-C02 · schema recreation
문법 해부
CREATE SCHEMA statement 하나가 w16_project namespace를 database catalog에 추가한다.
실제 값 추적
앞 reset 뒤 이름이 비어 있으면 이후 qualified object를 담을 새 schema가 생성된다.
정상 예
w16_project가 없는 disposable database에서 statement가 성공하는 것이 현재 범위의 정상 결과다.
틀린 예·반례
reset 없이 같은 schema가 이미 존재하는 상태에서 재실행하면 duplicate schema error가 날 수 있다.
착각 방지
schema 생성은 table·row·privilege·owner policy를 함께 만들어 주지 않는다.
하지 않는 일
authorization, search_path 변경, cleanup lifecycle과 fixture completeness는 이 단일 DDL 밖의 책임이다.
다음 연결
다음 ‘account and ledger tables’ 범위가 두 relation과 PK·FK·nullability를 선언한다.
F02-C03 · account and ledger tables
문법 해부
account는 bigint id PK와 NOT NULL balance를, ledger_entry는 bigint id PK·account FK·NOT NULL signed_amount·created_at을 선언한다.
실제 값 추적
catalog에는 두 table과 두 primary key, account를 향하는 nullable account_id FK, 두 필수 ledger value column이 생긴다.
정상 예
존재하는 account id를 참조하고 signed_amount·created_at을 채운 ledger row는 이 DDL 제약 모양에 맞는다.
틀린 예·반례
account_id를 생략한 NULL ledger row가 FK 때문에 반드시 거부된다고 말하면 틀리며 이 column에는 NOT NULL이 없다.
착각 방지
REFERENCES는 non-NULL key의 대상 존재를 검사하지만 nullability·ON DELETE policy·금액 부호 규칙을 자동 추가하지 않는다.
하지 않는 일
balance와 signed sum 대사, double-entry, status, currency, index·성능과 생성 transaction 경계는 보장하지 않는다.
다음 연결
다음 ‘three accounts’ 범위가 id 1·2·3과 balance 100·50·0을 심는다.
F02-C04 · three accounts
문법 해부
한 INSERT VALUES가 account에 `(1,100)`, `(2,50)`, `(3,0)` 세 tuple을 넣는다.
실제 값 추적
성공 뒤 account relation의 fixture row 수는 3이고 저장 balance 합은 150이다.
정상 예
비어 있는 방금 만든 table에 세 distinct PK tuple을 한 번 넣는 것은 constraint를 만족한다.
틀린 예·반례
같은 fixture를 reset 없이 다시 넣으면 id 1부터 primary-key conflict가 날 수 있다.
착각 방지
세 숫자는 고정 lab input일 뿐 원장 계산에서 자동 파생되거나 production balance 불변식이 아니다.
하지 않는 일
customer ownership·currency·status·동시 update와 balance 변경 이력은 이 INSERT에 없다.
다음 연결
다음 ‘three ledger rows’ 범위가 account 1·2에 signed entries를 넣어 exact 합계를 만든다.
F02-C05 · three ledger rows
문법 해부
ledger_entry INSERT가 id 1의 account1 +100, id 2·3의 account2 +70/-20과 두 UTC created_at을 기록한다.
실제 값 추적
account 1 signed sum은 100, account 2는 50, account 3은 0 rows이며 global signed sum은 150이 된다.
정상 예
앞 table·account rows가 존재할 때 세 distinct ledger PK와 valid FK를 넣는 것은 현재 fixture의 정상 seed다.
틀린 예·반례
account 3에 0 금액 entry가 있다고 읽거나 account 2의 두 row를 한 row로 합쳐 물리 row 수를 2라 하면 틀리다.
착각 방지
+70과 -20은 두 별도 event이고 no-ledger account 3의 zero-row 상태와 signed sum 0을 구분해야 한다.
하지 않는 일
entry business meaning·status·double-entry pair·원장 completeness와 timezone 업무일 정책은 이 작은 seed가 증명하지 않는다.
다음 연결
다음 ‘psql fail-fast’ 범위부터 Q1 logical-order invariant를 실행한다.
global 합의 함정히토리 → 니지카 → 료 → 키타
  1. 히토리

    ledger_sum 150만 맞으면 대사 완료 아닌가요?

  2. 니지카

    계좌 사이 +10·-10 오류는 전체 합에서 서로 사라질 수 있어.

  3. Q3의 account-grain HAVING과 marker의 global sum은 별도 predicate다.

  4. 키타

    100·50·0과 150을 각각 계산해 둘게요.

08

STEP 08 / 13

실제 값 따라가기

같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.

순서들어온 값코드가 하는 일나온 값·상태경계
existing schema reset이미 table이 있는 w16_projectpg_namespace 존재 branch에서 DROP SCHEMA CASCADE를 실행한다.schema와 dependent objects가 제거되어 같은 이름을 새로 만들 수 있다.Container mode에서도 동일하며 runner 성공 후 새 schema가 남는다.
minimal relations빈 database namespaceschema·account·ledger_entry CREATE를 순서대로 실행한다.두 relation과 PK·FK·NOT NULL 최소 규칙이 생긴다.account_id nullable, balance sign, ledger business semantics는 열려 있다.
positive and zero accounts(1,100),(2,50),(3,0)positional account INSERT를 수행한다.positive balance 두 행과 zero balance 한 행이 고정된다.zero balance와 ledger 0건은 다음 INSERT까지 같은 뜻이 아니다.
ledger sumsaccount1:+100, account2:+70/-20세 ledger tuple을 저장한다.계좌별 합 100·50, account3 no-row=coalesce 0, global 150이 된다.fixture SHA는 learner file 세 개의 result hash 목록에 포함되지 않는다.
fixture SHA 빈자리히토리 → 니지카 → 료 → 키타
  1. 히토리

    runner가 canonical fixture를 읽으니 hash도 자동 기록되겠죠?

  2. 니지카

    읽는 경로와 results loop에 넣는 경로는 같은 말이 아니야.

  3. manifest hashes=3은 logical·anti·aggregation learner files에만 대응한다.

  4. 키타

    fixture path와 세 result SHA source를 원문에서 대조하겠습니다.

09

STEP 09 / 13

PowerShell·SQL·DB 내부에서 벌어지는 일

PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.

psql

meta-command를 처리하고 뒤 SQL을 같은 connection에 순서대로 보낸다.

explicit BEGIN이 없어 파일 전체 atomic transaction은 아니다.
PostgreSQL catalog

namespace existence를 보고 schema drop/create와 relation metadata를 갱신한다.

DROP CASCADE 대상의 업무 가치를 판단하지 않는다.
constraint engine

PK·FK·NOT NULL을 각 INSERT에 적용한다.

nullable FK·negative balance·accounting invariant는 표현된 predicate만큼만 본다.
runner provenance

runner가 packaged fixture를 별도로 읽어 psql에 pipe한다.

그 bytes를 manifest hashes=3에 넣거나 allowlist와 비교하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

그럴듯하지만 틀린 해석을 반례로 고칩니다.

❌ w16_project reset은 새 Compose database에서만 일어난다.

왜 틀리나 runner의 두 mode가 같은 fixture를 실행한다.

바르게 읽기 supplied Container도 disposable이어야 한다고 명시한다.

반례 Container mode 대상에 기존 w16_project가 있으면 CASCADE drop된다.

❌ balance NOT NULL이면 음수도 막힌다.

왜 틀리나 nullability와 numeric predicate는 다른 constraint다.

바르게 읽기 필요하면 CHECK(balance>=0)를 별도로 선언한다.

반례 -1은 null이 아니므로 현재 account definition의 그 문턱을 통과한다.

❌ REFERENCES가 account_id를 필수로 만든다.

왜 틀리나 해당 column에 NOT NULL token이 없다.

바르게 읽기 존재 FK와 값 필수성 constraint를 따로 읽는다.

반례 NULL account_id는 ordinary FK comparison을 요구하지 않을 수 있다.

❌ global sum 150이면 계좌별 balance도 전부 맞다.

왜 틀리나 global conservation은 분배 오류를 상쇄할 수 있다.

바르게 읽기 account_id grain으로 group해 각 balance와 비교한다.

반례 account1 90·account2 60도 global 150이지만 저장 100·50과 다르다.

❌ runner hashes=3이 fixture까지 포함한다.

왜 틀리나 results loop는 learner query files만 돈다.

바르게 읽기 fixture source hash와 expected allowlist를 별도 manifest field로 결박한다.

반례 fixture bytes가 변해도 learner file SHA 세 개는 그대로일 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

이 코드가 책임지지 않는 일을 분리합니다.

destructive scope

기존 w16_project가 지워도 되는 lab data뿐임

이 책임을 맡는 곳: disposable target selection and preflight ownership guard
schema completeness

운영 account·ledger constraints와 동일함

이 책임을 맡는 곳: canonical migration and schema contract tests
row provenance

executed fixture bytes가 manifest SHA에 결박됨

이 책임을 맡는 곳: fixture hash allowlist and execution manifest
transactionality

drop·create·insert 전체가 하나의 rollback 단위임

이 책임을 맡는 곳: explicit transaction design and failure replay test
business validity

signed rows가 상태·double-entry·authorization 정책을 만족함

이 책임을 맡는 곳: domain schema, service transaction, reconciliation evidence
12

STEP 12 / 13

직접 다시 써보기

뜻 → 조각 → 전체 코드 순서로 다시 씁니다.

1단계 · 뜻부터 복원

각 statement 옆에 destructive DDL·constructive DDL·parent seed·child seed를 표시한다.

2단계 · 코드 조각 재조립

  1. account1 → balance100 / sum100
  2. account2 → balance50 / 70-20=50
  3. account3 → balance0 / ledger 0건
  4. global signed sum → 150

3단계 · 파일 전체 다시 쓰기

빈 disposable schema에서 같은 fixture를 다시 쓰고 account grain과 global grain 결과를 따로 계산한다.

자가 점검
  • DROP CASCADE가 두 mode에 적용됨을 표시했는가?
  • account_id FK와 NOT NULL을 혼동하지 않았는가?
  • 계좌별 합·global 합·zero-row 경계를 구분했는가?
  • fixture가 manifest hash에서 빠짐을 적었는가?
13

STEP 13 / 13

전체 원본 정답

감사로 고정한 전체 source를 가감 없이 확인합니다.

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcesql/w16/fixture.sqlSHA-256 00c614526736e1c452e61af0af58c656096aaae2efd0f594dfdd55e4bfe2e529
fixture.sql — w16_project reset과 세 계좌·세 원장 row 전체
\set ON_ERROR_STOP on
DO $ddl$ BEGIN IF EXISTS(SELECT 1 FROM pg_namespace WHERE nspname='w16_project') THEN EXECUTE 'DROP SCHEMA w16_project CASCADE'; END IF; END $ddl$;
CREATE SCHEMA w16_project;
CREATE TABLE w16_project.account(id bigint PRIMARY KEY,balance bigint NOT NULL);
CREATE TABLE w16_project.ledger_entry(id bigint PRIMARY KEY,account_id bigint REFERENCES w16_project.account(id),signed_amount bigint NOT NULL,created_at timestamptz NOT NULL);
INSERT INTO w16_project.account VALUES (1,100),(2,50),(3,0);
INSERT INTO w16_project.ledger_entry VALUES (1,1,100,'2026-01-01Z'),(2,2,70,'2026-01-01Z'),(3,2,-20,'2026-01-02Z');
03

logical-order.sql — positive top-two count와 conditional Q1 marker

sql/w16/logical-order.sql

정본 W16 query invariant · 정본 · W16-F03
3줄 연결3줄 번역3 chunks
01

STEP 01 / 13

오늘 이 코드에서 해결할 문제

무엇을 이해해야 하는지 질문부터 잡습니다.

오늘의 한 문장

Q1 DO guard와 marker SELECT가 어떤 fixed predicate를 각각 보고 runner가 왜 둘 다 필요한지 읽는다.

  1. LIMIT 뒤 count 2는 무엇을 증명할까?
  2. DO query와 marker query의 WHERE는 같을까?
  3. top tie는 어떤 secondary key로 깨질까?
  4. conditional SELECT 0행은 psql error일까?
  5. 논리 처리 순서와 optimizer physical plan은 같은가?
이 파일에서 끝까지 다시 쓰는 값positive rows after LIMIT=2order=balance DESC,idtop_id=1marker=W16_Q1 rows=2 top_id=1DO WHERE presentmarker WHERE absent
02

STEP 02 / 13

아주 짧게: 이 코드는 왜 필요할까?

웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.

STARRY가 계좌표를 정렬해 먼저 개수 경보를 확인하고 다음 표에서 top id를 확인한다.

두 장을 세는 검사와 맨 위 번호를 보는 표찰

DO block은 positive account를 balance 내림차순·id 오름차순으로 두 장만 남긴 뒤 count 2를 요구한다.

marker SELECT는 전체 account의 top id가 1일 때만 W16_Q1 한 행을 반환한다.

딱 여기까지만 두 query의 WHERE가 다르고 fixed fixture oracle일 뿐 실제 optimizer operator 순서·모든 top row·marker suffix 신뢰를 증명하지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

생활 비유와 실제 코드의 경계를 함께 확인합니다.

안쪽 query를 먼저 작은 표로 보기

WHERE·ORDER BY·LIMIT 결과를 x라는 derived table로 만든 뒤 바깥 count가 센다.

코드 연결
2줄
비유
큰 카드 더미에서 조건에 맞는 두 장을 뽑아 작은 쟁반에 놓고 센다.
비유의 끝
count는 쟁반 안 카드의 순서와 id 값을 보여 주지 않는다.

LIMIT 뒤 count

positive row가 둘 이상이면 count는 항상 2가 될 수 있다.

코드 연결
2줄
비유
입구에서 두 명까지만 들인 뒤 방 안 인원을 세는 것과 같다.
비유의 끝
밖에 세 번째 사람이 있었는지는 알 수 없다.

secondary order

balance가 같으면 작은 id가 먼저 온다.

코드 연결
2~3줄
비유
점수가 같을 때 번호가 작은 표를 앞에 둔다.
비유의 끝
id uniqueness는 fixture table PK에 의존한다.

WHERE가 false인 SELECT

조건이 거짓이면 marker가 0행이지만 SQL error는 아닐 수 있다.

코드 연결
3줄
비유
합격표를 안 찍었지만 프린터가 고장 난 것은 아니다.
비유의 끝
runner가 marker 부재를 별도 실패로 바꿔야 한다.
count가 잃는 정보히토리 → 니지카 → 료 → 키타
  1. 히토리

    count=2면 top 계좌가 1번과 2번임도 확인됐죠?

  2. 니지카

    count는 쟁반에 두 장 있다는 것만 남기고 이름과 순서는 버려.

  3. 2줄 terminal observable에는 id projection assertion이 없다.

  4. 키타

    count proof와 top-id proof를 서로 다른 칸에 적겠습니다.

LIMIT의 위치히토리 → 니지카 → 료 → 키타
  1. 히토리

    positive account가 정확히 두 개라는 검사인가요?

  2. 니지카

    먼저 두 장까지만 자르고 세기 때문에 셋 이상이어도 count는 2야.

  3. 이 predicate의 lower condition은 positive rows at least two에 가깝다.

  4. 키타

    id3=200 mutation으로 2줄 결과를 다시 계산할게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

감사 규칙상 연결 대상인 원본 3줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.정본 W16 query invariant에서 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.3 / 3 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F03-L01 \set ON_ERROR_STOP on STARRY가 Q1 검사 중 SQL error가 나면 다음 장으로 넘어가지 않는 정지 표지를 켠다. psql에서 ON_ERROR_STOP을 on으로 설정한다.
입력
client가 첫 meta-command를 처리해 error-handling variable을 갱신한다.
결과·효과
뒤 DO 또는 SELECT가 SQL error를 내면 이 파일 input 처리가 nonzero로 중단될 수 있다.
비유의 한계
조건이 false라 SELECT가 0행인 상황은 SQL error가 아니며 transaction atomicity도 만들지 않는다.
2줄F03-L02 DO $q$ BEGIN IF (SELECT count(*) FROM (SELECT id FROM w16_project.account WHERE balance>0 ORDER BY balance DESC,id LIMIT 2)x)<>2 THEN RAISE EXCEPTION 'W16 Q1 rows'; END IF; END $q$; 양수 잔액표를 큰 값·작은 id 순으로 두 장만 집어 실제 두 장인지 세고 아니면 경보를 울린다. balance>0 account를 `balance DESC,id`로 정렬해 LIMIT 2한 derived table의 count가 2인지 DO block에서 검사한다.
입력
fixed w16_project.account rows와 nested SELECT·count comparison이 anonymous block에 들어온다.
결과·효과
positive row가 둘 이상이면 count 2로 block이 끝나고 둘 미만이면 `W16 Q1 rows` exception이 발생한다.
비유의 한계
LIMIT 뒤 count라 정확한 top id·출력 순서·전체 positive row 수 또는 optimizer의 physical 순서를 증명하지 않는다.
3줄F03-L03 SELECT 'W16_Q1 rows=2 top_id=1' WHERE (SELECT id FROM w16_project.account ORDER BY balance DESC,id LIMIT 1)=1; 모든 계좌를 같은 저울에 세워 맨 위 id가 1일 때만 Q1 합격 표찰 한 줄을 내보낸다. 전체 account의 top id가 1이면 literal `W16_Q1 rows=2 top_id=1`을 반환하는 conditional SELECT다.
입력
scalar subquery가 `ORDER BY balance DESC,id LIMIT 1`로 한 id를 만들고 equality 1을 평가한다.
결과·효과
fixture top이 id 1이면 marker 1행이 출력되고 아니면 정상 exit 가능한 0행이 된다.
비유의 한계
앞 DO의 balance>0 predicate를 반복하지 않으며 marker suffix 진위를 runner가 별도 parse하지 않는다.
서로 다른 WHERE히토리 → 니지카 → 료 → 키타
  1. 히토리

    3줄은 2줄 결과를 그대로 marker로 출력하나요?

  2. 니지카

    3줄 scalar query는 balance>0을 쓰지 않고 전체 account에서 top을 골라.

  3. fixed fixture coincidence를 query equivalence로 일반화하면 안 된다.

  4. 키타

    두 줄의 WHERE token을 나란히 대조하겠습니다.

05

STEP 05 / 13

원본 코드 조각

원본을 3개 의미 조각으로 나누어 그대로 확인합니다.

파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.

F03-C01 · psql fail-fast1–1줄
1–1줄 원본
\set ON_ERROR_STOP on
F03-C02 · positive top-two invariant2–2줄
2–2줄 원본
DO $q$ BEGIN IF (SELECT count(*) FROM (SELECT id FROM w16_project.account WHERE balance>0 ORDER BY balance DESC,id LIMIT 2)x)<>2 THEN RAISE EXCEPTION 'W16 Q1 rows'; END IF; END $q$;
F03-C03 · top-id marker3–3줄
3–3줄 원본
SELECT 'W16_Q1 rows=2 top_id=1' WHERE (SELECT id FROM w16_project.account ORDER BY balance DESC,id LIMIT 1)=1;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

비어 있지 않은 3줄을 모두 한국어로 옮깁니다.

전체 번역 3 / 3

비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.

원본한국어 번역
1\set ON_ERROR_STOP onpsql에서 ON_ERROR_STOP을 on으로 설정한다.
2DO $q$ BEGIN IF (SELECT count(*) FROM (SELECT id FROM w16_project.account WHERE balance>0 ORDER BY balance DESC,id LIMIT 2)x)<>2 THEN RAISE EXCEPTION 'W16 Q1 rows'; END IF; END $q$;balance>0 account를 `balance DESC,id`로 정렬해 LIMIT 2한 derived table의 count가 2인지 DO block에서 검사한다.
3SELECT 'W16_Q1 rows=2 top_id=1' WHERE (SELECT id FROM w16_project.account ORDER BY balance DESC,id LIMIT 1)=1;전체 account의 top id가 1이면 literal `W16_Q1 rows=2 top_id=1`을 반환하는 conditional SELECT다.
07

STEP 07 / 13

기존 수준의 한 줄 읽기·문법 해부

쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.

한 줄로 읽기

이 파일은 fixed fixture의 nested count와 top-id marker를 나눠 관찰하며 logical-order 교재 제목보다 좁은 invariant만 직접 검사한다.

문법 해부

  • DO block은 result row를 반환하지 않고 predicate failure를 exception으로 바꾼다.
  • derived table x는 ORDER BY·LIMIT 적용 뒤 count input이 된다.
  • `ORDER BY balance DESC,id`의 두 번째 key는 기본 ASC다.
  • SELECT literal WHERE condition은 false에서 정상 0행이 가능하다.

실행 순서

  1. psql error-stop policy를 켠다.
  2. DO 안 positive account top-two derived table을 평가한다.
  3. 그 count가 2가 아니면 exception으로 중단한다.
  4. 전체 account top id가 1인지 별도 scalar subquery로 본다.
  5. 조건이 참일 때 marker text 한 행을 출력한다.

원래 W6 수준의 조각별 정밀 해설

F03-C01 · psql fail-fast
문법 해부
backslash meta-command가 현재 psql input의 ON_ERROR_STOP variable을 on으로 설정한다.
실제 값 추적
뒤 command error가 발생하면 psql이 남은 file 처리를 중단할 조건이 활성화된다.
정상 예
이 파일을 psql로 읽을 때 첫 줄이 처리되고 session variable이 on인 상태는 유효한 시작이다.
틀린 예·반례
이 설정만 보고 file 전체가 explicit transaction block 안에서 실행되며 이전 change도 rollback된다고 결론 내리면 안 된다.
착각 방지
fail-fast와 transaction atomicity는 서로 다른 제어이며 meta-command는 database transaction을 열지 않는다.
하지 않는 일
뒤 query predicate·expected rows·marker authenticity나 schema cleanup은 이 한 줄이 책임지지 않는다.
다음 연결
다음 ‘positive top-two invariant’ 범위가 양수 balance row를 정렬·LIMIT하고 exact count를 검사한다.
F03-C02 · positive top-two invariant
문법 해부
DO block 안 subquery가 balance>0 account를 balance DESC,id 순으로 LIMIT 2하고 바깥 count가 2가 아니면 exception을 낸다.
실제 값 추적
fixture에서 id1 balance100과 id2 balance50만 양수라 ordered inner set은 두 row이고 count comparison은 통과한다.
정상 예
세 account fixture를 그대로 둔 실행에서 anonymous block이 W16 Q1 rows exception 없이 끝나는 것이 valid oracle이다.
틀린 예·반례
balance가 양수인 row를 하나로 줄이거나 LIMIT을 한 건으로 바꾸면 count<>2가 되어 이 gate는 실패한다.
착각 방지
ORDER BY는 inner row 선택을 안정화하지만 count 2만으로 첫 id 값이나 모든 account predicate를 인증하지 않는다.
하지 않는 일
production top-N correctness·index plan·tie business policy·뒤 출력 문자열은 이 block이 보장하지 않는다.
다음 연결
다음 ‘top-id marker’ 범위는 WHERE 없는 별도 top-one query로 id 1일 때만 W16_Q1 row를 출력한다.
F03-C03 · top-id marker
문법 해부
conditional SELECT가 전체 account를 balance DESC,id 순으로 LIMIT 1한 scalar id가 1일 때 literal W16_Q1 rows=2 top_id=1을 반환한다.
실제 값 추적
fixture의 100·50·0 정렬에서는 top id가 1이라 marker 한 row가 output에 나타난다.
정상 예
id1 balance100이 최고인 frozen state에서 exact literal이 한 번 보이는 것은 이 statement의 valid 결과다.
틀린 예·반례
id2 balance를 101로 바꾸면 predicate가 false여서 0 rows가 될 수 있지만 그것만으로 psql syntax error가 생기지는 않는다.
착각 방지
앞 block은 balance>0 predicate를 썼지만 이 scalar query에는 WHERE가 없어 두 row-set 계약이 일반적으로 같지 않다.
하지 않는 일
marker text는 executable assertion이나 source hash 인증이 아니며 0-row output을 실패로 바꾸는 외부 gate가 필요하다.
다음 연결
다음 Q2 ‘psql fail-fast’ 범위가 no-ledger anti-join invariant file을 연다.
0행과 error히토리 → 니지카 → 료 → 키타
  1. 히토리

    top id가 다르면 ON_ERROR_STOP이 멈추겠죠?

  2. 니지카

    WHERE가 거짓인 SELECT는 정상적으로 0행을 반환할 수 있어.

  3. nonzero는 SQL error에서, semantic failure는 caller marker check에서 만들어진다.

  4. 키타

    psql exit와 runner prefix 조건을 따로 기록할게요.

08

STEP 08 / 13

실제 값 따라가기

같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.

순서들어온 값코드가 하는 일나온 값·상태경계
positive filterid1=100,id2=50,id3=0balance>0을 적용한다.id 1과 2 두 row만 derived table 후보가 된다.marker scalar query에는 이 filter가 없다.
top-two countpositive ids 1·2balance DESC,id로 정렬하고 LIMIT 2 뒤 count한다.count=2라 DO block이 exception 없이 끝난다.count는 두 id나 정렬 출력 자체를 assertion하지 않는다.
marker top전체 ids 1=100,2=50,3=0WHERE 없이 같은 order로 LIMIT 1한다.top id=1이라 marker 한 행이 반환된다.runner는 prefix만 찾아 suffix oracle을 독립 parse하지 않는다.
predicate divergence mutationid3 balance=200으로 변경DO와 marker를 다시 평가한다.positive가 셋이어도 LIMIT count는 2라 DO가 통과하지만 marker는 0행이 된다.runner가 marker 부재를 확인해야 전체 file gate가 실패한다.
논리 순서와 plan히토리 → 니지카 → 료 → 키타
  1. 히토리

    SELECT 논리 순서를 외우면 실제 실행 plan도 그 순서인가요?

  2. 니지카

    논리는 결과 의미를 설명하고 optimizer는 같은 결과의 physical 길을 고른다.

  3. source만으로 sort·scan operator order나 cost를 확정할 수 없다.

  4. 키타

    EXPLAIN이 필요한 claim과 현재 SQL semantics를 분리하겠습니다.

09

STEP 09 / 13

PowerShell·SQL·DB 내부에서 벌어지는 일

PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.

psql client

ON_ERROR_STOP과 server statement exit를 process status에 반영한다.

0-row SELECT를 error로 재분류하지 않는다.
PostgreSQL executor

filter·sort/top-N·limit·aggregate·scalar subquery semantics로 결과를 계산한다.

optimizer는 physical operator를 논리 설명과 다른 방식으로 배치할 수 있다.
PL/pgSQL DO

count comparison이 false면 정상 종료하고 true면 RAISE EXCEPTION한다.

top id와 marker string을 이 block이 직접 검사하지 않는다.
PowerShell runner

native exit 0과 output의 `W16_Q1 ` prefix를 함께 요구한다.

expected source SHA나 exact marker suffix를 allowlist로 비교하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

그럴듯하지만 틀린 해석을 반례로 고칩니다.

❌ count 2가 top id 1·2 순서를 증명한다.

왜 틀리나 count aggregate는 row identity와 order를 투영하지 않는다.

바르게 읽기 필요한 id·ordinal을 직접 SELECT하고 exact result를 assert한다.

반례 id 4·5가 top이어도 derived table count는 2다.

❌ DO와 marker는 똑같은 query를 반복한다.

왜 틀리나 2줄에는 balance>0이 있고 3줄에는 없다.

바르게 읽기 각 subquery의 FROM·WHERE·ORDER·LIMIT를 별도로 적는다.

반례 negative magnitude가 큰 row는 marker top 후보지만 DO filter에서는 빠질 수 있다.

❌ SQL 논리 순서대로 DB가 물리 실행한다.

왜 틀리나 optimizer는 동등한 결과를 위한 plan을 고른다.

바르게 읽기 logical semantics와 EXPLAIN의 physical plan을 구분한다.

반례 top-N sort나 index scan이 clause 교재 순서와 다른 operator 모양을 보일 수 있다.

❌ marker가 0행이면 psql이 nonzero다.

왜 틀리나 false WHERE는 정상 query 결과다.

바르게 읽기 caller가 expected marker 행 존재를 별도 검사한다.

반례 top id가 2여도 SELECT는 error 없이 빈 output을 낼 수 있다.

❌ marker 문자열 전체가 canonical oracle로 검증된다.

왜 틀리나 runner는 `W16_Q1 ` prefix 존재만 찾는다.

바르게 읽기 exact structured output 또는 canonical expected marker를 비교한다.

반례 같은 prefix 뒤 잘못된 suffix도 current runner match를 통과할 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

이 코드가 책임지지 않는 일을 분리합니다.

fixture scope

arbitrary account data에서도 positive rows와 top id가 같음

이 책임을 맡는 곳: broader fixtures, property tests, production query contract
predicate equivalence

DO와 marker subquery가 같은 row set을 읽음

이 책임을 맡는 곳: shared CTE/query definition or exact predicate audit
physical plan

FROM→WHERE→ORDER→LIMIT 순서의 physical operators

이 책임을 맡는 곳: EXPLAIN ANALYZE on a pinned PostgreSQL/workload
marker authenticity

output suffix와 source bytes가 canonical expected 값임

이 책임을 맡는 곳: exact output schema and SHA allowlist
transactionality

Q1과 다른 learner files가 하나의 atomic transaction임

이 책임을 맡는 곳: explicit transaction wrapper and failure cleanup test
12

STEP 12 / 13

직접 다시 써보기

뜻 → 조각 → 전체 코드 순서로 다시 씁니다.

1단계 · 뜻부터 복원

2줄과 3줄의 FROM·WHERE·ORDER BY·LIMIT·observable을 나란히 적는다.

2단계 · 코드 조각 재조립

  1. DO → positive filter / count after LIMIT
  2. marker → no filter / scalar top id
  3. false marker condition → 0 rows, not SQL error
  4. runner → native exit + prefix presence

3단계 · 파일 전체 다시 쓰기

id3 balance를 200으로 바꾼 반례를 손으로 계산해 DO 통과와 marker 부재를 재현한다.

자가 점검
  • LIMIT 전·후 count 의미를 구분했는가?
  • 두 subquery의 WHERE 차이를 적었는가?
  • logical semantics와 physical plan을 분리했는가?
  • 0-row SELECT와 nonzero error를 혼동하지 않았는가?
  • runner가 exact suffix를 검증하지 않음을 표시했는가?
13

STEP 13 / 13

전체 원본 정답

감사로 고정한 전체 source를 가감 없이 확인합니다.

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcesql/w16/logical-order.sqlSHA-256 bb4a98f81400ccd51a1f8d6fbb4baf96f9d8389693d95cd60cb563fc29679f98
logical-order.sql — positive top-two count와 conditional Q1 marker 전체
\set ON_ERROR_STOP on
DO $q$ BEGIN IF (SELECT count(*) FROM (SELECT id FROM w16_project.account WHERE balance>0 ORDER BY balance DESC,id LIMIT 2)x)<>2 THEN RAISE EXCEPTION 'W16 Q1 rows'; END IF; END $q$;
SELECT 'W16_Q1 rows=2 top_id=1' WHERE (SELECT id FROM w16_project.account ORDER BY balance DESC,id LIMIT 1)=1;
04

anti-join.sql - 대응 원장이 없는 계좌 한 건

sql/w16/anti-join.sql

정본 W16 query invariant · 정본 · W16-F04
3줄 연결3줄 번역3 chunks
01

STEP 01 / 13

오늘 이 코드에서 해결할 문제

무엇을 이해해야 하는지 질문부터 잡습니다.

오늘의 한 문장

고정 fixture에서 ledger_entry가 전혀 없는 account가 정확히 한 건이고 그 id가 3인지 NOT EXISTS와 marker로 확인한다.

  1. NOT EXISTS의 안쪽 SELECT 1은 무엇을 찾을까?
  2. count=1과 id=3은 왜 두 관찰인가?
  3. ledger 합이 0인 것과 ledger 행이 0건인 것은 같을까?
  4. ON_ERROR_STOP은 rollback을 뜻할까?
  5. runner가 marker suffix까지 인증할까?
이 파일에서 끝까지 다시 쓰는 값account rows=3ledger rows=3missing-ledger count=1account_id=3marker=W16_Q2 rows=1 account_id=3
02

STEP 02 / 13

아주 짧게: 이 코드는 왜 필요할까?

웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.

STARRY가 계좌 명단과 원장 명단을 나란히 놓고 대응 원장이 하나도 없는 계좌를 찾는다.

짝 없는 계좌 찾기

NOT EXISTS는 바깥 account마다 같은 account_id의 ledger가 한 행이라도 있는지 묻는다.

fixture에서는 account 3만 짝이 없어서 count 1과 id 3 marker가 차례로 성립한다.

딱 여기까지만 짝이 없다는 사실은 balance가 옳거나 모든 업무 거래가 기록됐다는 증명이 아니다.

03

STEP 03 / 13

초등학생도 이해하는 설명

생활 비유와 실제 코드의 경계를 함께 확인합니다.

바깥 한 행

account a 한 행을 잡고 안쪽 query를 실행한다.

코드 연결
2줄
비유
계좌 카드 한 장을 들고 원장 서랍을 찾아본다.
비유의 끝
DB가 실제로 매번 같은 방식으로 scan한다고 단정하지 않는다.

존재의 반대

같은 account_id ledger가 하나도 없을 때 NOT EXISTS가 참이다.

코드 연결
2~3줄
비유
이름표가 맞는 기록이 0장일 때만 빈 칸 도장을 찍는다.
비유의 끝
금액 0과 행 0건을 섞지 않는다.

두 문턱

DO는 개수 1을, SELECT는 유일 id 3을 본다.

코드 연결
2~3줄
비유
빈 칸 수와 그 칸 번호를 따로 적는다.
비유의 끝
runner는 marker text의 canonical suffix를 검증하지 않는다.
짝이 없다는 뜻히토리 → 니지카 → 료 → 키타
  1. 히토리

    account 3의 balance가 0이라서 잡힌 건가요?

  2. 니지카

    아니, 같은 account_id ledger 행이 하나도 없어서야.

  3. 값 0과 행 0건은 다른 predicate다.

  4. 키타

    account 2의 +70과 -20도 존재 행으로 세겠습니다.

count와 id히토리 → 니지카 → 료 → 키타
  1. 히토리

    한 건이라고 알면 3번도 알 수 있지 않나요?

  2. 니지카

    개수만으로 어느 id인지는 정할 수 없어.

  3. DO와 marker SELECT가 서로 다른 관찰을 맡는다.

  4. 키타

    fixture를 바꾼 반례로 두 문턱을 확인할게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

감사 규칙상 연결 대상인 원본 3줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.정본 W16 query invariant에서 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.3 / 3 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F04-L01 \set ON_ERROR_STOP on SQL 오류가 나면 남은 종이를 넘기지 않는 정지 표지를 세운다. psql의 ON_ERROR_STOP 변수를 on으로 설정하는 meta-command다.
입력
psql이 이 파일의 첫 물리 줄을 읽고 session 변수를 바꾼다.
결과·효과
뒤 SQL error가 psql의 nonzero 종료로 이어질 fail-fast 조건이 켜진다.
비유의 한계
transaction을 시작하거나 이미 반영된 statement를 자동 rollback하지는 않는다.
2줄F04-L02 DO $q$ BEGIN IF (SELECT count(*) FROM w16_project.account a WHERE NOT EXISTS(SELECT 1 FROM w16_project.ledger_entry l WHERE l.account_id=a.id))<>1 THEN RAISE EXCEPTION 'W16 Q2 rows'; END IF; END $q$; 세 계좌 명단에서 원장 짝이 하나도 없는 칸의 개수가 한 개인지 센다. anonymous DO block이 account별 NOT EXISTS를 세고 결과가 1이 아니면 W16 Q2 rows 예외를 낸다.
입력
fixture의 account 세 행과 account_id가 연결된 ledger_entry 세 행을 anti-join 조건에 넣는다.
결과·효과
account 3만 대응 ledger가 없어 count=1이 되고 이 block은 예외 없이 끝난다.
비유의 한계
이 count predicate는 누락 계좌의 id·balance·ledger 합을 반환하거나 검사하지 않는다.
3줄F04-L03 SELECT 'W16_Q2 rows=1 account_id=3' WHERE (SELECT id FROM w16_project.account a WHERE NOT EXISTS(SELECT 1 FROM w16_project.ledger_entry l WHERE l.account_id=a.id))=3; 비어 있던 한 칸의 번호가 3일 때만 Q2 합격표 한 줄을 내보낸다. 같은 NOT EXISTS scalar subquery가 id 3이면 W16_Q2 rows=1 account_id=3 marker를 SELECT한다.
입력
앞 block을 통과한 fixture state에서 ledger 없는 유일한 account id를 조회한다.
결과·효과
scalar 값이 3이므로 exact marker 한 행이 psql stdout에 나타난다.
비유의 한계
runner는 이 suffix 전체가 아니라 W16_Q2와 공백 prefix 존재만 검사해 marker 의미를 인증하지 않는다.
NOT IN과 NULL히토리 → 니지카 → 료 → 키타
  1. 히토리

    NOT IN으로 바꾸면 더 짧지 않나요?

  2. 니지카

    안쪽 값에 NULL이 있으면 UNKNOWN 함정이 생길 수 있어.

  3. 여기 FK 열은 schema상 nullable이라 일반화에 주의해야 한다.

  4. 키타

    source에서 nullable FK와 l.account_id=a.id correlation을 함께 확인하겠습니다.

05

STEP 05 / 13

원본 코드 조각

원본을 3개 의미 조각으로 나누어 그대로 확인합니다.

파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.

F04-C01 · psql fail-fast1–1줄
1–1줄 원본
\set ON_ERROR_STOP on
F04-C02 · exact anti-join count2–2줄
2–2줄 원본
DO $q$ BEGIN IF (SELECT count(*) FROM w16_project.account a WHERE NOT EXISTS(SELECT 1 FROM w16_project.ledger_entry l WHERE l.account_id=a.id))<>1 THEN RAISE EXCEPTION 'W16 Q2 rows'; END IF; END $q$;
F04-C03 · account-id marker3–3줄
3–3줄 원본
SELECT 'W16_Q2 rows=1 account_id=3' WHERE (SELECT id FROM w16_project.account a WHERE NOT EXISTS(SELECT 1 FROM w16_project.ledger_entry l WHERE l.account_id=a.id))=3;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

비어 있지 않은 3줄을 모두 한국어로 옮깁니다.

전체 번역 3 / 3

비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.

원본한국어 번역
1\set ON_ERROR_STOP onpsql이 SQL error 뒤에도 계속 읽지 않도록 ON_ERROR_STOP을 켠다.
2DO $q$ BEGIN IF (SELECT count(*) FROM w16_project.account a WHERE NOT EXISTS(SELECT 1 FROM w16_project.ledger_entry l WHERE l.account_id=a.id))<>1 THEN RAISE EXCEPTION 'W16 Q2 rows'; END IF; END $q$;ledger 대응 행이 없는 account 수가 정확히 1인지 확인하고 아니면 예외를 낸다.
3SELECT 'W16_Q2 rows=1 account_id=3' WHERE (SELECT id FROM w16_project.account a WHERE NOT EXISTS(SELECT 1 FROM w16_project.ledger_entry l WHERE l.account_id=a.id))=3;ledger가 없는 account id가 3일 때 W16_Q2 marker 한 행을 출력한다.
07

STEP 07 / 13

기존 수준의 한 줄 읽기·문법 해부

쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.

한 줄로 읽기

correlated NOT EXISTS로 absence set을 만든 뒤 cardinality와 exact id를 두 statement로 관찰한다.

문법 해부

  • 안쪽 l.account_id=a.id가 outer row와 subquery를 연결한다.
  • DO block의 IF count<>1은 잘못된 fixture를 exception으로 바꾼다.
  • 조건부 SELECT는 predicate가 false면 error가 아니라 0행이다.

실행 순서

  1. psql fail-fast on
  2. anti-join count
  3. count exception gate
  4. unique id scalar
  5. conditional marker row

원래 W6 수준의 조각별 정밀 해설

F04-C01 · psql fail-fast
문법 해부
psql client variable ON_ERROR_STOP을 on으로 바꾸는 한 meta-command로 anti-join file을 시작한다.
실제 값 추적
뒤 statement error가 발생하면 다음 input을 계속 읽지 않는 client 상태가 된다.
정상 예
psql이 backslash command를 인식하는 실행 경로에서 setting이 적용되는 것은 정상 전제다.
틀린 예·반례
일반 SQL engine에 이 줄을 그대로 보내면서 표준 SQL SET 문으로 동작할 것이라 기대하면 syntax가 맞지 않는다.
착각 방지
backslash command는 psql 전용이며 error-stop을 요청할 뿐 자동 transaction wrapper·rollback을 추가하지 않는다.
하지 않는 일
no-ledger row 수·식별자·output marker와 source authenticity는 이 시작 줄에서 검사되지 않는다.
다음 연결
다음 ‘exact anti-join count’ 범위가 correlated NOT EXISTS로 대응 원장 없는 account 수를 확인한다.
F04-C02 · exact anti-join count
문법 해부
DO block이 각 account a에 대해 같은 account_id의 ledger_entry l이 존재하지 않는 row를 고르고 count가 1이 아니면 Q2 exception을 낸다.
실제 값 추적
fixture의 account1·2는 대응 ledger가 있고 account3만 없어서 NOT EXISTS set cardinality가 정확히 1이다.
정상 예
세 account·세 ledger seed에서 block이 exception 없이 끝나는 것은 current anti-join count의 valid 결과다.
틀린 예·반례
account3 ledger를 하나 추가하면 missing set이 0이 되어 `<>1` branch가 exception을 발생시킨다.
착각 방지
EXISTS 안의 SELECT 1은 값 1인 ledger를 찾는 표현이 아니라 correlation WHERE를 만족하는 row 존재만 본다.
하지 않는 일
이 count는 유일 row의 id·balance·signed sum이나 NOT IN의 NULL 동작을 직접 증명하지 않는다.
다음 연결
다음 ‘account-id marker’ 범위가 같은 absence predicate의 scalar id가 3인지 별도로 본다.
F04-C03 · account-id marker
문법 해부
scalar anti-join query의 id가 3일 때 literal W16_Q2 rows=1 account_id=3을 선택하는 conditional SELECT다.
실제 값 추적
앞 fixture state에서는 ledger가 없는 유일 account id가 3이므로 marker row 한 건이 출력된다.
정상 예
missing-ledger set이 정확히 `{3}`인 실행은 scalar value와 literal 조건을 만족한다.
틀린 예·반례
유일 missing account가 id2라면 cardinality는 여전히 1이어도 이 SELECT는 0 rows가 된다.
착각 방지
count=1은 정체가 3임을 뜻하지 않아 cardinality와 exact-id 관찰을 분리해야 한다.
하지 않는 일
conditional marker는 ledger sum·balance reconciliation·canonical source bytes를 인증하지 않는다.
다음 연결
다음 Q3 ‘psql fail-fast’ 범위가 account별 aggregate reconciliation file을 연다.
중단과 rollback히토리 → 니지카 → 료 → 키타
  1. 히토리

    ON_ERROR_STOP이면 앞 SQL도 되돌아가나요?

  2. 니지카

    그건 남은 입력을 멈추는 설정이지 transaction 선언이 아니야.

  3. 별도 psql process와 autocommit 경계도 함께 봐야 한다.

  4. 키타

    중단·rollback·cleanup을 세 칸으로 나누겠습니다.

08

STEP 08 / 13

실제 값 따라가기

같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.

순서들어온 값코드가 하는 일나온 값·상태경계
1account id 1ledger account_id=1 존재를 찾는다.entry 1이 있어 NOT EXISTS=false다.signed_amount가 100인지 여부는 absence 판정에 쓰지 않는다.
2account id 2ledger account_id=2 존재를 찾는다.entry 2와 3이 있어 NOT EXISTS=false다.두 행의 합계 50은 Q3가 담당한다.
3account id 3대응 ledger를 검색한다.0행이라 NOT EXISTS=true이고 유일 id 3 marker가 나온다.새 ledger 한 행을 넣으면 exact oracle은 즉시 달라진다.
marker의 약한 인증히토리 → 니지카 → 료 → 키타
  1. 히토리

    W16_Q2가 출력되면 이 파일 hash도 맞나요?

  2. 니지카

    runner는 hash를 기록하지만 canonical 값과 비교하지 않아.

  3. prefix substring만으로 query 의미까지 인증할 수 없다.

  4. 키타

    Green 문구와 source authenticity를 분리해 적을게요.

09

STEP 09 / 13

PowerShell·SQL·DB 내부에서 벌어지는 일

PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.

correlated subquery

outer a.id를 안쪽 equality에 전달해 대응 행 존재를 판단한다.

optimizer가 anti join plan으로 바꿀 수 있어 문법 모양과 물리 plan은 같지 않다.
PL/pgSQL DO

count가 1이 아니면 exception으로 psql 실패를 만든다.

이 block은 결과 row를 반환하지 않는다.
scalar subquery

유일한 missing account id를 한 값으로 비교한다.

여러 행이면 scalar-subquery error지만 canonical DO가 먼저 exact one을 요구한다.
runner

native exit와 W16_Q2 prefix 포함 여부를 읽는다.

query source hash allowlist나 marker suffix equality는 없다.
10

STEP 10 / 13

흔한 착각과 틀린 예

그럴듯하지만 틀린 해석을 반례로 고칩니다.

❌ SELECT 1은 숫자 1인 ledger만 찾는다.

왜 틀리나 EXISTS 안의 projection 값은 존재 판정에 중요하지 않다.

바르게 읽기 WHERE correlation이 대응 행을 정하고 SELECT 1은 관용적 상수다.

반례 SELECT NULL로 바꿔도 같은 행 존재 집합이면 EXISTS 결과는 같다.

❌ ledger 합이 0이면 NOT EXISTS가 참이다.

왜 틀리나 합이 0인 여러 행도 존재하는 행이다.

바르게 읽기 행 존재와 signed sum을 Q2/Q3로 분리한다.

반례 +70과 -70 두 행은 합 0이지만 EXISTS는 참이다.

❌ DO count=1이면 id도 자동으로 3이다.

왜 틀리나 cardinality는 값 정체를 정하지 않는다.

바르게 읽기 다음 SELECT의 id=3 관찰을 별도 gate로 유지한다.

반례 ledger를 account 3에 넣고 account 2의 행을 지우면 count 1이지만 missing id는 2다.

❌ ON_ERROR_STOP이 전체 파일을 transaction으로 묶는다.

왜 틀리나 meta-command는 error 뒤 중단만 요청한다.

바르게 읽기 원자 rollback이 필요하면 명시적 BEGIN/ROLLBACK 정책을 둔다.

반례 첫 statement가 commit된 뒤 다음 statement가 실패할 수 있다.

❌ W16_Q2 문구가 있으면 canonical query가 실행됐다.

왜 틀리나 runner는 output substring prefix만 본다.

바르게 읽기 source hash allowlist와 exact result 계약이 없음을 공개한다.

반례 다른 SELECT가 같은 prefix를 출력해도 현재 runner 조건을 만족할 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

이 코드가 책임지지 않는 일을 분리합니다.

absence

balance와 ledger sum 일치

이 책임을 맡는 곳: aggregation.sql
fixture scope

production에 ledger-less account가 정확히 한 건

이 책임을 맡는 곳: production reconciliation and policy
source authenticity

canonical SQL bytes가 실행됨

이 책임을 맡는 곳: hash-pinned runner
physical plan

특정 anti join node나 latency

이 책임을 맡는 곳: EXPLAIN and benchmark
12

STEP 12 / 13

직접 다시 써보기

뜻 → 조각 → 전체 코드 순서로 다시 씁니다.

1단계 · 뜻부터 복원

account 1·2는 대응 ledger가 있고 account 3만 없어서 count 1과 id 3이 된다고 말한다.

2단계 · 코드 조각 재조립

  1. ON_ERROR_STOP
  2. NOT EXISTS correlation
  3. count<>1 exception
  4. id=3 marker

3단계 · 파일 전체 다시 쓰기

세 물리 줄을 정확히 다시 쓰고 SHA-256 1002e131...f62a와 대조한다.

자가 점검
  • SELECT 1을 ledger 값 비교로 오해하지 않는다.
  • 행 0건과 합 0을 분리한다.
  • count와 id gate를 각각 설명한다.
  • marker prefix 검사의 한계를 적는다.
13

STEP 13 / 13

전체 원본 정답

감사로 고정한 전체 source를 가감 없이 확인합니다.

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcesql/w16/anti-join.sqlSHA-256 1002e13115bdcfbe4f4e0c1b9ac4afb2e23e7ce6e469525b5d59696cdaadf62a
anti-join.sql - 대응 원장이 없는 계좌 한 건 전체
\set ON_ERROR_STOP on
DO $q$ BEGIN IF (SELECT count(*) FROM w16_project.account a WHERE NOT EXISTS(SELECT 1 FROM w16_project.ledger_entry l WHERE l.account_id=a.id))<>1 THEN RAISE EXCEPTION 'W16 Q2 rows'; END IF; END $q$;
SELECT 'W16_Q2 rows=1 account_id=3' WHERE (SELECT id FROM w16_project.account a WHERE NOT EXISTS(SELECT 1 FROM w16_project.ledger_entry l WHERE l.account_id=a.id))=3;
05

aggregation.sql - 계좌별 저장 잔액과 원장 합 대사

sql/w16/aggregation.sql

정본 W16 query invariant · 정본 · W16-F05
3줄 연결3줄 번역3 chunks
01

STEP 01 / 13

오늘 이 코드에서 해결할 문제

무엇을 이해해야 하는지 질문부터 잡습니다.

오늘의 한 문장

LEFT JOIN과 GROUP BY로 원장 0건 계좌도 보존해 account별 mismatch가 없는지 검사하고 global signed sum 150 marker를 별도로 출력한다.

  1. 왜 INNER JOIN 대신 LEFT JOIN일까?
  2. SUM(NULL)을 왜 COALESCE할까?
  3. account별 mismatch 0과 global 150은 같은 조건일까?
  4. signed sum 일치가 double-entry를 증명할까?
  5. marker는 앞 DO를 다시 실행할까?
이 파일에서 끝까지 다시 쓰는 값account1 balance/sum=100/100account2=50/(70-20)=50account3=0/COALESCE(NULL,0)=0mismatches=0global ledger_sum=150
02

STEP 02 / 13

아주 짧게: 이 코드는 왜 필요할까?

웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.

STARRY가 계좌 표의 저장 잔액과 원장 표의 signed 합계를 계좌별로 대조한다.

세 장부 칸 맞추기

LEFT JOIN은 원장 0건인 account 3도 결과에 남겨 NULL 합계를 0으로 바꾼다.

첫 문턱은 계좌별 불일치가 0인지 보고 둘째 출력은 전체 합계 150을 별도로 본다.

딱 여기까지만 숫자 합이 맞아도 거래 상태·원인·차변대변 구조가 올바르다는 뜻은 아니다.

03

STEP 03 / 13

초등학생도 이해하는 설명

생활 비유와 실제 코드의 경계를 함께 확인합니다.

계좌를 보존

account에서 시작해 ledger가 없어도 account 행을 남긴다.

코드 연결
2줄
비유
기록이 없는 계좌 카드도 대사 책상에서 치우지 않는다.
비유의 끝
LEFT JOIN이 자동으로 0을 만들지는 않는다.

빈 합을 0으로

SUM 결과 NULL을 COALESCE로 0으로 바꾼다.

코드 연결
2줄
비유
원장 종이가 없으면 합계 칸에 0을 적는다.
비유의 끝
모든 NULL을 업무상 0으로 바꿔도 된다는 일반 규칙은 아니다.

두 대사 축

per-account mismatch와 global sum marker를 나눈다.

코드 연결
2~3줄
비유
각 봉투 합과 전체 상자 합을 다른 검산표에서 본다.
비유의 끝
전체 합 하나로 계좌별 배분 오류를 찾을 수 없다.
왜 LEFT JOIN인가히토리 → 니지카 → 료 → 키타
  1. 히토리

    원장 없는 계좌는 빼도 되지 않나요?

  2. 니지카

    그러면 검사를 피한 행이 되어 0건 정책을 확인할 수 없어.

  3. parent grain을 보존한 뒤 nullable sum을 처리한다.

  4. 키타

    account 3이 실제로 남는지 표로 그릴게요.

NULL과 0히토리 → 니지카 → 료 → 키타
  1. 히토리

    SUM이 없으면 자동으로 0 아닌가요?

  2. 니지카

    빈 aggregate의 SUM은 NULL이라 COALESCE가 필요해.

  3. COUNT와 SUM의 empty-input 규칙을 분리해야 한다.

  4. 키타

    0건 fixture로 두 함수를 비교하겠습니다.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

감사 규칙상 연결 대상인 원본 3줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.정본 W16 query invariant에서 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.3 / 3 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F05-L01 \set ON_ERROR_STOP on 대사식이 깨지면 다음 줄로 숨지 못하게 정지 장치를 켠다. psql session에서 ON_ERROR_STOP을 활성화하는 meta-command다.
입력
aggregation file을 여는 psql이 첫 줄의 backslash command를 처리한다.
결과·효과
뒤 exception이나 SQL error가 native failure로 전달될 조건이 마련된다.
비유의 한계
명시적 transaction이나 실패 뒤 schema 복구를 제공하지 않는다.
2줄F05-L02 DO $q$ BEGIN IF EXISTS(SELECT a.id FROM w16_project.account a LEFT JOIN w16_project.ledger_entry l ON l.account_id=a.id GROUP BY a.id,a.balance HAVING a.balance<>coalesce(sum(l.signed_amount),0)) THEN RAISE EXCEPTION 'W16 Q3 mismatch'; END IF; END $q$; 각 계좌의 저장 잔액과 원장 합계를 나란히 놓고 다른 칸이 하나라도 있는지 찾는다. DO block이 account를 ledger_entry에 LEFT JOIN해 account별 sum을 만들고 balance와 다르면 exception을 낸다.
입력
fixture의 balance 100·50·0과 ledger signed_amount 100·70·-20을 group별로 비교한다.
결과·효과
account별 합이 100·50·NULL이고 COALESCE가 마지막을 0으로 바꿔 mismatch set이 비어 있다.
비유의 한계
row 합 일치는 원장 사건 completeness, 상태 유효성, double-entry 균형을 직접 증명하지 않는다.
3줄F05-L03 SELECT 'W16_Q3 mismatches=0 ledger_sum=150' WHERE (SELECT sum(signed_amount) FROM w16_project.ledger_entry)=150; 전체 원장 합계가 150일 때만 Q3 결과표를 한 줄 내민다. global SUM(signed_amount)=150이면 W16_Q3 mismatches=0 ledger_sum=150 marker를 SELECT한다.
입력
같은 fixture의 ledger 세 행 전체를 account grouping 없이 합산한다.
결과·효과
100+70-20=150이라 exact marker 한 행이 출력된다.
비유의 한계
global SUM predicate는 앞 DO의 per-account mismatch set을 다시 계산하지 않고 runner도 marker suffix 전체를 인증하지 않는다.
group의 한 행히토리 → 니지카 → 료 → 키타
  1. 히토리

    GROUP BY에 balance도 왜 넣었나요?

  2. 니지카

    SELECT와 HAVING에서 account별 저장값을 함께 쓰기 위해서야.

  3. 현재 output grain은 a.id와 a.balance 조합이다.

  4. 키타

    한 계좌가 한 group이라는 문장을 먼저 쓰겠습니다.

05

STEP 05 / 13

원본 코드 조각

원본을 3개 의미 조각으로 나누어 그대로 확인합니다.

파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.

F05-C01 · psql fail-fast1–1줄
1–1줄 원본
\set ON_ERROR_STOP on
F05-C02 · per-account reconciliation invariant2–2줄
2–2줄 원본
DO $q$ BEGIN IF EXISTS(SELECT a.id FROM w16_project.account a LEFT JOIN w16_project.ledger_entry l ON l.account_id=a.id GROUP BY a.id,a.balance HAVING a.balance<>coalesce(sum(l.signed_amount),0)) THEN RAISE EXCEPTION 'W16 Q3 mismatch'; END IF; END $q$;
F05-C03 · global-sum marker3–3줄
3–3줄 원본
SELECT 'W16_Q3 mismatches=0 ledger_sum=150' WHERE (SELECT sum(signed_amount) FROM w16_project.ledger_entry)=150;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

비어 있지 않은 3줄을 모두 한국어로 옮깁니다.

전체 번역 3 / 3

비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.

원본한국어 번역
1\set ON_ERROR_STOP onpsql이 첫 SQL error에서 중단하도록 ON_ERROR_STOP을 켠다.
2DO $q$ BEGIN IF EXISTS(SELECT a.id FROM w16_project.account a LEFT JOIN w16_project.ledger_entry l ON l.account_id=a.id GROUP BY a.id,a.balance HAVING a.balance<>coalesce(sum(l.signed_amount),0)) THEN RAISE EXCEPTION 'W16 Q3 mismatch'; END IF; END $q$;각 account balance와 COALESCE된 ledger signed sum이 다르면 W16 Q3 exception을 낸다.
3SELECT 'W16_Q3 mismatches=0 ledger_sum=150' WHERE (SELECT sum(signed_amount) FROM w16_project.ledger_entry)=150;전체 ledger signed sum이 150일 때 W16_Q3 marker 한 행을 출력한다.
07

STEP 07 / 13

기존 수준의 한 줄 읽기·문법 해부

쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.

한 줄로 읽기

LEFT JOIN group reconciliation과 별도 global aggregate marker로 두 독립 관찰을 만든다.

문법 해부

  • GROUP BY a.id,a.balance가 output grain을 account 한 행으로 만든다.
  • HAVING은 group 뒤 계산된 balance<>sum 조건을 고른다.
  • EXISTS는 mismatch row가 하나라도 있으면 exception branch를 연다.

실행 순서

  1. psql fail-fast
  2. left join
  3. account grouping
  4. COALESCE comparison
  5. mismatch exception
  6. global sum marker

원래 W6 수준의 조각별 정밀 해설

F05-C01 · psql fail-fast
문법 해부
aggregation file 첫 meta-command가 psql ON_ERROR_STOP을 활성화한다.
실제 값 추적
이후 statement error가 남은 source 입력을 중단시킬 수 있다.
정상 예
psql session에서 이 variable이 on으로 확인되는 것은 aggregate gate 실행의 올바른 시작 상태다.
틀린 예·반례
database server가 이 backslash 줄을 SQL transaction command로 처리한다고 가정하면 client/server 경계를 혼동한 것이다.
착각 방지
error 뒤 중단은 앞 autocommit statement의 rollback이나 schema 원복을 의미하지 않는다.
하지 않는 일
계좌별 합·global 합·marker result와 cleanup postcondition은 이 meta-command 범위 밖이다.
다음 연결
다음 ‘per-account reconciliation invariant’ 범위가 LEFT JOIN·GROUP BY·HAVING mismatch를 검사한다.
F05-C02 · per-account reconciliation invariant
문법 해부
DO block이 account를 ledger에 LEFT JOIN하고 account id·balance로 group한 뒤 balance와 COALESCE된 signed sum이 다른 group 존재 시 exception을 낸다.
실제 값 추적
account1은 100=100, account2는 50=70-20, account3은 LEFT JOIN NULL sum을 0으로 바꿔 0=0이라 mismatch set이 비어 있다.
정상 예
원장 0건 account3까지 parent row를 보존해 세 account 모두 일치하는 frozen fixture는 valid reconciliation 예다.
틀린 예·반례
INNER JOIN으로 바꾸면 account3이 비교에서 사라지고 balance 0의 zero-ledger case를 검증하지 못한다.
착각 방지
LEFT JOIN 자체가 0을 만들지는 않으며 SUM의 empty/null result를 COALESCE하는 expression이 0 해석을 맡는다.
하지 않는 일
mismatch 0은 거래 상태·event completeness·double-entry·global ledger total이나 currency policy를 증명하지 않는다.
다음 연결
다음 ‘global-sum marker’ 범위는 account grouping 없이 signed_amount 전체 합 150만 별도로 본다.
F05-C03 · global-sum marker
문법 해부
ledger_entry 전체 SUM(signed_amount)가 150일 때 W16_Q3 mismatches=0 ledger_sum=150 literal을 반환한다.
실제 값 추적
fixture의 +100,+70,-20을 global grain에서 더하면 150이므로 marker 한 row가 나온다.
정상 예
세 ledger row를 그대로 둔 state에서 scalar SUM equality가 true인 것은 이 statement의 valid oracle이다.
틀린 예·반례
account1에서 10을 빼고 account2에 10을 더하면 global 150은 유지돼도 account별 reconciliation은 깨질 수 있다.
착각 방지
marker text의 `mismatches=0`은 이 SELECT가 per-account mismatch를 다시 계산한 결과가 아니라 literal이다.
하지 않는 일
회계 완전성·차변대변 균형·source hash·앞 DO 실행 사실은 global equality 하나로 인증되지 않는다.
다음 연결
다음 ‘parameters and resolved paths’ 범위가 W16 project regression runner의 input surface를 연다.
전체 합의 함정히토리 → 니지카 → 료 → 키타
  1. 히토리

    150만 맞으면 대사 완료라고 해도 될까요?

  2. 니지카

    계좌 사이에 10이 잘못 옮겨도 전체 합은 그대로일 수 있어.

  3. global invariant와 partition invariant는 다른 축이다.

  4. 키타

    두 반례를 같은 표에 놓아 보겠습니다.

08

STEP 08 / 13

실제 값 따라가기

같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.

순서들어온 값코드가 하는 일나온 값·상태경계
1account 1 balance 100, ledger +100group sum과 저장값을 비교한다.100=100이라 mismatch가 아니다.해당 +100이 올바른 업무 사건인지까지 보지 않는다.
2account 2 balance 50, ledger +70와 -20두 signed_amount를 합한다.50=50이라 mismatch set에 들어가지 않는다.행 두 개의 상쇄가 항상 정상이라는 뜻은 아니다.
3account 3 balance 0, ledger 0건LEFT JOIN NULL sum을 COALESCE 0으로 바꾼다.0=0이라 0건 계좌도 정상 비교된다.원장 누락을 balance 0 정책상 허용하는 fixture 가정에 묶인다.
4ledger 전체 +100,+70,-20account 구분 없이 SUM한다.150이 되어 marker가 한 행 출력된다.계좌 간 금액을 서로 바꿔도 global 합만은 유지될 수 있다.
대사의 작은 범위히토리 → 니지카 → 료 → 키타
  1. 히토리

    mismatch 0이면 회계가 완벽한 거죠?

  2. 니지카

    이 SQL은 저장 balance와 signed sum만 비교해.

  3. 상태·원거래·차변대변 pair는 별도 계약이다.

  4. 키타

    증명한 것과 안 한 것을 경계 목록에 적겠습니다.

09

STEP 09 / 13

PowerShell·SQL·DB 내부에서 벌어지는 일

PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.

join

account를 보존하고 ledger를 optional child로 붙인다.

다른 1:N table을 함께 붙이면 row multiplication이 생길 수 있다.
aggregate

account grain에서 signed_amount를 sum한다.

SUM은 NULL input과 zero rows의 의미를 업무 정책 대신 정해 주지 않는다.
reconciliation gate

EXISTS mismatch를 exception으로 전환한다.

tolerance, currency, pending-status 정책은 이 source에 없다.
marker

global sum condition이 true일 때 문자열 한 행을 낸다.

marker는 DO result를 재검증하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

그럴듯하지만 틀린 해석을 반례로 고칩니다.

❌ COUNT(*)처럼 SUM도 0건이면 0이다.

왜 틀리나 SQL SUM의 빈 입력 결과는 NULL이다.

바르게 읽기 업무상 0으로 해석할 위치에서 COALESCE를 명시한다.

반례 account 3의 joined signed_amount는 NULL이고 COALESCE 전 sum도 NULL이다.

❌ INNER JOIN이어도 0건 계좌가 0으로 나온다.

왜 틀리나 child가 없으면 parent 행 자체가 사라진다.

바르게 읽기 account를 보존하는 LEFT JOIN 뒤 nullable child PK나 sum을 처리한다.

반례 account 3은 INNER JOIN 결과에 한 행도 남지 않는다.

❌ global sum 150이면 계좌별 balance도 모두 맞다.

왜 틀리나 계좌 사이 오분류는 전체 합에서 상쇄될 수 있다.

바르게 읽기 per-account group mismatch와 global marker를 독립 관찰로 둔다.

반례 account1에서 10을 빼 account2에 더하면 전체 합은 같지만 두 계좌가 틀린다.

❌ mismatch 0은 double-entry proof다.

왜 틀리나 source에는 차변·대변 pair나 transaction completeness 규칙이 없다.

바르게 읽기 이 gate를 저장 balance와 signed ledger의 fixture 대사로만 말한다.

반례 잘못된 단일 원장 행으로도 저장 balance를 같은 값으로 맞출 수 있다.

❌ marker line이 per-account DO를 다시 확인한다.

왜 틀리나 마지막 SELECT predicate는 global sum 하나다.

바르게 읽기 앞 DO와 뒤 SELECT의 조건을 따로 읽는다.

반례 분배 mismatch가 있어도 전체 sum 150인 state에서는 마지막 SELECT만 보면 marker가 나올 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

이 코드가 책임지지 않는 일을 분리합니다.

grain

여러 child table join에서 row multiplication 방지

이 책임을 맡는 곳: pre-aggregation design
accounting

double-entry, transaction completeness, status validity

이 책임을 맡는 곳: domain reconciliation rules
currency

currency partition, rounding, tolerance

이 책임을 맡는 곳: money policy
authenticity

canonical source or exact suffix execution

이 책임을 맡는 곳: hash-pinned exact-output runner
12

STEP 12 / 13

직접 다시 써보기

뜻 → 조각 → 전체 코드 순서로 다시 씁니다.

1단계 · 뜻부터 복원

account1 100, account2 70-20, account3 zero rows를 각각 balance와 비교한 뒤 전체 150을 따로 말한다.

2단계 · 코드 조각 재조립

  1. LEFT JOIN
  2. GROUP BY account
  3. COALESCE SUM
  4. HAVING mismatch
  5. global SUM marker

3단계 · 파일 전체 다시 쓰기

세 물리 줄을 그대로 재작성하고 SHA-256 0f0553c3...5606과 맞춘다.

자가 점검
  • 0건 계좌가 result grain에 남는지 본다.
  • NULL과 0의 변환 위치를 설명한다.
  • per-account와 global 조건을 섞지 않는다.
  • double-entry claim을 하지 않는다.
13

STEP 13 / 13

전체 원본 정답

감사로 고정한 전체 source를 가감 없이 확인합니다.

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcesql/w16/aggregation.sqlSHA-256 0f0553c35cd28435ed7d663e849e1c6a63f4898b005aeaf272755f37c28b5606
aggregation.sql - 계좌별 저장 잔액과 원장 합 대사 전체
\set ON_ERROR_STOP on
DO $q$ BEGIN IF EXISTS(SELECT a.id FROM w16_project.account a LEFT JOIN w16_project.ledger_entry l ON l.account_id=a.id GROUP BY a.id,a.balance HAVING a.balance<>coalesce(sum(l.signed_amount),0)) THEN RAISE EXCEPTION 'W16 Q3 mismatch'; END IF; END $q$;
SELECT 'W16_Q3 mismatches=0 ledger_sum=150' WHERE (SELECT sum(signed_amount) FROM w16_project.ledger_entry)=150;
06

run-w16-project-regression.ps1 - 세 SQL을 묶는 owner

scripts/run-w16-project-regression.ps1

정본 W16 project SQL 실행기 · 정본 · W16-F06
11줄 연결11줄 번역7 chunks
01

STEP 01 / 13

오늘 이 코드에서 해결할 문제

무엇을 이해해야 하는지 질문부터 잡습니다.

오늘의 한 문장

runtime 선택, destructive canonical fixture, 세 learner SQL process, prefix/exit gate, record-only hash, JSON evidence, mode별 cleanup을 한 PowerShell owner로 연결한다.

  1. fixture와 learner SQL 중 무엇을 hash할까?
  2. 세 SQL은 같은 transaction일까?
  3. marker 검사는 exact suffix일까?
  4. cleanup=1은 어느 mode에서도 실제 cleanup일까?
  5. $owned flag가 cleanup을 제어할까?
이 파일에서 끝까지 다시 쓰는 값files=3schema=w16_projectmarkers=W16_Q1/Q2/Q3 prefixeshashes=3 learner filesCompose default password=w16-disposable-passwordreported cleanup=1
02

STEP 02 / 13

아주 짧게: 이 코드는 왜 필요할까?

웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.

STARRY가 같은 작은 fixture 실험실에서 Q1·Q2·Q3 답안을 순서대로 검사하고 증거 카드를 만든다.

세 답안과 한 실험실 관리자

manager는 먼저 w16_project를 초기 fixture로 바꾼 뒤 세 learner SQL을 서로 다른 psql process에서 실행한다.

각 exit와 W16_Q prefix를 본 뒤 learner file hash를 기록하고 Compose mode에서만 down -v를 요청한다.

딱 여기까지만 Green 문구의 cleanup=1과 hashes=3은 실제 Container cleanup이나 canonical source 인증을 뜻하지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

생활 비유와 실제 코드의 경계를 함께 확인합니다.

두 mode

Compose는 owner DB를 만들고 Container는 caller DB를 사용한다.

코드 연결
1·6~8줄
비유
새 실험실을 빌리거나 남의 지정 실험실 열쇠를 받는다.
비유의 끝
두 통로 모두 w16_project라는 이름의 기존 schema를 reset한다.

세 개의 별도 실행

Q1·Q2·Q3 file을 차례로 separate psql에 보낸다.

코드 연결
3·9줄
비유
같은 칠판을 쓰지만 시험 창구는 세 번 새로 연다.
비유의 끝
한 transaction rollback이나 동일 session 설정을 공유하지 않는다.

약한 marker와 기록 hash

exit 0과 prefix를 보고 file SHA를 결과에 적는다.

코드 연결
9줄
비유
봉투 표지 글자와 지문을 기록하지만 정답 지문표와 대조하지 않는다.
비유의 끝
recorded hash와 prefix만으로 canonical bytes를 인증하지 않는다.

말과 실제 cleanup

manifest는 cleanup=1을 쓰지만 finally는 Compose에만 down -v한다.

코드 연결
10~11줄
비유
정리 완료 도장을 먼저 인쇄하고 실제 철거 요청은 빌린 방에만 보낸다.
비유의 끝
Container schema 잔류와 down 실패를 직접 검사하지 않는다.
두 실행 통로히토리 → 니지카 → 료 → 키타
  1. 히토리

    Container mode면 안전하게 읽기만 하나요?

  2. 니지카

    아니, 같은 fixture가 w16_project를 DROP CASCADE해.

  3. 소유 container 여부와 named schema mutation은 별개다.

  4. 키타

    실행 전에 destructive target을 적겠습니다.

세 hash의 정체히토리 → 니지카 → 료 → 키타
  1. 히토리

    hashes=3에 fixture도 들어 있나요?

  2. 니지카

    세 learner SQL만 loop에서 hash해.

  3. 그 hash도 canonical expected 값과 비교하지 않고 기록한다.

  4. 키타

    fixture gap과 record-only를 두 줄로 나누겠습니다.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

감사 규칙상 연결 대상인 원본 11줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.정본 W16 project SQL 실행기에서 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.11 / 11 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F06-L01 param([Parameter(Mandatory=$true)][string]$SqlRoot,[Parameter(Mandatory=$true)][string]$EvidenceDir,[ValidateSet('Compose','Container')][string]$Mode='Compose',[string]$Container='',[string]$ComposeProject='w16-project-lab') SQL 폴더·증거 폴더·두 실행 통로를 고르는 조작판을 펼친다. mandatory SqlRoot/EvidenceDir, Compose/Container Mode, optional Container, ComposeProject parameters를 선언한다.
입력
caller가 named arguments를 binder에 넘기고 Mode를 두 allowed string 중 하나로 고른다.
결과·효과
script scope에 다섯 parameter가 배치되고 default mode와 project name이 적용될 수 있다.
비유의 한계
ValidateSet은 Mode만 제한하며 path containment·container 존재·project 이름 안전성은 아직 검사하지 않는다.
2줄F06-L02 $ErrorActionPreference='Stop';$root=Split-Path -Parent $PSScriptRoot;$sql=(Resolve-Path -LiteralPath $SqlRoot).Path;$e=[IO.Path]::GetFullPath($EvidenceDir);New-Item -ItemType Directory -Force $e|Out-Null 오류 경보를 Stop에 두고 reference·입력·증거 주소를 계산한 뒤 증거 상자를 만든다. ErrorActionPreference, script-parent root, resolved SqlRoot, full EvidenceDir를 정하고 Directory -Force를 실행한다.
입력
현재 script 위치와 caller의 두 mandatory path 문자열을 filesystem resolution에 넣는다.
결과·효과
$root/$sql/$e가 정해지고 evidence directory가 존재하는 상태가 된다.
비유의 한계
EvidenceDir가 learner root 안인지 확인하지 않으며 기존 evidence를 비우거나 atomic staging하지 않는다.
3줄F06-L03 $files=@('logical-order.sql','anti-join.sql','aggregation.sql');foreach($name in $files){if(!(Test-Path -LiteralPath (Join-Path $sql $name) -PathType Leaf)){throw "missing learner project SQL: $name"}} Q1·Q2·Q3 세 답안 봉투가 모두 있는지 표지만 점검한다. logical-order.sql, anti-join.sql, aggregation.sql leaf 존재를 loop로 확인한다.
입력
resolved SqlRoot 아래 세 fixed filename의 filesystem type을 Test-Path로 읽는다.
결과·효과
셋이 모두 file이면 다음 줄로 가고 하나라도 없으면 missing learner project SQL exception이 난다.
비유의 한계
존재만 보며 bytes·hash·SQL syntax·canonical 내용은 이 preflight에서 검사하지 않는다.
4줄F06-L04 $owned=$false;$compose=Join-Path $root 'compose.yaml' 아직 Compose를 소유하지 않았다는 불을 끄고 packaged compose 주소를 적는다. $owned=false를 초기화하고 reference root 아래 compose.yaml path를 만든다.
입력
packaged script root와 새 boolean variable slot을 사용한다.
결과·효과
memory에 false flag와 compose path 문자열이 생긴다.
비유의 한계
$owned가 뒤에서 cleanup 조건에 사용되는지는 이 줄만으로 알 수 없고 compose file 존재도 아직 검사하지 않는다.
5줄F06-L05 try{ 작업 내용을 아직 넣지 않은 보호 봉투의 입구만 연다. PowerShell try statement의 body opener를 선언한다.
입력
parser가 `try` keyword와 여는 중괄호를 읽고 앞선 control state 다음에 새 statement-list scope를 연다.
결과·효과
try body가 열린 상태가 되지만 이 행만으로 child statement는 하나도 실행되지 않는다.
비유의 한계
뒤 child의 command·token·value·result와 닫는 구조는 이 opener만으로 알 수 없다.
6줄F06-L06 if($Mode-eq'Compose'){if($Container){throw 'W16 Compose mode rejects Container'};if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w16-disposable-password'};& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw "W16 compose up exit=$LASTEXITCODE"};$owned=$true;$Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()}elseif(!$Container){throw 'W16 Container mode requires Container'} Compose면 일회용 DB를 올리고, 아니면 supplied container 이름표를 요구하는 긴 분기 레일을 탄다. Compose branch가 Container 동시 입력을 거절하고 blank password를 default로 바꾼 뒤 compose up --wait와 db id 조회를 수행한다; 대체 branch는 Container truthiness를 요구한다.
입력
Mode, Container, FCL_DB_PASSWORD, compose path, project name과 Docker CLI 상태를 한 physical line에서 사용한다.
결과·효과
Compose 성공 시 owned=true와 resolved container id가 생기고, Container mode는 nonempty 입력이면 다음 guard로 간다.
비유의 한계
default password는 secret 관리가 아니며 compose ps native exit를 별도 검사하지 않고 whitespace Container는 이 branch 뒤 guard에서야 거절된다.
7줄F06-L07 if([string]::IsNullOrWhiteSpace($Container)){throw 'W16 container resolution failed'} 해결된 container 이름표가 공백이면 DB 문 앞에서 멈춘다. IsNullOrWhiteSpace(Container)이 true면 W16 container resolution failed exception을 던진다.
입력
앞 branch가 만든 또는 caller가 준 Container string을 검사한다.
결과·효과
nonblank이면 fixture 단계로 진행하고 null·empty·whitespace이면 script 흐름이 중단된다.
비유의 한계
문자열 nonblank는 container 존재·running·PostgreSQL image·전용 schema를 증명하지 않는다.
8줄F06-L08 Get-Content -Raw (Join-Path $root 'sql/w16/fixture.sql')|docker exec -i $Container psql -v ON_ERROR_STOP=1 -U app -d financial_core|Out-Null;if($LASTEXITCODE-ne 0){throw 'W16 fixture failed'} 기준 fixture 두루마리를 통째로 supplied DB에 밀어 넣어 w16_project 방을 갈아엎는다. packaged sql/w16/fixture.sql을 raw로 읽어 docker exec psql ON_ERROR_STOP=1에 pipe하고 nonzero exit를 예외로 바꾼다.
입력
reference root의 un-hashed fixture bytes와 resolved Container의 financial_core/app connection을 사용한다.
결과·효과
성공하면 기존 w16_project가 DROP CASCADE된 뒤 세 account와 세 ledger row로 재생성된다.
비유의 한계
fixture SHA를 manifest에 기록하지 않고 explicit outer transaction도 없으며 Container mode의 기존 named schema를 파괴한다.
9줄F06-L09 $results=@();for($i=0;$i-lt 3;$i++){$path=Join-Path $sql $files[$i];$out=@(Get-Content -Raw $path|docker exec -i $Container psql -At -v ON_ERROR_STOP=1 -U app -d financial_core 2>&1);$exit=$LASTEXITCODE;$marker="W16_Q$($i+1) ";if($exit-ne 0-or($out-join"`n")-cnotmatch [regex]::Escape($marker)){throw "W16 SQL invariant failed file=$($files[$i]) exit=$exit output=$out"};$results+=[ordered]@{ordinal=$i+1;file=$files[$i];marker=($out|Where-Object{$_-like "$marker*"}|Select-Object -Last 1);native_exit=0;sha256=(Get-FileHash -Algorithm SHA256 $path).Hash.ToLowerInvariant()}} 세 learner SQL을 차례로 별도 창구에 내고 exit·Q prefix·hash를 각 결과 카드에 적는다. for loop가 각 file을 separate docker exec/psql process로 실행해 output과 exit를 받고 W16_Qn substring을 검사한 뒤 last prefix line과 SHA를 result에 추가한다.
입력
$files order, shared database state, learner file text, native output, LASTEXITCODE를 ordinal 1~3마다 반복 사용한다.
결과·효과
세 gate가 통과하면 results에 ordinal/file/marker/native_exit=0/recorded SHA object 세 개가 순서대로 쌓인다.
비유의 한계
세 실행은 한 transaction/session이 아니고 prefix는 line 중간 substring도 gate를 통과할 수 있으며 hash는 expected value와 비교하지 않는다.
10줄F06-L10 $manifest=[ordered]@{schema='w16_project';canonical_logical_order='logical-order.sql';files=3;rows=3;invariants=@('rows=2 top_id=1','rows=1 account_id=3','mismatches=0 ledger_sum=150');native_exits=0;hashes=3;cleanup=1;results=$results};$manifest|ConvertTo-Json -Depth 5|Set-Content -Encoding utf8 (Join-Path $e 'project-regression.json');"W16_PROJECT_SQL_GREEN files=3 rows=3 invariants=3 native_exits=0 hashes=3 cleanup=1" 통과 카드 세 장을 고정 숫자표로 포장해 JSON과 Green 문구를 내보낸다. literal schema/files/rows/invariants/native_exits/hashes/cleanup fields와 results를 manifest로 만들고 Set-Content한 뒤 W16_PROJECT_SQL_GREEN string을 출력한다.
입력
앞 loop의 results와 fixed literal values, EvidenceDir를 사용한다.
결과·효과
project-regression.json이 덮어써지고 stdout에 files=3...cleanup=1 marker가 방출된다.
비유의 한계
rows/invariants/cleanup은 재계산값이 아니라 literals이고 write는 atomic replace/reread가 아니며 cleanup=1은 아직 실제 자원 제거를 관찰하지 않는다.
11줄F06-L11 }finally{if($Mode-eq'Compose'){& docker compose -f $compose -p $ComposeProject down -v|Out-Null}} 봉투를 닫으며 Compose 통로일 때만 project와 volume 제거 요청을 보낸다. finally에서 Mode가 Compose이면 docker compose down -v를 실행하고 output을 버린다.
입력
Mode string, compose path, ComposeProject와 Docker engine state를 사용한다.
결과·효과
Compose mode에는 down -v 요청이 가고 Container mode에는 아무 schema/container cleanup statement도 실행되지 않는다.
비유의 한계
down exit code와 resource 부재를 검사하지 않으며 Container mode의 reset w16_project는 남고 $owned flag는 사용되지 않는다.
세 psql process히토리 → 니지카 → 료 → 키타
  1. 히토리

    Q1부터 Q3까지 한 transaction이죠?

  2. 니지카

    file마다 docker exec와 psql을 새로 실행해.

  3. database state는 이어져도 session·transaction은 새로 열린다.

  4. 키타

    앞 file mutation 반례를 만들어 보겠습니다.

05

STEP 05 / 13

원본 코드 조각

원본을 7개 의미 조각으로 나누어 그대로 확인합니다.

파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.

F06-C01 · parameters and resolved paths1–2줄
1–2줄 원본
param([Parameter(Mandatory=$true)][string]$SqlRoot,[Parameter(Mandatory=$true)][string]$EvidenceDir,[ValidateSet('Compose','Container')][string]$Mode='Compose',[string]$Container='',[string]$ComposeProject='w16-project-lab')
$ErrorActionPreference='Stop';$root=Split-Path -Parent $PSScriptRoot;$sql=(Resolve-Path -LiteralPath $SqlRoot).Path;$e=[IO.Path]::GetFullPath($EvidenceDir);New-Item -ItemType Directory -Force $e|Out-Null
F06-C02 · learner file set and compose path3–4줄
3–4줄 원본
$files=@('logical-order.sql','anti-join.sql','aggregation.sql');foreach($name in $files){if(!(Test-Path -LiteralPath (Join-Path $sql $name) -PathType Leaf)){throw "missing learner project SQL: $name"}}
$owned=$false;$compose=Join-Path $root 'compose.yaml'
F06-C03 · mode startup and container resolution5–7줄
5–7줄 원본
try{
 if($Mode-eq'Compose'){if($Container){throw 'W16 Compose mode rejects Container'};if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w16-disposable-password'};& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw "W16 compose up exit=$LASTEXITCODE"};$owned=$true;$Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()}elseif(!$Container){throw 'W16 Container mode requires Container'}
 if([string]::IsNullOrWhiteSpace($Container)){throw 'W16 container resolution failed'}
F06-C04 · canonical destructive fixture apply8–8줄
8–8줄 원본
 Get-Content -Raw (Join-Path $root 'sql/w16/fixture.sql')|docker exec -i $Container psql -v ON_ERROR_STOP=1 -U app -d financial_core|Out-Null;if($LASTEXITCODE-ne 0){throw 'W16 fixture failed'}
F06-C05 · three separate learner SQL processes9–9줄
9–9줄 원본
 $results=@();for($i=0;$i-lt 3;$i++){$path=Join-Path $sql $files[$i];$out=@(Get-Content -Raw $path|docker exec -i $Container psql -At -v ON_ERROR_STOP=1 -U app -d financial_core 2>&1);$exit=$LASTEXITCODE;$marker="W16_Q$($i+1) ";if($exit-ne 0-or($out-join"`n")-cnotmatch [regex]::Escape($marker)){throw "W16 SQL invariant failed file=$($files[$i]) exit=$exit output=$out"};$results+=[ordered]@{ordinal=$i+1;file=$files[$i];marker=($out|Where-Object{$_-like "$marker*"}|Select-Object -Last 1);native_exit=0;sha256=(Get-FileHash -Algorithm SHA256 $path).Hash.ToLowerInvariant()}}
F06-C06 · hard-coded manifest and Green marker10–10줄
10–10줄 원본
 $manifest=[ordered]@{schema='w16_project';canonical_logical_order='logical-order.sql';files=3;rows=3;invariants=@('rows=2 top_id=1','rows=1 account_id=3','mismatches=0 ledger_sum=150');native_exits=0;hashes=3;cleanup=1;results=$results};$manifest|ConvertTo-Json -Depth 5|Set-Content -Encoding utf8 (Join-Path $e 'project-regression.json');"W16_PROJECT_SQL_GREEN files=3 rows=3 invariants=3 native_exits=0 hashes=3 cleanup=1"
F06-C07 · Compose-only cleanup11–11줄
11–11줄 원본
}finally{if($Mode-eq'Compose'){& docker compose -f $compose -p $ComposeProject down -v|Out-Null}}
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

비어 있지 않은 11줄을 모두 한국어로 옮깁니다.

전체 번역 11 / 11

비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.

원본한국어 번역
1param([Parameter(Mandatory=$true)][string]$SqlRoot,[Parameter(Mandatory=$true)][string]$EvidenceDir,[ValidateSet('Compose','Container')][string]$Mode='Compose',[string]$Container='',[string]$ComposeProject='w16-project-lab')두 mandatory path와 Compose/Container mode, optional container, project name parameter를 선언한다.
2$ErrorActionPreference='Stop';$root=Split-Path -Parent $PSScriptRoot;$sql=(Resolve-Path -LiteralPath $SqlRoot).Path;$e=[IO.Path]::GetFullPath($EvidenceDir);New-Item -ItemType Directory -Force $e|Out-Nullfail-fast preference를 켜고 reference root, resolved learner SQL root, full evidence path를 만든다.
3$files=@('logical-order.sql','anti-join.sql','aggregation.sql');foreach($name in $files){if(!(Test-Path -LiteralPath (Join-Path $sql $name) -PathType Leaf)){throw "missing learner project SQL: $name"}}learner SQL root에 logical-order, anti-join, aggregation 세 파일이 모두 있는지 확인한다.
4$owned=$false;$compose=Join-Path $root 'compose.yaml'$owned를 false로 두고 packaged compose.yaml path를 만든다.
5try{mode 실행과 evidence 생성을 감싸는 try block을 연다.
6 if($Mode-eq'Compose'){if($Container){throw 'W16 Compose mode rejects Container'};if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w16-disposable-password'};& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw "W16 compose up exit=$LASTEXITCODE"};$owned=$true;$Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()}elseif(!$Container){throw 'W16 Container mode requires Container'}Compose mode는 disposable password와 owned service를 준비하고 Container mode는 supplied id를 요구한다.
7 if([string]::IsNullOrWhiteSpace($Container)){throw 'W16 container resolution failed'}최종 Container 문자열이 null·empty·whitespace이면 실패한다.
8 Get-Content -Raw (Join-Path $root 'sql/w16/fixture.sql')|docker exec -i $Container psql -v ON_ERROR_STOP=1 -U app -d financial_core|Out-Null;if($LASTEXITCODE-ne 0){throw 'W16 fixture failed'}packaged fixture.sql을 psql로 실행해 w16_project를 reset하고 native exit를 확인한다.
9 $results=@();for($i=0;$i-lt 3;$i++){$path=Join-Path $sql $files[$i];$out=@(Get-Content -Raw $path|docker exec -i $Container psql -At -v ON_ERROR_STOP=1 -U app -d financial_core 2>&1);$exit=$LASTEXITCODE;$marker="W16_Q$($i+1) ";if($exit-ne 0-or($out-join"`n")-cnotmatch [regex]::Escape($marker)){throw "W16 SQL invariant failed file=$($files[$i]) exit=$exit output=$out"};$results+=[ordered]@{ordinal=$i+1;file=$files[$i];marker=($out|Where-Object{$_-like "$marker*"}|Select-Object -Last 1);native_exit=0;sha256=(Get-FileHash -Algorithm SHA256 $path).Hash.ToLowerInvariant()}}세 learner SQL을 separate psql process로 실행해 prefix/exit를 gate하고 marker와 record-only hash를 모은다.
10 $manifest=[ordered]@{schema='w16_project';canonical_logical_order='logical-order.sql';files=3;rows=3;invariants=@('rows=2 top_id=1','rows=1 account_id=3','mismatches=0 ledger_sum=150');native_exits=0;hashes=3;cleanup=1;results=$results};$manifest|ConvertTo-Json -Depth 5|Set-Content -Encoding utf8 (Join-Path $e 'project-regression.json');"W16_PROJECT_SQL_GREEN files=3 rows=3 invariants=3 native_exits=0 hashes=3 cleanup=1"literal summary와 results를 project-regression.json에 쓰고 W16 Green marker를 출력한다.
11}finally{if($Mode-eq'Compose'){& docker compose -f $compose -p $ComposeProject down -v|Out-Null}}finally에서 Compose mode만 down -v를 요청하고 Container mode는 reset schema를 남긴다.
07

STEP 07 / 13

기존 수준의 한 줄 읽기·문법 해부

쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.

한 줄로 읽기

fail-fast orchestration이지만 source authenticity, cross-file atomicity, Container cleanup을 닫지 않은 prefix-based regression owner다.

문법 해부

  • Resolve-Path는 SqlRoot 존재를 요구하지만 EvidenceDir containment는 없다.
  • Get-Content -Raw | docker exec -i psql은 file마다 새 native process를 만든다.
  • -cnotmatch는 case-sensitive substring이고 later -like는 prefix wildcard selection이다.
  • finally의 external down exit는 명시적으로 검사되지 않는다.

실행 순서

  1. bind paths
  2. preflight three files
  3. choose mode
  4. resolve container
  5. reset fixture
  6. run Q1/Q2/Q3 separately
  7. write literal manifest
  8. emit marker
  9. Compose-only cleanup request

원래 W6 수준의 조각별 정밀 해설

F06-C01 · parameters and resolved paths
문법 해부
param block은 mandatory SqlRoot·EvidenceDir, Compose/Container Mode, optional Container, ComposeProject를 받고 다음 줄이 stop-on-error·project root·absolute SQL/evidence paths와 directory를 만든다.
실제 값 추적
valid SqlRoot는 Resolve-Path로 canonical absolute path가 되고 EvidenceDir은 full path directory로 생성되며 Mode 기본값은 Compose다.
정상 예
존재하는 SQL directory와 쓰기 가능한 evidence directory를 supplied하면 path-preparation phase가 끝난다.
틀린 예·반례
없는 SqlRoot는 Resolve-Path terminating error가 되고, arbitrary EvidenceDir이 project 밖이어도 containment check는 거부하지 않는다.
착각 방지
Mandatory는 값 제공을 요구할 뿐 caller authorization·trusted root·safe evidence ownership을 검증하지 않는다.
하지 않는 일
path canonicalization은 symlink policy, atomic evidence write, Docker availability와 database isolation을 보장하지 않는다.
다음 연결
다음 ‘learner file set and compose path’ 범위가 세 file 이름·existence와 packaged compose 위치를 정한다.
F06-C02 · learner file set and compose path
문법 해부
files array를 logical-order.sql·anti-join.sql·aggregation.sql로 고정해 각 leaf 존재를 검사하고, owned=false와 project-root compose.yaml path를 준비한다.
실제 값 추적
세 named SQL leaf가 SqlRoot 아래 모두 있으면 preflight loop가 끝나고 compose 변수는 packaged root의 YAML 위치를 가리킨다.
정상 예
정확한 세 filename을 가진 regular files와 existing root를 supplied하는 것은 이 range의 valid input shape다.
틀린 예·반례
anti-join.sql 하나가 없으면 `missing learner project SQL` throw가 발생하며 leaf contents나 SHA가 달라도 존재 gate는 통과한다.
착각 방지
leaf existence는 canonical bytes·safe SQL·display order 의미를 인증하지 않고 owned 변수는 현재 여기서 false로 초기화될 뿐이다.
하지 않는 일
compose artifact 존재·hash, learner-root containment, fixture source와 database state는 아직 검증되지 않는다.
다음 연결
다음 ‘mode startup and container resolution’ 범위가 Compose owner 또는 supplied Container 통로를 선택한다.
F06-C03 · mode startup and container resolution
문법 해부
try 안에서 Compose mode는 supplied Container를 거부하고 whitespace password를 disposable 값으로 바꾼 뒤 compose up --wait와 ps -q를 실행하며, Container mode는 nonblank ID를 요구한다.
실제 값 추적
Compose native exit 0과 nonblank ps result이면 owned=true·resolved Container가 되고, caller mode에서는 supplied nonblank string이 그대로 다음 단계 입력이 된다.
정상 예
blank password의 Compose run이 w16-disposable-password를 넣고 db container ID를 얻는 경로는 source에 적힌 정상 branch다.
틀린 예·반례
Compose와 Container를 동시에 주거나 Container mode에서 ID를 비우면 명시 throw가 나며 blank resolution도 별도 거부된다.
착각 방지
compose `--wait`와 nonblank ID는 image digest·database credential·schema·query result Green을 보장하지 않는다.
하지 않는 일
Container provenance, caller database 소유권, port safety와 post-run resource state는 startup branch가 확인하지 않는다.
다음 연결
다음 ‘canonical destructive fixture apply’ 범위가 packaged fixture.sql을 resolved container database에 pipe한다.
F06-C04 · canonical destructive fixture apply
문법 해부
packaged `sql/w16/fixture.sql` raw text를 docker exec -i psql에 보내고 stdout을 버린 뒤 native exit가 nonzero면 fixture failed를 throw한다.
실제 값 추적
resolved container의 financial_core database에서 fixture가 성공하면 fixture-defined schema가 reset되고 exact three-account·three-ledger state가 만들어진다.
정상 예
격리된 lab container에 canonical fixture bytes를 적용해 psql exit 0을 얻는 것은 이 range의 valid execution이다.
틀린 예·반례
supplied Container의 같은 schema를 보존해야 하는 상황에서 실행하면 mode와 무관하게 DROP CASCADE로 기존 contents를 잃을 수 있다.
착각 방지
fixture output은 Out-Null로 버리지만 native exit는 검사하며, fixture source SHA는 여기서 계산하거나 later evidence record에 결속하지 않는다.
하지 않는 일
transaction-wide rollback, pre-reset backup, schema absence postcondition과 caller authorization은 이 pipeline이 보장하지 않는다.
다음 연결
다음 ‘three separate learner SQL processes’ 범위가 세 learner file의 exit·marker prefix·recorded hash를 차례로 모은다.
F06-C05 · three separate learner SQL processes
문법 해부
3회 loop가 각 learner file을 새 docker exec/psql process로 실행해 output·LASTEXITCODE를 받고 W16_Qn-space prefix substring을 검사한 뒤 ordinal·file·last prefix line·0 exit·SHA를 results에 추가한다.
실제 값 추적
Q1→Q2→Q3 순서로 세 native exit가 0이고 각 output 어딘가에 case-sensitive marker prefix가 있으면 results object가 세 개 쌓인다.
정상 예
각 frozen learner SQL이 expected marker를 내는 fixture run은 ordinal 1·2·3과 세 observed source hash를 수집한다.
틀린 예·반례
다른 SQL이 같은 W16_Q prefix를 출력하거나 canonical suffix 뒤 거짓 text를 붙여도 substring gate만으로는 source 의미를 인증하지 못한다.
착각 방지
joined output gate는 prefix substring을 찾지만 stored marker selector는 prefix-start line의 마지막 값이며 nonnull을 거절하지 않아 mid-line spoof가 통과하면서 marker가 null일 수 있다.
하지 않는 일
세 files는 별도 psql process라 하나의 session·transaction이 아니고, recorded SHA는 expected allowlist와 비교되지 않으며 Get-Content 뒤 hash 계산 사이 bytes 변경도 결박하지 않는다.
다음 연결
다음 ‘hard-coded manifest and Green marker’ 범위가 collected results와 literal summary를 evidence JSON에 쓴다.
F06-C06 · hard-coded manifest and Green marker
문법 해부
ordered manifest에 schema·file names·counts·세 invariant literal·native_exits=0·hashes=3·cleanup=1·results를 넣어 JSON Set-Content하고 W16_PROJECT_SQL_GREEN 한 줄을 출력한다.
실제 값 추적
세 loop result가 있을 때 project-regression.json에는 learner hash 세 개가 포함되고 Green text의 files/rows/invariants/native_exits/hashes/cleanup 숫자는 모두 3/3/3/0/3/1로 나온다.
정상 예
results가 세 object인 successful run에서 depth 5 JSON과 exact Green summary를 생성하는 것은 current source의 expected path다.
틀린 예·반례
cleanup=1을 실제 resource/schema absence 관찰값으로 읽거나 hashes=3에 separately executed fixture SHA까지 포함된다고 말하면 틀리다.
착각 방지
invariants·rows·cleanup은 literal summary이며 learner hashes도 기록만 되고 expected digest와 대조되지 않는다.
하지 않는 일
failure가 이 line 전에 나면 기존 evidence file이 stale하게 남을 수 있고 Set-Content도 atomic replace·reread·signature를 제공하지 않는다.
다음 연결
다음 ‘Compose-only cleanup’ 범위가 finally에서 Mode 문자열이 Compose일 때 down -v를 요청한다.
F06-C07 · Compose-only cleanup
문법 해부
finally condition이 Mode=Compose일 때만 같은 compose/project 인수로 `down -v`를 호출해 output을 버린다.
실제 값 추적
Compose owner path에서는 container·network·named volume 제거 요청이 나가고 Container mode에서는 이 body를 건너뛴다.
정상 예
정상 Compose 실행 뒤 down -v native command가 resource를 제거하는 것은 의도된 cleanup 경로다.
틀린 예·반례
supplied Container mode 성공 뒤에도 w16_project schema가 남으며 manifest의 cleanup=1 literal은 그 잔류를 반영하지 않는다.
착각 방지
cleanup predicate는 `$owned`를 읽지 않고 Mode string만 보며 down의 LASTEXITCODE나 resource absence도 검사하지 않는다.
하지 않는 일
teardown 성공·Container schema 복구·volume backup·Green marker 철회와 cleanup evidence는 이 finally가 보장하지 않는다.
다음 연결
다음 dependency인 Q23 ‘provenance and assumptions’가 고정 시계 7일 집계의 비정본 정책을 먼저 공개한다.
prefix를 속일 수 있는가히토리 → 니지카 → 료 → 키타
  1. 히토리

    W16_Q3만 보이면 mismatch도 0인가요?

  2. 니지카

    현재 gate는 prefix substring과 exit만 확인해.

  3. last prefix line을 기록하지만 없을 때 null을 별도 거절하지도 않는다.

  4. 키타

    exact suffix와 structured field가 필요한 이유를 적을게요.

08

STEP 08 / 13

실제 값 따라가기

같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.

순서들어온 값코드가 하는 일나온 값·상태경계
1Mode=Compose, blank FCL_DB_PASSWORDdisposable password를 넣고 compose up --wait db를 호출한다.container id가 nonblank이면 fixture 단계로 간다.image digest와 down 성공은 고정하지 않는다.
2기존 w16_project가 있는 supplied databasepackaged fixture.sql을 실행한다.schema가 DROP CASCADE 후 3+3 row로 재생성된다.fixture bytes는 output hash 3개에 포함되지 않는다.
3세 learner SQL file각각 새 psql process에서 exit와 W16_Qn substring을 본다.통과할 때 marker 후보와 file SHA가 results에 쌓인다.hash allowlist와 exact suffix comparison이 없다.
4results 세 objectliteral summary와 함께 JSON을 쓰고 Green string을 출력한다.evidence가 생긴 뒤 finally로 control이 이동한다.Container mode에서도 cleanup=1 literal은 바뀌지 않는다.
5Mode=Containerfinally condition을 평가한다.down -v가 실행되지 않고 caller container와 w16_project가 남는다.$owned 여부도 cleanup 분기에 읽히지 않는다.
cleanup 숫자와 현실히토리 → 니지카 → 료 → 키타
  1. 히토리

    manifest가 cleanup=1이면 정리된 거 아닌가요?

  2. 니지카

    그 값은 mode와 결과를 읽어 계산한 게 아니라 literal이야.

  3. Container에는 down이 없고 Compose down exit도 확인하지 않는다.

  4. 키타

    postcondition 없는 완료 도장을 경계로 표시하겠습니다.

09

STEP 09 / 13

PowerShell·SQL·DB 내부에서 벌어지는 일

PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.

PowerShell binder/filesystem

parameters와 absolute paths를 만들고 evidence directory를 준비한다.

authorization과 containment는 검사하지 않는다.
Docker Compose

Compose mode에서 disposable PostgreSQL service를 시작·조회한다.

Container mode runtime provenance는 검사하지 않는다.
PostgreSQL fixture

named schema를 destructive reset해 deterministic rows를 만든다.

fixture source hash가 evidence에 묶이지 않는다.
three psql clients

shared database state에서 learner files를 순차 실행한다.

session/transaction atomicity는 공유하지 않는다.
evidence serializer

literal summary와 result hashes를 JSON text로 쓴다.

atomic replace, reread, signature가 없다.
finally cleanup

Mode string이 Compose일 때만 down -v를 호출한다.

exit/resource absence를 gate하지 않고 Container cleanup은 없다.
10

STEP 10 / 13

흔한 착각과 틀린 예

그럴듯하지만 틀린 해석을 반례로 고칩니다.

❌ hashes=3이면 fixture와 세 답안이 모두 hash-bound다.

왜 틀리나 $files loop에는 learner SQL 세 개만 있다.

바르게 읽기 fixture hash gap과 learner hash record-only 성격을 따로 공개한다.

반례 fixture.sql bytes가 바뀌어도 result hash count는 계속 3이다.

❌ 세 답안은 한 transaction이라 하나가 실패하면 전부 rollback된다.

왜 틀리나 loop마다 docker exec/psql process를 새로 연다.

바르게 읽기 earlier file mutation이 later file에 보일 수 있는 sequential shared-state 실행으로 설명한다.

반례 Q1 file이 commit한 UPDATE 뒤 Q2가 실패해도 Q1 change가 남을 수 있다.

❌ W16_Q1 prefix는 canonical invariant를 인증한다.

왜 틀리나 runner는 substring 존재와 exit만 본다.

바르게 읽기 exact output·source allowlist가 없는 weak marker gate라고 부른다.

반례 다른 문장 중간에 W16_Q1 공백을 넣어도 -cnotmatch gate는 통과할 수 있다.

❌ cleanup=1이면 Container schema도 제거됐다.

왜 틀리나 field는 literal이고 finally는 Compose에만 down -v한다.

바르게 읽기 mode별 실제 cleanup statement와 residue를 읽는다.

반례 Mode=Container 성공 뒤 w16_project는 재생성된 채 남는다.

❌ $owned가 true인 경우에만 finally cleanup한다.

왜 틀리나 finally condition은 Mode-eq Compose이고 $owned는 읽히지 않는다.

바르게 읽기 변수 이름보다 실제 consumer를 찾는다.

반례 $owned assignment를 삭제해도 현재 cleanup predicate text는 그대로다.

❌ down -v 호출이 실패하면 Green marker가 자동 취소된다.

왜 틀리나 runner는 down LASTEXITCODE나 resource absence를 검사하지 않는다.

바르게 읽기 cleanup execution/result를 별도 gate로 확인해야 한다.

반례 Docker cleanup error 뒤에도 manifest의 literal cleanup=1은 이미 쓰였다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

이 코드가 책임지지 않는 일을 분리합니다.

destructive scope

supplied Container의 기존 w16_project 보존

이 책임을 맡는 곳: caller isolation and schema ownership
source binding

fixture 또는 canonical learner SQL bytes

이 책임을 맡는 곳: expected-hash allowlist
atomicity

Q1-Q3 all-or-nothing rollback

이 책임을 맡는 곳: single transaction/session orchestration
semantic result

canonical marker suffix와 invariant meaning

이 책임을 맡는 곳: exact structured result validation
cleanup

Container schema removal 또는 Compose resource absence

이 책임을 맡는 곳: mode-aware postcondition audit
evidence durability

atomic JSON replace, reread, signature

이 책임을 맡는 곳: durable evidence writer
12

STEP 12 / 13

직접 다시 써보기

뜻 → 조각 → 전체 코드 순서로 다시 씁니다.

1단계 · 뜻부터 복원

경로→세 file 존재→mode→fixture reset→세 separate psql→prefix/hash record→literal manifest→Compose-only cleanup 순서로 말한다.

2단계 · 코드 조각 재조립

  1. parameters/path resolution
  2. $files three
  3. Compose/Container branch
  4. fixture pipe
  5. three-file loop
  6. literal manifest
  7. finally down -v

3단계 · 파일 전체 다시 쓰기

11개 물리 줄을 줄바꿈 위치까지 재작성하고 SHA-256 1e71fcfc...47d0과 비교한다.

자가 점검
  • fixture와 learner hash 범위를 분리한다.
  • psql process 수와 transaction 수를 혼동하지 않는다.
  • prefix substring과 exact suffix를 구분한다.
  • cleanup=1을 observation으로 말하지 않는다.
  • $owned의 consumer가 없음을 확인한다.
  • Container mode residue를 적는다.
13

STEP 13 / 13

전체 원본 정답

감사로 고정한 전체 source를 가감 없이 확인합니다.

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcescripts/run-w16-project-regression.ps1SHA-256 1e71fcfc0ab1d140d14fd3f1d658f51edecfededbd39a33fd4a982a8c89847d0
run-w16-project-regression.ps1 - 세 SQL을 묶는 owner 전체
param([Parameter(Mandatory=$true)][string]$SqlRoot,[Parameter(Mandatory=$true)][string]$EvidenceDir,[ValidateSet('Compose','Container')][string]$Mode='Compose',[string]$Container='',[string]$ComposeProject='w16-project-lab')
$ErrorActionPreference='Stop';$root=Split-Path -Parent $PSScriptRoot;$sql=(Resolve-Path -LiteralPath $SqlRoot).Path;$e=[IO.Path]::GetFullPath($EvidenceDir);New-Item -ItemType Directory -Force $e|Out-Null
$files=@('logical-order.sql','anti-join.sql','aggregation.sql');foreach($name in $files){if(!(Test-Path -LiteralPath (Join-Path $sql $name) -PathType Leaf)){throw "missing learner project SQL: $name"}}
$owned=$false;$compose=Join-Path $root 'compose.yaml'
try{
 if($Mode-eq'Compose'){if($Container){throw 'W16 Compose mode rejects Container'};if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w16-disposable-password'};& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw "W16 compose up exit=$LASTEXITCODE"};$owned=$true;$Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()}elseif(!$Container){throw 'W16 Container mode requires Container'}
 if([string]::IsNullOrWhiteSpace($Container)){throw 'W16 container resolution failed'}
 Get-Content -Raw (Join-Path $root 'sql/w16/fixture.sql')|docker exec -i $Container psql -v ON_ERROR_STOP=1 -U app -d financial_core|Out-Null;if($LASTEXITCODE-ne 0){throw 'W16 fixture failed'}
 $results=@();for($i=0;$i-lt 3;$i++){$path=Join-Path $sql $files[$i];$out=@(Get-Content -Raw $path|docker exec -i $Container psql -At -v ON_ERROR_STOP=1 -U app -d financial_core 2>&1);$exit=$LASTEXITCODE;$marker="W16_Q$($i+1) ";if($exit-ne 0-or($out-join"`n")-cnotmatch [regex]::Escape($marker)){throw "W16 SQL invariant failed file=$($files[$i]) exit=$exit output=$out"};$results+=[ordered]@{ordinal=$i+1;file=$files[$i];marker=($out|Where-Object{$_-like "$marker*"}|Select-Object -Last 1);native_exit=0;sha256=(Get-FileHash -Algorithm SHA256 $path).Hash.ToLowerInvariant()}}
 $manifest=[ordered]@{schema='w16_project';canonical_logical_order='logical-order.sql';files=3;rows=3;invariants=@('rows=2 top_id=1','rows=1 account_id=3','mismatches=0 ledger_sum=150');native_exits=0;hashes=3;cleanup=1;results=$results};$manifest|ConvertTo-Json -Depth 5|Set-Content -Encoding utf8 (Join-Path $e 'project-regression.json');"W16_PROJECT_SQL_GREEN files=3 rows=3 invariants=3 native_exits=0 hashes=3 cleanup=1"
}finally{if($Mode-eq'Compose'){& docker compose -f $compose -p $ComposeProject down -v|Out-Null}}
07

W16-SQL-Q23.sql — 고정 시계의 반열린 7일 고객 집계

illustrative/sql/W16-SQL-Q23.sql

fixture 기반 학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W16-F07
23줄 연결23줄 번역5 chunks
01

STEP 01 / 13

오늘 이 코드에서 해결할 문제

무엇을 이해해야 하는지 질문부터 잡습니다.

오늘의 한 문장

fixture_clock의 고정 as_of를 기준으로 최근 7일 transaction을 골라 account를 통해 customer별 건수와 금액 합을 계산하되, 예시가 선택한 status·join 정책을 숨기지 않는다.

  1. 왜 wall clock 대신 fixture_clock.as_of를 쓸까?
  2. [start,end)에서 두 경계의 equality는 어떻게 처리될까?
  3. FAILED와 PROCESSING도 왜 합계에 들어갈까?
  4. 거래가 없는 customer가 0건 행으로 보일까?
  5. COUNT(*)와 SUM(amount)의 group grain은 무엇일까?
이 파일에서 끝까지 다시 쓰는 값as_of=2027-01-03 00:00:00+09window=[2026-12-27 00:00:00+09, 2027-01-03 00:00:00+09)customer1=7 rows / 1,201,699customer2=4 rows / 3,303all statuses; output rows=2
02

STEP 02 / 13

아주 짧게: 이 코드는 왜 필요할까?

웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.

STARRY가 고정 시계에서 7일짜리 반열린 띠를 잘라 거래 카드를 고객별 바구니에 모은다.

고정 시계로 자른 7일 거래 바구니

이 예시는 fixture_clock.as_of로 반열린 7일 구간을 만들고 해당 business_tx를 CTE에 분리한다.

계좌를 통해 customer_id로 묶으면 고객 1은 7건·1,201,699, 고객 2는 4건·3,303이 된다.

딱 여기까지만 prompt에 status filter가 없어 FAILED·PROCESSING도 포함하고 거래 없는 고객을 보존하지 않는 audit-authored 비정본 예시다.

03

STEP 03 / 13

초등학생도 이해하는 설명

생활 비유와 실제 코드의 경계를 함께 확인합니다.

움직이지 않는 기준 시계

현재 PC 시간이 아니라 fixture_clock의 as_of를 끝점으로 쓴다.

코드 연결
6~8줄
비유
모두 같은 벽시계를 보고 시험을 시작한다.
비유의 끝
fixture_clock이 정확히 한 행이라는 사실은 별도 fixture 계약에서 온다.

반열린 일주일

시작과 같은 시각은 포함하고 끝과 같은 시각은 제외한다.

코드 연결
13~14줄
비유
입구 선은 밟아도 되지만 출구 선은 다음 구간 소속이다.
비유의 끝
calendar date 7개가 아니라 timestamptz instant interval이다.

status 색을 가리지 않음

WHERE에는 occurred_at 조건만 있고 status predicate가 없다.

코드 연결
3·10~14줄
비유
시간 띠 안이면 성공·실패 색을 모두 같은 바구니에 담는다.
비유의 끝
이 선택은 prompt 해석이지 모든 금융 합계의 권장 정책이 아니다.

거래에서 고객으로

recent_tx를 account에 inner join해 customer_id를 얻는다.

코드 연결
20~22줄
비유
거래표의 계좌 번호로 계좌 카드에서 고객 이름표를 찾는다.
비유의 끝
거래가 없는 고객은 출발 row가 없어 0건 그룹이 만들어지지 않는다.
최근의 기준 시계히토리 → 니지카 → 료 → 키타
  1. 히토리

    최근 7일이면 오늘 시간을 바로 쓰는 게 자연스럽지 않나요?

  2. 니지카

    이 연습은 언제 돌려도 같아야 해서 fixture_clock의 as_of를 끝점으로 써.

  3. clock source가 달라지면 같은 SQL 문구라도 oracle row set이 달라진다.

  4. 키타

    8줄의 source와 2027-01-03 값을 함께 확인하겠습니다.

두 경계의 equality히토리 → 니지카 → 료 → 키타
  1. 히토리

    시작과 끝 시각에 딱 맞는 거래는 둘 다 포함되나요?

  2. 니지카

    시작은 >=라 들어오고 끝은 <라 빠져.

  3. 반열린 구간은 인접 window 사이에서 같은 instant를 두 번 세지 않게 한다.

  4. 키타

    13·14줄 연산자를 괄호 표기 `[start,end)`와 맞춰 볼게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

감사 규칙상 연결 대상인 원본 23줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.fixture 기반 학습용 SQL 예시 · 정본 답안 아님에서 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.23 / 23 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F07-L01 -- W16-SQL-Q23 illustrative example; not a shipped workbook answer. STARRY가 첫 장에 ‘학습용 예시’ 도장을 찍어 정본 답안 봉투와 분리한다. 이 파일이 W16 Q23의 illustrative example이며 shipped workbook answer가 아니라고 밝히는 주석이다.
입력
독자가 실행 전에 파일의 provenance 문구를 읽는다.
결과·효과
이후 SQL을 비정본 학습 예시로 해석해야 한다는 표시가 남는다.
비유의 한계
주석은 query 정확성·fixture 적합성·실행 성공을 스스로 검사하지 않는다.
2줄F07-L02 -- Assumption: recent seven days is the half-open interval [fixture_clock.as_of - 7 days, fixture_clock.as_of). 고정 시계에서 정확히 7일 전 표찰을 떼고 끝 시각 직전까지만 받는 띠를 그린다. recent seven days를 `[as_of - 7 days, as_of)` 반열린 구간으로 정한 가정 주석이다.
입력
fixture_clock.as_of와 7-day duration을 window 계약 입력으로 삼는다.
결과·효과
시작 경계 포함·끝 경계 제외라는 시간 정책이 문서에 고정된다.
비유의 한계
이 설명만으로 실제 WHERE가 생기거나 timezone·fixture row 수가 검증되지는 않는다.
3줄F07-L03 -- All statuses are included because the prompt does not request a status filter; only customers with rows appear. 상태 색과 무관하게 시간 띠 안의 표를 받되 빈 고객 바구니는 진열하지 않는 안내판을 단다. prompt에 status filter가 없으므로 모든 상태를 포함하고 거래 row가 있는 customer만 나타난다고 선언한다.
입력
SUCCESS·FAILED·UNKNOWN·PROCESSING 가능성과 zero-row customer 정책을 해석 입력으로 둔다.
결과·효과
FAILED·PROCESSING도 집계 대상이며 결과가 inner shape라는 책임 범위가 공개된다.
비유의 한계
주석 자체는 status 조건 부재나 join 형태를 실행 시 강제하지 않는다.
4줄F07-L04 SET search_path TO :"workbook_schema", public; STARRY가 workbook_schema 서랍을 먼저, public 서랍을 다음으로 찾게 주소표를 바꾼다. 현재 psql session의 search_path를 supplied `workbook_schema`와 public 순서로 설정한다.
입력
psql variable `workbook_schema`의 identifier 값과 public schema 이름을 받는다.
결과·효과
뒤의 unqualified relation 이름을 앞선 schema부터 resolve할 session 상태가 된다.
비유의 한계
변수가 올바르게 supplied됐는지, schema가 존재하는지, 원하는 object가 선택되는지는 보장하지 않는다.
6줄F07-L06 WITH bounds AS ( 시간 창 계산표를 만들기 위해 `bounds`라는 새 접이식 표의 표지를 연다. WITH 절에서 `bounds` CTE 정의를 시작한다.
입력
SQL parser가 CTE 이름과 여는 괄호를 읽는다.
결과·효과
bounds용 query body를 받을 scope가 열리지만 아직 column이나 row는 만들어지지 않는다.
비유의 한계
이 opener만으로 기준 시각·기간·source relation은 정해지지 않는다.
7줄F07-L07 SELECT as_of - INTERVAL '7 days' AS window_start, as_of AS window_end 한 시각표에서 7일을 뺀 시작 칸과 원래 시각인 끝 칸을 나란히 적는다. scope 안의 `as_of`에서 7 days를 뺀 값을 window_start, 원값을 window_end로 projection한다.
입력
timestamp 값 `as_of`와 PostgreSQL interval literal을 expression 입력으로 사용한다.
결과·효과
relation binding이 완성되면 한 source row마다 두 이름 붙은 경계값이 계산될 모양이 된다.
비유의 한계
as_of 경계 projection만으로는 값의 source·cardinality나 frozen 2027-01-03 instant를 결속하지 못한다.
8줄F07-L08 FROM fixture_clock 경계 계산표가 값을 가져올 고정 시계 상자를 fixture_clock으로 연결한다. bounds SELECT의 FROM source를 fixture_clock으로 지정한다.
입력
현재 search_path에서 resolve된 fixture_clock relation rows를 읽는다.
결과·효과
각 fixture_clock row의 as_of가 앞 projection에 공급되어 bounds row가 산출될 수 있다.
비유의 한계
이 FROM만으로 singleton row가 정확히 하나인지나 clock 값이 최신인지 검증하지 않는다.
9줄F07-L09 ), recent_tx AS ( 첫 계산표를 접어 등록하고 이어서 `recent_tx`라는 두 번째 표의 빈 칸을 연다. bounds CTE를 닫은 뒤 comma로 연결해 recent_tx CTE 정의를 시작한다.
입력
parser가 닫는 괄호·CTE separator·새 이름·여는 괄호를 처리한다.
결과·효과
bounds가 뒤 CTE에서 참조 가능한 이름이 되고 recent_tx body scope가 열린다.
비유의 한계
현재 줄에는 transaction source·projection·시간 predicate가 하나도 없다.
10줄F07-L10 SELECT t.tx_id, t.account_id, t.amount, t.status 거래 카드에서 번호·계좌·금액·상태 네 칸만 복사할 투명 필름을 놓는다. alias t의 tx_id, account_id, amount, status를 recent_tx output column으로 projection한다.
입력
query scope에서 t로 식별될 row의 네 field 값을 expression 입력으로 받는다.
결과·효과
FROM binding이 완성되면 선택된 row마다 네 column을 내보낼 projection이 구성된다.
비유의 한계
이 projection은 row를 최근 기간으로 거르거나 status를 제한하지 않는다.
11줄F07-L11 FROM business_tx AS t 복사할 거래 카드 더미를 business_tx로 고르고 짧은 이름표 t를 붙인다. recent_tx의 driving relation을 business_tx로 정하고 alias t를 부여한다.
입력
search_path로 resolve된 business_tx의 모든 candidate row가 FROM 입력이 된다.
결과·효과
앞 projection의 t-qualified field들이 이 relation에 결속된다.
비유의 한계
FROM 선언 자체에는 row를 줄이는 predicate가 없어 relation의 전체 row가 candidate일 뿐이다.
12줄F07-L12 CROSS JOIN bounds AS b 거래 카드마다 시간 경계표 한 장을 옆에 붙여 비교 준비를 한다. business_tx alias t와 bounds alias b를 CROSS JOIN한다.
입력
두 relation의 현재 row 집합을 Cartesian pairing 입력으로 사용한다.
결과·효과
각 transaction candidate가 각 bounds row의 window_start·window_end를 볼 수 있게 된다.
비유의 한계
bounds CROSS JOIN 문법은 singleton cardinality를 강제하지 않아 여러 bounds rows면 transaction을 곱한다.
13줄F07-L13 WHERE t.occurred_at >= b.window_start 시작 표찰보다 이른 거래 카드는 돌려보내고 정확히 시작에 놓인 카드는 받는다. t.occurred_at이 b.window_start 이상인 row만 통과시키는 WHERE 하한 predicate다.
입력
각 paired row의 occurred_at과 window_start timestamp를 비교한다.
결과·효과
시작 이전 row는 제거되고 시작 경계와 그 이후 row는 일단 남는다.
비유의 한계
이 하한만으로 끝 경계 이후 row를 제거하거나 status를 거르지는 않는다.
14줄F07-L14 AND t.occurred_at < b.window_end 끝 표찰에 닿기 전 거래만 남기고 끝과 같은 시각부터는 다음 구간으로 보낸다. t.occurred_at이 b.window_end보다 작은 row만 추가로 허용하는 상한 predicate다.
입력
앞 하한을 통과한 row의 occurred_at과 window_end를 다시 비교한다.
결과·효과
두 predicate가 합쳐져 `[window_start, window_end)`의 transaction set이 남는다.
비유의 한계
status filter·customer zero preservation·현재 시각 사용은 여기서 추가되지 않는다.
15줄F07-L15 ) 시간 조건을 통과한 거래표의 마지막 덮개를 닫아 recent_tx로 보관한다. recent_tx CTE query body를 닫는다.
입력
parser가 앞 SELECT·FROM·JOIN·WHERE로 구성된 괄호 scope의 종료를 읽는다.
결과·효과
뒤 main query가 recent_tx라는 relation 이름을 참조할 수 있게 CTE 정의가 완결된다.
비유의 한계
닫는 괄호는 row order·materialization·oracle row 수를 보장하지 않는다.
16줄F07-L16 SELECT 최종 결과표에 넣을 열을 고르기 위해 빈 SELECT 머리표를 연다. main query의 SELECT projection list를 시작한다.
입력
SQL parser가 SELECT keyword를 입력받는다.
결과·효과
뒤 expression들을 output column으로 모을 query 단계가 열린다.
비유의 한계
현재 줄만으로 어떤 output expression이나 source relation도 드러나지 않는다.
17줄F07-L17 a.customer_id, a 이름표가 가리키는 row의 고객 번호를 결과표 첫 칸에 적는다. alias a의 customer_id를 첫 output column으로 projection한다.
입력
뒤 FROM scope에서 alias a로 제공될 row의 customer_id를 받는다.
결과·효과
query가 유효해지면 customer identifier expression이 output list에 포함된다.
비유의 한계
이 expression만으로 alias a의 relation 정체·group grain·zero-row 보존은 정해지지 않는다.
18줄F07-L18 COUNT(*) AS tx_count, 한 aggregate 묶음에 들어온 거래 카드 수를 세고 tx_count 표찰을 붙인다. 현재 group의 input row 수를 COUNT(*)로 계산해 tx_count로 이름 붙인다.
입력
뒤 query clauses가 정할 각 aggregate input row collection을 받는다.
결과·효과
존재하는 group마다 NULL 여부와 무관한 row cardinality 정수가 계산된다.
비유의 한계
DISTINCT 거래 수가 아니며 row가 전혀 없는 customer를 0으로 생성하지 않는다.
19줄F07-L19 SUM(r.amount) AS total_amount 같은 aggregate 묶음의 거래 금액을 모두 더해 total_amount 칸에 기록한다. group별 r.amount 합을 SUM으로 계산하고 total_amount alias를 부여한다.
입력
각 eventual aggregate group의 r.amount 값들을 입력으로 쓴다.
결과·효과
한 group의 amount 총합이 두 번째 metric column으로 산출된다.
비유의 한계
금액의 부호·currency·status를 바꾸지 않으며 빈 group을 0으로 채우지도 않는다.
20줄F07-L20 FROM recent_tx AS r 시간 필터를 마친 recent_tx 더미를 최종 집계의 출발 카드로 놓고 r이라 부른다. main query의 FROM source를 recent_tx CTE로 지정하고 alias r을 붙인다.
입력
앞 CTE가 내놓은 시간 구간 내 transaction rows를 driving input으로 사용한다.
결과·효과
main query candidate set이 recent_tx에 실제로 존재하는 rows로 정해진다.
비유의 한계
이 source line만으로 row 소유 주체나 aggregate group key는 아직 붙지 않는다.
21줄F07-L21 JOIN account AS a ON a.account_id = r.account_id 거래 카드의 account_id와 같은 계좌 카드를 붙여 고객 번호를 찾아낸다. account를 alias a로 INNER JOIN하고 a.account_id와 r.account_id의 equality로 연결한다.
입력
recent_tx row와 account row의 account_id 값을 join key로 비교한다.
결과·효과
matching account가 있는 transaction마다 customer_id를 제공하는 joined row가 남는다.
비유의 한계
unmatched row와 거래 없는 customer를 보존하는 LEFT JOIN이 아니며 account balance는 쓰지 않는다.
22줄F07-L22 GROUP BY a.customer_id 같은 고객 번호표를 가진 joined row를 한 바구니씩 묶는다. joined rows를 a.customer_id 값으로 GROUP BY한다.
입력
앞 join 결과의 customer_id를 grouping key로 입력한다.
결과·효과
present customer마다 한 aggregate group이 생겨 COUNT와 SUM의 grain이 정해진다.
비유의 한계
거래 없는 customer group이나 account별 세부 행을 따로 남기지 않는다.
23줄F07-L23 ORDER BY a.customer_id; 완성된 고객 바구니를 번호가 작은 것부터 진열한다. aggregate result를 a.customer_id 오름차순으로 정렬한다.
입력
GROUP BY 뒤 생성된 customer-level rows와 그 key를 sort 입력으로 받는다.
결과·효과
output row가 customer_id ascending 순서로 반환된다.
비유의 한계
정렬은 count·sum의 정확성이나 고객 누락을 검증하지 않는다.
24줄F07-L24 -- Fixture oracle: customer 1 = 7 rows / 1,201,699; customer 2 = 4 rows / 3,303. 마지막 검산표에 고객 1과 2의 고정 건수·합계를 정확한 숫자로 적어 둔다. frozen fixture oracle을 customer 1=7/1,201,699와 customer 2=4/3,303으로 기록한 주석이다.
입력
동결 seed, half-open interval, all-status policy, inner-join query를 전제로 삼는다.
결과·효과
예상 output은 customer 두 행이며 각 tx_count와 total_amount가 문서화된다.
비유의 한계
실행 assertion이 아니고 fixture·window·status policy가 달라지면 숫자도 달라진다.
FAILED도 합산되는가히토리 → 니지카 → 료 → 키타
  1. 히토리

    거래 합계라면 실패 건은 DB가 알아서 제외하겠죠?

  2. 니지카

    WHERE에 status가 없어서 시간만 맞으면 FAILED와 PROCESSING도 들어가.

  3. customer 2의 네 건은 그 정책을 숫자로 드러내는 반례다.

  4. 키타

    100+101+102+3000을 직접 더해 3,303을 재검산하겠습니다.

05

STEP 05 / 13

원본 코드 조각

원본을 5개 의미 조각으로 나누어 그대로 확인합니다.

파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.

F07-C01 · provenance and assumptions1–4줄
1–4줄 원본
-- W16-SQL-Q23 illustrative example; not a shipped workbook answer.
-- Assumption: recent seven days is the half-open interval [fixture_clock.as_of - 7 days, fixture_clock.as_of).
-- All statuses are included because the prompt does not request a status filter; only customers with rows appear.
SET search_path TO :"workbook_schema", public;
F07-C02 · frozen bounds CTE5–9줄
5–9줄 원본

WITH bounds AS (
    SELECT as_of - INTERVAL '7 days' AS window_start, as_of AS window_end
    FROM fixture_clock
), recent_tx AS (
F07-C03 · recent transaction CTE10–15줄
10–15줄 원본
    SELECT t.tx_id, t.account_id, t.amount, t.status
    FROM business_tx AS t
    CROSS JOIN bounds AS b
    WHERE t.occurred_at >= b.window_start
      AND t.occurred_at < b.window_end
)
F07-C04 · customer aggregation16–23줄
16–23줄 원본
SELECT
    a.customer_id,
    COUNT(*) AS tx_count,
    SUM(r.amount) AS total_amount
FROM recent_tx AS r
JOIN account AS a ON a.account_id = r.account_id
GROUP BY a.customer_id
ORDER BY a.customer_id;
F07-C05 · fixture oracle24–24줄
24–24줄 원본
-- Fixture oracle: customer 1 = 7 rows / 1,201,699; customer 2 = 4 rows / 3,303.
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

비어 있지 않은 23줄을 모두 한국어로 옮깁니다.

전체 번역 23 / 23

비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.

원본한국어 번역
1-- W16-SQL-Q23 illustrative example; not a shipped workbook answer.W16 Q23용 학습 예시이며 배포된 workbook 정답이 아님을 밝힌다.
2-- Assumption: recent seven days is the half-open interval [fixture_clock.as_of - 7 days, fixture_clock.as_of).최근 7일을 fixture_clock.as_of에서 7일 전 이상, as_of 미만으로 가정한다.
3-- All statuses are included because the prompt does not request a status filter; only customers with rows appear.status를 거르지 않고 거래 row가 있는 customer만 결과에 나타난다고 선언한다.
4SET search_path TO :"workbook_schema", public;이 session의 search_path를 supplied workbook schema와 public 순서로 설정한다.
6WITH bounds AS (bounds라는 첫 CTE 정의를 연다.
7 SELECT as_of - INTERVAL '7 days' AS window_start, as_of AS window_endas_of-7 days를 window_start, as_of를 window_end로 선택한다.
8 FROM fixture_clockbounds가 fixture_clock에서 as_of를 읽는다.
9), recent_tx AS (bounds를 닫고 recent_tx라는 다음 CTE를 연다.
10 SELECT t.tx_id, t.account_id, t.amount, t.statusrecent_tx에 거래 id·account id·amount·status를 투영한다.
11 FROM business_tx AS tbusiness_tx를 t라는 이름으로 읽는다.
12 CROSS JOIN bounds AS b각 business_tx row에 bounds row를 CROSS JOIN한다.
13 WHERE t.occurred_at >= b.window_startoccurred_at이 window_start 이상인 row만 남긴다.
14 AND t.occurred_at < b.window_endoccurred_at이 window_end보다 작은 row만 추가로 남긴다.
15)recent_tx CTE 정의를 닫는다.
16SELECT최종 SELECT projection을 시작한다.
17 a.customer_id,customer_id를 결과 열로 선택한다.
18 COUNT(*) AS tx_count,customer group의 row 수를 tx_count로 계산한다.
19 SUM(r.amount) AS total_amountcustomer group의 amount 합을 total_amount로 계산한다.
20FROM recent_tx AS rrecent_tx를 최종 query의 driving relation r로 읽는다.
21JOIN account AS a ON a.account_id = r.account_idaccount_id가 같은 account를 inner join해 customer를 연결한다.
22GROUP BY a.customer_id결과를 customer_id별로 그룹화한다.
23ORDER BY a.customer_id;customer_id 오름차순으로 결과를 정렬한다.
24-- Fixture oracle: customer 1 = 7 rows / 1,201,699; customer 2 = 4 rows / 3,303.fixture oracle은 customer 1이 7건·1,201,699, customer 2가 4건·3,303이라고 기록한다.
07

STEP 07 / 13

기존 수준의 한 줄 읽기·문법 해부

쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.

한 줄로 읽기

frozen timestamp로 half-open window를 만든 뒤 all-status transaction을 account-owner grain으로 inner aggregate하는 fixture-bound illustrative query다.

문법 해부

  • bounds와 recent_tx 두 CTE가 시간 경계 계산과 row filtering을 분리한다.
  • TIMESTAMPTZ끼리 >=와 <를 사용해 시작 포함·끝 제외를 표현한다.
  • COUNT(*)는 joined group row 수이고 SUM(r.amount)는 status별 부호 조정을 하지 않은 positive amount 합이다.
  • recent_tx에서 시작하는 INNER JOIN shape는 zero-transaction customer를 생성하지 않는다.

실행 순서

  1. search_path setting
  2. fixture_clock bounds
  3. business_tx cross join bounds
  4. half-open time filter
  5. account inner join
  6. customer grouping
  7. count/sum
  8. customer-id sort

원래 W6 수준의 조각별 정밀 해설

F07-C01 · provenance and assumptions
문법 해부
세 comment가 non-shipped illustrative provenance, `[as_of-7 days,as_of)` 구간, all-status·present-customer 정책을 밝히고 SET search_path가 supplied workbook schema를 public보다 앞에 둔다.
실제 값 추적
실행 전에 시작 포함·끝 제외, FAILED/PROCESSING 포함, zero-row customer 미보존과 unqualified name resolution 순서가 공개된다.
정상 예
workbook_schema variable을 올바른 identifier로 supplied하고 이 예시를 fixture-bound 학습 SQL로 읽는 것은 선언 범위와 맞는다.
틀린 예·반례
comment를 shipped canonical answer나 executable assertion으로 취급하고 SUCCESS-only 결과를 기대하면 source 정책과 어긋난다.
착각 방지
‘모든 상태’와 ‘row가 있는 customer만’은 누락된 조건이 아니라 이 illustrative example이 명시한 가정이다.
하지 않는 일
schema 존재·object authenticity·query output·업무 승인 status policy는 provenance·SET 네 줄이 검증하지 않는다.
다음 연결
다음 ‘frozen bounds CTE’ 범위가 fixture_clock.as_of에서 두 timestamp 경계를 계산한다.
F07-C02 · frozen bounds CTE
문법 해부
blank separator 뒤 bounds CTE가 fixture_clock의 as_of에서 INTERVAL 7 days를 빼 window_start로, 원값을 window_end로 projection한다.
실제 값 추적
frozen as_of 2027-01-03 00:00:00+09 한 row라면 bounds는 start 2026-12-27 00:00:00+09와 같은 end 한 row를 만든다.
정상 예
동결 singleton clock row를 읽어 두 named timestamptz expressions를 얻는 것은 deterministic valid example다.
틀린 예·반례
fixture_clock에 여러 row가 있어도 bounds가 자동 하나로 축약되거나 system wall clock이 사용된다고 보면 틀리다.
착각 방지
CTE 이름은 계산 관계를 분리하지만 문법만으로 materialization·physical scan 순서를 고정하지 않는다.
하지 않는 일
거래 source·time predicate·status·customer grain과 output rows는 이 bounds slice에서 아직 결정되지 않는다.
다음 연결
다음 ‘recent transaction CTE’ 범위가 transaction rows를 두 경계와 비교해 시간 창을 적용한다.
F07-C03 · recent transaction CTE
문법 해부
recent_tx가 business_tx의 tx_id·account_id·amount·status를 bounds와 CROSS JOIN하고 occurred_at>=start 및 <end 두 predicate로 거른다.
실제 값 추적
frozen seed에서는 2026-12-27 이상 2027-01-03 미만의 11 transaction rows가 통과하며 상태 column은 projection될 뿐 filter되지 않는다.
정상 예
start와 정확히 같은 occurred_at은 포함되고 end와 같은 instant는 제외되는 row는 half-open condition의 valid 경계 예다.
틀린 예·반례
BETWEEN으로 양 끝을 포함하거나 WHERE에 보이지 않는 SUCCESS predicate가 암묵 적용된다고 가정하면 row set이 달라진다.
착각 방지
CROSS JOIN은 bounds cardinality가 여러 개면 transaction을 곱할 수 있고 current fixture의 singleton authority에 의존한다.
하지 않는 일
이 slice는 transaction 후보만 만들며 row owner grouping·zero-customer preservation·금액 sign/currency 의미를 정하지 않는다.
다음 연결
다음 ‘customer aggregation’ 범위가 recent rows를 account에 연결해 customer grain의 count와 sum을 만든다.
F07-C04 · customer aggregation
문법 해부
main SELECT가 recent_tx r을 account a에 account_id equality로 inner join하고 customer_id별 GROUP BY 뒤 COUNT(*)와 SUM(r.amount)를 계산해 customer_id 순으로 정렬한다.
실제 값 추적
recent row가 있는 account가 owner customer로 연결되고 present customer group마다 한 row·row count·positive stored amount 합이 만들어진다.
정상 예
matching account FK를 가진 recent transactions를 customer_id 하나의 grain으로 묶는 것은 projection·join·aggregate shape와 맞는다.
틀린 예·반례
customer에서 시작하지 않았는데 거래 없는 customer도 COUNT=0 row로 자동 생기거나 REVERSAL amount가 음수로 변환된다고 보면 틀리다.
착각 방지
COUNT(*)는 joined rows를 세고 SUM은 stored amount를 그대로 더하며 status·tx_type에 따른 distinct/sign 규칙이 없다.
하지 않는 일
zero preservation·currency conversion·SUCCESS-only total·business-date grouping과 exact fixture 숫자 assertion은 이 query body가 제공하지 않는다.
다음 연결
다음 ‘fixture oracle’ comment가 이 query shape와 frozen seed로 계산한 두 customer row의 exact 숫자를 적는다.
F07-C05 · fixture oracle
문법 해부
마지막 comment가 customer 1의 7 rows/1,201,699와 customer 2의 4 rows/3,303을 frozen fixture oracle로 기록한다.
실제 값 추적
두 group 합은 총 11 rows이며 customer1에는 account101·105 rows, customer2에는 account102 rows가 포함된다.
정상 예
동결 seed와 half-open all-status inner policy에서 두 output row가 exact count/sum과 일치하는 것은 valid reference result다.
틀린 예·반례
status filter를 SUCCESS로 바꾸거나 window end를 포함하면 같은 주석 숫자를 유지할 수 없다.
착각 방지
customer2의 3,303은 FAILED 100+101+102와 PROCESSING 3,000을 포함해 all-status 경계를 드러낸다.
하지 않는 일
comment는 DB가 실행하는 assertion이 아니며 shipped workbook canonical answer·production invariant를 뜻하지 않는다.
다음 연결
다음 Q24 ‘provenance and thresholds’ 범위가 balance band 수치가 illustrative assumption임을 먼저 밝힌다.
0건 customer의 행히토리 → 니지카 → 료 → 키타
  1. 히토리

    COUNT(*)면 거래 없는 고객도 0으로 표시되지 않나요?

  2. 니지카

    COUNT는 이미 생긴 group을 셀 뿐 없는 group을 만들어 주진 않아.

  3. recent_tx가 driving relation이라 zero customer는 join 전에 사라져 있다.

  4. 키타

    customer에서 시작하는 LEFT JOIN 대안과 현재 20줄을 비교하겠습니다.

08

STEP 08 / 13

실제 값 따라가기

같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.

순서들어온 값코드가 하는 일나온 값·상태경계
windowas_of=2027-01-03 00:00:00+097 days를 빼고 두 timestamptz 경계를 만든다.start=2026-12-27 00:00:00+09, end=2027-01-03 00:00:00+09다.end와 정확히 같은 row는 `<` 때문에 제외된다.
customer 1 rowsaccounts 101·105의 window 내 7 transactionsFAILED·SUCCESS 구분 없이 amount를 합한다.99,999+100,000+1,000,000+300+200+600+600=1,201,699다.reversal도 amount를 음수로 바꾸지 않고 stored positive amount 그대로 더한다.
customer 2 rowsaccount 102의 FAILED 100·101·102와 PROCESSING 3,000네 row를 한 customer group으로 센 뒤 합한다.tx_count=4, total_amount=3,303이다.status predicate를 추가하면 이 oracle은 달라진다.
absent groupsrecent row가 없는 customers 3·4·5·6recent_tx driving set과 account inner join을 따른다.해당 customer group은 0값 행이 아니라 결과에서 사라진다.zero preservation이 필요하면 customer에서 시작하는 outer join 설계가 따로 필요하다.
oracle과 실행 증거히토리 → 니지카 → 료 → 키타
  1. 히토리

    마지막 주석에 숫자가 맞으면 회귀 테스트도 통과한 건가요?

  2. 니지카

    주석은 예상표이고 DB output을 비교하는 assertion은 아니야.

  3. 직접 증거는 query result, reference 증거는 pinned seed와 재계산이다.

  4. 키타

    두 결과 행을 structured comparator로 확인하는 단계를 따로 적을게요.

09

STEP 09 / 13

PowerShell·SQL·DB 내부에서 벌어지는 일

PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.

psql name resolution

identifier variable로 받은 workbook schema를 search_path 첫 순서로 둔다.

wrong schema object나 search_path spoof를 hash로 인증하지 않는다.
PostgreSQL CTE planner

bounds와 recent_tx의 relational expressions를 main query plan에 통합하거나 materialize할 수 있다.

WITH 문법만 보고 특정 materialization·scan 순서를 단정할 수 없다.
timestamp comparison

TIMESTAMPTZ instants를 lower-inclusive, upper-exclusive predicate로 비교한다.

business_date 기준 집계나 지역 달력 일수 계약은 아니다.
join and grain

transaction→account FK match로 customer_id를 붙여 customer grain을 만든다.

customer master를 driving table로 보존하지 않는다.
aggregate executor

각 present customer group의 row count와 BIGINT amount sum을 계산한다.

currency·status·signed cash-flow 의미를 자동 해석하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

그럴듯하지만 틀린 해석을 반례로 고칩니다.

❌ 최근 7일이면 current_timestamp를 써야 한다.

왜 틀리나 이 workbook은 해마다 같은 oracle을 위해 fixture_clock.as_of를 고정한다.

바르게 읽기 문제 계약의 clock source를 먼저 확인한다.

반례 실행일이 2028년이면 current_timestamp 기준으로 frozen 2026~2027 rows가 모두 빠질 수 있다.

❌ BETWEEN으로 바꾸면 같은 7일 창이다.

왜 틀리나 BETWEEN은 양 끝을 포함해 as_of와 같은 row까지 받을 수 있다.

바르게 읽기 연속 구간은 >= start AND < end를 유지한다.

반례 occurred_at=2027-01-03 00:00:00+09 row는 현재 query에서 제외된다.

❌ 금융 거래 합계니까 SUCCESS만 포함된다.

왜 틀리나 source에는 status WHERE가 없고 3줄 주석도 all statuses를 명시한다.

바르게 읽기 FAILED·PROCESSING 포함을 oracle 정책으로 공개한다.

반례 customer 2의 3,303은 FAILED 303과 PROCESSING 3,000으로만 구성된다.

❌ 모든 customer가 tx_count=0이라도 한 행씩 나온다.

왜 틀리나 recent_tx에서 시작해 account만 inner join한다.

바르게 읽기 zero row가 필요하면 customer driving relation과 LEFT JOIN을 설계한다.

반례 customer 5는 account조차 없고 결과에 나타나지 않는다.

❌ REVERSAL amount는 SUM에서 자동으로 음수다.

왜 틀리나 query는 positive business_tx.amount를 그대로 합치며 tx_type에 따른 sign 변환이 없다.

바르게 읽기 signed 정책이 필요하면 명시적 CASE나 ledger source를 사용한다.

반례 두 reversal 600+600이 customer 1 합에 +1,200으로 들어간다.

❌ oracle 주석이 자동 회귀 assertion이다.

왜 틀리나 마지막 줄은 SQL expression이 아닌 comment다.

바르게 읽기 실행 output을 structured expected rows와 별도로 비교한다.

반례 seed가 바뀌어도 query는 실행되고 오래된 comment는 실패를 일으키지 않는다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

이 코드가 책임지지 않는 일을 분리합니다.

canonicality

shipped workbook canonical answer 또는 유일한 정답

이 책임을 맡는 곳: workbook answer authority and review
status policy

SUCCESS-only financial total

이 책임을 맡는 곳: explicit prompt/business status contract
zero preservation

거래 없는 customer의 zero row

이 책임을 맡는 곳: customer-driven outer-join query
money semantics

currency conversion·signed cash flow·reversal netting

이 책임을 맡는 곳: money model and explicit sign/currency policy
oracle enforcement

주석의 두 expected row가 실제 output과 일치함

이 책임을 맡는 곳: executable result comparator
12

STEP 12 / 13

직접 다시 써보기

뜻 → 조각 → 전체 코드 순서로 다시 씁니다.

1단계 · 뜻부터 복원

clock→bounds→recent_tx→account join→customer group→count/sum 순서와 all-status·inner-only 경계를 말한다.

2단계 · 코드 조각 재조립

  1. fixture_clock as_of
  2. >= window_start
  3. < window_end
  4. no status predicate
  5. recent_tx JOIN account
  6. GROUP BY customer_id

3단계 · 파일 전체 다시 쓰기

24개 물리 줄을 정확히 다시 쓰고 SHA-256 0a0d7511...46d3과 대조한다.

자가 점검
  • 시작 포함·끝 제외를 말했는가?
  • FAILED·PROCESSING 포함을 숨기지 않았는가?
  • 거래 없는 customer가 사라지는 이유를 설명했는가?
  • 7/1,201,699와 4/3,303을 seed로 재계산했는가?
  • 비정본 예시와 shipped answer를 구분했는가?
13

STEP 13 / 13

전체 원본 정답

감사로 고정한 전체 source를 가감 없이 확인합니다.

학습용 예시 전체 확인하기 · 정본 답안 아님
학습용 예시 · 정본 답안 아님illustrative/sql/W16-SQL-Q23.sqlSHA-256 0a0d7511df0e802b59bd88010a3e313ddfc98599bc1ae9be7be5c9d3f2b146d3
W16-SQL-Q23.sql — 고정 시계의 반열린 7일 고객 집계 전체
-- W16-SQL-Q23 illustrative example; not a shipped workbook answer.
-- Assumption: recent seven days is the half-open interval [fixture_clock.as_of - 7 days, fixture_clock.as_of).
-- All statuses are included because the prompt does not request a status filter; only customers with rows appear.
SET search_path TO :"workbook_schema", public;

WITH bounds AS (
    SELECT as_of - INTERVAL '7 days' AS window_start, as_of AS window_end
    FROM fixture_clock
), recent_tx AS (
    SELECT t.tx_id, t.account_id, t.amount, t.status
    FROM business_tx AS t
    CROSS JOIN bounds AS b
    WHERE t.occurred_at >= b.window_start
      AND t.occurred_at < b.window_end
)
SELECT
    a.customer_id,
    COUNT(*) AS tx_count,
    SUM(r.amount) AS total_amount
FROM recent_tx AS r
JOIN account AS a ON a.account_id = r.account_id
GROUP BY a.customer_id
ORDER BY a.customer_id;
-- Fixture oracle: customer 1 = 7 rows / 1,201,699; customer 2 = 4 rows / 3,303.
08

W16-SQL-Q24.sql — 두 threshold의 순서형 balance band

illustrative/sql/W16-SQL-Q24.sql

fixture 기반 학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W16-F08
15줄 연결15줄 번역4 chunks
01

STEP 01 / 13

오늘 이 코드에서 해결할 문제

무엇을 이해해야 하는지 질문부터 잡습니다.

오늘의 한 문장

prompt에 없던 두 threshold를 example assumption으로 공개하고 account.balance를 ordered CASE의 SMALL·MEDIUM·LARGE 세 band로 분류한다.

  1. 100000과 500000은 prompt 사실일까 예시 가정일까?
  2. searched CASE는 WHEN을 어떤 순서로 평가할까?
  3. 100000·499999·500000은 각각 어느 branch에 갈까?
  4. ELSE LARGE가 nullable column에서도 >=500000만 뜻할까?
  5. amount_band가 business_tx.amount나 currency band일까?
이 파일에서 끝까지 다시 쓰는 값SMALL: balance < 100000, count=4MEDIUM: 100000 <= balance < 500000, count=2LARGE: balance >= 500000 under NOT NULL fixture, count=2100000=MEDIUM; 499999=MEDIUM; 500000=LARGEaccount rows=8
02

STEP 02 / 13

아주 짧게: 이 코드는 왜 필요할까?

웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.

STARRY가 account.balance 카드를 100,000과 500,000 문턱에 순서대로 통과시킨다.

두 문턱을 차례로 지나는 잔액 카드

이 예시는 account.balance가 100,000 미만이면 SMALL, 500,000 미만이면 MEDIUM, 나머지는 LARGE로 분류한다.

고정 fixture에서는 SMALL 4·MEDIUM 2·LARGE 2이며 100,000과 500,000이 서로 다른 경계편에 놓인다.

딱 여기까지만 threshold는 prompt 원문 수치가 아니라 예시의 명시적 가정이고 business_tx.amount·currency 정책으로 확장하지 않는 비정본 SQL이다.

03

STEP 03 / 13

초등학생도 이해하는 설명

생활 비유와 실제 코드의 경계를 함께 확인합니다.

위에서 첫 참

CASE는 첫 WHEN이 참이면 아래 WHEN을 더 선택하지 않는다.

코드 연결
9~13줄
비유
첫 문턱에서 도장을 받으면 다음 줄로 다시 분류하지 않는다.
비유의 끝
optimizer 구현 순서가 아니라 searched CASE의 result semantics를 말한다.

equality는 다음 칸

`<`이므로 100000은 SMALL이 아니고 500000은 MEDIUM이 아니다.

코드 연결
10~12줄
비유
문턱선과 같은 숫자는 그 문을 통과하지 못하고 다음 바구니로 간다.
비유의 끝
threshold가 inclusive로 바뀌면 경계 분류도 바뀐다.

ELSE의 실제 범위

frozen balance는 NOT NULL이라 앞 두 조건이 false인 500000 이상이 LARGE다.

코드 연결
12줄 + schema authority
비유
두 문턱에서 선택되지 않은 카드를 마지막 바구니가 받는다.
비유의 끝
nullable source로 옮기면 NULL도 ELSE에 들어갈 수 있다.

분류 대상은 account.balance

FROM account의 balance만 읽고 transaction amount는 보지 않는다.

코드 연결
8·10~14줄
비유
계좌 카드의 현재 잔액 칸만 자로 잰다.
비유의 끝
currency·available balance·업무 segment 계약은 이 예시에 없다.
두 수치의 출처히토리 → 니지카 → 료 → 키타
  1. 히토리

    100000과 500000은 문제에 적힌 공식 경계인가요?

  2. 니지카

    아니, prompt에 숫자가 없어서 이 illustrative example이 assumption으로 정했어.

  3. source truth와 policy choice를 분리하지 않으면 예시가 요구사항처럼 굳어진다.

  4. 키타

    2줄 주석을 title과 한계 카드에도 그대로 결속하겠습니다.

100000의 자리히토리 → 니지카 → 료 → 키타
  1. 히토리

    SMALL 문턱에 100000이 적혀 있으니 그 값도 SMALL 아닌가요?

  2. 니지카

    연산자가 `<`라서 같은 값은 첫 문을 통과하지 못해.

  3. 다음 `<500000`이 true라 정확히 100000은 MEDIUM이다.

  4. 키타

    10·11줄에 숫자를 대입해 boolean 두 개를 적겠습니다.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

감사 규칙상 연결 대상인 원본 15줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.fixture 기반 학습용 SQL 예시 · 정본 답안 아님에서 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.15 / 15 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F08-L01 -- W16-SQL-Q24 illustrative example; not a shipped workbook answer. STARRY가 Q24 카드 위에도 ‘설명용 예시’ 표찰을 붙여 공식 답안과 섞이지 않게 한다. 첫 comment가 이 SQL을 shipped workbook answer와 구별되는 W16 Q24 학습 artifact로 분류한다.
입력
독자가 첫 provenance 주석을 query 해석의 출발 정보로 읽는다.
결과·효과
뒤 CASE를 audit-authored 비정본 예시로 다뤄야 한다는 범위가 표시된다.
비유의 한계
이 문장만으로 threshold·table·oracle의 타당성이나 실행 결과는 증명되지 않는다.
2줄F08-L02 -- Assumption: classify account.balance as SMALL <100000, MEDIUM <500000, otherwise LARGE. 잔액 카드를 100,000과 500,000 두 문턱에 순서대로 통과시키는 분류 규칙을 게시한다. account.balance를 SMALL<100000, MEDIUM<500000, 그 밖은 LARGE로 나눈다는 예시 가정이다.
입력
두 explicit integer threshold와 세 label을 분류 계약 입력으로 둔다.
결과·효과
prompt에 없던 수치를 이 example이 선택했다는 사실과 band 순서가 문서화된다.
비유의 한계
주석은 CASE를 실행하지 않으며 이 threshold가 업무 승인값이라는 근거도 제공하지 않는다.
3줄F08-L03 -- The frozen 100000/499999/500000 balances execute both CASE boundaries. 문턱 바로 아래·위 카드를 준비해 두 갈림길을 모두 밟는지 확인 표시를 한다. frozen balances 100000, 499999, 500000이 두 CASE boundary를 실행한다고 적은 주석이다.
입력
동결 account fixture의 세 경계값을 branch coverage 근거로 사용한다.
결과·효과
첫 threshold의 equality와 둘째 threshold 양쪽을 관찰할 seed가 있음을 알린다.
비유의 한계
세 boundary seed를 언급한 comment에는 전체 band count와 나머지 balance row의 분류가 없다.
4줄F08-L04 SET search_path TO :"workbook_schema", public; 계좌표를 찾을 때 workbook_schema 선반을 public보다 먼저 살피도록 안내 화살표를 세운다. psql session이 unqualified identifier를 찾을 schema precedence에 supplied workbook schema 뒤 public을 기록한다.
입력
identifier variable `workbook_schema`와 literal public을 session setting 입력으로 받는다.
결과·효과
뒤의 unqualified object name이 앞선 schema부터 해석될 환경이 마련된다.
비유의 한계
schema 존재·object bytes·privilege나 variable 값의 안전성은 확인하지 않는다.
6줄F08-L06 SELECT 계좌별 분류 결과표에 넣을 칸을 고르려고 SELECT 머리표만 먼저 펼친다. main query의 projection list를 여는 SELECT keyword다.
입력
SQL parser가 SELECT token을 받는다.
결과·효과
뒤 column·expression을 output으로 받을 query scope가 시작된다.
비유의 한계
이 opener에는 source relation·분기 기준값·정렬 정보가 없다.
7줄F08-L07 account_id, 각 분류 카드가 어느 계좌인지 잃지 않도록 account_id 번호칸을 복사한다. account_id를 첫 output column으로 projection한다.
입력
뒤 FROM scope에서 제공될 row의 account_id 값을 받는다.
결과·효과
query가 완성되면 source identifier가 결과 각 행에 보존된다.
비유의 한계
현재 expression만으로 source table·row count·order를 정할 수 없다.
8줄F08-L08 balance, 문턱을 통과시킬 원래 숫자를 함께 보려고 balance 칸도 결과표에 붙인다. balance를 두 번째 output column으로 projection한다.
입력
뒤 FROM scope가 제공할 row의 balance 값을 expression 입력으로 읽는다.
결과·효과
각 결과 행이 band label과 대조할 원본 잔액을 보여 줄 준비가 된다.
비유의 한계
balance projection 자체는 branch를 실행하지 않고 currency·available-balance 의미도 정의하지 않는다.
9줄F08-L09 CASE 잔액별 이름표를 고르는 순서형 갈림길의 입구를 연다. searched CASE expression을 시작한다.
입력
SQL evaluator가 뒤 boolean WHEN branch들을 위에서부터 평가할 expression scope를 받는다.
결과·효과
첫 true branch의 값을 선택할 분기 구조가 열리지만 아직 조건과 label은 없다.
비유의 한계
CASE opener만으로 threshold·ELSE·NULL 처리는 결정되지 않는다.
10줄F08-L10 WHEN balance < 100000 THEN 'SMALL' 첫 문턱 100,000보다 작은 잔액 카드에 SMALL 도장을 찍는다. balance < 100000이 true인 첫 branch의 result를 `SMALL`로 지정한다.
입력
현재 row의 balance와 integer 100000을 비교한다.
결과·효과
0·10000·20300처럼 threshold 미만인 row는 첫 true branch에서 SMALL을 선택한다.
비유의 한계
100000 equality와 그보다 큰 값은 이 branch에 들지 않으며 이후 label은 아직 정하지 않는다.
11줄F08-L11 WHEN balance < 500000 THEN 'MEDIUM' 첫 문턱을 지난 카드 중 500,000보다 작은 잔액에는 MEDIUM 표찰을 붙인다. 첫 WHEN이 false이고 balance < 500000이 true인 branch result를 MEDIUM으로 둔다.
입력
앞 branch를 통과하지 않은 balance와 integer 500000을 순서대로 비교한다.
결과·효과
100000과 499999는 두 번째 true branch에서 MEDIUM으로 선택된다.
비유의 한계
500000 equality·그 이상은 여전히 매칭되지 않으며 WHEN 순서를 바꾸면 구간이 달라질 수 있다.
12줄F08-L12 ELSE 'LARGE' 두 문턱 어디에도 들어가지 않은 잔액 카드를 LARGE 바구니로 보낸다. 앞 WHEN condition이 모두 true가 아닐 때 ELSE result로 LARGE를 반환한다.
입력
두 comparison에서 선택되지 않은 account balance row를 fallback 입력으로 받는다.
결과·효과
NOT NULL인 frozen account.balance에서는 500000 이상 값이 LARGE를 선택한다.
비유의 한계
nullable column에 재사용하면 NULL도 ELSE로 갈 수 있어 ‘항상 >=500000’이라는 일반 규칙은 아니다.
13줄F08-L13 END AS amount_band 분류 갈림길을 닫고 완성된 label 열에 amount_band라는 이름표를 단다. CASE expression을 END로 닫고 derived output column alias를 amount_band로 지정한다.
입력
앞 세 branch가 선택한 text result를 하나의 scalar expression으로 받는다.
결과·효과
각 candidate row가 SMALL·MEDIUM·LARGE 중 하나의 amount_band 값을 내보낼 준비가 끝난다.
비유의 한계
alias 이름은 account balance를 분류한 사실을 amount나 currency 정책으로 바꾸지 않는다.
14줄F08-L14 FROM account 분류할 원본 카드 더미로 account 표 전체를 작업대에 올린다. query의 FROM relation을 account로 지정한다.
입력
현재 search_path에서 resolve된 account rows를 driving input으로 읽는다.
결과·효과
account rows가 projection과 CASE에 공급될 source candidates가 된다.
비유의 한계
이 FROM line 자체는 customer·status·currency predicate나 transaction amount source를 추가하지 않는다.
15줄F08-L15 ORDER BY account_id; 완성된 여덟 계좌 카드를 account_id가 작은 순서대로 줄 세운다. result rows를 account_id 오름차순으로 정렬한다.
입력
account source에서 projection된 rows와 identifier를 sort 입력으로 사용한다.
결과·효과
동일 fixture에서는 101부터 108까지 안정적으로 읽기 좋은 순서가 된다.
비유의 한계
band별 grouping·count를 수행하거나 label 자체의 정렬 순서를 정의하지 않는다.
16줄F08-L16 -- Fixture oracle: SMALL=4, MEDIUM=2, LARGE=2; 100000=MEDIUM, 499999=MEDIUM, 500000=LARGE. 분류가 끝난 뒤 세 바구니 수와 경계 카드 결과를 검산표에 적는다. fixture oracle을 SMALL=4, MEDIUM=2, LARGE=2와 세 boundary assignment로 기록한다.
입력
동결 account balances와 source의 ordered CASE thresholds를 전제로 계산한다.
결과·효과
100000·499999는 MEDIUM, 500000은 LARGE이며 total 8개라는 예상이 문서화된다.
비유의 한계
실행 assertion이 아니고 fixture·threshold·CASE order가 바뀌면 oracle도 다시 계산해야 한다.
WHEN 순서의 영향히토리 → 니지카 → 료 → 키타
  1. 히토리

    두 WHEN은 조건이 분명하니 순서를 바꿔도 되죠?

  2. 니지카

    넓은 500000 문을 먼저 놓으면 10000도 거기서 MEDIUM을 받아.

  3. searched CASE는 first true result라 branch order가 구간 정의의 일부다.

  4. 키타

    순서를 뒤집은 10000 counterexample을 실행해 보겠습니다.

05

STEP 05 / 13

원본 코드 조각

원본을 4개 의미 조각으로 나누어 그대로 확인합니다.

파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.

F08-C01 · provenance and thresholds1–4줄
1–4줄 원본
-- W16-SQL-Q24 illustrative example; not a shipped workbook answer.
-- Assumption: classify account.balance as SMALL <100000, MEDIUM <500000, otherwise LARGE.
-- The frozen 100000/499999/500000 balances execute both CASE boundaries.
SET search_path TO :"workbook_schema", public;
F08-C02 · account projection and CASE opener5–9줄
5–9줄 원본

SELECT
    account_id,
    balance,
    CASE
F08-C03 · ordered CASE branches10–13줄
10–13줄 원본
        WHEN balance < 100000 THEN 'SMALL'
        WHEN balance < 500000 THEN 'MEDIUM'
        ELSE 'LARGE'
    END AS amount_band
F08-C04 · source, stable order, and oracle14–16줄
14–16줄 원본
FROM account
ORDER BY account_id;
-- Fixture oracle: SMALL=4, MEDIUM=2, LARGE=2; 100000=MEDIUM, 499999=MEDIUM, 500000=LARGE.
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

비어 있지 않은 15줄을 모두 한국어로 옮깁니다.

전체 번역 15 / 15

비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.

원본한국어 번역
1-- W16-SQL-Q24 illustrative example; not a shipped workbook answer.W16 Q24의 학습 예시이며 배포된 workbook 정답이 아님을 밝힌다.
2-- Assumption: classify account.balance as SMALL <100000, MEDIUM <500000, otherwise LARGE.account.balance를 100000 미만 SMALL, 500000 미만 MEDIUM, 나머지 LARGE로 분류한다고 가정한다.
3-- The frozen 100000/499999/500000 balances execute both CASE boundaries.동결된 100000·499999·500000 잔액이 두 CASE 경계를 실행한다고 적는다.
4SET search_path TO :"workbook_schema", public;이 session의 search_path를 supplied workbook schema와 public 순서로 설정한다.
6SELECT최종 SELECT projection을 시작한다.
7 account_id,account_id를 결과 열로 선택한다.
8 balance,balance를 결과 열로 선택한다.
9 CASE순서형 searched CASE expression을 연다.
10 WHEN balance < 100000 THEN 'SMALL'balance가 100000보다 작으면 SMALL을 반환한다.
11 WHEN balance < 500000 THEN 'MEDIUM'첫 branch가 아니면서 balance가 500000보다 작으면 MEDIUM을 반환한다.
12 ELSE 'LARGE'앞 조건에 해당하지 않으면 LARGE를 반환한다.
13 END AS amount_bandCASE를 닫고 결과 열 이름을 amount_band로 정한다.
14FROM accountaccount의 모든 row를 source로 읽는다.
15ORDER BY account_id;결과를 account_id 오름차순으로 정렬한다.
16-- Fixture oracle: SMALL=4, MEDIUM=2, LARGE=2; 100000=MEDIUM, 499999=MEDIUM, 500000=LARGE.fixture oracle은 SMALL 4·MEDIUM 2·LARGE 2와 경계값의 분류를 기록한다.
07

STEP 07 / 13

기존 수준의 한 줄 읽기·문법 해부

쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.

한 줄로 읽기

ordered searched CASE가 NOT NULL BIGINT account.balance를 audit-selected thresholds로 분류하는 fixture-exercised illustrative projection이다.

문법 해부

  • searched CASE는 각 WHEN boolean을 위에서부터 보고 첫 true result를 선택한다.
  • 두 `<` predicate가 SMALL (-∞,100000), MEDIUM [100000,500000), ELSE remainder를 만든다.
  • account.balance의 NOT NULL CHECK(balance>=0)는 ELSE를 frozen schema에서 >=500000으로 좁힌다.
  • ORDER BY account_id는 row presentation만 안정화하고 band count를 계산하지 않는다.

실행 순서

  1. search_path setting
  2. account scan
  3. project id/balance
  4. first threshold
  5. second threshold
  6. ELSE fallback
  7. amount_band alias
  8. account-id sort

원래 W6 수준의 조각별 정밀 해설

F08-C01 · provenance and thresholds
문법 해부
comments가 non-shipped Q24 example, SMALL<100000·MEDIUM<500000·otherwise LARGE assumption과 100000/499999/500000 boundary seed를 공개하고 search_path를 설정한다.
실제 값 추적
독자는 두 threshold가 prompt 원문이 아닌 example 선택이며 three frozen balances가 equality 양쪽을 관찰한다는 사실을 실행 전 알게 된다.
정상 예
supplied workbook schema에서 audit-authored threshold policy를 학습 예시로 평가하는 것은 이 header 계약과 맞는다.
틀린 예·반례
100000·500000을 production 승인 수치나 shipped canonical answer로 인용하고 comment가 classification을 강제한다고 보면 틀리다.
착각 방지
두 `<` 수치의 출처와 provenance를 숨기지 않는 것이 핵심이며 boundary coverage comment는 count result가 아니다.
하지 않는 일
schema identity·row source·NULL semantics·band counts·currency policy는 이 header 범위만으로 검증되지 않는다.
다음 연결
다음 ‘account projection and CASE opener’ 범위가 output columns와 searched CASE scope를 연다.
F08-C02 · account projection and CASE opener
문법 해부
blank separator 뒤 SELECT가 account_id와 balance를 projection하고 searched CASE expression을 시작한다.
실제 값 추적
각 eventual input row에서 identifier와 원래 balance를 output에 보존하면서 boolean branches의 첫 true result를 받을 scope가 열린다.
정상 예
두 base column을 detail row에 그대로 보여 주고 derived label expression을 준비하는 것은 valid projection shape다.
틀린 예·반례
CASE opener만 보고 특정 label·threshold·fallback result가 이미 선택됐거나 group count가 계산된다고 말하면 선취다.
착각 방지
searched CASE는 뒤 conditions를 순서대로 평가하지만 이 slice에는 아직 한 branch도 적혀 있지 않다.
하지 않는 일
source relation·row filter·sort·NULL constraint와 실제 band policy 실행은 이 다섯 물리 줄이 끝내지 않는다.
다음 연결
다음 ‘ordered CASE branches’ 범위가 두 `<` comparison과 ELSE를 순서대로 배치하고 amount_band alias를 완성한다.
F08-C03 · ordered CASE branches
문법 해부
CASE는 balance<100000이면 SMALL, 그 branch가 아니며 balance<500000이면 MEDIUM, 나머지는 LARGE를 선택하고 result alias를 amount_band로 둔다.
실제 값 추적
99999는 SMALL, 100000·499999는 MEDIUM, 500000은 LARGE가 되며 first-true order가 구간을 정의한다.
정상 예
두 equality boundary와 바로 아래 값을 expression에 대입해 세 label을 얻는 것은 valid branch example다.
틀린 예·반례
넓은 `<500000` WHEN을 먼저 옮기면 10000도 MEDIUM에서 멈추므로 같은 분류라고 할 수 없다.
착각 방지
현재 pinned account.balance는 NOT NULL이지만 nullable column에 복사하면 NULL comparison이 UNKNOWN이라 ELSE LARGE로 흐를 수 있다.
하지 않는 일
threshold 업무 승인·currency·available balance·transaction amount와 band별 count는 CASE branch slice가 책임지지 않는다.
다음 연결
다음 ‘source, stable order, and oracle’ 범위가 account relation·id 정렬과 exact 4/2/2 fixture result를 결속한다.
F08-C04 · source, stable order, and oracle
문법 해부
FROM account가 all account detail rows를 공급하고 ORDER BY account_id가 표시 순서를 고정하며 final comment가 SMALL=4·MEDIUM=2·LARGE=2와 equality assignments를 적는다.
실제 값 추적
balances 10000·20300·0·0은 SMALL, 100000·499999는 MEDIUM, 500000·1000000은 LARGE라 total 8 rows가 id 101→108 순으로 나온다.
정상 예
frozen account seed를 그대로 읽으면 세 band count와 100000=MEDIUM, 499999=MEDIUM, 500000=LARGE oracle이 모두 맞는다.
틀린 예·반례
CLOSED account107을 제외하거나 business_tx.amount를 source로 바꾸면 current all-account 4/2/2 result와 달라진다.
착각 방지
WHERE가 없으므로 status를 가리지 않으며 detail SELECT 자체에는 GROUP BY/COUNT assertion이 없어 comment 수치는 별도 비교가 필요하다.
하지 않는 일
alias amount_band는 currency·risk·available-funds policy가 아니고 fixture oracle은 production invariant나 canonical answer가 아니다.
다음 연결
W16 source closure의 마지막 range다. 전체를 다시 볼 때 threshold assumption·ordered CASE·NOT NULL authority·4/2/2 reference를 함께 분리한다.
ELSE와 NULL히토리 → 니지카 → 료 → 키타
  1. 히토리

    ELSE LARGE는 언제나 balance>=500000의 짧은 표현인가요?

  2. 니지카

    여기서는 balance가 NOT NULL이라 그렇게 읽을 수 있어.

  3. nullable column이면 NULL comparisons가 UNKNOWN이라 ELSE로 흐른다.

  4. 키타

    schema constraint와 CASE source를 같이 인용하겠습니다.

08

STEP 08 / 13

실제 값 따라가기

같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.

순서들어온 값코드가 하는 일나온 값·상태경계
SMALLbalances 10000, 20300, 0, 0첫 predicate balance<100000을 평가한다.네 account가 SMALL을 선택한다.negative balance는 schema CHECK가 막지만 CASE 자체는 음수도 SMALL로 분류한다.
first equalitybalance=100000첫 `<100000`은 false, 둘째 `<500000`은 true가 된다.account 105가 MEDIUM으로 분류된다.첫 operator를 <=로 바꾸면 결과가 SMALL로 이동한다.
upper medium edgebalance=499999첫 branch를 지나 둘째 threshold와 비교한다.account 106이 MEDIUM을 선택한다.500000과 한 단위 차이가 서로 다른 band를 만든다.
LARGE boundarybalances 500000 and 1000000두 WHEN이 모두 false라 ELSE를 사용한다.accounts 107·108 두 행이 LARGE다.account 107의 CLOSED status는 이 query의 predicate가 아니다.
alias가 말하지 않는 것히토리 → 니지카 → 료 → 키타
  1. 히토리

    amount_band니까 거래 금액이나 통화도 함께 분류한 거죠?

  2. 니지카

    표현식은 account.balance 하나뿐이고 currency column은 없어.

  3. alias 이름보다 FROM과 expression lineage가 실제 책임 범위를 정한다.

  4. 키타

    8·14줄만 따라가 분류 대상을 다시 적을게요.

09

STEP 09 / 13

PowerShell·SQL·DB 내부에서 벌어지는 일

PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.

psql/session

workbook schema identifier를 search_path에 놓아 unqualified account를 resolve한다.

schema identity를 content hash나 OID로 고정하지 않는다.
PostgreSQL expression executor

각 account row에 searched CASE result를 하나 계산한다.

band policy의 업무 적합성을 판단하지 않는다.
type/constraint authority

balance는 BIGINT NOT NULL CHECK(balance>=0)인 frozen schema column이다.

다른 nullable·decimal·multi-currency column에 같은 ELSE 해석을 옮길 수 없다.
projection

source id·balance와 derived text label을 한 output row에 둔다.

GROUP BY나 count aggregate는 수행하지 않는다.
sort

account_id key로 display order를 고정한다.

band 우선순위·업무 ranking·index 사용을 보장하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

그럴듯하지만 틀린 해석을 반례로 고칩니다.

❌ 100000·500000은 Q24 prompt가 요구한 공식 수치다.

왜 틀리나 prompt에는 threshold 숫자가 없고 illustrative source가 assumption으로 추가했다.

바르게 읽기 수치를 답안 사실이 아니라 공개된 example policy로 표시한다.

반례 업무 owner가 50000·300000을 승인하면 같은 CASE 구조라도 oracle이 달라진다.

❌ 100000도 SMALL이다.

왜 틀리나 첫 comparison은 <=가 아니라 <다.

바르게 읽기 경계값을 연산자와 함께 직접 대입한다.

반례 100000<100000은 false이고 다음 100000<500000이 true다.

❌ WHEN 두 줄의 순서를 바꿔도 같은 세 구간이다.

왜 틀리나 넓은 `<500000` branch가 먼저 오면 SMALL 후보도 첫 true에서 MEDIUM이 된다.

바르게 읽기 좁은 threshold부터 위에 두고 ordered semantics를 검토한다.

반례 balance 10000은 순서를 뒤집으면 첫 branch에서 MEDIUM을 선택한다.

❌ ELSE는 SQL 어디서나 `balance>=500000`과 완전히 같다.

왜 틀리나 nullable input에서는 comparison이 UNKNOWN이 되어 NULL도 ELSE로 갈 수 있다.

바르게 읽기 현재 equivalence를 account.balance NOT NULL authority에 묶는다.

반례 nullable_balance=NULL이면 두 WHEN이 true가 아니어서 LARGE text가 반환된다.

❌ amount_band는 business_tx.amount의 구간이다.

왜 틀리나 source는 FROM account와 balance expression만 사용한다.

바르게 읽기 alias보다 expression lineage를 따라 분류 대상을 적는다.

반례 한 transaction의 amount가 1이어도 account balance가 500000이면 이 query의 label은 LARGE다.

❌ 마지막 oracle 주석이 band count를 SQL로 검사한다.

왜 틀리나 query는 account별 detail row만 반환하고 COUNT/GROUP BY가 없다.

바르게 읽기 여덟 detail row를 별도 comparator에서 count한다.

반례 주석이 틀려도 SELECT native exit는 그대로 0일 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

이 코드가 책임지지 않는 일을 분리합니다.

policy authority

threshold 100000·500000이 production 승인 기준임

이 책임을 맡는 곳: explicit product/risk policy
canonicality

shipped workbook의 유일한 Q24 answer

이 책임을 맡는 곳: canonical workbook answer source
data lineage

transaction amount·available balance·currency-adjusted value 분류

이 책임을 맡는 곳: approved metric definition
NULL portability

nullable input에서 ELSE가 오직 >=500000임

이 책임을 맡는 곳: column constraint and explicit NULL branch
oracle enforcement

SMALL4·MEDIUM2·LARGE2가 실행 중 assert됨

이 책임을 맡는 곳: detail-row result comparator
12

STEP 12 / 13

직접 다시 써보기

뜻 → 조각 → 전체 코드 순서로 다시 씁니다.

1단계 · 뜻부터 복원

account source→id/balance projection→ordered thresholds→ELSE→alias→sort 순서와 비정본 policy 경계를 말한다.

2단계 · 코드 조각 재조립

  1. balance < 100000
  2. balance < 500000
  3. first true branch
  4. ELSE remainder
  5. FROM account
  6. ORDER BY account_id

3단계 · 파일 전체 다시 쓰기

16개 물리 줄을 줄바꿈까지 재작성하고 SHA-256 b23fe0ba...8f1d와 비교한다.

자가 점검
  • threshold가 prompt가 아닌 example assumption임을 적었는가?
  • 100000·499999·500000을 각 branch에 대입했는가?
  • WHEN 순서를 바꾼 반례를 설명했는가?
  • NOT NULL authority와 nullable 반례를 분리했는가?
  • account.balance와 transaction amount를 혼동하지 않았는가?
13

STEP 13 / 13

전체 원본 정답

감사로 고정한 전체 source를 가감 없이 확인합니다.

학습용 예시 전체 확인하기 · 정본 답안 아님
학습용 예시 · 정본 답안 아님illustrative/sql/W16-SQL-Q24.sqlSHA-256 b23fe0ba8283b51bf7ac5afe1cb9dc949120d0cb13ad95fcb95d36c8cc458f1d
W16-SQL-Q24.sql — 두 threshold의 순서형 balance band 전체
-- W16-SQL-Q24 illustrative example; not a shipped workbook answer.
-- Assumption: classify account.balance as SMALL <100000, MEDIUM <500000, otherwise LARGE.
-- The frozen 100000/499999/500000 balances execute both CASE boundaries.
SET search_path TO :"workbook_schema", public;

SELECT
    account_id,
    balance,
    CASE
        WHEN balance < 100000 THEN 'SMALL'
        WHEN balance < 500000 THEN 'MEDIUM'
        ELSE 'LARGE'
    END AS amount_band
FROM account
ORDER BY account_id;
-- Fixture oracle: SMALL=4, MEDIUM=2, LARGE=2; 100000=MEDIUM, 499999=MEDIUM, 500000=LARGE.