STARRY PASS 코드 뒤풀이 · W16 Ver2 전체
16주차 코드 — SQL 논리 순서·JOIN·NULL·집계
고정 fixture와 W16 실행기의 실제 보장 범위를 읽고, 논리 순서·anti-join·집계 대사와 Q23/Q24의 명시적 가정을 초보자 눈높이로 확인합니다.
01compose.yaml — PostgreSQL db service·healthcheck·disposable volume 선언
runtime/compose.yaml
정본 PostgreSQL Compose runtime · 정본 · W16-F0118줄 연결18줄 번역5 chunks
compose.yaml — PostgreSQL db service·healthcheck·disposable volume 선언
runtime/compose.yaml
정본 PostgreSQL Compose runtime · 정본 · W16-F01STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
W16 runner가 올릴 PostgreSQL service의 image·port·환경값·health·storage 계약을 source line에서 읽는다.
- 두 variable interpolation form은 unset과 empty를 어떻게 다르게 처리할까?
- pg_isready가 직접 증명하는 범위는 어디까지일까?
- tag와 digest는 왜 다른가?
- named volume은 runner cleanup에서 어떻게 사라질까?
- 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/dataSTEP 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에서는 보관함도 제거된다.
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줄을 읽자마자 db container가 생긴 건가요?
-
services 표지만 열렸고 아직 어떤 service 이름도 나오지 않았어.
-
YAML opener의 downstream state는 nested key를 받을 scope까지다.
-
1줄 설명에서 db·image 값을 빼고 현재 scope만 확인하겠습니다.
두 interpolation form
-
port와 password 모두 변수가 비면 default가 들어가죠?
-
5줄의 `:-`만 fallback이고 9줄의 `:?`는 message와 함께 멈춰.
-
unset뿐 아니라 empty도 두 form의 branch 조건에 포함된다.
-
빈 문자열을 넣은 config 결과를 두 줄별로 대조할게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 18줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F01-L01 | services: |
STARRY가 여러 무대를 적을 운영표의 최상단 표지를 펼친다. | Compose 문서에서 top-level `services` mapping을 연다.
|
| 2줄F01-L02 | db: |
운영표 안에 데이터베이스 무대용 `db` 칸을 새로 만든다. | services 아래 `db` service의 설정 mapping을 시작한다.
|
| 3줄F01-L03 | image: |
db 무대에 PostgreSQL 17.10 alpine 포장 상자를 올린다. | db service가 사용할 image tag를 `postgres:17.10-alpine`으로 지정한다.
|
| 4줄F01-L04 | ports: |
외부 입구 번호표를 달기 위한 빈 목록 걸이를 설치한다. | db service의 port publication list를 여는 `ports` key다.
|
| 5줄F01-L05 | - "${ |
밖의 문은 사용자가 고르되 빈 표찰이면 5432, 안쪽 문은 항상 5432로 단다. | host port는 FCL_DB_PORT의 unset·empty fallback 5432를 쓰고 container port 5432에 publish한다.
|
| 6줄F01-L06 | environment: |
PostgreSQL 상자 안에 넣을 환경표의 빈 봉투를 연다. | db container environment mapping의 시작을 선언한다.
|
| 7줄F01-L07 | POSTGRES_DB: |
첫 환경표에 새로 만들 기본 database 이름을 `financial_core`로 적는다. | PostgreSQL image의 POSTGRES_DB 값을 financial_core로 설정한다.
|
| 8줄F01-L08 | POSTGRES_USER: |
두 번째 환경표에는 database 주 사용자 이름 `app`을 적어 둔다. | POSTGRES_USER environment value를 app으로 지정한다.
|
| 9줄F01-L09 | POSTGRES_PASSWORD: |
비밀번호 표가 비었으면 무대를 열지 않고 정확한 준비 문구를 보여 준다. | FCL_DB_PASSWORD가 unset 또는 empty이면 지정 message로 interpolation을 실패시키고 아니면 그 값을 전달한다.
|
| 10줄F01-L10 | healthcheck: |
db 무대에 반복 건강 검진표를 붙일 빈 칸을 연다. | service healthcheck configuration mapping을 시작한다.
|
| 11줄F01-L11 | test: |
검진표가 shell에게 app·financial_core 주소로 접수창이 열렸는지 물어본다. | CMD-SHELL 방식으로 `pg_isready -U app -d financial_core`를 health probe로 실행한다.
|
| 12줄F01-L12 | interval: |
건강 질문을 한 번 한 뒤 다음 질문까지 2초 간격을 둔다. | healthcheck의 반복 interval을 2초로 설정한다.
|
| 13줄F01-L13 | timeout: |
한 번의 건강 답변을 기다리는 모래시계도 2초에서 멈춘다. | 각 health probe의 timeout 한도를 2초로 둔다.
|
| 14줄F01-L14 | retries: |
검진 실패 도장이 연속 30번 쌓이면 unhealthy 표찰을 붙이게 한다. | healthcheck retry threshold를 30으로 지정한다.
|
| 15줄F01-L15 | volumes: |
db 상자에 붙일 저장 공간 목록용 고리를 연다. | db service의 volume mount list를 시작한다.
|
| 16줄F01-L16 | - financial-core-db: |
이름 있는 보관함을 PostgreSQL data directory 자리에 연결한다. | named volume `financial-core-db`를 `/var/lib/postgresql/data`에 mount한다.
|
| 18줄F01-L18 | volumes: |
service 표 바깥에 이름 있는 보관함 목록의 최상단 표지를 다시 연다. | top-level named volumes mapping을 시작한다.
|
| 19줄F01-L19 | financial-core-db: |
마지막으로 `financial-core-db` 보관함 이름을 기본 설정으로 등록한다. | top-level named volume `financial-core-db`를 빈 option mapping으로 선언한다.
|
healthy의 좁은 뜻
-
pg_isready가 성공하면 W16 query도 통과했다고 써도 되나요?
-
창구가 연결을 받을 수 있다는 신호와 답안 SQL 결과는 다른 표야.
-
credential·schema·marker predicate는 healthcheck source에 없다.
-
11줄에는 server acceptance만 남기고 별도 psql gate를 적겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 5개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
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:
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 18줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | services: | Compose 문서에서 top-level `services` mapping을 연다. |
| 2 | db: | services 아래 `db` service의 설정 mapping을 시작한다. |
| 3 | image: postgres:17.10-alpine | db 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_core | PostgreSQL image의 POSTGRES_DB 값을 financial_core로 설정한다. |
| 8 | POSTGRES_USER: app | POSTGRES_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: 2s | healthcheck의 반복 interval을 2초로 설정한다. |
| 13 | timeout: 2s | 각 health probe의 timeout 한도를 2초로 둔다. |
| 14 | retries: 30 | healthcheck retry threshold를 30으로 지정한다. |
| 15 | volumes: | db service의 volume mount list를 시작한다. |
| 16 | - financial-core-db:/var/lib/postgresql/data | named volume `financial-core-db`를 `/var/lib/postgresql/data`에 mount한다. |
| 18 | volumes: | top-level named volumes mapping을 시작한다. |
| 19 | financial-core-db: | top-level named volume `financial-core-db`를 빈 option mapping으로 선언한다. |
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을 참조한다.
실행 순서
- Compose가 YAML과 variable interpolation을 해석한다.
- engine이 image와 service configuration을 resolve한다.
- container가 새 data directory를 초기화할 수 있다.
- health scheduler가 pg_isready를 반복한다.
- 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
-
버전까지 적힌 tag면 image hash와 같지 않나요?
-
사람이 읽는 tag와 content-addressed digest는 식별 방식이 달라.
-
현재 source에는 `@sha256:` digest가 없어서 immutable-byte claim은 금지다.
-
3줄 exact image 문자열과 미증명 provenance를 함께 기록하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| blank password | FCL_DB_PORT unset, FCL_DB_PASSWORD empty | Compose interpolation을 평가한다. | port는 5432를 고르지만 password `:?`에서 config가 실패한다. | runner는 Compose mode에서 blank password를 disposable 값으로 먼저 바꿀 수 있다. |
| resolved service | FCL_DB_PORT=55432, nonempty password | db service model을 만든다. | host 55432→container 5432, financial_core/app 환경값과 image tag가 잡힌다. | host 55432가 사용 가능하거나 firewall policy가 적절한지는 아직 모른다. |
| health probe | container가 실행 중이나 schema가 비어 있음 | pg_isready를 app·financial_core 이름으로 호출한다. | server가 연결을 받을 상태면 healthy가 될 수 있다. | 실제 password 로그인과 W16 table/query는 검사하지 않는다. |
| owned teardown | runner가 만든 Compose project와 named volume | runner finally의 down -v가 실행된다. | 그 project의 container·network·volume이 제거 대상이 된다. | Compose file 단독 실행이나 external backup의 lifecycle까지 말하지 않는다. |
volume이 사라지는 때
-
named volume이면 연습 결과가 계속 남겠네요?
-
container와 분리돼도 owner가 down -v를 부르면 volume도 지워져.
-
lifecycle claim은 compose 선언과 runner finally를 함께 읽어야 한다.
-
16·19줄 mount와 runner cleanup 경계를 원문에서 다시 찾겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
YAML nesting과 environment interpolation을 runtime model로 변환한다.
engine이 image를 pull하거나 port를 bind하기 전 단계다.image tag를 resolve하고 port·environment·mount configuration으로 container를 만든다.
tag bytes와 host resource availability는 실행 시점에 달라질 수 있다.interval·timeout·retries 조건으로 pg_isready command exit를 누적한다.
application-level correctness와 credential 검증은 범위 밖이다.project-scoped named volume을 PostgreSQL data path에 mount한다.
backup·encryption·retention이나 down -v 뒤 복구를 제공하지 않는다.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이 제거된다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
같은 tag가 항상 같은 image bytes임
이 책임을 맡는 곳: digest pinning, signed provenance, registry policyapp credential·schema·sentinel query가 성공함
이 책임을 맡는 곳: authenticated psql/application smoke and migration gatefallback host port가 free이고 공개 정책이 안전함
이 책임을 맡는 곳: host runtime, firewall, port ownership checkspassword strength·confidentiality·rotation
이 책임을 맡는 곳: secret manager and operational credential policyrunner 종료 뒤 database volume 보존·복구
이 책임을 맡는 곳: explicit backup/restore outside disposable Compose projectSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
각 YAML key 옆에 parse-time·container-time·health-time 중 어느 단계인지 표시한다.
2단계 · 코드 조각 재조립
- `:-` → unset/empty fallback
- `:?` → unset/empty error
- pg_isready → server acceptance only
- 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을 구분했는가?
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
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:
02fixture.sql — w16_project reset과 세 계좌·세 원장 row
sql/w16/fixture.sql
정본 W16 파괴적 회귀 fixture · 정본 · W16-F027줄 연결7줄 번역5 chunks
fixture.sql — w16_project reset과 세 계좌·세 원장 row
sql/w16/fixture.sql
정본 W16 파괴적 회귀 fixture · 정본 · W16-F02STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
Q1·Q2·Q3가 공유하는 schema reset, table 최소 모양, account·ledger exact fixture를 읽는다.
- 기존 w16_project가 있으면 어떤 범위가 지워질까?
- account_id FK가 NULL까지 막을까?
- 세 account의 positive·zero 경계는 무엇일까?
- 계좌별 signed 합과 global 합은 얼마일까?
- 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=150STEP 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·회계 완전성을 나타내지 않는다.
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를 지우나
-
Container mode면 기존 schema를 보존하겠죠?
-
mode와 무관하게 같은 fixture 2줄이 w16_project를 CASCADE로 지워.
-
cleanup 차이는 끝날 때이고 destructive reset은 시작에 공통이다.
-
2줄 target과 supplied Container 경고를 함께 적겠습니다.
FK가 null을 막나
-
account_id가 REFERENCES라서 빈 값은 못 들어가죠?
-
부모 id가 있는지 보는 끈과 반드시 끈을 달라는 규칙은 달라.
-
5줄 account_id clause에는 NOT NULL이 없다.
-
정확한 token을 보고 존재 O·필수 X를 확인할게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 7줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F02-L01 | \set ON_ERROR_STOP on |
STARRY가 SQL 묶음 중 한 장이 실패하면 뒤 장을 넘기지 말라는 빨간 표지를 세운다. | psql variable ON_ERROR_STOP을 on으로 설정한다.
|
| 2줄F02-L02 | DO $ddl$ BEGIN IF EXISTS( |
기존 W16 연습장이 있으면 안의 물건까지 함께 철거하는 조건부 지우개를 실행한다. | pg_namespace에 w16_project가 있으면 dynamic SQL로 `DROP SCHEMA w16_project CASCADE`를 실행하는 DO block이다.
|
| 3줄F02-L03 | CREATE SCHEMA w16_project; |
철거가 끝난 자리에 `w16_project`라는 새 빈 연습 구역을 만든다. | w16_project schema를 생성한다.
|
| 4줄F02-L04 | CREATE TABLE w16_project. |
연습 구역에 고유 번호와 필수 잔액만 적는 아주 작은 계좌표를 만든다. | account table을 id BIGINT primary key와 balance BIGINT not-null 두 column으로 생성한다.
|
| 5줄F02-L05 | CREATE TABLE w16_project. |
원장표에는 고유 번호, 선택 가능한 계좌 끈, 필수 signed 값과 시각 칸을 단다. | ledger_entry를 만들고 account_id FK, signed_amount·created_at NOT NULL, id PK를 선언한다.
|
| 6줄F02-L06 | INSERT INTO w16_project. |
계좌표에 1번 100, 2번 50, 3번 0이라는 세 고정 표본을 차례로 놓는다. | account에 `(1,100)`, `(2,50)`, `(3,0)` 세 positional row를 insert한다.
|
| 7줄F02-L07 | INSERT INTO w16_project. |
원장표에는 1번 계좌 +100 한 장, 2번 계좌 +70·-20 두 장을 시간표와 함께 놓는다. | ledger_entry에 id 1~3, account 1/2/2, signed 100/70/-20, 두 UTC timestamp를 insert한다.
|
세 계좌의 역할
-
account 3의 balance가 0이라 ledger도 0건이라고 자동인가요?
-
balance row와 child row 수는 따로 심고 따로 관찰해야 해.
-
6줄은 zero balance, 7줄의 account_id 목록 부재가 no-ledger fixture를 만든다.
-
두 줄을 분리해 account3 경계를 검산하겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 5개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
\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');
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 7줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | \set ON_ERROR_STOP on | psql variable ON_ERROR_STOP을 on으로 설정한다. |
| 2 | 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$; | pg_namespace에 w16_project가 있으면 dynamic SQL로 `DROP SCHEMA w16_project CASCADE`를 실행하는 DO block이다. |
| 3 | CREATE SCHEMA w16_project; | w16_project schema를 생성한다. |
| 4 | CREATE TABLE w16_project.account(id bigint PRIMARY KEY,balance bigint NOT NULL); | account table을 id BIGINT primary key와 balance BIGINT not-null 두 column으로 생성한다. |
| 5 | 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); | ledger_entry를 만들고 account_id FK, signed_amount·created_at NOT NULL, id PK를 선언한다. |
| 6 | INSERT INTO w16_project.account VALUES (1,100),(2,50),(3,0); | account에 `(1,100)`, `(2,50)`, `(3,0)` 세 positional row를 insert한다. |
| 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'); | ledger_entry에 id 1~3, account 1/2/2, signed 100/70/-20, 두 UTC timestamp를 insert한다. |
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에 의존한다.
실행 순서
- psql error-stop 정책을 켠다.
- 기존 w16_project가 있으면 CASCADE drop한다.
- 새 schema와 account·ledger_entry를 만든다.
- account 세 행을 먼저 넣어 FK parent를 준비한다.
- 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 합의 함정
-
ledger_sum 150만 맞으면 대사 완료 아닌가요?
-
계좌 사이 +10·-10 오류는 전체 합에서 서로 사라질 수 있어.
-
Q3의 account-grain HAVING과 marker의 global sum은 별도 predicate다.
-
100·50·0과 150을 각각 계산해 둘게요.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| existing schema reset | 이미 table이 있는 w16_project | pg_namespace 존재 branch에서 DROP SCHEMA CASCADE를 실행한다. | schema와 dependent objects가 제거되어 같은 이름을 새로 만들 수 있다. | Container mode에서도 동일하며 runner 성공 후 새 schema가 남는다. |
| minimal relations | 빈 database namespace | schema·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 sums | account1:+100, account2:+70/-20 | 세 ledger tuple을 저장한다. | 계좌별 합 100·50, account3 no-row=coalesce 0, global 150이 된다. | fixture SHA는 learner file 세 개의 result hash 목록에 포함되지 않는다. |
fixture SHA 빈자리
-
runner가 canonical fixture를 읽으니 hash도 자동 기록되겠죠?
-
읽는 경로와 results loop에 넣는 경로는 같은 말이 아니야.
-
manifest hashes=3은 logical·anti·aggregation learner files에만 대응한다.
-
fixture path와 세 result SHA source를 원문에서 대조하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
meta-command를 처리하고 뒤 SQL을 같은 connection에 순서대로 보낸다.
explicit BEGIN이 없어 파일 전체 atomic transaction은 아니다.namespace existence를 보고 schema drop/create와 relation metadata를 갱신한다.
DROP CASCADE 대상의 업무 가치를 판단하지 않는다.PK·FK·NOT NULL을 각 INSERT에 적용한다.
nullable FK·negative balance·accounting invariant는 표현된 predicate만큼만 본다.runner가 packaged fixture를 별도로 읽어 psql에 pipe한다.
그 bytes를 manifest hashes=3에 넣거나 allowlist와 비교하지 않는다.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 세 개는 그대로일 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
기존 w16_project가 지워도 되는 lab data뿐임
이 책임을 맡는 곳: disposable target selection and preflight ownership guard운영 account·ledger constraints와 동일함
이 책임을 맡는 곳: canonical migration and schema contract testsexecuted fixture bytes가 manifest SHA에 결박됨
이 책임을 맡는 곳: fixture hash allowlist and execution manifestdrop·create·insert 전체가 하나의 rollback 단위임
이 책임을 맡는 곳: explicit transaction design and failure replay testsigned rows가 상태·double-entry·authorization 정책을 만족함
이 책임을 맡는 곳: domain schema, service transaction, reconciliation evidenceSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
각 statement 옆에 destructive DDL·constructive DDL·parent seed·child seed를 표시한다.
2단계 · 코드 조각 재조립
- account1 → balance100 / sum100
- account2 → balance50 / 70-20=50
- account3 → balance0 / ledger 0건
- 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에서 빠짐을 적었는가?
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
\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');
03logical-order.sql — positive top-two count와 conditional Q1 marker
sql/w16/logical-order.sql
정본 W16 query invariant · 정본 · W16-F033줄 연결3줄 번역3 chunks
logical-order.sql — positive top-two count와 conditional Q1 marker
sql/w16/logical-order.sql
정본 W16 query invariant · 정본 · W16-F03STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
Q1 DO guard와 marker SELECT가 어떤 fixed predicate를 각각 보고 runner가 왜 둘 다 필요한지 읽는다.
- LIMIT 뒤 count 2는 무엇을 증명할까?
- DO query와 marker query의 WHERE는 같을까?
- top tie는 어떤 secondary key로 깨질까?
- conditional SELECT 0행은 psql error일까?
- 논리 처리 순서와 optimizer physical plan은 같은가?
positive rows after LIMIT=2order=balance DESC,idtop_id=1marker=W16_Q1 rows=2 top_id=1DO WHERE presentmarker WHERE absentSTEP 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 신뢰를 증명하지 않는다.
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가 잃는 정보
-
count=2면 top 계좌가 1번과 2번임도 확인됐죠?
-
count는 쟁반에 두 장 있다는 것만 남기고 이름과 순서는 버려.
-
2줄 terminal observable에는 id projection assertion이 없다.
-
count proof와 top-id proof를 서로 다른 칸에 적겠습니다.
LIMIT의 위치
-
positive account가 정확히 두 개라는 검사인가요?
-
먼저 두 장까지만 자르고 세기 때문에 셋 이상이어도 count는 2야.
-
이 predicate의 lower condition은 positive rows at least two에 가깝다.
-
id3=200 mutation으로 2줄 결과를 다시 계산할게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 3줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F03-L01 | \set ON_ERROR_STOP on |
STARRY가 Q1 검사 중 SQL error가 나면 다음 장으로 넘어가지 않는 정지 표지를 켠다. | psql에서 ON_ERROR_STOP을 on으로 설정한다.
|
| 2줄F03-L02 | DO $q$ BEGIN IF ( |
양수 잔액표를 큰 값·작은 id 순으로 두 장만 집어 실제 두 장인지 세고 아니면 경보를 울린다. | balance>0 account를 `balance DESC,id`로 정렬해 LIMIT 2한 derived table의 count가 2인지 DO block에서 검사한다.
|
| 3줄F03-L03 | SELECT 'W16_Q1 rows= |
모든 계좌를 같은 저울에 세워 맨 위 id가 1일 때만 Q1 합격 표찰 한 줄을 내보낸다. | 전체 account의 top id가 1이면 literal `W16_Q1 rows=2 top_id=1`을 반환하는 conditional SELECT다.
|
서로 다른 WHERE
-
3줄은 2줄 결과를 그대로 marker로 출력하나요?
-
3줄 scalar query는 balance>0을 쓰지 않고 전체 account에서 top을 골라.
-
fixed fixture coincidence를 query equivalence로 일반화하면 안 된다.
-
두 줄의 WHERE token을 나란히 대조하겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 3개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
\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;
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 3줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | \set ON_ERROR_STOP on | psql에서 ON_ERROR_STOP을 on으로 설정한다. |
| 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$; | balance>0 account를 `balance DESC,id`로 정렬해 LIMIT 2한 derived table의 count가 2인지 DO block에서 검사한다. |
| 3 | SELECT '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다. |
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행이 가능하다.
실행 순서
- psql error-stop policy를 켠다.
- DO 안 positive account top-two derived table을 평가한다.
- 그 count가 2가 아니면 exception으로 중단한다.
- 전체 account top id가 1인지 별도 scalar subquery로 본다.
- 조건이 참일 때 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
-
top id가 다르면 ON_ERROR_STOP이 멈추겠죠?
-
WHERE가 거짓인 SELECT는 정상적으로 0행을 반환할 수 있어.
-
nonzero는 SQL error에서, semantic failure는 caller marker check에서 만들어진다.
-
psql exit와 runner prefix 조건을 따로 기록할게요.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| positive filter | id1=100,id2=50,id3=0 | balance>0을 적용한다. | id 1과 2 두 row만 derived table 후보가 된다. | marker scalar query에는 이 filter가 없다. |
| top-two count | positive ids 1·2 | balance DESC,id로 정렬하고 LIMIT 2 뒤 count한다. | count=2라 DO block이 exception 없이 끝난다. | count는 두 id나 정렬 출력 자체를 assertion하지 않는다. |
| marker top | 전체 ids 1=100,2=50,3=0 | WHERE 없이 같은 order로 LIMIT 1한다. | top id=1이라 marker 한 행이 반환된다. | runner는 prefix만 찾아 suffix oracle을 독립 parse하지 않는다. |
| predicate divergence mutation | id3 balance=200으로 변경 | DO와 marker를 다시 평가한다. | positive가 셋이어도 LIMIT count는 2라 DO가 통과하지만 marker는 0행이 된다. | runner가 marker 부재를 확인해야 전체 file gate가 실패한다. |
논리 순서와 plan
-
SELECT 논리 순서를 외우면 실제 실행 plan도 그 순서인가요?
-
논리는 결과 의미를 설명하고 optimizer는 같은 결과의 physical 길을 고른다.
-
source만으로 sort·scan operator order나 cost를 확정할 수 없다.
-
EXPLAIN이 필요한 claim과 현재 SQL semantics를 분리하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
ON_ERROR_STOP과 server statement exit를 process status에 반영한다.
0-row SELECT를 error로 재분류하지 않는다.filter·sort/top-N·limit·aggregate·scalar subquery semantics로 결과를 계산한다.
optimizer는 physical operator를 논리 설명과 다른 방식으로 배치할 수 있다.count comparison이 false면 정상 종료하고 true면 RAISE EXCEPTION한다.
top id와 marker string을 이 block이 직접 검사하지 않는다.native exit 0과 output의 `W16_Q1 ` prefix를 함께 요구한다.
expected source SHA나 exact marker suffix를 allowlist로 비교하지 않는다.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를 통과할 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
arbitrary account data에서도 positive rows와 top id가 같음
이 책임을 맡는 곳: broader fixtures, property tests, production query contractDO와 marker subquery가 같은 row set을 읽음
이 책임을 맡는 곳: shared CTE/query definition or exact predicate auditFROM→WHERE→ORDER→LIMIT 순서의 physical operators
이 책임을 맡는 곳: EXPLAIN ANALYZE on a pinned PostgreSQL/workloadoutput suffix와 source bytes가 canonical expected 값임
이 책임을 맡는 곳: exact output schema and SHA allowlistQ1과 다른 learner files가 하나의 atomic transaction임
이 책임을 맡는 곳: explicit transaction wrapper and failure cleanup testSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
2줄과 3줄의 FROM·WHERE·ORDER BY·LIMIT·observable을 나란히 적는다.
2단계 · 코드 조각 재조립
- DO → positive filter / count after LIMIT
- marker → no filter / scalar top id
- false marker condition → 0 rows, not SQL error
- 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를 검증하지 않음을 표시했는가?
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
\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;
04anti-join.sql - 대응 원장이 없는 계좌 한 건
sql/w16/anti-join.sql
정본 W16 query invariant · 정본 · W16-F043줄 연결3줄 번역3 chunks
anti-join.sql - 대응 원장이 없는 계좌 한 건
sql/w16/anti-join.sql
정본 W16 query invariant · 정본 · W16-F04STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
고정 fixture에서 ledger_entry가 전혀 없는 account가 정확히 한 건이고 그 id가 3인지 NOT EXISTS와 marker로 확인한다.
- NOT EXISTS의 안쪽 SELECT 1은 무엇을 찾을까?
- count=1과 id=3은 왜 두 관찰인가?
- ledger 합이 0인 것과 ledger 행이 0건인 것은 같을까?
- ON_ERROR_STOP은 rollback을 뜻할까?
- runner가 marker suffix까지 인증할까?
account rows=3ledger rows=3missing-ledger count=1account_id=3marker=W16_Q2 rows=1 account_id=3STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 계좌 명단과 원장 명단을 나란히 놓고 대응 원장이 하나도 없는 계좌를 찾는다.
짝 없는 계좌 찾기
NOT EXISTS는 바깥 account마다 같은 account_id의 ledger가 한 행이라도 있는지 묻는다.
fixture에서는 account 3만 짝이 없어서 count 1과 id 3 marker가 차례로 성립한다.
딱 여기까지만 짝이 없다는 사실은 balance가 옳거나 모든 업무 거래가 기록됐다는 증명이 아니다.
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를 검증하지 않는다.
짝이 없다는 뜻
-
account 3의 balance가 0이라서 잡힌 건가요?
-
아니, 같은 account_id ledger 행이 하나도 없어서야.
-
값 0과 행 0건은 다른 predicate다.
-
account 2의 +70과 -20도 존재 행으로 세겠습니다.
count와 id
-
한 건이라고 알면 3번도 알 수 있지 않나요?
-
개수만으로 어느 id인지는 정할 수 없어.
-
DO와 marker SELECT가 서로 다른 관찰을 맡는다.
-
fixture를 바꾼 반례로 두 문턱을 확인할게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 3줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F04-L01 | \set ON_ERROR_STOP on |
SQL 오류가 나면 남은 종이를 넘기지 않는 정지 표지를 세운다. | psql의 ON_ERROR_STOP 변수를 on으로 설정하는 meta-command다.
|
| 2줄F04-L02 | DO $q$ BEGIN IF ( |
세 계좌 명단에서 원장 짝이 하나도 없는 칸의 개수가 한 개인지 센다. | anonymous DO block이 account별 NOT EXISTS를 세고 결과가 1이 아니면 W16 Q2 rows 예외를 낸다.
|
| 3줄F04-L03 | SELECT 'W16_Q2 rows= |
비어 있던 한 칸의 번호가 3일 때만 Q2 합격표 한 줄을 내보낸다. | 같은 NOT EXISTS scalar subquery가 id 3이면 W16_Q2 rows=1 account_id=3 marker를 SELECT한다.
|
NOT IN과 NULL
-
NOT IN으로 바꾸면 더 짧지 않나요?
-
안쪽 값에 NULL이 있으면 UNKNOWN 함정이 생길 수 있어.
-
여기 FK 열은 schema상 nullable이라 일반화에 주의해야 한다.
-
source에서 nullable FK와 l.account_id=a.id correlation을 함께 확인하겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 3개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
\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;
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 3줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | \set ON_ERROR_STOP on | psql이 SQL error 뒤에도 계속 읽지 않도록 ON_ERROR_STOP을 켠다. |
| 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$; | ledger 대응 행이 없는 account 수가 정확히 1인지 확인하고 아니면 예외를 낸다. |
| 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; | ledger가 없는 account id가 3일 때 W16_Q2 marker 한 행을 출력한다. |
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행이다.
실행 순서
- psql fail-fast on
- anti-join count
- count exception gate
- unique id scalar
- 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
-
ON_ERROR_STOP이면 앞 SQL도 되돌아가나요?
-
그건 남은 입력을 멈추는 설정이지 transaction 선언이 아니야.
-
별도 psql process와 autocommit 경계도 함께 봐야 한다.
-
중단·rollback·cleanup을 세 칸으로 나누겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | account id 1 | ledger account_id=1 존재를 찾는다. | entry 1이 있어 NOT EXISTS=false다. | signed_amount가 100인지 여부는 absence 판정에 쓰지 않는다. |
| 2 | account id 2 | ledger account_id=2 존재를 찾는다. | entry 2와 3이 있어 NOT EXISTS=false다. | 두 행의 합계 50은 Q3가 담당한다. |
| 3 | account id 3 | 대응 ledger를 검색한다. | 0행이라 NOT EXISTS=true이고 유일 id 3 marker가 나온다. | 새 ledger 한 행을 넣으면 exact oracle은 즉시 달라진다. |
marker의 약한 인증
-
W16_Q2가 출력되면 이 파일 hash도 맞나요?
-
runner는 hash를 기록하지만 canonical 값과 비교하지 않아.
-
prefix substring만으로 query 의미까지 인증할 수 없다.
-
Green 문구와 source authenticity를 분리해 적을게요.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
outer a.id를 안쪽 equality에 전달해 대응 행 존재를 판단한다.
optimizer가 anti join plan으로 바꿀 수 있어 문법 모양과 물리 plan은 같지 않다.count가 1이 아니면 exception으로 psql 실패를 만든다.
이 block은 결과 row를 반환하지 않는다.유일한 missing account id를 한 값으로 비교한다.
여러 행이면 scalar-subquery error지만 canonical DO가 먼저 exact one을 요구한다.native exit와 W16_Q2 prefix 포함 여부를 읽는다.
query source hash allowlist나 marker suffix equality는 없다.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 조건을 만족할 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
balance와 ledger sum 일치
이 책임을 맡는 곳: aggregation.sqlproduction에 ledger-less account가 정확히 한 건
이 책임을 맡는 곳: production reconciliation and policycanonical SQL bytes가 실행됨
이 책임을 맡는 곳: hash-pinned runner특정 anti join node나 latency
이 책임을 맡는 곳: EXPLAIN and benchmarkSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
account 1·2는 대응 ledger가 있고 account 3만 없어서 count 1과 id 3이 된다고 말한다.
2단계 · 코드 조각 재조립
- ON_ERROR_STOP
- NOT EXISTS correlation
- count<>1 exception
- id=3 marker
3단계 · 파일 전체 다시 쓰기
세 물리 줄을 정확히 다시 쓰고 SHA-256 1002e131...f62a와 대조한다.
자가 점검
- SELECT 1을 ledger 값 비교로 오해하지 않는다.
- 행 0건과 합 0을 분리한다.
- count와 id gate를 각각 설명한다.
- marker prefix 검사의 한계를 적는다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
\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;
05aggregation.sql - 계좌별 저장 잔액과 원장 합 대사
sql/w16/aggregation.sql
정본 W16 query invariant · 정본 · W16-F053줄 연결3줄 번역3 chunks
aggregation.sql - 계좌별 저장 잔액과 원장 합 대사
sql/w16/aggregation.sql
정본 W16 query invariant · 정본 · W16-F05STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
LEFT JOIN과 GROUP BY로 원장 0건 계좌도 보존해 account별 mismatch가 없는지 검사하고 global signed sum 150 marker를 별도로 출력한다.
- 왜 INNER JOIN 대신 LEFT JOIN일까?
- SUM(NULL)을 왜 COALESCE할까?
- account별 mismatch 0과 global 150은 같은 조건일까?
- signed sum 일치가 double-entry를 증명할까?
- marker는 앞 DO를 다시 실행할까?
account1 balance/sum=100/100account2=50/(70-20)=50account3=0/COALESCE(NULL,0)=0mismatches=0global ledger_sum=150STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 계좌 표의 저장 잔액과 원장 표의 signed 합계를 계좌별로 대조한다.
세 장부 칸 맞추기
LEFT JOIN은 원장 0건인 account 3도 결과에 남겨 NULL 합계를 0으로 바꾼다.
첫 문턱은 계좌별 불일치가 0인지 보고 둘째 출력은 전체 합계 150을 별도로 본다.
딱 여기까지만 숫자 합이 맞아도 거래 상태·원인·차변대변 구조가 올바르다는 뜻은 아니다.
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인가
-
원장 없는 계좌는 빼도 되지 않나요?
-
그러면 검사를 피한 행이 되어 0건 정책을 확인할 수 없어.
-
parent grain을 보존한 뒤 nullable sum을 처리한다.
-
account 3이 실제로 남는지 표로 그릴게요.
NULL과 0
-
SUM이 없으면 자동으로 0 아닌가요?
-
빈 aggregate의 SUM은 NULL이라 COALESCE가 필요해.
-
COUNT와 SUM의 empty-input 규칙을 분리해야 한다.
-
0건 fixture로 두 함수를 비교하겠습니다.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 3줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F05-L01 | \set ON_ERROR_STOP on |
대사식이 깨지면 다음 줄로 숨지 못하게 정지 장치를 켠다. | psql session에서 ON_ERROR_STOP을 활성화하는 meta-command다.
|
| 2줄F05-L02 | DO $q$ BEGIN IF EXISTS( |
각 계좌의 저장 잔액과 원장 합계를 나란히 놓고 다른 칸이 하나라도 있는지 찾는다. | DO block이 account를 ledger_entry에 LEFT JOIN해 account별 sum을 만들고 balance와 다르면 exception을 낸다.
|
| 3줄F05-L03 | SELECT 'W16_Q3 mismatches= |
전체 원장 합계가 150일 때만 Q3 결과표를 한 줄 내민다. | global SUM(signed_amount)=150이면 W16_Q3 mismatches=0 ledger_sum=150 marker를 SELECT한다.
|
group의 한 행
-
GROUP BY에 balance도 왜 넣었나요?
-
SELECT와 HAVING에서 account별 저장값을 함께 쓰기 위해서야.
-
현재 output grain은 a.id와 a.balance 조합이다.
-
한 계좌가 한 group이라는 문장을 먼저 쓰겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 3개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
\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;
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 3줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | \set ON_ERROR_STOP on | psql이 첫 SQL error에서 중단하도록 ON_ERROR_STOP을 켠다. |
| 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$; | 각 account balance와 COALESCE된 ledger signed sum이 다르면 W16 Q3 exception을 낸다. |
| 3 | SELECT '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 한 행을 출력한다. |
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를 연다.
실행 순서
- psql fail-fast
- left join
- account grouping
- COALESCE comparison
- mismatch exception
- 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를 연다.
전체 합의 함정
-
150만 맞으면 대사 완료라고 해도 될까요?
-
계좌 사이에 10이 잘못 옮겨도 전체 합은 그대로일 수 있어.
-
global invariant와 partition invariant는 다른 축이다.
-
두 반례를 같은 표에 놓아 보겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | account 1 balance 100, ledger +100 | group sum과 저장값을 비교한다. | 100=100이라 mismatch가 아니다. | 해당 +100이 올바른 업무 사건인지까지 보지 않는다. |
| 2 | account 2 balance 50, ledger +70와 -20 | 두 signed_amount를 합한다. | 50=50이라 mismatch set에 들어가지 않는다. | 행 두 개의 상쇄가 항상 정상이라는 뜻은 아니다. |
| 3 | account 3 balance 0, ledger 0건 | LEFT JOIN NULL sum을 COALESCE 0으로 바꾼다. | 0=0이라 0건 계좌도 정상 비교된다. | 원장 누락을 balance 0 정책상 허용하는 fixture 가정에 묶인다. |
| 4 | ledger 전체 +100,+70,-20 | account 구분 없이 SUM한다. | 150이 되어 marker가 한 행 출력된다. | 계좌 간 금액을 서로 바꿔도 global 합만은 유지될 수 있다. |
대사의 작은 범위
-
mismatch 0이면 회계가 완벽한 거죠?
-
이 SQL은 저장 balance와 signed sum만 비교해.
-
상태·원거래·차변대변 pair는 별도 계약이다.
-
증명한 것과 안 한 것을 경계 목록에 적겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
account를 보존하고 ledger를 optional child로 붙인다.
다른 1:N table을 함께 붙이면 row multiplication이 생길 수 있다.account grain에서 signed_amount를 sum한다.
SUM은 NULL input과 zero rows의 의미를 업무 정책 대신 정해 주지 않는다.EXISTS mismatch를 exception으로 전환한다.
tolerance, currency, pending-status 정책은 이 source에 없다.global sum condition이 true일 때 문자열 한 행을 낸다.
marker는 DO result를 재검증하지 않는다.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가 나올 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
여러 child table join에서 row multiplication 방지
이 책임을 맡는 곳: pre-aggregation designdouble-entry, transaction completeness, status validity
이 책임을 맡는 곳: domain reconciliation rulescurrency partition, rounding, tolerance
이 책임을 맡는 곳: money policycanonical source or exact suffix execution
이 책임을 맡는 곳: hash-pinned exact-output runnerSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
account1 100, account2 70-20, account3 zero rows를 각각 balance와 비교한 뒤 전체 150을 따로 말한다.
2단계 · 코드 조각 재조립
- LEFT JOIN
- GROUP BY account
- COALESCE SUM
- HAVING mismatch
- global SUM marker
3단계 · 파일 전체 다시 쓰기
세 물리 줄을 그대로 재작성하고 SHA-256 0f0553c3...5606과 맞춘다.
자가 점검
- 0건 계좌가 result grain에 남는지 본다.
- NULL과 0의 변환 위치를 설명한다.
- per-account와 global 조건을 섞지 않는다.
- double-entry claim을 하지 않는다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
\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;
06run-w16-project-regression.ps1 - 세 SQL을 묶는 owner
scripts/run-w16-project-regression.ps1
정본 W16 project SQL 실행기 · 정본 · W16-F0611줄 연결11줄 번역7 chunks
run-w16-project-regression.ps1 - 세 SQL을 묶는 owner
scripts/run-w16-project-regression.ps1
정본 W16 project SQL 실행기 · 정본 · W16-F06STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
runtime 선택, destructive canonical fixture, 세 learner SQL process, prefix/exit gate, record-only hash, JSON evidence, mode별 cleanup을 한 PowerShell owner로 연결한다.
- fixture와 learner SQL 중 무엇을 hash할까?
- 세 SQL은 같은 transaction일까?
- marker 검사는 exact suffix일까?
- cleanup=1은 어느 mode에서도 실제 cleanup일까?
- $owned flag가 cleanup을 제어할까?
files=3schema=w16_projectmarkers=W16_Q1/Q2/Q3 prefixeshashes=3 learner filesCompose default password=w16-disposable-passwordreported cleanup=1STEP 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 인증을 뜻하지 않는다.
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 실패를 직접 검사하지 않는다.
두 실행 통로
-
Container mode면 안전하게 읽기만 하나요?
-
아니, 같은 fixture가 w16_project를 DROP CASCADE해.
-
소유 container 여부와 named schema mutation은 별개다.
-
실행 전에 destructive target을 적겠습니다.
세 hash의 정체
-
hashes=3에 fixture도 들어 있나요?
-
세 learner SQL만 loop에서 hash해.
-
그 hash도 canonical expected 값과 비교하지 않고 기록한다.
-
fixture gap과 record-only를 두 줄로 나누겠습니다.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 11줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F06-L01 | param( |
SQL 폴더·증거 폴더·두 실행 통로를 고르는 조작판을 펼친다. | mandatory SqlRoot/EvidenceDir, Compose/Container Mode, optional Container, ComposeProject parameters를 선언한다.
|
| 2줄F06-L02 | $ErrorActionPreference= |
오류 경보를 Stop에 두고 reference·입력·증거 주소를 계산한 뒤 증거 상자를 만든다. | ErrorActionPreference, script-parent root, resolved SqlRoot, full EvidenceDir를 정하고 Directory -Force를 실행한다.
|
| 3줄F06-L03 | $files= |
Q1·Q2·Q3 세 답안 봉투가 모두 있는지 표지만 점검한다. | logical-order.sql, anti-join.sql, aggregation.sql leaf 존재를 loop로 확인한다.
|
| 4줄F06-L04 | $owned= |
아직 Compose를 소유하지 않았다는 불을 끄고 packaged compose 주소를 적는다. | $owned=false를 초기화하고 reference root 아래 compose.yaml path를 만든다.
|
| 5줄F06-L05 | try{ |
작업 내용을 아직 넣지 않은 보호 봉투의 입구만 연다. | PowerShell try statement의 body opener를 선언한다.
|
| 6줄F06-L06 | if( |
Compose면 일회용 DB를 올리고, 아니면 supplied container 이름표를 요구하는 긴 분기 레일을 탄다. | Compose branch가 Container 동시 입력을 거절하고 blank password를 default로 바꾼 뒤 compose up --wait와 db id 조회를 수행한다; 대체 branch는 Container truthiness를 요구한다.
|
| 7줄F06-L07 | if( |
해결된 container 이름표가 공백이면 DB 문 앞에서 멈춘다. | IsNullOrWhiteSpace(Container)이 true면 W16 container resolution failed exception을 던진다.
|
| 8줄F06-L08 | Get-Content -Raw ( |
기준 fixture 두루마리를 통째로 supplied DB에 밀어 넣어 w16_project 방을 갈아엎는다. | packaged sql/w16/fixture.sql을 raw로 읽어 docker exec psql ON_ERROR_STOP=1에 pipe하고 nonzero exit를 예외로 바꾼다.
|
| 9줄F06-L09 | $results= |
세 learner SQL을 차례로 별도 창구에 내고 exit·Q prefix·hash를 각 결과 카드에 적는다. | for loop가 각 file을 separate docker exec/psql process로 실행해 output과 exit를 받고 W16_Qn substring을 검사한 뒤 last prefix line과 SHA를 result에 추가한다.
|
| 10줄F06-L10 | $manifest= |
통과 카드 세 장을 고정 숫자표로 포장해 JSON과 Green 문구를 내보낸다. | literal schema/files/rows/invariants/native_exits/hashes/cleanup fields와 results를 manifest로 만들고 Set-Content한 뒤 W16_PROJECT_SQL_GREEN string을 출력한다.
|
| 11줄F06-L11 | }finally{ |
봉투를 닫으며 Compose 통로일 때만 project와 volume 제거 요청을 보낸다. | finally에서 Mode가 Compose이면 docker compose down -v를 실행하고 output을 버린다.
|
세 psql process
-
Q1부터 Q3까지 한 transaction이죠?
-
file마다 docker exec와 psql을 새로 실행해.
-
database state는 이어져도 session·transaction은 새로 열린다.
-
앞 file mutation 반례를 만들어 보겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 7개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
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}}
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 11줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | param([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-Null | fail-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를 만든다. |
| 5 | try{ | 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를 남긴다. |
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는 명시적으로 검사되지 않는다.
실행 순서
- bind paths
- preflight three files
- choose mode
- resolve container
- reset fixture
- run Q1/Q2/Q3 separately
- write literal manifest
- emit marker
- 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를 속일 수 있는가
-
W16_Q3만 보이면 mismatch도 0인가요?
-
현재 gate는 prefix substring과 exit만 확인해.
-
last prefix line을 기록하지만 없을 때 null을 별도 거절하지도 않는다.
-
exact suffix와 structured field가 필요한 이유를 적을게요.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| 1 | Mode=Compose, blank FCL_DB_PASSWORD | disposable password를 넣고 compose up --wait db를 호출한다. | container id가 nonblank이면 fixture 단계로 간다. | image digest와 down 성공은 고정하지 않는다. |
| 2 | 기존 w16_project가 있는 supplied database | packaged 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이 없다. |
| 4 | results 세 object | literal summary와 함께 JSON을 쓰고 Green string을 출력한다. | evidence가 생긴 뒤 finally로 control이 이동한다. | Container mode에서도 cleanup=1 literal은 바뀌지 않는다. |
| 5 | Mode=Container | finally condition을 평가한다. | down -v가 실행되지 않고 caller container와 w16_project가 남는다. | $owned 여부도 cleanup 분기에 읽히지 않는다. |
cleanup 숫자와 현실
-
manifest가 cleanup=1이면 정리된 거 아닌가요?
-
그 값은 mode와 결과를 읽어 계산한 게 아니라 literal이야.
-
Container에는 down이 없고 Compose down exit도 확인하지 않는다.
-
postcondition 없는 완료 도장을 경계로 표시하겠습니다.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
parameters와 absolute paths를 만들고 evidence directory를 준비한다.
authorization과 containment는 검사하지 않는다.Compose mode에서 disposable PostgreSQL service를 시작·조회한다.
Container mode runtime provenance는 검사하지 않는다.named schema를 destructive reset해 deterministic rows를 만든다.
fixture source hash가 evidence에 묶이지 않는다.shared database state에서 learner files를 순차 실행한다.
session/transaction atomicity는 공유하지 않는다.literal summary와 result hashes를 JSON text로 쓴다.
atomic replace, reread, signature가 없다.Mode string이 Compose일 때만 down -v를 호출한다.
exit/resource absence를 gate하지 않고 Container cleanup은 없다.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은 이미 쓰였다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
supplied Container의 기존 w16_project 보존
이 책임을 맡는 곳: caller isolation and schema ownershipfixture 또는 canonical learner SQL bytes
이 책임을 맡는 곳: expected-hash allowlistQ1-Q3 all-or-nothing rollback
이 책임을 맡는 곳: single transaction/session orchestrationcanonical marker suffix와 invariant meaning
이 책임을 맡는 곳: exact structured result validationContainer schema removal 또는 Compose resource absence
이 책임을 맡는 곳: mode-aware postcondition auditatomic JSON replace, reread, signature
이 책임을 맡는 곳: durable evidence writerSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
경로→세 file 존재→mode→fixture reset→세 separate psql→prefix/hash record→literal manifest→Compose-only cleanup 순서로 말한다.
2단계 · 코드 조각 재조립
- parameters/path resolution
- $files three
- Compose/Container branch
- fixture pipe
- three-file loop
- literal manifest
- 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를 적는다.
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
정본 전체 코드 확인하기
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}}
07W16-SQL-Q23.sql — 고정 시계의 반열린 7일 고객 집계
illustrative/sql/W16-SQL-Q23.sql
fixture 기반 학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W16-F0723줄 연결23줄 번역5 chunks
W16-SQL-Q23.sql — 고정 시계의 반열린 7일 고객 집계
illustrative/sql/W16-SQL-Q23.sql
fixture 기반 학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W16-F07STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
fixture_clock의 고정 as_of를 기준으로 최근 7일 transaction을 골라 account를 통해 customer별 건수와 금액 합을 계산하되, 예시가 선택한 status·join 정책을 숨기지 않는다.
- 왜 wall clock 대신 fixture_clock.as_of를 쓸까?
- [start,end)에서 두 경계의 equality는 어떻게 처리될까?
- FAILED와 PROCESSING도 왜 합계에 들어갈까?
- 거래가 없는 customer가 0건 행으로 보일까?
- 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=2STEP 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 비정본 예시다.
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건 그룹이 만들어지지 않는다.
최근의 기준 시계
-
최근 7일이면 오늘 시간을 바로 쓰는 게 자연스럽지 않나요?
-
이 연습은 언제 돌려도 같아야 해서 fixture_clock의 as_of를 끝점으로 써.
-
clock source가 달라지면 같은 SQL 문구라도 oracle row set이 달라진다.
-
8줄의 source와 2027-01-03 값을 함께 확인하겠습니다.
두 경계의 equality
-
시작과 끝 시각에 딱 맞는 거래는 둘 다 포함되나요?
-
시작은 >=라 들어오고 끝은 <라 빠져.
-
반열린 구간은 인접 window 사이에서 같은 instant를 두 번 세지 않게 한다.
-
13·14줄 연산자를 괄호 표기 `[start,end)`와 맞춰 볼게요.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 23줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F07-L01 | -- W16-SQL-Q23 illustrative example; |
STARRY가 첫 장에 ‘학습용 예시’ 도장을 찍어 정본 답안 봉투와 분리한다. | 이 파일이 W16 Q23의 illustrative example이며 shipped workbook answer가 아니라고 밝히는 주석이다.
|
| 2줄F07-L02 | -- Assumption: |
고정 시계에서 정확히 7일 전 표찰을 떼고 끝 시각 직전까지만 받는 띠를 그린다. | recent seven days를 `[as_of - 7 days, as_of)` 반열린 구간으로 정한 가정 주석이다.
|
| 3줄F07-L03 | -- All statuses are included because the prompt does not request a status filter; |
상태 색과 무관하게 시간 띠 안의 표를 받되 빈 고객 바구니는 진열하지 않는 안내판을 단다. | prompt에 status filter가 없으므로 모든 상태를 포함하고 거래 row가 있는 customer만 나타난다고 선언한다.
|
| 4줄F07-L04 | SET search_path TO : |
STARRY가 workbook_schema 서랍을 먼저, public 서랍을 다음으로 찾게 주소표를 바꾼다. | 현재 psql session의 search_path를 supplied `workbook_schema`와 public 순서로 설정한다.
|
| 6줄F07-L06 | WITH bounds AS ( |
시간 창 계산표를 만들기 위해 `bounds`라는 새 접이식 표의 표지를 연다. | WITH 절에서 `bounds` CTE 정의를 시작한다.
|
| 7줄F07-L07 | SELECT as_of - INTERVAL '7 days' AS window_start, |
한 시각표에서 7일을 뺀 시작 칸과 원래 시각인 끝 칸을 나란히 적는다. | scope 안의 `as_of`에서 7 days를 뺀 값을 window_start, 원값을 window_end로 projection한다.
|
| 8줄F07-L08 | FROM fixture_clock |
경계 계산표가 값을 가져올 고정 시계 상자를 fixture_clock으로 연결한다. | bounds SELECT의 FROM source를 fixture_clock으로 지정한다.
|
| 9줄F07-L09 | ), |
첫 계산표를 접어 등록하고 이어서 `recent_tx`라는 두 번째 표의 빈 칸을 연다. | bounds CTE를 닫은 뒤 comma로 연결해 recent_tx CTE 정의를 시작한다.
|
| 10줄F07-L10 | SELECT t. |
거래 카드에서 번호·계좌·금액·상태 네 칸만 복사할 투명 필름을 놓는다. | alias t의 tx_id, account_id, amount, status를 recent_tx output column으로 projection한다.
|
| 11줄F07-L11 | FROM business_tx AS t |
복사할 거래 카드 더미를 business_tx로 고르고 짧은 이름표 t를 붙인다. | recent_tx의 driving relation을 business_tx로 정하고 alias t를 부여한다.
|
| 12줄F07-L12 | CROSS JOIN bounds AS b |
거래 카드마다 시간 경계표 한 장을 옆에 붙여 비교 준비를 한다. | business_tx alias t와 bounds alias b를 CROSS JOIN한다.
|
| 13줄F07-L13 | WHERE t. |
시작 표찰보다 이른 거래 카드는 돌려보내고 정확히 시작에 놓인 카드는 받는다. | t.occurred_at이 b.window_start 이상인 row만 통과시키는 WHERE 하한 predicate다.
|
| 14줄F07-L14 | AND t. |
끝 표찰에 닿기 전 거래만 남기고 끝과 같은 시각부터는 다음 구간으로 보낸다. | t.occurred_at이 b.window_end보다 작은 row만 추가로 허용하는 상한 predicate다.
|
| 15줄F07-L15 | ) |
시간 조건을 통과한 거래표의 마지막 덮개를 닫아 recent_tx로 보관한다. | recent_tx CTE query body를 닫는다.
|
| 16줄F07-L16 | SELECT |
최종 결과표에 넣을 열을 고르기 위해 빈 SELECT 머리표를 연다. | main query의 SELECT projection list를 시작한다.
|
| 17줄F07-L17 | a. |
a 이름표가 가리키는 row의 고객 번호를 결과표 첫 칸에 적는다. | alias a의 customer_id를 첫 output column으로 projection한다.
|
| 18줄F07-L18 | COUNT( |
한 aggregate 묶음에 들어온 거래 카드 수를 세고 tx_count 표찰을 붙인다. | 현재 group의 input row 수를 COUNT(*)로 계산해 tx_count로 이름 붙인다.
|
| 19줄F07-L19 | SUM( |
같은 aggregate 묶음의 거래 금액을 모두 더해 total_amount 칸에 기록한다. | group별 r.amount 합을 SUM으로 계산하고 total_amount alias를 부여한다.
|
| 20줄F07-L20 | FROM recent_tx AS r |
시간 필터를 마친 recent_tx 더미를 최종 집계의 출발 카드로 놓고 r이라 부른다. | main query의 FROM source를 recent_tx CTE로 지정하고 alias r을 붙인다.
|
| 21줄F07-L21 | JOIN account AS a ON a. |
거래 카드의 account_id와 같은 계좌 카드를 붙여 고객 번호를 찾아낸다. | account를 alias a로 INNER JOIN하고 a.account_id와 r.account_id의 equality로 연결한다.
|
| 22줄F07-L22 | GROUP BY a. |
같은 고객 번호표를 가진 joined row를 한 바구니씩 묶는다. | joined rows를 a.customer_id 값으로 GROUP BY한다.
|
| 23줄F07-L23 | ORDER BY a. |
완성된 고객 바구니를 번호가 작은 것부터 진열한다. | aggregate result를 a.customer_id 오름차순으로 정렬한다.
|
| 24줄F07-L24 | -- Fixture oracle: |
마지막 검산표에 고객 1과 2의 고정 건수·합계를 정확한 숫자로 적어 둔다. | frozen fixture oracle을 customer 1=7/1,201,699와 customer 2=4/3,303으로 기록한 주석이다.
|
FAILED도 합산되는가
-
거래 합계라면 실패 건은 DB가 알아서 제외하겠죠?
-
WHERE에 status가 없어서 시간만 맞으면 FAILED와 PROCESSING도 들어가.
-
customer 2의 네 건은 그 정책을 숫자로 드러내는 반례다.
-
100+101+102+3000을 직접 더해 3,303을 재검산하겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 5개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
-- 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.
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 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만 결과에 나타난다고 선언한다. |
| 4 | SET search_path TO :"workbook_schema", public; | 이 session의 search_path를 supplied workbook schema와 public 순서로 설정한다. |
| 6 | WITH bounds AS ( | bounds라는 첫 CTE 정의를 연다. |
| 7 | SELECT as_of - INTERVAL '7 days' AS window_start, as_of AS window_end | as_of-7 days를 window_start, as_of를 window_end로 선택한다. |
| 8 | FROM fixture_clock | bounds가 fixture_clock에서 as_of를 읽는다. |
| 9 | ), recent_tx AS ( | bounds를 닫고 recent_tx라는 다음 CTE를 연다. |
| 10 | SELECT t.tx_id, t.account_id, t.amount, t.status | recent_tx에 거래 id·account id·amount·status를 투영한다. |
| 11 | FROM business_tx AS t | business_tx를 t라는 이름으로 읽는다. |
| 12 | CROSS JOIN bounds AS b | 각 business_tx row에 bounds row를 CROSS JOIN한다. |
| 13 | WHERE t.occurred_at >= b.window_start | occurred_at이 window_start 이상인 row만 남긴다. |
| 14 | AND t.occurred_at < b.window_end | occurred_at이 window_end보다 작은 row만 추가로 남긴다. |
| 15 | ) | recent_tx CTE 정의를 닫는다. |
| 16 | SELECT | 최종 SELECT projection을 시작한다. |
| 17 | a.customer_id, | customer_id를 결과 열로 선택한다. |
| 18 | COUNT(*) AS tx_count, | customer group의 row 수를 tx_count로 계산한다. |
| 19 | SUM(r.amount) AS total_amount | customer group의 amount 합을 total_amount로 계산한다. |
| 20 | FROM recent_tx AS r | recent_tx를 최종 query의 driving relation r로 읽는다. |
| 21 | JOIN account AS a ON a.account_id = r.account_id | account_id가 같은 account를 inner join해 customer를 연결한다. |
| 22 | GROUP BY a.customer_id | 결과를 customer_id별로 그룹화한다. |
| 23 | ORDER 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이라고 기록한다. |
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를 생성하지 않는다.
실행 순서
- search_path setting
- fixture_clock bounds
- business_tx cross join bounds
- half-open time filter
- account inner join
- customer grouping
- count/sum
- 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의 행
-
COUNT(*)면 거래 없는 고객도 0으로 표시되지 않나요?
-
COUNT는 이미 생긴 group을 셀 뿐 없는 group을 만들어 주진 않아.
-
recent_tx가 driving relation이라 zero customer는 join 전에 사라져 있다.
-
customer에서 시작하는 LEFT JOIN 대안과 현재 20줄을 비교하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| window | as_of=2027-01-03 00:00:00+09 | 7 days를 빼고 두 timestamptz 경계를 만든다. | start=2026-12-27 00:00:00+09, end=2027-01-03 00:00:00+09다. | end와 정확히 같은 row는 `<` 때문에 제외된다. |
| customer 1 rows | accounts 101·105의 window 내 7 transactions | FAILED·SUCCESS 구분 없이 amount를 합한다. | 99,999+100,000+1,000,000+300+200+600+600=1,201,699다. | reversal도 amount를 음수로 바꾸지 않고 stored positive amount 그대로 더한다. |
| customer 2 rows | account 102의 FAILED 100·101·102와 PROCESSING 3,000 | 네 row를 한 customer group으로 센 뒤 합한다. | tx_count=4, total_amount=3,303이다. | status predicate를 추가하면 이 oracle은 달라진다. |
| absent groups | recent row가 없는 customers 3·4·5·6 | recent_tx driving set과 account inner join을 따른다. | 해당 customer group은 0값 행이 아니라 결과에서 사라진다. | zero preservation이 필요하면 customer에서 시작하는 outer join 설계가 따로 필요하다. |
oracle과 실행 증거
-
마지막 주석에 숫자가 맞으면 회귀 테스트도 통과한 건가요?
-
주석은 예상표이고 DB output을 비교하는 assertion은 아니야.
-
직접 증거는 query result, reference 증거는 pinned seed와 재계산이다.
-
두 결과 행을 structured comparator로 확인하는 단계를 따로 적을게요.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
identifier variable로 받은 workbook schema를 search_path 첫 순서로 둔다.
wrong schema object나 search_path spoof를 hash로 인증하지 않는다.bounds와 recent_tx의 relational expressions를 main query plan에 통합하거나 materialize할 수 있다.
WITH 문법만 보고 특정 materialization·scan 순서를 단정할 수 없다.TIMESTAMPTZ instants를 lower-inclusive, upper-exclusive predicate로 비교한다.
business_date 기준 집계나 지역 달력 일수 계약은 아니다.transaction→account FK match로 customer_id를 붙여 customer grain을 만든다.
customer master를 driving table로 보존하지 않는다.각 present customer group의 row count와 BIGINT amount sum을 계산한다.
currency·status·signed cash-flow 의미를 자동 해석하지 않는다.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는 실패를 일으키지 않는다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
shipped workbook canonical answer 또는 유일한 정답
이 책임을 맡는 곳: workbook answer authority and reviewSUCCESS-only financial total
이 책임을 맡는 곳: explicit prompt/business status contract거래 없는 customer의 zero row
이 책임을 맡는 곳: customer-driven outer-join querycurrency conversion·signed cash flow·reversal netting
이 책임을 맡는 곳: money model and explicit sign/currency policy주석의 두 expected row가 실제 output과 일치함
이 책임을 맡는 곳: executable result comparatorSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
clock→bounds→recent_tx→account join→customer group→count/sum 순서와 all-status·inner-only 경계를 말한다.
2단계 · 코드 조각 재조립
- fixture_clock as_of
- >= window_start
- < window_end
- no status predicate
- recent_tx JOIN account
- GROUP BY customer_id
3단계 · 파일 전체 다시 쓰기
24개 물리 줄을 정확히 다시 쓰고 SHA-256 0a0d7511...46d3과 대조한다.
자가 점검
- 시작 포함·끝 제외를 말했는가?
- FAILED·PROCESSING 포함을 숨기지 않았는가?
- 거래 없는 customer가 사라지는 이유를 설명했는가?
- 7/1,201,699와 4/3,303을 seed로 재계산했는가?
- 비정본 예시와 shipped answer를 구분했는가?
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
-- 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.
08W16-SQL-Q24.sql — 두 threshold의 순서형 balance band
illustrative/sql/W16-SQL-Q24.sql
fixture 기반 학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W16-F0815줄 연결15줄 번역4 chunks
W16-SQL-Q24.sql — 두 threshold의 순서형 balance band
illustrative/sql/W16-SQL-Q24.sql
fixture 기반 학습용 SQL 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W16-F08STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
prompt에 없던 두 threshold를 example assumption으로 공개하고 account.balance를 ordered CASE의 SMALL·MEDIUM·LARGE 세 band로 분류한다.
- 100000과 500000은 prompt 사실일까 예시 가정일까?
- searched CASE는 WHEN을 어떤 순서로 평가할까?
- 100000·499999·500000은 각각 어느 branch에 갈까?
- ELSE LARGE가 nullable column에서도 >=500000만 뜻할까?
- 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=8STEP 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이다.
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 계약은 이 예시에 없다.
두 수치의 출처
-
100000과 500000은 문제에 적힌 공식 경계인가요?
-
아니, prompt에 숫자가 없어서 이 illustrative example이 assumption으로 정했어.
-
source truth와 policy choice를 분리하지 않으면 예시가 요구사항처럼 굳어진다.
-
2줄 주석을 title과 한계 카드에도 그대로 결속하겠습니다.
100000의 자리
-
SMALL 문턱에 100000이 적혀 있으니 그 값도 SMALL 아닌가요?
-
연산자가 `<`라서 같은 값은 첫 문을 통과하지 못해.
-
다음 `<500000`이 true라 정확히 100000은 MEDIUM이다.
-
10·11줄에 숫자를 대입해 boolean 두 개를 적겠습니다.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 15줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F08-L01 | -- W16-SQL-Q24 illustrative example; |
STARRY가 Q24 카드 위에도 ‘설명용 예시’ 표찰을 붙여 공식 답안과 섞이지 않게 한다. | 첫 comment가 이 SQL을 shipped workbook answer와 구별되는 W16 Q24 학습 artifact로 분류한다.
|
| 2줄F08-L02 | -- Assumption: |
잔액 카드를 100,000과 500,000 두 문턱에 순서대로 통과시키는 분류 규칙을 게시한다. | account.balance를 SMALL<100000, MEDIUM<500000, 그 밖은 LARGE로 나눈다는 예시 가정이다.
|
| 3줄F08-L03 | -- The frozen 100000/ |
문턱 바로 아래·위 카드를 준비해 두 갈림길을 모두 밟는지 확인 표시를 한다. | frozen balances 100000, 499999, 500000이 두 CASE boundary를 실행한다고 적은 주석이다.
|
| 4줄F08-L04 | SET search_path TO : |
계좌표를 찾을 때 workbook_schema 선반을 public보다 먼저 살피도록 안내 화살표를 세운다. | psql session이 unqualified identifier를 찾을 schema precedence에 supplied workbook schema 뒤 public을 기록한다.
|
| 6줄F08-L06 | SELECT |
계좌별 분류 결과표에 넣을 칸을 고르려고 SELECT 머리표만 먼저 펼친다. | main query의 projection list를 여는 SELECT keyword다.
|
| 7줄F08-L07 | account_id, |
각 분류 카드가 어느 계좌인지 잃지 않도록 account_id 번호칸을 복사한다. | account_id를 첫 output column으로 projection한다.
|
| 8줄F08-L08 | balance, |
문턱을 통과시킬 원래 숫자를 함께 보려고 balance 칸도 결과표에 붙인다. | balance를 두 번째 output column으로 projection한다.
|
| 9줄F08-L09 | CASE |
잔액별 이름표를 고르는 순서형 갈림길의 입구를 연다. | searched CASE expression을 시작한다.
|
| 10줄F08-L10 | WHEN balance < 100000 THEN 'SMALL' |
첫 문턱 100,000보다 작은 잔액 카드에 SMALL 도장을 찍는다. | balance < 100000이 true인 첫 branch의 result를 `SMALL`로 지정한다.
|
| 11줄F08-L11 | WHEN balance < 500000 THEN 'MEDIUM' |
첫 문턱을 지난 카드 중 500,000보다 작은 잔액에는 MEDIUM 표찰을 붙인다. | 첫 WHEN이 false이고 balance < 500000이 true인 branch result를 MEDIUM으로 둔다.
|
| 12줄F08-L12 | ELSE 'LARGE' |
두 문턱 어디에도 들어가지 않은 잔액 카드를 LARGE 바구니로 보낸다. | 앞 WHEN condition이 모두 true가 아닐 때 ELSE result로 LARGE를 반환한다.
|
| 13줄F08-L13 | END AS amount_band |
분류 갈림길을 닫고 완성된 label 열에 amount_band라는 이름표를 단다. | CASE expression을 END로 닫고 derived output column alias를 amount_band로 지정한다.
|
| 14줄F08-L14 | FROM account |
분류할 원본 카드 더미로 account 표 전체를 작업대에 올린다. | query의 FROM relation을 account로 지정한다.
|
| 15줄F08-L15 | ORDER BY account_id; |
완성된 여덟 계좌 카드를 account_id가 작은 순서대로 줄 세운다. | result rows를 account_id 오름차순으로 정렬한다.
|
| 16줄F08-L16 | -- Fixture oracle: |
분류가 끝난 뒤 세 바구니 수와 경계 카드 결과를 검산표에 적는다. | fixture oracle을 SMALL=4, MEDIUM=2, LARGE=2와 세 boundary assignment로 기록한다.
|
WHEN 순서의 영향
-
두 WHEN은 조건이 분명하니 순서를 바꿔도 되죠?
-
넓은 500000 문을 먼저 놓으면 10000도 거기서 MEDIUM을 받아.
-
searched CASE는 first true result라 branch order가 구간 정의의 일부다.
-
순서를 뒤집은 10000 counterexample을 실행해 보겠습니다.
STEP 05 / 13
원본 코드 조각
원본을 4개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
-- 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.
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 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 경계를 실행한다고 적는다. |
| 4 | SET search_path TO :"workbook_schema", public; | 이 session의 search_path를 supplied workbook schema와 public 순서로 설정한다. |
| 6 | SELECT | 최종 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_band | CASE를 닫고 결과 열 이름을 amount_band로 정한다. |
| 14 | FROM account | account의 모든 row를 source로 읽는다. |
| 15 | ORDER 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와 경계값의 분류를 기록한다. |
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를 계산하지 않는다.
실행 순서
- search_path setting
- account scan
- project id/balance
- first threshold
- second threshold
- ELSE fallback
- amount_band alias
- 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
-
ELSE LARGE는 언제나 balance>=500000의 짧은 표현인가요?
-
여기서는 balance가 NOT NULL이라 그렇게 읽을 수 있어.
-
nullable column이면 NULL comparisons가 UNKNOWN이라 ELSE로 흐른다.
-
schema constraint와 CASE source를 같이 인용하겠습니다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| SMALL | balances 10000, 20300, 0, 0 | 첫 predicate balance<100000을 평가한다. | 네 account가 SMALL을 선택한다. | negative balance는 schema CHECK가 막지만 CASE 자체는 음수도 SMALL로 분류한다. |
| first equality | balance=100000 | 첫 `<100000`은 false, 둘째 `<500000`은 true가 된다. | account 105가 MEDIUM으로 분류된다. | 첫 operator를 <=로 바꾸면 결과가 SMALL로 이동한다. |
| upper medium edge | balance=499999 | 첫 branch를 지나 둘째 threshold와 비교한다. | account 106이 MEDIUM을 선택한다. | 500000과 한 단위 차이가 서로 다른 band를 만든다. |
| LARGE boundary | balances 500000 and 1000000 | 두 WHEN이 모두 false라 ELSE를 사용한다. | accounts 107·108 두 행이 LARGE다. | account 107의 CLOSED status는 이 query의 predicate가 아니다. |
alias가 말하지 않는 것
-
amount_band니까 거래 금액이나 통화도 함께 분류한 거죠?
-
표현식은 account.balance 하나뿐이고 currency column은 없어.
-
alias 이름보다 FROM과 expression lineage가 실제 책임 범위를 정한다.
-
8·14줄만 따라가 분류 대상을 다시 적을게요.
STEP 09 / 13
PowerShell·SQL·DB 내부에서 벌어지는 일
PowerShell·SQL·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
workbook schema identifier를 search_path에 놓아 unqualified account를 resolve한다.
schema identity를 content hash나 OID로 고정하지 않는다.각 account row에 searched CASE result를 하나 계산한다.
band policy의 업무 적합성을 판단하지 않는다.balance는 BIGINT NOT NULL CHECK(balance>=0)인 frozen schema column이다.
다른 nullable·decimal·multi-currency column에 같은 ELSE 해석을 옮길 수 없다.source id·balance와 derived text label을 한 output row에 둔다.
GROUP BY나 count aggregate는 수행하지 않는다.account_id key로 display order를 고정한다.
band 우선순위·업무 ranking·index 사용을 보장하지 않는다.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일 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
threshold 100000·500000이 production 승인 기준임
이 책임을 맡는 곳: explicit product/risk policyshipped workbook의 유일한 Q24 answer
이 책임을 맡는 곳: canonical workbook answer sourcetransaction amount·available balance·currency-adjusted value 분류
이 책임을 맡는 곳: approved metric definitionnullable input에서 ELSE가 오직 >=500000임
이 책임을 맡는 곳: column constraint and explicit NULL branchSMALL4·MEDIUM2·LARGE2가 실행 중 assert됨
이 책임을 맡는 곳: detail-row result comparatorSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
account source→id/balance projection→ordered thresholds→ELSE→alias→sort 순서와 비정본 policy 경계를 말한다.
2단계 · 코드 조각 재조립
- balance < 100000
- balance < 500000
- first true branch
- ELSE remainder
- FROM account
- 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를 혼동하지 않았는가?
STEP 13 / 13
전체 원본 정답
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
-- 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.