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

13주차 코드 10항목 — 같은 조회를 인덱스 전후로 재고 증거로 남기기

환경과 10만 행 fixture를 고정하고, 같은 top-N 조회를 인덱스 전후로 측정해 무엇이 빨라졌는지와 아직 증명하지 못한 범위를 실제 source 값으로 따라갑니다.

YAML 정본 1개SQL 정본 4개PowerShell 정본 1개PowerShell 인라인 Gate 1개SQL 학습용 예시 · 정본 답안 아님 3개항목마다 13단계연결 261줄번역 261줄
01

compose.yaml — 같은 PostgreSQL 실험실 한 개를 준비하는 설정

compose.yaml

YAML 정본 · 정본 · W13-F01
18줄 연결18줄 번역5 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

W13 성능 실험이 PostgreSQL image tag·database·user·healthcheck·volume 설정을 공유하도록 한 개의 db service를 선언한다.

  1. 왜 SQL보다 먼저 container 설정을 확인할까?
  2. FCL_DB_PORT가 없을 때 어느 port를 쓸까?
  3. 비밀번호가 없으면 어느 단계에서 멈출까?
  4. healthy는 query가 빠르다는 뜻일까?
  5. 왜 이 파일은 다섯 SourceHashes 밖에 있을까?
이 파일에서 끝까지 다시 쓰는 값service=dbpostgres:17.10-alpinehost=${FCL_DB_PORT:-5432}container=5432database=financial_coreuser=appinterval=2s timeout=2s retries=30volume=financial-core-db
02

STEP 02 / 13

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

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

STARRY가 같은 조건으로 SQL 실험을 시작할 작은 DB 방을 준비한다.

한 개짜리 PostgreSQL 실험실

이 파일은 PostgreSQL 실험실 하나를 Docker로 띄울 때 쓸 이미지·포트·데이터베이스 이름·사용자를 정한다.

비밀번호가 비어 있으면 시작하지 않고 2초마다 준비 상태를 확인한다. 데이터는 financial-core-db volume에 연결한다.

딱 여기까지만 이 설정은 seed·query·index를 실행하지 않으며 SourceHashes 다섯 파일에도 들어가지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

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

DB 방 하나

services 아래 db 하나만 두어 runner가 찾을 이름을 고정한다.

코드 연결
1~3줄
비유
연습실 열쇠에 db라는 방 번호와 PostgreSQL 상자 이름을 붙인다.
비유의 끝
방 이름만으로 database가 준비되지는 않는다.

밖 문과 안 문

host port는 환경값 또는 5432이고 container 안 PostgreSQL port는 5432다.

코드 연결
4~5줄
비유
건물 밖 초인종 번호를 방 안 5432번 전화로 연결한다.
비유의 끝
이미 쓰는 host port면 충돌할 수 있다.

필수 비밀번호

FCL_DB_PASSWORD가 없으면 Compose config 자체가 오류가 되어 빈 비밀번호로 시작하지 않는다.

코드 연결
6~9줄
비유
세 번째 출입 쪽지가 비어 있으면 접수대가 열쇠를 내주지 않는다.
비유의 끝
환경변수는 secret vault가 아니다.

준비 확인과 임시 저장

pg_isready로 server의 accepting-connections 상태를 반복 확인하고 data directory를 named volume에 연결한다.

코드 연결
10~19줄
비유
문이 방문 요청을 받는 상태인지 확인하고 기록 상자는 별도 창고 선반에 둔다.
비유의 끝
runner의 down -v 뒤에는 volume이 삭제될 수 있다.
왜 먼저 보는 파일인가
  1. SQL부터 읽으면 되는 것 아닌가요?

  2. SQL을 받을 PostgreSQL 방의 버전과 이름이 먼저 같아야 비교 조건이 맞아.

  3. 이 파일은 실행 dependency이고 측정 source 다섯 hash와는 다른 범위다.

  4. service가 db 하나인지부터 확인할게요.

두 개의 5432
  1. 5432가 두 번 나오면 DB가 두 개인가요?

  2. 왼쪽은 host 문 번호, 오른쪽은 container 안 PostgreSQL 문 번호야.

  3. ${FCL_DB_PORT:-5432}는 변수가 미설정이거나 빈 문자열일 때 왼쪽 기본값 5432를 쓴다.

  4. 미설정·빈 문자열·55432 세 경우를 나눠 port를 적어 볼게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 18줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.YAML 정본의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.18 / 18 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F01-L01 services: STARRY 임시 연습실 목록의 맨 표지를 펼친다. Compose 문서에서 services 최상위 mapping을 시작한다.
입력
docker compose가 이 YAML에서 실행할 service 목록을 찾기 시작한다.
결과·효과
YAML parser가 service 정의들을 담을 최상위 객체를 얻는다.
비유의 한계
이 표지만으로 service가 생성되거나 실행되지는 않는다.
2줄F01-L02 db: 목록에 `db`라고 적힌 한 칸짜리 방 열쇠를 건다. services 아래에 이름이 db인 service 정의를 연다.
입력
Compose가 services 안의 첫 이름을 읽어 service 식별자를 정한다.
결과·효과
최종 Compose service 집합에 이름이 db인 항목이 하나 잡힌다.
비유의 한계
db라는 이름은 PostgreSQL 접속 성공이나 데이터 생성을 뜻하지 않는다.
3줄F01-L03 image: postgres:17.10-alpine DB 방의 재료 상자에 PostgreSQL 17.10 Alpine 꼬리표를 붙인다. db container image를 postgres:17.10-alpine으로 지정한다.
입력
Docker가 db container를 만들 때 가져올 image reference를 해석한다.
결과·효과
container 생성 단계에서 postgres:17.10-alpine tag reference가 pull·실행 대상으로 해석된다.
비유의 한계
image tag는 immutable digest가 아니며 host·kernel·운영 부하까지 동일하게 만들지도 않는다.
4줄F01-L04 ports: 건물 밖과 DB 방 안을 잇는 문 번호 묶음을 펼친다. db service의 ports 배열을 시작한다.
입력
Compose가 container port를 host에 공개할 규칙을 받을 준비를 한다.
결과·효과
다음 list item을 port binding 문자열로 해석할 자리가 열린다.
비유의 한계
ports key만 있고 항목이 없으면 실제 port publish 규칙은 생기지 않는다.
5줄F01-L05 - "${FCL_DB_PORT:-5432}:5432" 밖 번호표가 미설정이거나 빈칸이면 5432를 쓰고 안쪽 5432번 문에 연결한다. FCL_DB_PORT가 미설정이거나 빈 문자열이면 기본 5432를, 값이 있으면 그 값을 host port로 써 container 5432에 publish한다.
입력
Compose가 FCL_DB_PORT의 미설정·빈 문자열·비어 있지 않은 값 세 경우에 `:-` 치환을 적용한다.
결과·효과
미설정·빈 문자열은 5432:5432가 되고, 예를 들어 55432는 55432:5432 binding이 된다.
비유의 한계
port 공개는 인증·방화벽·충돌 회피를 보장하지 않으며 이미 쓰는 port면 시작이 실패할 수 있다.
6줄F01-L06 environment: DB가 시작할 때 받을 설정 쪽지 봉투를 연다. db container에 전달할 environment mapping을 시작한다.
입력
PostgreSQL image의 초기화 변수를 묶어 전달하려고 Compose가 environment를 읽는다.
결과·효과
이어지는 세 key가 db process에 전달될 환경 변수로 묶인다.
비유의 한계
environment 표지 자체는 값의 비밀 보관이나 암호화를 수행하지 않는다.
7줄F01-L07 POSTGRES_DB: financial_core 첫 쪽지에 만들 database 이름 `financial_core`를 적는다. POSTGRES_DB 환경값을 financial_core로 설정한다.
입력
새 PostgreSQL data directory가 초기화될 때 기본 database 이름을 전달한다.
결과·효과
새 volume 초기화 시 financial_core database 생성 요청이 image entrypoint에 전달된다.
비유의 한계
이미 초기화된 volume에서는 이 값이 기존 database를 다시 만들지 않을 수 있다.
8줄F01-L08 POSTGRES_USER: app 둘째 쪽지에는 접속 사용자 이름 `app`을 쓴다. POSTGRES_USER 환경값을 app으로 설정한다.
입력
PostgreSQL image가 초기 superuser와 psql 접속 이름으로 app을 받는다.
결과·효과
초기 PostgreSQL 접속 주체 이름이 app으로 준비된다.
비유의 한계
app 이름만으로 최소 권한·role 분리·운영 권한 정책이 완성되지는 않는다.
9줄F01-L09 POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal} 셋째 쪽지는 terminal에 비밀번호가 없으면 접수대에서 돌려보낸다. FCL_DB_PASSWORD가 반드시 있어야 POSTGRES_PASSWORD 값으로 치환되게 한다.
입력
Compose config가 현재 terminal의 FCL_DB_PASSWORD를 검사하고 container 환경값에 넣는다.
결과·효과
비밀번호가 없으면 config 단계에서 오류가 나고, 있으면 그 값이 container 설정에 들어간다.
비유의 한계
필수값 검사는 secret manager가 아니며 process·환경 노출 위험을 없애지 않는다.
10줄F01-L10 healthcheck: 문을 열어도 되는지 확인할 건강 점검표 칸을 펼친다. db service의 healthcheck 설정을 시작한다.
입력
Docker가 container 실행 상태와 별도로 PostgreSQL 준비 상태를 판정할 규칙을 읽는다.
결과·효과
Docker가 db를 starting과 healthy로 구분할 검사 묶음을 얻는다.
비유의 한계
healthcheck 표지만으로 실제 SQL schema나 seed 상태를 검사하지 않는다.
11줄F01-L11 test: ["CMD-SHELL", "pg_isready -U app -d financial_core"] `app`·`financial_core` 목적지표를 달고 서버가 연결을 받는지만 묻는 초인종을 단다. CMD-SHELL에서 -U app과 -d financial_core를 probe target으로 붙여 pg_isready를 실행한다.
입력
Docker health monitor가 container 안 shell에서 PostgreSQL의 accepting-connections 상태를 확인한다.
결과·효과
server가 connection을 받을 상태면 probe가 exit 0을 내고 Docker health 판정에 사용된다.
비유의 한계
pg_isready 성공은 app 인증·financial_core 존재·SQL query·index·성능 성공을 보장하지 않는다.
12줄F01-L12 interval: 2s 준비 확인 벨이 2초마다 다시 울리도록 간격을 맞춘다. healthcheck 실행 간격을 2초로 지정한다.
입력
한 번의 health test가 끝난 뒤 다음 점검까지 Docker가 2초 주기를 사용한다.
결과·효과
health test 재호출 주기가 2초로 scheduling된다.
비유의 한계
2초 간격은 전체 startup 최대시간이나 query timeout을 정하지 않는다.
13줄F01-L13 timeout: 2s 한 번 문을 두드리고 기다릴 시간은 2초로 자른다. 각 healthcheck 시도의 timeout을 2초로 지정한다.
입력
pg_isready 한 번이 2초 안에 끝나지 않을 때 해당 시도를 실패로 처리한다.
결과·효과
한 시도가 2초를 넘기면 해당 health result가 실패로 기록된다.
비유의 한계
health timeout은 psql query나 runner phase의 실행 제한이 아니다.
14줄F01-L14 retries: 30 준비 확인이 30번 연속 실패하면 `unhealthy` 표를 붙이는 기준을 둔다. 연속 healthcheck 실패 30회를 unhealthy 판정 임계값으로 지정한다.
입력
Docker가 연속 실패 수가 30에 닿으면 service를 unhealthy로 표시하고 이후에도 healthcheck를 계속 실행한다.
결과·효과
연속 실패 허용 횟수가 30으로 설정되어 그 뒤 unhealthy 판정 근거가 된다.
비유의 한계
retries=30은 전체 점검을 30회로 끝내는 상한이나 정확한 60초 startup 보장이 아니다.
15줄F01-L15 volumes: DB 방 바닥과 기록 창고를 이을 끈 목록을 펼친다. db service에 연결할 volumes 배열을 시작한다.
입력
Compose가 이 service의 filesystem mount 항목을 받을 준비를 한다.
결과·효과
다음 list item이 db container의 mount 규칙으로 처리된다.
비유의 한계
volumes key만으로 backup·replication·삭제 방지가 생기지 않는다.
16줄F01-L16 - financial-core-db:/var/lib/postgresql/data `financial-core-db` 창고를 PostgreSQL 자료 선반에 바로 잇는다. named volume financial-core-db를 /var/lib/postgresql/data에 mount한다.
입력
PostgreSQL이 쓰는 data directory를 container layer 대신 named volume에 연결한다.
결과·효과
PostgreSQL data files가 financial-core-db resource 쪽에 기록된다.
비유의 한계
runner가 down -v를 쓰면 disposable 실험의 volume도 삭제되며 영구 보관 계약이 아니다.
18줄F01-L18 volumes: 서비스 밖에서도 부를 공용 기록 창고 명부를 연다. 문서 최상위 volumes mapping을 시작한다.
입력
Compose가 service mount에서 참조한 named volume 정의를 찾는다.
결과·효과
service 밖에서 참조할 named volume registry가 YAML 객체에 생긴다.
비유의 한계
최상위 선언은 volume 내용·용량·backup 정책을 지정하지 않는다.
19줄F01-L19 financial-core-db: 명부에 `financial-core-db` 창고 이름만 등록한다. financial-core-db named volume을 기본 옵션으로 선언한다.
입력
Compose project가 사용할 volume resource를 기본 driver 설정으로 준비한다.
결과·효과
Compose가 project 범위의 financial-core-db resource를 생성·재사용할 수 있다.
비유의 한계
SourceHashes 함수는 이 YAML과 volume 설정을 hash 목록에 포함하지 않는다.
필수 비밀번호
  1. 비밀번호가 비어도 실험용이면 그냥 켜지지 않나요?

  2. 물음표 보간식이 config 단계에서 빈 값을 거절해.

  3. 존재 검사는 보안 저장이 아니라 fail-fast configuration이다.

  4. terminal에 값을 둔 경우와 안 둔 경우를 나눠 보겠습니다.

05

STEP 05 / 13

원본 코드 조각

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

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

F01-C01 · db service, image, and port1–5줄
1–5줄 원본
services:
  db:
    image: postgres:17.10-alpine
    ports:
      - "${FCL_DB_PORT:-5432}:5432"
F01-C02 · database environment6–9줄
6–9줄 원본
    environment:
      POSTGRES_DB: financial_core
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}
F01-C03 · health check10–14줄
10–14줄 원본
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]
      interval: 2s
      timeout: 2s
      retries: 30
F01-C04 · service volume mount15–17줄
15–17줄 원본
    volumes:
      - financial-core-db:/var/lib/postgresql/data
F01-C05 · named volume declaration18–19줄
18–19줄 원본
volumes:
  financial-core-db:
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 18 / 18

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

원본한국어 번역
1services:Compose 문서에서 services 최상위 mapping을 시작한다.
2 db:services 아래에 이름이 db인 service 정의를 연다.
3 image: postgres:17.10-alpinedb container image를 postgres:17.10-alpine으로 지정한다.
4 ports:db service의 ports 배열을 시작한다.
5 - "${FCL_DB_PORT:-5432}:5432"FCL_DB_PORT가 미설정이거나 빈 문자열이면 기본 5432를, 값이 있으면 그 값을 host port로 써 container 5432에 publish한다.
6 environment:db container에 전달할 environment mapping을 시작한다.
7 POSTGRES_DB: financial_corePOSTGRES_DB 환경값을 financial_core로 설정한다.
8 POSTGRES_USER: appPOSTGRES_USER 환경값을 app으로 설정한다.
9 POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}FCL_DB_PASSWORD가 반드시 있어야 POSTGRES_PASSWORD 값으로 치환되게 한다.
10 healthcheck:db service의 healthcheck 설정을 시작한다.
11 test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]CMD-SHELL에서 -U app과 -d financial_core를 probe target으로 붙여 pg_isready를 실행한다.
12 interval: 2shealthcheck 실행 간격을 2초로 지정한다.
13 timeout: 2s각 healthcheck 시도의 timeout을 2초로 지정한다.
14 retries: 30연속 healthcheck 실패 30회를 unhealthy 판정 임계값으로 지정한다.
15 volumes:db service에 연결할 volumes 배열을 시작한다.
16 - financial-core-db:/var/lib/postgresql/datanamed volume financial-core-db를 /var/lib/postgresql/data에 mount한다.
18volumes:문서 최상위 volumes mapping을 시작한다.
19 financial-core-db:financial-core-db named volume을 기본 옵션으로 선언한다.
07

STEP 07 / 13

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

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

한 줄로 읽기

Compose는 PostgreSQL 17.10 Alpine db service 하나에 port·init 환경·healthcheck·named volume 계약을 적용한다.

문법 해부

  • YAML 들여쓰기는 services→db→각 설정의 소속을 결정한다.
  • ${FCL_DB_PORT:-5432}는 변수가 미설정이거나 빈 문자열일 때 5432를 쓰는 Compose 보간식이다.
  • ${FCL_DB_PASSWORD:?message}는 값이 없으면 config 오류를 내는 필수 보간식이다.
  • host:container port와 volume:container-path는 콜론 양쪽 역할이 다르다.

실행 순서

  1. YAML parse
  2. 환경변수 치환
  3. image·port·environment config
  4. container start
  5. pg_isready healthcheck
  6. named volume data 사용

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

F01-C01 · db service, image, and port
문법 해부
YAML `services.db` 아래에 image와 ports를 들여써 하나의 db service로 묶는다.
실제 값 추적
기본값이면 postgres:17.10-alpine의 5432를 host 5432에 연결한다.
정상 예
FCL_DB_PORT=55432이면 host 55432→container 5432로 열린다.
틀린 예·반례
ports 들여쓰기를 services와 같은 높이에 두면 db 속성이 아니어서 compose 해석이 달라진다.
착각 방지
17.10-alpine은 명시 tag일 뿐 immutable digest pin이 아니므로 같은 문자열이 image bytes나 성능을 고정했다는 증거가 아니다.
하지 않는 일
이 조각은 restart 정책, 계정 값, health 판정, 데이터 volume을 정하지 않는다.
다음 연결
다음 environment 조각이 db 이름·사용자·초기화 비밀번호를 공급한다.
F01-C02 · database environment
문법 해부
`environment` mapping의 세 key가 POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD를 컨테이너에 전달한다.
실제 값 추적
빈 PostgreSQL data directory를 처음 초기화할 때 financial_core DB와 app 사용자를 만들고 FCL_DB_PASSWORD 값을 초기 비밀번호로 쓴다.
정상 예
fresh named volume에서 FCL_DB_PASSWORD=w13-disposable-password를 주면 세 초기화 값이 entrypoint에 전달된다.
틀린 예·반례
FCL_DB_PASSWORD가 unset이거나 empty면 `${...:?...}`가 container 시작 전 Compose interpolation/config 단계에서 실패한다.
착각 방지
기존에 초기화된 data volume에 값을 다시 전달한다고 role·DB·비밀번호가 자동 재생성되거나 회전되지는 않는다.
하지 않는 일
비밀 보관, port 충돌, query fixture 생성은 이 mapping의 책임이 아니다.
다음 연결
healthcheck가 -U app과 -d financial_core를 probe 인자로 쓰지만 이 비밀번호 값을 직접 전달하지는 않는다.
F01-C03 · health check
문법 해부
CMD-SHELL healthcheck가 `pg_isready -U app -d financial_core`를 실행하고 interval=2s, timeout=2s, retries=30을 적용한다.
실제 값 추적
각 probe는 최대 2초 안에 PostgreSQL server의 connection status를 받고, 30회 연속 실패하면 container가 unhealthy가 된다.
정상 예
server가 accepting-connections 상태를 반환하면 probe exit 0으로 healthy가 되어 runner의 Compose `up --wait`가 끝날 수 있다.
틀린 예·반례
rejecting/no-response가 30회 연속 이어지면 unhealthy가 되지만, 이를 영구히 probe를 딱 30번만 실행한다는 뜻으로 읽으면 안 된다.
착각 방지
pg_isready 성공은 app 사용자·비밀번호가 인증됐거나 financial_core DB가 실제 존재함을 증명하지 않는다.
하지 않는 일
schema seed, query 성공, Docker engine 가용성, cleanup은 이 healthcheck가 책임지지 않는다.
다음 연결
healthy 뒤 PostgreSQL cluster 파일은 다음 named-volume mount에 기록된다.
F01-C04 · service volume mount
문법 해부
service의 `volumes` 배열이 named volume financial-core-db를 PostgreSQL data directory에 mount한다.
실제 값 추적
컨테이너가 /var/lib/postgresql/data에 쓴 cluster 파일이 named volume에 놓인다.
정상 예
같은 Compose project를 유지해 restart하면 데이터 directory를 다시 붙일 수 있다.
틀린 예·반례
mount target 철자를 바꾸면 PostgreSQL 데이터가 의도한 volume 밖에 생길 수 있다.
착각 방지
mount는 transaction 정확성이나 benchmark 격리를 자동 보장하지 않는다.
하지 않는 일
volume 자체의 선언과 삭제 시점은 다른 범위다.
다음 연결
마지막 YAML 조각이 참조한 named volume을 top-level에 선언한다.
F01-C05 · named volume declaration
문법 해부
top-level `volumes` 아래 빈 mapping으로 financial-core-db라는 named volume을 선언한다.
실제 값 추적
Compose가 project 이름을 붙인 volume을 만들고 db mount 참조를 해석한다.
정상 예
runner가 `down -v`를 실행하면 해당 disposable project의 volume도 제거된다.
틀린 예·반례
`down`에서 -v를 빼면 이전 seed 데이터가 다음 실행에 남을 수 있다.
착각 방지
선언 존재가 SourceHashes 다섯 파일에 compose.yaml을 포함시키지는 않는다.
하지 않는 일
이 두 줄은 누가 cleanup을 호출하는지 정하지 않는다.
다음 연결
seed.sql이 깨끗한 schema와 10만 행 fixture를 채운다.
healthy의 범위
  1. healthy면 app 인증과 index 선택도 끝났다는 말인가요?

  2. 아니, -U app과 -d financial_core는 probe target이고 server의 accepting-connections 상태만 봐.

  3. pg_isready는 인증 성공·database 존재·query 성공을 증명하지 않고 plan node도 확인하지 않는다.

  4. server health와 인증·EXPLAIN 결과를 서로 다른 증거 칸에 둘게요.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1FCL_DB_PORT 미지정 또는 빈 문자열, FCL_DB_PASSWORD=lab-secret두 Compose 보간식을 평가한다.host 5432→container 5432, password lab-secret 설정이 된다.password가 없으면 container 시작 전 config가 실패한다.
2postgres:17.10-alpine과 database/user 환경값새 db container와 빈 volume을 초기화한다.financial_core database와 app 사용자가 준비된다.기존 data volume이면 init 환경값이 재적용되지 않을 수 있다.
3process는 실행 중이지만 아직 connection을 받지 않는 PostgreSQL2초 간격·2초 timeout으로 pg_isready를 반복한다.pg_isready가 accepting 상태를 exit 0으로 알리면 db service가 healthy로 바뀐다.이 결과는 app 인증·financial_core 존재를 확인하지 않으며 retries=30 뒤에도 점검은 계속되고 정확한 60초 상한도 아니다.
4PostgreSQL이 /var/lib/postgresql/data에 쓰는 pagefinancial-core-db mount로 저장한다.container layer 밖의 named volume에 data files가 놓인다.이 W13 runner의 Compose 정리는 down -v라 실험 뒤 volume도 제거한다.
volume과 hash 경계
  1. 창고가 있으니 결과가 영원히 보관되고 hash에도 들어가겠죠?

  2. runner가 down -v로 정리하면 이 실험 창고도 없어질 수 있어.

  3. SourceHashes 목록은 runner와 SQL 네 개뿐이라 Compose 환경은 봉인하지 않는다.

  4. volume 수명과 source hash 수를 따로 적겠습니다.

09

STEP 09 / 13

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

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

Compose YAML

들여쓰기된 mapping과 list를 service model로 바꾸고 환경 보간을 먼저 검증한다.

YAML parse 성공은 Docker engine 사용 가능성을 보장하지 않는다.
Docker engine

image에서 container를 만들고 host port·환경값·mount·healthcheck를 적용한다.

host port와 volume lifecycle은 project 이름·실행 명령의 영향도 받는다.
PostgreSQL image

빈 data directory일 때 POSTGRES_DB·USER·PASSWORD로 초기 cluster를 만든다.

이미 채운 data directory는 같은 init 과정을 반복하지 않는다.
Health monitor

pg_isready exit code를 주기적으로 읽어 container health 상태를 바꾼다.

schema·seed·index·latency를 검사하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ healthcheck가 Green이면 W13 query가 빠르다는 뜻이다.

왜 틀리나 pg_isready는 -U/-d를 probe target으로 붙이지만 server가 connection을 받을 상태인지만 보고한다.

바르게 읽기 준비 상태와 EXPLAIN·benchmark 결과를 분리한다.

반례 잘못된 user·database 이름이어도 server 상태 응답은 받을 수 있고 index가 없어도 성공할 수 있다.

❌ named volume은 runner가 끝난 뒤에도 반드시 남는다.

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

바르게 읽기 이 문서의 volume은 disposable 실험 저장소로 읽는다.

반례 down만 쓰면 남을 수 있지만 down -v는 volume 제거를 요청한다.

❌ SourceHashes가 W13 환경 전체를 봉인하니 compose.yaml도 hash된다.

왜 틀리나 runner의 다섯 경로 목록에는 이 YAML이 없다.

바르게 읽기 packaged dependency와 five-file hash closure를 따로 센다.

반례 Compose image tag를 바꿔도 SourceHashes 배열의 다섯 결과는 그대로일 수 있다.

❌ 필수 환경변수면 비밀번호가 안전하게 비밀 저장된다.

왜 틀리나 보간식은 값 존재만 확인해 container 환경으로 전달한다.

바르게 읽기 실험용 fail-fast와 운영 secret 관리를 구분한다.

반례 Compose config나 process 환경 취급에 따라 값이 노출될 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

source integrity

compose.yaml이 SourceHashes 다섯 digest에 포함됨

이 책임을 맡는 곳: 별도 environment manifest 또는 Compose hash binding
database content

perf_w13 schema·10만 행·index가 존재함

이 책임을 맡는 곳: seed.sql, create-index.sql, assert.sql
performance

query plan 선택·상대 개선·운영 latency

이 책임을 맡는 곳: measure.sql과 runner의 plan/benchmark 단계
operations

backup·HA·TLS·최소권한·production 배포 보안

이 책임을 맡는 곳: 운영 인프라·보안·복구 설계
12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

db 한 개→image→port→세 환경값→healthcheck→data volume 순서로 말한다.

2단계 · 코드 조각 재조립

  1. services/db/image/ports
  2. POSTGRES_DB/USER/PASSWORD
  3. pg_isready/2s/2s/30
  4. service mount/top-level volume

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

실제 blank 한 줄을 포함한 19개 물리 줄을 다시 쓰고 canonical SHA-256과 대조한다.

자가 점검
  • service 이름은 db 하나인지 본다.
  • image tag는 17.10-alpine인지 본다.
  • password 보간식의 물음표 계약을 지킨다.
  • host와 container port 방향을 바꾸지 않는다.
  • SourceHashes 포함이라고 쓰지 않는다.
13

STEP 13 / 13

전체 원본 정답

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

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

volumes:
  financial-core-db:
02

seed.sql — 90% hot 계좌가 있는 결정적 10만 행 fixture

sql/w13/seed.sql

SQL 정본 · 정본 · W13-F02
13줄 연결13줄 번역10 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

매 측정 전에 perf_w13을 다시 만들고 계좌 1,000·거래 100,000·원장 100,000행을 같은 값과 같은 쏠림으로 채운다.

  1. 왜 지난 schema를 먼저 지울까?
  2. 계좌 1에는 왜 정확히 90,000행을 넣을까?
  3. 나머지 10,000행은 999계좌에 어떻게 나뉠까?
  4. MD5 UUID는 보안 증거일까?
  5. ANALYZE는 query를 측정하는 명령일까?
이 파일에서 끝까지 다시 쓰는 값schema=perf_w13accounts=1000business_tx=100000ledger=100000account1=90000 rowsaccounts2..11=11 rows eachaccounts12..1000=10 rows eachPERF_CREDIT/signed_amount=1base time=2026-01-01T00:00:00Z
02

STEP 02 / 13

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

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

STARRY가 인덱스 전후를 공정하게 비교하려고 같은 영수증 더미를 다시 만든다.

언제나 같은 10만 건 연습 장부

이 SQL은 perf_w13 공간을 새로 만들고 계좌 1,000개, 거래 100,000개, 원장 100,000개를 같은 규칙으로 넣는다.

원장 90,000개는 계좌 1에 몰고 나머지는 계좌 2~1,000에 나눈다. 끝에 원장 합계를 잔액으로 옮기고 통계를 만든다.

딱 여기까지만 전부 +1인 합성 자료라 실제 돈 흐름·고객 분포·운영 성능을 대신하지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

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

매번 새 연습장

지난 perf_w13을 지우고 같은 이름의 빈 schema를 다시 만든다.

코드 연결
1~3줄
비유
전에 풀던 공책을 치우고 같은 칸 수의 새 공책을 편다.
비유의 끝
운영 schema에서 실행할 cleanup 문장이 아니다.

연결된 세 표

원장 한 줄은 존재하는 거래 UUID와 계좌 ID를 각각 가리킨다.

코드 연결
4~6줄
비유
원장표에 거래 영수증과 계좌 서랍으로 가는 두 끈을 묶는다.
비유의 끝
이 단순 표에는 운영 domain 전체 열이 없다.

90% 쏠림

100,000 원장 중 90,000은 계좌 1, 나머지는 계좌 2~1,000에 순환 배치한다.

코드 연결
7~11줄
비유
영수증 열 장 중 아홉 장꼴을 한 손님 상자에 몰아 넣는다.
비유의 끝
일부러 만든 성능 반례이지 실제 고객 비율이 아니다.

합계와 통계

원장 합계를 balance로 옮기고 planner가 분포를 보도록 ANALYZE한다.

코드 연결
12~13줄
비유
상자별 장수를 표지에 적고 길찾기 담당에게 새 재고표를 준다.
비유의 끝
통계만으로 빠른 plan 선택이 보장되지는 않는다.
첫 오류에서 멈추기
  1. 중간 INSERT가 실패해도 뒤 ANALYZE까지 가면 안 되나요?

  2. 부분 fixture를 정상처럼 쓰지 않도록 psql을 첫 SQL 오류에서 멈춰.

  3. ON_ERROR_STOP은 뒤 statement를 막을 뿐 transaction이 아니어서 앞서 성공한 변경을 자동 rollback하지 않는다.

  4. 중단 지점과 이미 반영된 statement를 따로 확인하겠습니다.

schema를 다시 만드는 이유
  1. 지난 실험 표를 그대로 쓰면 더 빠르지 않나요?

  2. 남은 index나 행이 before 조건에 섞이면 전후 비교가 깨져.

  3. DROP CASCADE는 그래서 disposable perf_w13에만 허용되는 강한 초기화다.

  4. 실행 DB와 schema 이름을 먼저 확인할게요.

90,000 경계
  1. g=90001도 계좌 1로 갈 것 같아요.

  2. 조건이 g<=90000이라 90001부터 else 식으로 넘어가.

  3. 첫 cold 값은 2+((90001-90001)%999)=2다.

  4. 90000과 90001을 나란히 계산하겠습니다.

남은 만 행 나누기
  1. 10,000을 999로 나누면 모두 10개씩 아닌가요?

  2. 9,990개 뒤 열 개가 더 돌아서 계좌 2부터 11이 하나씩 더 받아.

  3. 그래서 cold 분포도 11행과 10행 두 집단이다.

  4. 2~11과 12~1000의 개수를 따로 기록할게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 13줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.SQL 정본의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.13 / 13 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F02-L01 \set ON_ERROR_STOP on psql이 첫 잘못에서 바로 빨간 종을 울리게 한다. psql meta-command로 ON_ERROR_STOP을 켜 SQL 오류에서 즉시 중단한다.
입력
runner가 seed.sql 전체를 psql 표준 입력으로 보내기 시작한다.
결과·효과
뒤 SQL 하나가 실패하면 psql이 nonzero로 멈추고 남은 seed 문장은 실행하지 않는다.
비유의 한계
뒤 문장은 멈추지만 BEGIN이나 single-transaction이 없어 앞서 성공한 문장을 자동 rollback하지 않고 psql 밖 오류도 처리하지 않는다.
2줄F02-L02 DROP SCHEMA IF EXISTS perf_w13 CASCADE; 이전 `perf_w13` 연습장을 안의 물건과 함께 깨끗이 비운다. 존재하면 perf_w13 schema와 의존 객체를 CASCADE로 삭제한다.
입력
같은 이름의 지난 실험 schema가 남아 있을 수 있는 database에서 seed를 시작한다.
결과·효과
지난 perf_w13 table·constraint·행이 새 측정에 섞이지 않게 제거된다.
비유의 한계
대상 schema를 통째로 지우므로 disposable 실험 DB 밖에서 가볍게 실행할 문장이 아니다.
3줄F02-L03 CREATE SCHEMA perf_w13; 비운 자리에 새 `perf_w13` 연습장을 세운다. 빈 perf_w13 schema를 새로 생성한다.
입력
DROP이 끝난 database에 뒤의 세 table을 담을 namespace를 준비한다.
결과·효과
빈 perf_w13 namespace가 생겨 뒤 relation 이름들이 그 안에 만들어질 수 있다.
비유의 한계
schema 생성만으로 table·행·통계는 아직 존재하지 않는다.
4줄F02-L04 CREATE TABLE perf_w13.account(id bigint PRIMARY KEY,owner_id varchar(64) NOT NULL,balance bigint NOT NULL CHECK(balance>=0)); 계좌 번호·주인표·음수 방지 잔액칸이 있는 계좌 서랍을 만든다. perf_w13.account에 bigint PK id, 필수 owner_id, 0 이상 필수 balance를 선언한다.
입력
새 schema에서 성능 fixture의 계좌 dimension table을 정의한다.
결과·효과
account relation과 PK·NOT NULL·balance CHECK가 database catalog에 등록된다.
비유의 한계
owner_id의 실제 고객 참조나 통화·상태·version 규칙은 이 단순 table에 없다.
5줄F02-L05 CREATE TABLE perf_w13.business_tx(id uuid PRIMARY KEY,status varchar(16) NOT NULL); UUID 번호와 상태칸만 가진 업무 영수증 서랍을 놓는다. perf_w13.business_tx에 UUID PK id와 필수 status 열을 선언한다.
입력
ledger가 참조할 100,000개 업무 거래의 최소 부모 table을 준비한다.
결과·효과
business_tx relation이 생겨 ledger FK가 가리킬 UUID 부모 key를 받을 수 있다.
비유의 한계
status 허용값 CHECK·금액·계좌·시간 열은 이 실험용 정의에 없다.
6줄F02-L06 CREATE TABLE perf_w13.ledger_entry(id bigint PRIMARY KEY,business_tx_id uuid NOT NULL REFERENCES perf_w13.business_tx(id),account_id bigint NOT NULL REFERENCES perf_w13.account(id),entry_type varchar(32) NOT NULL,signed_amount bigint NOT NULL,created_at timestamptz NOT NULL); 거래표와 계좌표에 끈을 묶은 원장 한 줄 서랍을 조립한다. perf_w13.ledger_entry에 PK, 두 FK, type, signed amount, timestamp 필수 열을 선언한다.
입력
각 원장 행이 존재하는 business_tx와 account를 가리키도록 성능 측정 table을 만든다.
결과·효과
ledger_entry relation이 두 부모 table에 연결된 여섯 필수 열로 준비된다.
비유의 한계
signed_amount 부호 CHECK나 debit·credit 균형 제약은 두지 않는다.
7줄F02-L07 INSERT INTO perf_w13.account SELECT g,'perf-owner-'||g,0 FROM generate_series(1,1000) g; 1부터 1,000까지 번호표를 뽑아 빈 잔액 계좌 서랍을 채운다. generate_series(1,1000)으로 1,000계좌와 perf-owner-g, balance 0을 삽입한다.
입력
빈 account table에 결정적인 id·owner 문자열·초기 잔액을 대량 생성한다.
결과·효과
account에는 id 1~1000의 1,000행이 들어가고 모든 초기 balance는 0이 된다.
비유의 한계
perf-owner 값은 합성 문자열이며 실제 소유자나 권한 관계를 표현하지 않는다.
8줄F02-L08 INSERT INTO perf_w13.business_tx SELECT md5('perf-'||g)::uuid,'COMPLETED' FROM generate_series(1,100000) g; 1부터 100,000까지 같은 조리법으로 UUID 영수증을 찍는다. generate_series(1,100000)의 perf-g MD5를 UUID로 바꿔 COMPLETED 거래를 삽입한다.
입력
각 g에 대응하는 결정적 business_tx id를 부모 table에 미리 채운다.
결과·효과
business_tx에는 재실행해도 같은 UUID를 쓰는 COMPLETED 100,000행이 채워진다.
비유의 한계
여기서 MD5는 재현 가능한 UUID 재료일 뿐 보안 hash나 충돌 불가능성을 증명하지 않는다.
9줄F02-L09 INSERT INTO perf_w13.ledger_entry 이제 100,000줄 원장을 넣을 서랍 이름과 투입구를 연다. perf_w13.ledger_entry 대상의 INSERT INTO 문을 시작한다.
입력
뒤 SELECT가 만든 여섯 값을 ledger_entry 열 순서에 맞춰 넣을 준비를 한다.
결과·효과
뒤 SELECT가 내놓는 행들이 ledger_entry 새 행으로 들어갈 대상이 고정된다.
비유의 한계
이 줄만으로는 행 수·계좌 쏠림·시간 값이 아직 정해지지 않는다.
10줄F02-L10 SELECT g,md5('perf-'||g)::uuid,CASE WHEN g<=90000 THEN 1 ELSE 2+((g-90001)%999) END,'PERF_CREDIT',1,timestamptz '2026-01-01 00:00:00+00'+g*interval '1 second' 앞 90,000장은 1번 손님 더미에, 남은 장은 999개 상자에 돌려 담는다. g로 id와 거래 UUID를 만들고 90,000행은 account 1, 나머지는 account 2~1000에 순환 배치한다.
입력
ledger 한 행마다 PERF_CREDIT, signed_amount 1, 기준시각+g초 값을 계산한다.
결과·효과
각 g마다 거래·계좌·종류·금액·시각 여섯 값이 결정되고 hot/cold 배치가 계산된다.
비유의 한계
계좌 2~11은 11행, 12~1000은 10행인 의도적 쏠림이며 균등·운영 분포가 아니다.
11줄F02-L11 FROM generate_series(1,100000) g; 원장 번호표를 정확히 1부터 100,000까지 흘려보낸다. ledger SELECT의 입력 g를 generate_series(1,100000)에서 만든다.
입력
앞 SELECT 식을 100,000번 평가해 ledger_entry 삽입 행을 완성한다.
결과·효과
SELECT가 100,000회 평가되어 ledger_entry가 정확히 100,000행이 된다.
비유의 한계
series 범위는 이 fixture의 행 수만 고정하며 실제 동시 요청을 만들지 않는다.
12줄F02-L12 UPDATE perf_w13.account a SET balance=x.n FROM (SELECT account_id,sum(signed_amount)::bigint n FROM perf_w13.ledger_entry GROUP BY account_id)x WHERE x.account_id=a.id; 각 계좌 원장 합계를 세어 계좌 서랍의 잔액표에 옮겨 적는다. account_id별 signed_amount 합계를 account.balance로 update한다.
입력
삽입 뒤 account 1 잔액은 90,000, 계좌 2~11은 11, 나머지는 10으로 대사된다.
결과·효과
원장과 잔액 불일치가 0이 되고 hot 계좌 1의 balance는 90,000으로 바뀐다.
비유의 한계
전부 +1인 fixture 합계일 뿐 실제 입출금 규칙·거래 status·이중-entry 균형을 검증하지 않는다.
13줄F02-L13 ANALYZE perf_w13.account; ANALYZE perf_w13.business_tx; ANALYZE perf_w13.ledger_entry; 세 서랍의 크기와 쏠림을 PostgreSQL 길찾기 담당에게 새로 알려준다. account, business_tx, ledger_entry 세 table에 ANALYZE를 실행해 planner 통계를 수집한다.
입력
모든 seed 행과 잔액 update가 끝난 뒤 다음 EXPLAIN을 위한 통계를 갱신한다.
결과·효과
planner가 1,000/100,000/100,000 규모와 account_id 쏠림을 추정할 통계를 얻게 된다.
비유의 한계
통계 수집은 measured query 실행·index 생성·성능 개선 판정을 하지 않는다.
계좌 표의 안전턱
  1. balance가 음수인 행도 성능 자료면 넣어도 되나요?

  2. 이 fixture의 account 정의는 balance>=0 CHECK를 둬.

  3. 하지만 통화·상태·version 같은 application 규칙은 생략돼 있다.

  4. PK·NOT NULL·CHECK 세 가지를 줄에서 찾겠습니다.

통계와 실제 계획
  1. ANALYZE까지 했으니 index plan도 정해졌나요?

  2. 통계 재료만 준비했고 어떤 길을 고를지는 query와 index 상태에서 planner가 결정해.

  3. 실제 node는 EXPLAIN JSON과 runner의 FindIndex가 확인한다.

  4. seed 증거와 plan 증거를 다른 칸에 두겠습니다.

05

STEP 05 / 13

원본 코드 조각

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

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

F02-C01 · psql fail-fast1–1줄
1–1줄 원본
\set ON_ERROR_STOP on
F02-C02 · recreate disposable schema2–3줄
2–3줄 원본
DROP SCHEMA IF EXISTS perf_w13 CASCADE;
CREATE SCHEMA perf_w13;
F02-C03 · account table4–4줄
4–4줄 원본
CREATE TABLE perf_w13.account(id bigint PRIMARY KEY,owner_id varchar(64) NOT NULL,balance bigint NOT NULL CHECK(balance>=0));
F02-C04 · business transaction table5–5줄
5–5줄 원본
CREATE TABLE perf_w13.business_tx(id uuid PRIMARY KEY,status varchar(16) NOT NULL);
F02-C05 · ledger table and foreign keys6–6줄
6–6줄 원본
CREATE TABLE perf_w13.ledger_entry(id bigint PRIMARY KEY,business_tx_id uuid NOT NULL REFERENCES perf_w13.business_tx(id),account_id bigint NOT NULL REFERENCES perf_w13.account(id),entry_type varchar(32) NOT NULL,signed_amount bigint NOT NULL,created_at timestamptz NOT NULL);
F02-C06 · 1,000 accounts7–7줄
7–7줄 원본
INSERT INTO perf_w13.account SELECT g,'perf-owner-'||g,0 FROM generate_series(1,1000) g;
F02-C07 · 100,000 transactions8–8줄
8–8줄 원본
INSERT INTO perf_w13.business_tx SELECT md5('perf-'||g)::uuid,'COMPLETED' FROM generate_series(1,100000) g;
F02-C08 · 100,000 skewed ledger rows9–11줄
9–11줄 원본
INSERT INTO perf_w13.ledger_entry
SELECT g,md5('perf-'||g)::uuid,CASE WHEN g<=90000 THEN 1 ELSE 2+((g-90001)%999) END,'PERF_CREDIT',1,timestamptz '2026-01-01 00:00:00+00'+g*interval '1 second'
FROM generate_series(1,100000) g;
F02-C09 · balance reconciliation12–12줄
12–12줄 원본
UPDATE perf_w13.account a SET balance=x.n FROM (SELECT account_id,sum(signed_amount)::bigint n FROM perf_w13.ledger_entry GROUP BY account_id)x WHERE x.account_id=a.id;
F02-C10 · planner statistics13–13줄
13–13줄 원본
ANALYZE perf_w13.account; ANALYZE perf_w13.business_tx; ANALYZE perf_w13.ledger_entry;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 13 / 13

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

원본한국어 번역
1\set ON_ERROR_STOP onpsql meta-command로 ON_ERROR_STOP을 켜 SQL 오류에서 즉시 중단한다.
2DROP SCHEMA IF EXISTS perf_w13 CASCADE;존재하면 perf_w13 schema와 의존 객체를 CASCADE로 삭제한다.
3CREATE SCHEMA perf_w13;빈 perf_w13 schema를 새로 생성한다.
4CREATE TABLE perf_w13.account(id bigint PRIMARY KEY,owner_id varchar(64) NOT NULL,balance bigint NOT NULL CHECK(balance>=0));perf_w13.account에 bigint PK id, 필수 owner_id, 0 이상 필수 balance를 선언한다.
5CREATE TABLE perf_w13.business_tx(id uuid PRIMARY KEY,status varchar(16) NOT NULL);perf_w13.business_tx에 UUID PK id와 필수 status 열을 선언한다.
6CREATE TABLE perf_w13.ledger_entry(id bigint PRIMARY KEY,business_tx_id uuid NOT NULL REFERENCES perf_w13.business_tx(id),account_id bigint NOT NULL REFERENCES perf_w13.account(id),entry_type varchar(32) NOT NULL,signed_amount bigint NOT NULL,created_at timestamptz NOT NULL);perf_w13.ledger_entry에 PK, 두 FK, type, signed amount, timestamp 필수 열을 선언한다.
7INSERT INTO perf_w13.account SELECT g,'perf-owner-'||g,0 FROM generate_series(1,1000) g;generate_series(1,1000)으로 1,000계좌와 perf-owner-g, balance 0을 삽입한다.
8INSERT INTO perf_w13.business_tx SELECT md5('perf-'||g)::uuid,'COMPLETED' FROM generate_series(1,100000) g;generate_series(1,100000)의 perf-g MD5를 UUID로 바꿔 COMPLETED 거래를 삽입한다.
9INSERT INTO perf_w13.ledger_entryperf_w13.ledger_entry 대상의 INSERT INTO 문을 시작한다.
10SELECT g,md5('perf-'||g)::uuid,CASE WHEN g<=90000 THEN 1 ELSE 2+((g-90001)%999) END,'PERF_CREDIT',1,timestamptz '2026-01-01 00:00:00+00'+g*interval '1 second'g로 id와 거래 UUID를 만들고 90,000행은 account 1, 나머지는 account 2~1000에 순환 배치한다.
11FROM generate_series(1,100000) g;ledger SELECT의 입력 g를 generate_series(1,100000)에서 만든다.
12UPDATE perf_w13.account a SET balance=x.n FROM (SELECT account_id,sum(signed_amount)::bigint n FROM perf_w13.ledger_entry GROUP BY account_id)x WHERE x.account_id=a.id;account_id별 signed_amount 합계를 account.balance로 update한다.
13ANALYZE perf_w13.account; ANALYZE perf_w13.business_tx; ANALYZE perf_w13.ledger_entry;account, business_tx, ledger_entry 세 table에 ANALYZE를 실행해 planner 통계를 수집한다.
07

STEP 07 / 13

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

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

한 줄로 읽기

psql fail-fast로 perf_w13을 재생성하고 FK로 연결된 1k/100k/100k fixture와 0.9 hot 분포·대사 잔액·planner 통계를 만든다.

문법 해부

  • generate_series는 시작과 끝을 모두 포함해 결정적 행 수를 만든다.
  • CASE는 g<=90000이면 account 1, 아니면 modulo 식으로 2~1000을 고른다.
  • md5(text)::uuid는 같은 g에 같은 UUID를 제공하지만 보안 목적이 아니다.
  • UPDATE ... FROM 집계 subquery는 account_id별 SUM을 balance에 복사한다.
  • ANALYZE는 table 통계를 모으는 명령이며 EXPLAIN ANALYZE와 다르다.

실행 순서

  1. 오류 즉시 중단
  2. schema 삭제·생성
  3. 세 table 생성
  4. 계좌 1,000행
  5. 거래 100,000행
  6. 쏠린 원장 100,000행
  7. 잔액 대사
  8. 통계 수집

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

F02-C01 · psql fail-fast
문법 해부
psql meta-command `\set ON_ERROR_STOP on`이 SQL 오류 뒤 실행 중단을 켠다.
실제 값 추적
DROP·CREATE·INSERT 중 하나가 실패하면 뒤 statement로 진행하지 않는다.
정상 예
ledger insert 제약 위반이 나면 balance UPDATE와 ANALYZE가 실행되지 않는다.
틀린 예·반례
이 명령을 빼면 psql 호출 방식에 따라 뒤 statement가 이어져 반쪽 evidence가 생길 수 있다.
착각 방지
fail-fast는 전체 파일을 하나의 transaction으로 묶지 않는다.
하지 않는 일
Docker process exit와 evidence JSON 처리는 runner가 맡는다.
다음 연결
다음 조각이 perf_w13 schema를 제거하고 다시 만든다.
F02-C02 · recreate disposable schema
문법 해부
DROP SCHEMA ... CASCADE 뒤 CREATE SCHEMA를 실행해 동일 이름의 격리 공간을 새로 만든다.
실제 값 추적
이전 perf_w13 표·index가 있으면 모두 지운 뒤 빈 schema로 시작한다.
정상 예
재실행 시 오래된 100000행과 새 100000행이 합쳐지지 않는다.
틀린 예·반례
CASCADE 대상을 운영 schema로 바꾸면 복구하기 어려운 파괴가 된다.
착각 방지
schema 재생성은 Docker volume 전체를 지우지 않는다.
하지 않는 일
표 구조와 데이터 분포는 아직 없다.
다음 연결
세 table DDL 조각이 account, business_tx, ledger_entry를 만든다.
F02-C03 · account table
문법 해부
한 줄 CREATE TABLE이 bigint `id` PK, `owner_id` 문자열, 0 이상 CHECK가 붙은 bigint `balance`를 정의한다.
실제 값 추적
계좌 한 행은 id·owner_id·balance 세 값을 반드시 가진다.
정상 예
뒤 insert가 id=1, owner_id=perf-owner-1, balance=0인 행을 넣을 수 있다.
틀린 예·반례
balance가 음수이거나 id가 중복이면 table constraint가 seed를 중단시킨다.
착각 방지
owner_id 문자열은 실제 customer FK나 인증 소유권을 모델링하지 않는다.
하지 않는 일
통화·version·계좌 상태는 이 세 열에 없다.
다음 연결
business_tx가 UUID와 COMPLETED 상태만 가진 합성 거래 identity를 준비한다.
F02-C04 · business transaction table
문법 해부
business_tx DDL은 UUID `id` primary key와 NOT NULL `status` 두 열만 정의한다.
실제 값 추적
뒤 insert가 md5 기반 UUID마다 COMPLETED 상태 한 값을 저장할 수 있다.
정상 예
같은 UUID를 다시 넣으면 primary-key 중복으로 실패한다.
틀린 예·반례
account_id나 amount 열이 있다고 가정한 query는 이 실제 두 열 schema에서 실패한다.
착각 방지
이 표 자체는 거래를 계좌에 직접 연결하지 않는다.
하지 않는 일
금액·상대 계좌·멱등 key는 이 합성 schema의 business_tx에 없다.
다음 연결
ledger_entry의 business_tx_id FK가 거래 UUID를 account 원장 행과 연결한다.
F02-C05 · ledger table and foreign keys
문법 해부
ledger_entry DDL이 bigint `id` PK와 business_tx_id/account_id FK, entry_type, signed_amount, created_at을 정의한다.
실제 값 추적
원장 100000행이 거래 UUID 100000개와 계좌 1000개에 각각 연결된다.
정상 예
created_at은 measure.sql의 최신 50행 정렬 key가 된다.
틀린 예·반례
business_tx_id나 account_id가 부모에 없으면 FK 위반으로 insert가 멈춘다.
착각 방지
단일 PERF_CREDIT 종류는 운영 복식 원장 모델을 재현하지 않는다.
하지 않는 일
hot 계좌 90% 분포는 DDL이 아니라 insert 식에서 만들어진다.
다음 연결
generate_series 계좌 insert가 account FK 부모를 먼저 채운다.
F02-C06 · 1,000 accounts
문법 해부
generate_series(1,1000)이 account의 id를 만들고 `perf-owner-` 접두사 owner_id와 balance 0을 함께 넣는다.
실제 값 추적
정확히 1000개 PK 행이 생긴다.
정상 예
첫 행은 id=1,owner_id=perf-owner-1,balance=0이고 마지막 id는 1000이다.
틀린 예·반례
상한을 999로 바꾸면 assert의 account count가 실패한다.
착각 방지
초기 0은 최종 원장 합과 일치하도록 뒤 UPDATE가 바꿀 값이다.
하지 않는 일
고객 FK·실제 owner entity·hot skew는 이 insert가 만들지 않는다.
다음 연결
다음 insert가 100000개의 UUID/COMPLETED business_tx 행을 만든다.
F02-C07 · 100,000 transactions
문법 해부
generate_series(1,100000)의 g를 `md5('perf-'||g)::uuid`로 바꾸고 status COMPLETED와 함께 business_tx에 넣는다.
실제 값 추적
서로 다른 합성 UUID를 가진 100000개 거래 identity가 생긴다.
정상 예
g=1은 문자열 perf-1의 MD5 UUID와 COMPLETED 상태를 만든다.
틀린 예·반례
UUID 식을 상수로 바꾸면 두 번째 행부터 primary-key 중복으로 실패한다.
착각 방지
이 두 열 insert는 account_id나 amount를 생성·배분하지 않는다.
하지 않는 일
created_at과 hot 계좌 분포는 ledger_entry insert에서만 정해진다.
다음 연결
세 줄 ledger insert가 각 UUID를 원장에 연결하고 account 1에 90000행을 몰아준다.
F02-C08 · 100,000 skewed ledger rows
문법 해부
ledger INSERT-SELECT가 CASE로 account skew를 만들고 UUID·PERF_CREDIT·+1·초 단위 created_at과 함께 generate_series 100000행을 넣는다.
실제 값 추적
g<=90000은 account_id=1, 나머지는 2..1000에 배분되어 1번 계좌가 정확히 90000행을 가진다.
정상 예
g=90001은 account 2이고 g=100000은 modulus 계산상 account 11에 들어간다.
틀린 예·반례
CASE 경계나 modulus 999를 바꾸면 hot=90000과 나머지 10000 분포가 깨진다.
착각 방지
전부 PERF_CREDIT,+1인 합성 분포는 운영 selectivity를 대표하지 않는다.
하지 않는 일
계좌 balance는 insert 시점에 여전히 0이다.
다음 연결
다음 UPDATE가 각 account의 signed_amount 합으로 balance를 맞춘다.
F02-C09 · balance reconciliation
문법 해부
derived table x가 ledger_entry를 account_id별로 GROUP BY해 sum(signed_amount) n을 만들고, UPDATE ... FROM이 `x.account_id=a.id`인 account의 balance를 x.n으로 바꾼다.
실제 값 추적
모든 원장이 +1이므로 account 1은 90000, account 2~11은 각각 11, account 12~1000은 각각 10으로 갱신된다.
정상 예
현재 fixture처럼 모든 account에 원장이 있으면 각 target row가 정확히 자기 x 집계행 하나와 연결된다.
틀린 예·반례
WHERE x.account_id=a.id를 빼면 각 account가 여러 x 행과 결합되어 임의의 한 group 합을 받을 수 있고, 전체 100000 합을 일괄 대입하는 동작도 아니다.
착각 방지
이 statement는 COALESCE를 쓰지 않으므로 원장이 없는 account는 0으로 설정되는 대신 UPDATE 대상에서 빠져 기존 balance를 유지한다.
하지 않는 일
balance 일치는 합성 fixture invariant일 뿐 실제 회계 정책·index 존재·planner 통계를 증명하지 않는다.
다음 연결
세 ANALYZE statement가 방금 만든 세 table 분포를 planner에 알린다.
F02-C10 · planner statistics
문법 해부
한 physical line의 세 ANALYZE statement가 account, business_tx, ledger_entry를 차례로 통계 수집한다.
실제 값 추적
세 table의 1000/100000/100000행 분포와 ledger hot account 빈도가 planner 통계에 반영될 수 있다.
정상 예
BeforePlan은 세 table 분석이 끝난 뒤 같은 measure.sql을 실행한다.
틀린 예·반례
ANALYZE를 생략하면 planner가 stale/default 통계로 access path를 고를 수 있다.
착각 방지
통계 수집은 composite index를 생성하거나 query 시간을 측정하지 않는다.
하지 않는 일
정확한 estimate나 특정 plan 선택은 세 ANALYZE 성공만으로 보장되지 않는다.
다음 연결
measure.sql이 분석된 ledger_entry에서 hot 계좌 최신 50행 plan을 요청한다.
거래 표가 작은 까닭
  1. business_tx에 금액과 계좌가 왜 없나요?

  2. 여기서는 ledger FK가 가리킬 UUID와 status만 필요한 최소 부모 표야.

  3. workbook business_tx나 앱 V001과 같은 schema라고 보면 안 된다.

  4. perf_w13 전용 열 두 개만 적겠습니다.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1지난 perf_w13이 있거나 없는 databaseDROP IF EXISTS CASCADE 후 CREATE한다.이전 table·index·행이 없는 빈 perf_w13이 된다.CASCADE는 대상 schema 의존 객체까지 지우므로 disposable DB 전제다.
2g=1..1000account id, perf-owner-g, balance 0을 만든다.account 1,000행이 정확히 생긴다.owner 문자열은 권한·개인정보·실제 관계가 아니다.
3g=1..100000perf-g MD5 UUID와 COMPLETED를 만든다.ledger가 참조할 transaction 100,000행이 생긴다.MD5를 암호학적 안전성 증거로 사용하지 않는다.
4ledger g=90000과 g=90001CASE와 modulo 계좌 식을 적용한다.g=90000은 account1, g=90001은 account2로 간다.경계 하나가 맞아도 전체 100,000 분포는 별도 count로 확인해야 한다.
5남은 g=90001..100000 10,000개999계좌에 modulo 순환 배치한다.account2~11은 11행씩, account12~1000은 10행씩 받는다.cold 계좌도 완전 균등하지 않아 row estimate를 분포와 함께 읽어야 한다.
6각 ledger의 signed_amount=1account_id별 SUM을 balance에 update한다.account1 balance90000, 나머지는 각 10 또는11, mismatch0 기반이 된다.모든 행이 credit인 단순 합성 계산이다.
7완성된 세 table세 ANALYZE를 실행한다.다음 planner가 규모와 값 분포 통계를 사용할 수 있다.실제 선택 plan과 시간은 measure·runner에서 봐야 한다.
원장의 두 연결
  1. 원장 ID만 있으면 계좌와 거래를 나중에 문자열로 찾으면 되지 않나요?

  2. 두 FK가 없는 거래·없는 계좌를 가리키는 행을 막아 줘.

  3. 다만 signed_amount 부호 균형까지 FK가 검사하지는 않는다.

  4. business_tx_id와 account_id 참조 대상을 각각 확인할게요.

09

STEP 09 / 13

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

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

psql

ON_ERROR_STOP이 server SQL error를 받으면 script의 뒤 문장을 중단시킨다.

BEGIN·single-transaction이 없어 앞서 성공한 statement를 자동 취소하지 않으며 Docker failure는 runner가 맡는다.
PostgreSQL catalog

schema·table·PK·FK·CHECK 정의를 relation과 constraint metadata로 등록한다.

fixture table은 application migration table과 별도 perf_w13 namespace다.
Set-returning function

generate_series가 g 값을 순서대로 만들어 INSERT SELECT 입력 행이 된다.

동시 요청이나 실제 event arrival을 흉내 내지 않는다.
Planner statistics

ANALYZE가 sample을 읽고 row count·분포 추정 자료를 pg_statistic 계열에 갱신한다.

통계는 추정 자료이므로 actual rows와 항상 같지 않다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ 10만 행을 1,000계좌에 거의 똑같이 나눴다.

왜 틀리나 계좌 1 하나가 90,000행을 독점한다.

바르게 읽기 hot account와 999 cold accounts를 따로 센다.

반례 account1은 90,000행이지만 account12는 10행뿐이다.

❌ MD5 UUID를 썼으니 transaction ID가 보안상 안전함을 증명한다.

왜 틀리나 여기서는 perf-g를 같은 UUID로 재생성하려는 fixture 도구다.

바르게 읽기 결정적 식별자와 보안 hash 요구를 분리한다.

반례 source에는 secret·salt·threat model이 없다.

❌ ANALYZE 세 문장이 query 시간을 이미 측정한다.

왜 틀리나 table statistics 수집 명령이고 뒤 SELECT의 Execution Time을 내지 않는다.

바르게 읽기 ANALYZE table과 EXPLAIN (ANALYZE ...)를 구분한다.

반례 seed.sql에는 EXPLAIN이나 LIMIT 50이 없다.

❌ 잔액과 원장 합계가 맞으니 실제 송금 domain도 검증됐다.

왜 틀리나 모든 ledger가 PERF_CREDIT +1인 성능용 자료다.

바르게 읽기 fixture arithmetic과 업무 debit/credit 규칙을 나눈다.

반례 business_tx에는 amount·account·time 열조차 없다.

1,000계좌 만들기
  1. generate_series의 1000은 999개를 뜻하나요?

  2. PostgreSQL의 시작·끝을 모두 포함하니 1부터 1000까지 천 개야.

  3. owner도 perf-owner-g라는 합성 문자열일 뿐 실제 actor가 아니다.

  4. 첫 id1과 끝 id1000을 대입해 보겠습니다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

data realism

0.9 hot 비율이 실제 고객 traffic을 대표함

이 책임을 맡는 곳: 운영 통계·익명화 표본·용량 모델
domain integrity

debit/credit 균형·상태 전이·업무 금액의 정당성

이 책임을 맡는 곳: application schema·service·domain integration tests
performance

Seq Scan·Index Scan 선택 또는 index 후 개선

이 책임을 맡는 곳: measure.sql, create-index.sql, runner plan/benchmark
safety

운영 schema data 보존

이 책임을 맡는 곳: disposable Compose lifecycle과 실행 환경 분리
12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

fail-fast→새 schema→세 표→1k/100k/100k→90k hot→balance 합계→통계 순서로 말한다.

2단계 · 코드 조각 재조립

  1. ON_ERROR_STOP/DROP/CREATE
  2. account/business_tx/ledger_entry
  3. generate_series account/tx
  4. ledger CASE/modulo/time
  5. balance UPDATE/three ANALYZE

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

13개 물리 줄을 긴 SQL까지 그대로 다시 쓰고 source SHA-256·행 수·쏠림 식을 대조한다.

자가 점검
  • g 범위 끝값을 포함한다.
  • hot 경계는 <=90000이다.
  • cold 식의 +2와 %999를 지킨다.
  • created_at 기준시각과 g초를 지킨다.
  • MD5를 보안 증명이라고 쓰지 않는다.
  • ANALYZE를 측정 결과라고 쓰지 않는다.
결정적 UUID
  1. MD5면 안전한 UUID를 만든다는 뜻인가요?

  2. 여기서는 같은 perf-g가 매번 같은 UUID가 되게 하려는 선택이야.

  3. 재현성과 암호학적 안전성은 서로 다른 주장이다.

  4. SourceHashes와 transaction UUID 용도를 섞지 않겠습니다.

13

STEP 13 / 13

전체 원본 정답

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

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcesql/w13/seed.sqlSHA-256 cec3786fdae6fd9acda924f9932804fc4cd3aec15633eb2e71de7f6932ea4784
seed.sql — 90% hot 계좌가 있는 결정적 10만 행 fixture 전체
\set ON_ERROR_STOP on
DROP SCHEMA IF EXISTS perf_w13 CASCADE;
CREATE SCHEMA perf_w13;
CREATE TABLE perf_w13.account(id bigint PRIMARY KEY,owner_id varchar(64) NOT NULL,balance bigint NOT NULL CHECK(balance>=0));
CREATE TABLE perf_w13.business_tx(id uuid PRIMARY KEY,status varchar(16) NOT NULL);
CREATE TABLE perf_w13.ledger_entry(id bigint PRIMARY KEY,business_tx_id uuid NOT NULL REFERENCES perf_w13.business_tx(id),account_id bigint NOT NULL REFERENCES perf_w13.account(id),entry_type varchar(32) NOT NULL,signed_amount bigint NOT NULL,created_at timestamptz NOT NULL);
INSERT INTO perf_w13.account SELECT g,'perf-owner-'||g,0 FROM generate_series(1,1000) g;
INSERT INTO perf_w13.business_tx SELECT md5('perf-'||g)::uuid,'COMPLETED' FROM generate_series(1,100000) g;
INSERT INTO perf_w13.ledger_entry
SELECT g,md5('perf-'||g)::uuid,CASE WHEN g<=90000 THEN 1 ELSE 2+((g-90001)%999) END,'PERF_CREDIT',1,timestamptz '2026-01-01 00:00:00+00'+g*interval '1 second'
FROM generate_series(1,100000) g;
UPDATE perf_w13.account a SET balance=x.n FROM (SELECT account_id,sum(signed_amount)::bigint n FROM perf_w13.ledger_entry GROUP BY account_id)x WHERE x.account_id=a.id;
ANALYZE perf_w13.account; ANALYZE perf_w13.business_tx; ANALYZE perf_w13.ledger_entry;
03

measure.sql — hot 계좌의 최신 50행 실제 plan을 JSON으로 측정

sql/w13/measure.sql

SQL 정본 · 정본 · W13-F03
3줄 연결3줄 번역1 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

account_id 1의 최신 원장 50행 top-N query를 실제 실행하고 plan·buffer·Execution Time을 JSON 한 값으로 받아 runner가 읽게 한다.

  1. EXPLAIN에 ANALYZE가 붙으면 query를 실제로 실행할까?
  2. BUFFERS는 무엇을 더 보여 줄까?
  3. 왜 created_at 뒤에 id도 내림차순일까?
  4. LIMIT 50이면 90,000행을 전부 읽지 않는다고 보장할까?
  5. 이 source가 OFFSET과 keyset을 비교할까?
이 파일에서 끝까지 다시 쓰는 값EXPLAIN ANALYZEBUFFERSFORMAT JSONaccount_id=1created_at DESCid DESCLIMIT 50
02

STEP 02 / 13

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

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

STARRY가 계좌 1의 최신 원장 50줄을 찾으며 PostgreSQL이 택한 길을 기록한다.

최신 50건을 찾은 길을 JSON으로 보기

이 SQL은 계좌 1의 원장을 최신 시각·큰 ID 순으로 정렬해 최신 50건을 결과로 내보내고, 실제 실행계획을 JSON으로 돌려준다.

ANALYZE는 조회를 실제로 실행하고 BUFFERS는 읽은 저장 페이지 정보를 보탠다. runner는 이 JSON에서 실행 시간을 꺼낸다.

딱 여기까지만 OFFSET·cursor/keyset·다른 계좌는 재지 않으므로 일반적인 pagination 비교 결과가 아니다.

03

STEP 03 / 13

초등학생도 이해하는 설명

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

실제로 걸어 본 길

ANALYZE가 붙어 planner 예상만 보지 않고 SELECT를 실행해 actual 값도 적는다.

코드 연결
1줄
비유
지도만 그리는 대신 창고까지 직접 걸어가 걸린 시간을 함께 적는다.
비유의 끝
쓰기 query라면 side effect도 생길 수 있지만 여기서는 SELECT다.

읽은 저장칸

BUFFERS는 shared buffer hit/read 같은 page 이용 정보를 plan에 보탠다.

코드 연결
1줄
비유
찾는 동안 어느 선반 칸을 이미 기억했고 새로 열었는지 표시한다.
비유의 끝
buffer 수치 하나만으로 전체 I/O 원인을 확정하지 않는다.

최신 50줄

계좌 1만 고르고 시각·ID를 둘 다 역순으로 정해 앞 50행을 요청한다.

코드 연결
2~3줄
비유
1번 상자에서 날짜가 새롭고 같은 날짜면 번호가 큰 영수증부터 50장 꺼낸다.
비유의 끝
planner가 어떤 경로로 50장을 찾는지는 index 상태에 따라 달라진다.
ANALYZE는 실제 실행
  1. EXPLAIN이면 query를 안 돌리고 예상만 보여 주나요?

  2. ANALYZE가 붙었으니 이 SELECT를 실제로 실행하고 actual 값도 모아.

  3. 읽기 SELECT라 data 변경은 없지만 실행 비용과 cache 영향은 생긴다.

  4. plain EXPLAIN과 EXPLAIN ANALYZE를 구분해 적겠습니다.

cost와 시간
  1. plan cost가 42면 42ms인가요?

  2. cost는 planner가 경로를 비교하는 내부 숫자고 ms는 Execution Time 쪽이야.

  3. estimated rows도 통계 추정이고 Actual Rows와 별개다.

  4. JSON에서 예상값과 실제값을 다른 칸에 놓겠습니다.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 3줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.SQL 정본의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.3 / 3 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F03-L01 EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON) 길찾기 보고서에 실제 걸음·읽은 창고칸·JSON 양식을 모두 켠다. EXPLAIN에 ANALYZE, BUFFERS, FORMAT JSON 옵션을 지정한다.
입력
PostgreSQL이 뒤 SELECT의 실행계획 요청을 받아 실제 실행과 buffer 계측을 준비한다.
결과·효과
일반 조회 행 대신 실제 실행 정보와 buffer 수치를 담은 JSON plan 한 값이 반환 형식이 된다.
비유의 한계
planner cost는 millisecond가 아니고 estimated rows도 actual rows와 같은 값이라는 보장이 없다.
2줄F03-L02 SELECT id,entry_type,signed_amount,created_at FROM perf_w13.ledger_entry 보고서에서 보여 줄 원장표 네 칸을 골라 든다. perf_w13.ledger_entry에서 id, entry_type, signed_amount, created_at을 조회한다.
입력
EXPLAIN ANALYZE가 실행할 SELECT의 출력 열과 source table을 정한다.
결과·효과
실행된 top-N query의 후보 출력은 네 열로 정해지고 source relation은 ledger_entry가 된다.
비유의 한계
이 줄만 보면 대상 account·정렬·행 제한이 아직 적용되지 않았다.
3줄F03-L03 WHERE account_id=1 ORDER BY created_at DESC,id DESC LIMIT 50; 1번 손님 상자만 골라 최신 시각·큰 번호 순으로 50장을 잘라낸다. account_id=1을 filter하고 created_at DESC, id DESC로 정렬한 뒤 LIMIT 50을 적용한다.
입력
90,000행인 hot account 1에서 안정된 최신순 top-N 50행을 요청한다.
결과·효과
account 1의 최신 50행을 찾는 실제 경로·시간·buffer 정보가 plan JSON에 기록된다.
비유의 한계
OFFSET·cursor/keyset 조건과 다른 account parameter는 없어 pagination 비교를 직접 측정하지 않는다.
90,000에서 50
  1. LIMIT 50이면 앞에서 딱 50개만 보면 되죠?

  2. 맞는 index가 있으면 일찍 멈추기 쉽지만 없으면 filter와 sort 때문에 더 훑을 수 있어.

  3. output cardinality와 rows scanned는 같은 개념이 아니다.

  4. plan node의 실제 row와 buffer도 함께 보겠습니다.

05

STEP 05 / 13

원본 코드 조각

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

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

F03-C01 · one exact measured top-N statement1–3줄
1–3줄 원본
EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON)
SELECT id,entry_type,signed_amount,created_at FROM perf_w13.ledger_entry
WHERE account_id=1 ORDER BY created_at DESC,id DESC LIMIT 50;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 3 / 3

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

원본한국어 번역
1EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON)EXPLAIN에 ANALYZE, BUFFERS, FORMAT JSON 옵션을 지정한다.
2SELECT id,entry_type,signed_amount,created_at FROM perf_w13.ledger_entryperf_w13.ledger_entry에서 id, entry_type, signed_amount, created_at을 조회한다.
3WHERE account_id=1 ORDER BY created_at DESC,id DESC LIMIT 50;account_id=1을 filter하고 created_at DESC, id DESC로 정렬한 뒤 LIMIT 50을 적용한다.
07

STEP 07 / 13

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

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

한 줄로 읽기

EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)이 account1의 created_at/id DESC LIMIT50 SELECT를 실행해 기계 판독 가능한 실제 plan을 반환한다.

문법 해부

  • EXPLAIN 옵션 괄호는 실행·buffer·출력 형식을 한 statement에 적용한다.
  • WHERE는 account_id=1 후보군을 정하고 ORDER BY 두 열은 total order를 만든다.
  • LIMIT 50은 출력 행 수를 자르지만 접근 경로가 읽는 내부 행 수까지 고정하지 않는다.
  • FORMAT JSON 결과는 runner가 ConvertFrom-Json으로 읽을 수 있는 구조다.

실행 순서

  1. SQL parse
  2. planner 경로 선택
  3. account filter와 정렬/top-N 실행
  4. actual·buffer 계측
  5. JSON plan 반환

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

F03-C01 · one exact measured top-N statement
문법 해부
EXPLAIN ANALYZE/BUFFERS/JSON이 account_id=1 predicate, created_at·id 내림차순, LIMIT 50인 한 SELECT를 감싼다.
실제 값 추적
underlying SELECT를 실제 실행해 plan의 Limit node Actual Rows에서 최대 50을 관찰하고, client에는 ledger 50행 대신 JSON plan document와 Execution Time이 반환된다.
정상 예
runner의 Benchmark variant만 이 exact 파일을 index 전 30회와 index 후 30회 실행하며, BeforePlan과 AfterPlan은 각각 plan 한 번만 실행한다.
틀린 예·반례
BeforePlan·AfterPlan도 30회 분포를 만든다고 읽거나 EXPLAIN client가 ledger row 50개를 받는다고 말하면 source와 출력 형식을 모두 어긴다.
착각 방지
LIMIT 50은 underlying SELECT plan node가 내보낼 row 한도이지 executor가 내부에서 방문·검사한 행도 항상 50개라는 보장이 아니다.
하지 않는 일
이 statement는 30-sample 분포, page 1000, keyset 안정성, production latency를 책임지지 않는다.
다음 연결
create-index.sql이 이 predicate·order에 맞는 복합 index를 추가한다.
ID tie-breaker
  1. created_at이 있는데 id DESC도 꼭 필요한가요?

  2. 시각이 같은 행이 생겨도 id로 순서를 하나로 정할 수 있어.

  3. 현재 seed는 g초가 달라 동률이 없지만 query 계약은 더 안정적이다.

  4. 두 열 순서를 index와 나란히 비교할게요.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1seed 뒤 account1 ledger 90,000행WHERE account_id=1 후보를 만든다.논리 후보군은 hot 행 90,000개다.planner의 estimated rows는 통계 기반이라 actual 90,000과 다를 수 있다.
2서로 다른 created_at과 id 1..100000created_at DESC, id DESC로 정렬 의미를 정한다.가장 늦은 시각부터, 동률이면 큰 id부터라는 안정 순서가 된다.현재 seed는 시각 동률이 없지만 id tie-breaker 계약은 남는다.
3정렬된 account1 후보LIMIT 50을 적용한다.query 출력은 최신 50행이다.index가 없으면 50행만 반환해도 많은 행을 읽거나 sort할 수 있다.
4실제로 실행된 SELECTactual timing·rows와 buffer 사용을 모아 JSON으로 직렬화한다.runner가 object[0].Plan과 Execution Time을 읽을 수 있다.한 번의 Execution Time은 30회 분포나 운영 latency를 대표하지 않는다.
pagination 과장 금지
  1. OFFSET이 없으니 keyset을 이미 쓴 건가요?

  2. 아니, 이것은 첫 top-N이고 마지막 값을 받는 cursor predicate가 없어.

  3. D5 gate도 OFFSET token 부재만 볼 뿐 cursor correctness를 증명하지 않는다.

  4. 설명 제목을 최신 50건 측정으로 제한하겠습니다.

09

STEP 09 / 13

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

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

PostgreSQL planner

table statistics·index 목록·query 조건을 보고 Seq Scan, Index Scan 같은 후보 비용을 비교한다.

cost 단위는 wall-clock millisecond가 아니다.
Executor

고른 plan을 실제로 실행하며 Actual Rows·loops·timing을 수집한다.

ANALYZE overhead가 들어가고 같은 machine 상태에서도 값은 흔들릴 수 있다.
Buffer manager

query가 사용한 shared/local/temp buffer hit·read·write 정보를 node에 붙인다.

OS cache와 storage 전체 동작을 한 숫자로 모두 설명하지 않는다.
JSON formatter

plan tree와 Planning/Execution Time을 JSON array 구조로 출력한다.

format 변경은 query 결과 50행을 반환하는 API가 아니라 plan 설명을 반환한다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ EXPLAIN ANALYZE의 cost 100은 100ms라는 뜻이다.

왜 틀리나 cost는 planner 내부 비교 단위이고 실제 시간은 별도 Execution Time·actual time에 있다.

바르게 읽기 estimated cost와 measured milliseconds를 다른 열로 읽는다.

반례 cache 상태가 바뀌면 같은 cost plan의 actual time은 달라질 수 있다.

❌ LIMIT 50이 있으니 database도 정확히 50행만 읽는다.

왜 틀리나 정렬과 filter를 만족할 경로가 없으면 더 많은 행을 훑을 수 있다.

바르게 읽기 반환 행 수와 scan·sort 작업량을 plan node에서 따로 본다.

반례 index 전에는 Seq Scan과 Sort가 후보가 될 수 있다.

❌ id DESC가 있으니 keyset pagination을 측정한다.

왜 틀리나 cursor 값을 비교하는 WHERE predicate가 source에 없다.

바르게 읽기 stable top-N order와 keyset 다음-page 조건을 구분한다.

반례 WHERE에는 account_id=1만 있고 last_created_at·last_id 비교가 없다.

❌ account1 결과가 빠르면 모든 계좌에서도 같은 plan이 좋다.

왜 틀리나 account1은 전체 원장의 90%인 극단적 hot value다.

바르게 읽기 parameter와 분포 범위를 주장에 적는다.

반례 cold account는 10~11행이라 selectivity와 선택 plan이 다를 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

query scope

OFFSET·keyset·다음 page correctness 또는 성능

이 책임을 맡는 곳: 별도 pagination query·fixture·benchmark
parameters

cold account와 다른 LIMIT·정렬·filter의 plan

이 책임을 맡는 곳: 추가 representative parameter 측정
statistics

estimated rows와 actual rows가 항상 일치함

이 책임을 맡는 곳: 통계 품질·분포·plan actual 비교
performance claim

운영 환경의 절대 latency나 지속적 개선

이 책임을 맡는 곳: 반복·warm-up·순서 통제·운영 관측 설계
12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

실제 실행+buffers+JSON→네 출력 열→account1 filter→두 열 내림차순→50행 순서로 말한다.

2단계 · 코드 조각 재조립

  1. EXPLAIN option tuple
  2. SELECT four columns/FROM
  3. WHERE/ORDER BY/LIMIT

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

세 물리 줄의 긴 statement를 그대로 다시 쓰고 whitespace와 token을 canonical SHA-256으로 대조한다.

자가 점검
  • ANALYZE와 BUFFERS를 빠뜨리지 않는다.
  • FORMAT JSON을 유지한다.
  • account_id는 1이다.
  • created_at과 id 모두 DESC다.
  • LIMIT는 50이다.
  • OFFSET·cursor를 측정했다고 쓰지 않는다.
13

STEP 13 / 13

전체 원본 정답

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

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcesql/w13/measure.sqlSHA-256 4ffb00031356f973e4c6fe30857ef3a0ffe873704963d4f343a89451a8a98238
measure.sql — hot 계좌의 최신 50행 실제 plan을 JSON으로 측정 전체
EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON)
SELECT id,entry_type,signed_amount,created_at FROM perf_w13.ledger_entry
WHERE account_id=1 ORDER BY created_at DESC,id DESC LIMIT 50;
04

create-index.sql — top-N 조회 순서와 맞춘 복합 인덱스

sql/w13/create-index.sql

SQL 정본 · 정본 · W13-F04
2줄 연결2줄 번역2 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

account_id 한 계좌의 최신 원장 50건을 찾을 때 전체 원장을 다시 정렬하지 않도록 조회 조건과 정렬 순서에 맞는 접근 경로를 만든다.

  1. 복합 인덱스의 첫 열은 왜 account_id일까?
  2. created_at과 id에 DESC를 함께 둔 이유는 무엇일까?
  3. 인덱스가 존재하면 PostgreSQL이 반드시 사용할까?
이 파일에서 끝까지 다시 쓰는 값index=idx_ledger_account_created_idkeys=account_id, created_at DESC, id DESCtable=perf_w13.ledger_entryANALYZE after CREATE INDEX
02

STEP 02 / 13

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

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

STARRY 원장 보관함에서 한 계좌의 최신 기록 50장만 빨리 찾으려 한다.

최신 티켓을 바로 꺼내는 찾아보기

먼저 계좌별 칸을 고르고 그 안에서 최신 시각과 큰 번호부터 보도록 세 열 인덱스를 만든다. 이 순서는 실제 top-N SQL의 조건과 정렬 순서를 따른다.

인덱스를 만든 뒤 ANALYZE로 통계를 갱신한다. 다만 존재만으로 사용을 보장하지 않으므로 runner가 after plan에서 실제 Index Scan을 따로 확인한다.

딱 여기까지만 찾아보기 비유는 접근 순서를 설명할 뿐 planner 선택·실제 latency·운영 부하를 보장하지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

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

첫 열 account_id

WHERE account_id=1 같은 동등 조건을 먼저 좁힌다.

코드 연결
1줄 첫 key
비유
계좌별 서랍부터 고르기
비유의 끝
모든 계좌를 동시에 조회하는 SQL에는 같은 이점이 아닐 수 있다.

두 DESC 정렬

created_at이 최신인 행부터, 같은 시각이면 id가 큰 행부터 읽는 순서를 인덱스에 넣는다.

코드 연결
1줄 두 번째·세 번째 key
비유
최신 티켓과 큰 번호 순으로 꽂기
비유의 끝
동률 해결은 id 값의 의미와 query ORDER BY가 일치할 때만 성립한다.

ANALYZE

새 접근 경로를 만든 뒤 planner 통계를 다시 수집한다.

코드 연결
2줄
비유
새 찾아보기의 분포표 갱신
비유의 끝
특정 scan을 강제하지 않는다.
열 순서
  1. 왜 account_id가 맨 앞이어야 하지?

  2. 한 계좌라는 동등 조건을 먼저 좁혀야 뒤 DESC 순서를 이어 쓰기 쉽다.

  3. 세 열 존재보다 왼쪽 순서가 핵심이야.

  4. WHERE와 ORDER BY를 나란히 놓고 읽을게요.

두 번째 DESC
  1. created_at만 DESC면 충분하지 않아?

  2. 같은 시각이 생기면 id DESC가 안정적인 다음 순서를 준다.

  3. 동률을 방치하면 50번째 경계가 흔들릴 수 있어.

  4. 두 정렬 key를 한 쌍으로 기억할게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 2줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.SQL 정본의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.2 / 2 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F04-L01 CREATE INDEX idx_ledger_account_created_id ON perf_w13.ledger_entry(account_id,created_at DESC,id DESC); STARRY 티켓 보관함을 먼저 계좌별로 나누고, 각 칸에서는 최신 시각과 큰 티켓 번호부터 꺼내도록 세 겹 찾아보기를 붙인다. top-N 조회의 동등 조건 account_id를 선두 키로 두고 ORDER BY created_at DESC, id DESC를 뒤따르게 하는 idx_ledger_account_created_id를 생성한다.
입력
perf_w13.ledger_entry가 존재하고 동일 이름의 인덱스가 아직 없는 폐기형 DB 상태를 받는다.
결과·효과
PostgreSQL catalog와 저장장치에 세 열 순서를 가진 복합 B-tree 인덱스가 추가된다.
비유의 한계
인덱스 생성은 planner가 그 경로를 실제로 고른다는 증거가 아니며, 같은 이름이 이미 있으면 이 SQL은 실패할 수 있다.
2줄F04-L02 ANALYZE perf_w13.ledger_entry; 새 찾아보기를 단 뒤 료가 보관함의 분포표를 다시 세어 안내판을 최신 상태로 바꾼다. ANALYZE가 perf_w13.ledger_entry 표본 통계를 갱신해 이후 실행 계획 비용 계산에 사용할 정보를 제공한다.
입력
복합 인덱스 생성이 끝난 ledger_entry와 현재 PostgreSQL 통계 설정을 받는다.
결과·효과
planner가 참고할 ledger_entry 열 분포 통계가 새로 계산된다.
비유의 한계
ANALYZE는 행을 정렬하거나 특정 Index Scan을 강제하지 않고 실제 쿼리 지연시간도 측정하지 않는다.
ANALYZE 역할
  1. ANALYZE가 index 사용을 켜 주는 명령이야?

  2. 통계를 갱신할 뿐 선택은 planner가 비용을 보고 한다.

  3. 사용 증거는 after plan의 node에서 찾아.

  4. 통계 갱신과 선택 증거를 구분하겠습니다.

05

STEP 05 / 13

원본 코드 조각

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

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

F04-C01 · create the exact composite index1–1줄
1–1줄 원본
CREATE INDEX idx_ledger_account_created_id ON perf_w13.ledger_entry(account_id,created_at DESC,id DESC);
F04-C02 · refresh planner statistics2–2줄
2–2줄 원본
ANALYZE perf_w13.ledger_entry;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 2 / 2

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

원본한국어 번역
1CREATE INDEX idx_ledger_account_created_id ON perf_w13.ledger_entry(account_id,created_at DESC,id DESC);perf_w13.ledger_entry에 account_id, created_at 내림차순, id 내림차순 순서의 복합 인덱스를 만든다.
2ANALYZE perf_w13.ledger_entry;새 인덱스가 생긴 ledger_entry의 planner 통계를 다시 수집한다.
07

STEP 07 / 13

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

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

한 줄로 읽기

account_id 동등 조건 뒤 created_at DESC·id DESC 정렬을 잇는 복합 인덱스를 만들고 ANALYZE하지만 실제 plan 선택은 runner가 별도로 증명한다.

문법 해부

  • CREATE INDEX 이름 ON schema.table(columns)는 새 B-tree 접근 경로를 만든다.
  • 복합 인덱스에서는 왼쪽 열부터 조건과 정렬에 맞는지가 중요하다.
  • DESC는 해당 key의 내림차순 저장·탐색 방향을 명시한다.
  • ANALYZE는 planner용 table 통계를 갱신한다.

실행 순서

  1. PostgreSQL이 ledger_entry를 읽어 세 key의 index를 구축한다.
  2. catalog에 idx_ledger_account_created_id가 등록된다.
  3. ANALYZE가 ledger_entry 통계를 새로 계산한다.
  4. AfterPlan/Benchmark query가 planner 선택을 별도로 관찰한다.

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

F04-C01 · create the exact composite index
문법 해부
CREATE INDEX가 account_id, created_at DESC, id DESC 순서의 idx_ledger_account_created_id를 만든다.
실제 값 추적
account_id=1 equality 뒤 두 DESC key가 최신 50행 순서를 직접 제공할 후보가 된다.
정상 예
seed가 schema를 재생성한 AfterPlan에서 named index 하나가 생긴다.
틀린 예·반례
같은 schema에서 index가 이미 있는데 다시 실행하면 IF NOT EXISTS가 없어 duplicate-object 오류가 난다.
착각 방지
index 존재가 planner 선택이나 latency 개선을 무조건 보장하지 않는다.
하지 않는 일
write/storage 비용은 이 DDL에서 측정하지 않는다.
다음 연결
ANALYZE가 새 index와 분포를 planner에 다시 보인다.
F04-C02 · refresh planner statistics
문법 해부
ANALYZE ledger_entry가 새 index 생성 뒤 ledger_entry 열 값 분포의 planner 통계를 다시 수집한다.
실제 값 추적
planner는 catalog에 등록된 새 index와 갱신된 table 통계를 함께 보고 AfterPlan·Benchmark after query의 access path를 고른다.
정상 예
뒤 FindIndex가 JSON plan에서 정확한 index 이름을 실제 선택 결과로 확인할 수 있다.
틀린 예·반례
ANALYZE를 생략해 통계가 부정확하면 planner가 새 index의 비용을 덜 적절하게 추정할 수 있다.
착각 방지
ANALYZE 성공 자체는 별도의 index 전용 통계를 만들거나 Index Scan 선택을 증명하지 않는다.
하지 않는 일
row-count invariant는 assert.sql이 검사한다.
다음 연결
assert.sql이 다섯 count·mismatch 값과 별도 named-index count까지 여섯 검사값을 fail-closed로 확인한다.
존재와 사용
  1. assert.sql이 index 이름을 찾으면 끝난 거 아냐?

  2. 그 검사는 catalog 존재만 본다. runner의 FindIndex가 실제 plan을 본다.

  3. 명찰이 창고에 있음과 공연에서 사용함은 다르지.

  4. 두 gate의 증명 범위를 따로 적을게요.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1account_id=1, 최신 50건선두 key로 계좌 1 범위를 좁히고 DESC key 방향으로 읽는다.정렬과 LIMIT에 맞는 후보 접근 경로가 생긴다.실제 선택 여부는 EXPLAIN plan에서 확인한다.
2created_at 동률 두 행두 번째 정렬 key가 같으면 id DESC를 적용한다.더 큰 id가 먼저 오는 안정 순서가 가능하다.id가 업무상 시간 순서를 뜻한다는 보장은 없다.
3CREATE 후 ANALYZE새 index가 있는 table 통계를 수집한다.planner가 비용 계산에 쓸 최신 통계를 얻는다.비용 추정이 실제 시간과 같다는 뜻은 아니다.
운영 일반화
  1. 합성 lab이 빨라지면 운영도 무조건 빨라지지?

  2. 아니다. 분포·부하·cache·hardware가 다른 운영은 별도 측정이 필요하다.

  3. source verdict도 절대 latency claim을 거부해.

  4. 이번 결과를 로컬 상대 개선으로만 말하겠습니다.

09

STEP 09 / 13

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

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

PostgreSQL storage

ledger_entry의 세 key와 row 위치를 B-tree에 저장한다.

index 구축은 추가 저장공간과 write 비용을 만든다.
Planner statistics

ANALYZE 결과로 행 분포와 비용을 추정한다.

planner는 index가 있어도 sequential scan을 선택할 수 있다.
Runner plan gate

FindIndex가 실제 JSON plan에서 목표 이름과 scan type을 확인한다.

assert.sql의 catalog count만으로는 plan selection이 증명되지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ 인덱스만 만들면 query가 무조건 빨라진다.

왜 틀리나 planner 선택·cache·분포·쓰기 비용에 따라 결과가 달라진다.

바르게 읽기 AfterPlan의 실제 plan과 Benchmark의 두 metric을 함께 본다.

반례 작은 table에서는 sequential scan이 더 싸다고 판단될 수 있다.

❌ 열 세 개가 있으면 순서는 상관없다.

왜 틀리나 복합 index의 왼쪽 prefix가 WHERE와 ORDER BY 연결에 영향을 준다.

바르게 읽기 account_id를 먼저 두고 두 DESC key를 query 순서대로 둔다.

반례 created_at을 첫 열로 두면 특정 account 범위를 한 덩어리로 좁히기 어렵다.

❌ ANALYZE가 데이터를 정렬한다.

왜 틀리나 ANALYZE는 통계를 수집한다.

바르게 읽기 물리적 행 정렬과 planner 통계를 구분한다.

반례 ANALYZE 뒤 table row 순서 자체는 query ORDER BY 없이 보장되지 않는다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

계획 선택

목표 index가 실제 top-N plan에 쓰임

이 책임을 맡는 곳: run-w13-perf.ps1 AfterPlan/Benchmark FindIndex
성능

median·p95가 운영에서도 개선됨

이 책임을 맡는 곳: fixture-bound benchmark and production load test
쓰기 비용

INSERT/UPDATE 비용과 disk 증가가 허용 범위임

이 책임을 맡는 곳: write benchmark and capacity review
12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

  • WHERE와 ORDER BY를 보고 복합 key 순서를 말한다.
  • index 이름과 세 열 방향을 정확히 다시 쓴다.
  • CREATE INDEX·ANALYZE·plan 확인의 책임을 분리한다.

2단계 · 코드 조각 재조립

  1. 첫 열 account_id
  2. 두 DESC 정렬
  3. ANALYZE

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

정본을 보지 않고 2줄 전체를 다시 쓰고 비공백 mapping·번역 2행과 source-local test 0개를 대조한다.

자가 점검
  • 비공백 원문 2줄이 mapping과 번역에 정확히 한 번씩 있는지 본다.
  • 각 줄의 비유·실제 의미·입력·직접 결과·한계가 서로 다른 역할을 하는지 읽는다.
  • 고정 수치·파일명·분기 조건을 canonical source와 다시 대조한다.
  • 직접 보장과 다음 layer 책임을 반례로 설명한다.
13

STEP 13 / 13

전체 원본 정답

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

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcesql/w13/create-index.sqlSHA-256 bdb52b53e8b7074b8711070c6f87299c6985b99953c2799ba894ac05bf5f12ae
create-index.sql — top-N 조회 순서와 맞춘 복합 인덱스 전체
CREATE INDEX idx_ledger_account_created_id ON perf_w13.ledger_entry(account_id,created_at DESC,id DESC);
ANALYZE perf_w13.ledger_entry;
05

assert.sql — fixture 숫자·잔액 대사·인덱스 존재를 막는 SQL gate

sql/w13/assert.sql

SQL 정본 · 정본 · W13-F05
7줄 연결7줄 번역2 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

성능 비교 전에 합성 DB의 행 수와 hot 분포, 계좌 잔액 대사, 목표 인덱스 이름이 예상과 다른 상태를 fail-closed로 거부한다.

  1. a·t·l·h·m 다섯 변수는 각각 무엇을 셀까?
  2. m=0은 어떤 불일치가 없다는 뜻일까?
  3. 인덱스 이름 존재 검사가 실제 plan 선택도 증명할까?
이 파일에서 끝까지 다시 쓰는 값a=1000t=100000l=100000h=90000m=0index count=1marker=W13_ASSERT 1000|100000|100000|90000|0
02

STEP 02 / 13

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

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

STARRY가 초시계 결과를 발표하기 전에 장부가 처음 약속한 모양인지 확인한다.

성능표를 믿기 전 다섯 칸 대조

계좌 1천, 거래 10만, 원장 10만, 계좌 1의 hot 원장 9만을 센다. 이어 각 계좌 balance와 원장 signed_amount 합계가 다른 계좌 수가 0인지 본다.

목표 인덱스 이름도 catalog에 정확히 한 건 있어야 한다. 다만 여러 SELECT에 강한 isolation·writer lock이 없어서 동시 변경을 한 snapshot으로 묶지는 않으며, 실제 index plan 선택도 runner가 따로 본다.

딱 여기까지만 대조표 비유는 현재 synthetic fixture의 sentinel만 설명하며 모든 행 의미·동시성·운영 정합성을 포괄하지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

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

네 개 count

a·t·l·h에 전체 3개 table과 account_id=1 원장 수를 저장한다.

코드 연결
2줄
비유
네 장부의 종이 수 세기
비유의 끝
금액·시간·연결 관계는 세지 않는다.

잔액 대사 m

account.balance와 계좌별 signed_amount 합계가 다른 계좌 수를 센다.

코드 연결
3~4줄
비유
잔액표와 원장 합계표 맞대기
비유의 끝
m=0은 각 SELECT가 관찰한 값 조합의 대사이며 shared snapshot·동시 write 차단은 없다.

catalog gate

pg_indexes에서 schema와 이름이 맞는 index가 정확히 하나인지 본다.

코드 연결
5줄
비유
장비 목록의 명찰 한 장 확인
비유의 끝
정의·열 순서·plan 사용은 확인하지 않는다.
다섯 변수와 snapshot
  1. DO 안의 a·t·l·h count는 한순간을 동시에 찍나?

  2. 네 SELECT는 차례로 평가되고 stronger isolation이나 writer lock이 선언되지 않았다.

  3. 동시 commit이 있으면 서로 다른 상태를 볼 수 있어.

  4. 변수 뜻과 함께 비원자 snapshot 경계를 적겠습니다.

m의 뜻
  1. m=0이면 ledger 행이 0이라는 뜻이야?

  2. 아니다. balance와 계좌별 signed_amount 합계가 다른 계좌 수가 0이다.

  3. 행 수 l=100000과 전혀 다른 집계야.

  4. m을 mismatch account count로 기억할게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 7줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.SQL 정본의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.7 / 7 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F05-L01 DO $gate$ DECLARE a bigint;t bigint;l bigint;h bigint;m bigint; BEGIN 니지카가 공연 전 대조표를 펼치고 계좌·거래·원장·몰린 행·잔액 불일치 수를 적을 다섯 빈칸을 만든다. DO $gate$ 블록이 64비트 정수 변수 다섯 개의 범위를 시작하며 뒤의 SELECT와 IF를 한 실행 단위에 묶는다.
입력
perf_w13 fixture와 인덱스가 준비된 PostgreSQL 세션을 받는다.
결과·효과
a·t·l·h·m을 사용할 PL/pgSQL 실행 문맥이 생기고 블록 본문으로 진입한다.
비유의 한계
변수를 선언한 것만으로 값이 채워지거나 transaction commit이 보장되지는 않는다.
2줄F05-L02 SELECT count(*) INTO a FROM perf_w13.account; SELECT count(*) INTO t FROM perf_w13.business_tx; SELECT count(*) INTO l FROM perf_w13.ledger_entry; SELECT count(*) INTO h FROM perf_w13.ledger_entry WHERE account_id=1; 네 장부 질문을 차례로 던져 돌아온 수를 a·t·l·h 칸에 1천·10만·10만·9만 순서로 적는다. 네 SELECT count(*)가 전체 계좌·업무 거래·원장과 hot 계좌 1의 원장을 서로 다른 변수에 저장한다.
입력
현재 perf_w13.account, business_tx, ledger_entry 내용과 account_id=1 조건을 받는다.
결과·효과
a, t, l, h가 실제 행 수 네 개로 덮어써진다.
비유의 한계
네 SELECT에 repeatable-read/serializable 선언이나 writer lock이 없어 동시 commit 사이에서 서로 다른 상태를 볼 수 있다; 하나의 atomic snapshot 대조를 보장하지 않는다.
3줄F05-L03 SELECT count(*) INTO m FROM perf_w13.account x LEFT JOIN(SELECT account_id,sum(signed_amount) b FROM perf_w13.ledger_entry GROUP BY account_id)y ON y.account_id=x.id WHERE x.balance<>coalesce(y.b,0); 료가 모든 계좌 잔액표를 원장 합계표와 맞대고 숫자가 다른 계좌에만 표시한 뒤 그 표시 수를 센다. account를 계좌별 ledger 합계와 LEFT JOIN하고 coalesce로 원장 없는 계좌를 0으로 본 뒤 불일치 행을 집계한다.
입력
각 account.id와 balance, ledger_entry의 account_id·signed_amount 집합을 받는다.
결과·효과
m에는 balance가 원장 합계와 다른 계좌 개수가 저장된다.
비유의 한계
m=0은 이 합성 fixture의 현재 합계 대사만 뜻하며 원장 불변성 전체나 동시성 안전성을 증명하지 않는다.
4줄F05-L04 IF a<>1000 OR t<>100000 OR l<>100000 OR h<>90000 OR m<>0 THEN RAISE EXCEPTION 'W13 counts a=% t=% l=% h=% m=%',a,t,l,h,m; END IF; 다섯 검수칸 중 하나라도 목표 숫자와 다르면 키타가 현재 숫자를 모두 읽어 주고 리허설을 즉시 중단한다. OR로 연결된 다섯 sentinel 비교가 true일 때 RAISE EXCEPTION으로 W13 counts 오류와 a·t·l·h·m을 출력한다.
입력
직전 두 SELECT가 채운 실제 a, t, l, h, m 값과 고정 기대값 1000|100000|100000|90000|0을 받는다.
결과·효과
모든 값이 맞으면 다음 인덱스 검사로 진행하고, 하나라도 다르면 DO 블록이 예외로 종료된다.
비유의 한계
이 guard는 다섯 집계만 fail-closed로 확인하며 개별 행의 created_at·entry_type·business_tx 연결을 검사하지 않는다.
5줄F05-L05 IF (SELECT count(*) FROM pg_indexes WHERE schemaname='perf_w13' AND indexname='idx_ledger_account_created_id')<>1 THEN RAISE EXCEPTION 'W13 index missing'; END IF; 장비 목록에서 세 겹 찾아보기 명찰을 세어 정확히 하나가 아니면 료가 ‘인덱스 없음’ 경고를 낸다. catalog view의 schema와 indexname을 제한해 idx_ledger_account_created_id 존재 개수를 1과 비교한다.
입력
PostgreSQL pg_indexes catalog와 schemaname perf_w13, indexname idx_ledger_account_created_id를 받는다.
결과·효과
지정 이름이 한 건이면 블록 종료로 진행하고 0건 또는 2건 이상이면 W13 index missing 예외가 난다.
비유의 한계
이름 존재 검사는 열 순서·정의 동일성·실행 계획 선택을 증명하지 않는다; 실제 plan 선택은 runner의 FindIndex가 따로 본다.
6줄F05-L06 END $gate$; 다섯 숫자와 인덱스 명찰 검사를 모두 통과한 니지카가 검수표를 닫아 판정을 확정한다. END $gate$가 DO 블록의 제어 범위를 끝내며 앞에서 발생한 예외가 없다면 다음 SQL 문장으로 넘긴다.
입력
행 수·대사·인덱스 존재 guard가 모두 통과한 PL/pgSQL 상태를 받는다.
결과·효과
익명 블록이 정상 완료되고 뒤의 marker SELECT를 실행할 수 있게 된다.
비유의 한계
블록 종료 자체는 새 데이터나 성능 증거를 만들지 않는다.
7줄F05-L07 SELECT 'W13_ASSERT 1000|100000|100000|90000|0'; 검수가 끝나면 키타가 다섯 합격 숫자가 적힌 초록 확인표 한 장을 관객에게 보여 준다. 앞 gate가 예외 없이 끝났을 때 psql 출력에 정확한 W13_ASSERT sentinel 문자열을 남긴다.
입력
DO 블록이 성공한 같은 SQL 실행 흐름을 받는다.
결과·효과
결과 집합 한 행·한 열에 W13_ASSERT 1000|100000|100000|90000|0이 나온다.
비유의 한계
marker 문자열은 재검사를 수행하지 않으며 runner는 이 문자열을 별도로 파싱하지 않는다.
LEFT JOIN
  1. 원장이 없는 계좌는 비교에서 사라지지 않아?

  2. LEFT JOIN이 계좌를 남기고 coalesce가 없는 합계를 0으로 바꾼다.

  3. INNER JOIN이면 그런 계좌가 빠질 수 있어.

  4. 조인 방향까지 포함해 읽겠습니다.

05

STEP 05 / 13

원본 코드 조각

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

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

F05-C01 · DO block for counts, reconciliation, and index existence1–6줄
1–6줄 원본
DO $gate$ DECLARE a bigint;t bigint;l bigint;h bigint;m bigint; BEGIN
 SELECT count(*) INTO a FROM perf_w13.account; SELECT count(*) INTO t FROM perf_w13.business_tx; SELECT count(*) INTO l FROM perf_w13.ledger_entry; SELECT count(*) INTO h FROM perf_w13.ledger_entry WHERE account_id=1;
 SELECT count(*) INTO m FROM perf_w13.account x LEFT JOIN(SELECT account_id,sum(signed_amount) b FROM perf_w13.ledger_entry GROUP BY account_id)y ON y.account_id=x.id WHERE x.balance<>coalesce(y.b,0);
 IF a<>1000 OR t<>100000 OR l<>100000 OR h<>90000 OR m<>0 THEN RAISE EXCEPTION 'W13 counts a=% t=% l=% h=% m=%',a,t,l,h,m; END IF;
 IF (SELECT count(*) FROM pg_indexes WHERE schemaname='perf_w13' AND indexname='idx_ledger_account_created_id')<>1 THEN RAISE EXCEPTION 'W13 index missing'; END IF;
END $gate$;
F05-C02 · human-readable success row7–7줄
7–7줄 원본
SELECT 'W13_ASSERT 1000|100000|100000|90000|0';
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 7 / 7

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

원본한국어 번역
1DO $gate$ DECLARE a bigint;t bigint;l bigint;h bigint;m bigint; BEGIN익명 PL/pgSQL gate를 열고 다섯 bigint 변수 a, t, l, h, m을 선언한다.
2 SELECT count(*) INTO a FROM perf_w13.account; SELECT count(*) INTO t FROM perf_w13.business_tx; SELECT count(*) INTO l FROM perf_w13.ledger_entry; SELECT count(*) INTO h FROM perf_w13.ledger_entry WHERE account_id=1;account, business_tx, ledger_entry 전체 행 수와 account_id=1 원장 행 수를 각각 a, t, l, h에 넣는다.
3 SELECT count(*) INTO m FROM perf_w13.account x LEFT JOIN(SELECT account_id,sum(signed_amount) b FROM perf_w13.ledger_entry GROUP BY account_id)y ON y.account_id=x.id WHERE x.balance<>coalesce(y.b,0);계좌 balance와 계좌별 signed_amount 합계가 다른 계좌 수를 m에 넣는다.
4 IF a<>1000 OR t<>100000 OR l<>100000 OR h<>90000 OR m<>0 THEN RAISE EXCEPTION 'W13 counts a=% t=% l=% h=% m=%',a,t,l,h,m; END IF;a=1000, t=100000, l=100000, h=90000, m=0 중 하나라도 아니면 실제 다섯 값을 담아 실패한다.
5 IF (SELECT count(*) FROM pg_indexes WHERE schemaname='perf_w13' AND indexname='idx_ledger_account_created_id')<>1 THEN RAISE EXCEPTION 'W13 index missing'; END IF;pg_indexes에서 perf_w13의 지정 인덱스 이름이 정확히 한 건이 아니면 실패한다.
6END $gate$;gate 익명 블록을 닫고 실행한다.
7SELECT 'W13_ASSERT 1000|100000|100000|90000|0';고정 marker W13_ASSERT 1000|100000|100000|90000|0을 한 행으로 반환한다.
07

STEP 07 / 13

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

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

한 줄로 읽기

다섯 sentinel 1000|100000|100000|90000|0과 index 이름 한 건을 SQL 예외로 강제하되 실제 plan 선택은 증명하지 않는다.

문법 해부

  • DO $gate$ ... END $gate$는 익명 PL/pgSQL 블록이다.
  • SELECT count(*) INTO 변수는 집계값을 PL/pgSQL 변수에 넣는다.
  • LEFT JOIN과 coalesce는 원장이 없는 계좌도 balance 0과 비교하게 한다.
  • RAISE EXCEPTION은 조건 불일치 시 psql을 nonzero로 끝내게 한다.

실행 순서

  1. 다섯 bigint 변수를 만든다.
  2. 네 count와 balance mismatch count를 별도 SELECT로 계산한다; stronger isolation이나 writer lock은 선언하지 않는다.
  3. 고정 expected tuple과 대조한다.
  4. pg_indexes에서 target name count를 검사한다.
  5. 모두 통과하면 고정 W13_ASSERT marker를 반환한다.

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

F05-C01 · DO block for counts, reconciliation, and index existence
문법 해부
DO 블록이 a/t/l/h 네 count, m balance mismatch count, named index count까지 여섯 invariant를 두 IF에서 검사한다.
실제 값 추적
accounts=1000, transactions=100000, ledger=100000, hot=90000, balance mismatch=0, named index count=1을 요구한다.
정상 예
create-index 뒤 쓰기가 멈춘 정확한 fixture라면 두 RAISE EXCEPTION 경로를 모두 지나 블록이 정상 종료한다.
틀린 예·반례
index 이름이 없거나 같은 이름 count가 1이 아니면 둘째 IF가 실패한다.
착각 방지
a/t/l/h/m을 만드는 SELECT들은 따로 평가되며 REPEATABLE READ·SERIALIZABLE이나 writer lock을 설정하지 않아 동시 commit 중 하나의 atomic snapshot이라고 보장할 수 없다.
하지 않는 일
plan 선택, 30개 latency·median, D5 OFFSET/cursor 범위는 이 SQL이 검사하지 않는다.
다음 연결
마지막 SELECT는 count/mismatch 다섯 숫자만 사람이 읽을 marker로 낸다.
F05-C02 · human-readable success row
문법 해부
상수 문자열 SELECT가 `W13_ASSERT 1000|100000|100000|90000|0` marker를 출력한다.
실제 값 추적
psql 로그에 a/t/l/h/m 기대값 다섯 숫자가 정확한 구분자로 남는다.
정상 예
runner PsqlFile의 `-v ON_ERROR_STOP=1` 아래에서는 앞 DO 블록이 실패하면 psql이 멈춰 이 marker에 도달하지 않는다.
틀린 예·반례
assert.sql 자체에는 ON_ERROR_STOP 설정이 없으므로 fail-fast 옵션 없이 실행하면 DO 오류 뒤 marker SELECT가 계속될 수 있고, 문자열만 따로 실행해도 같은 글자가 나온다.
착각 방지
marker 텍스트에는 index=1이 없으므로 index 검사는 앞 DO block 성공과 fail-fast 실행 문맥으로만 뒷받침된다.
하지 않는 일
plan JSON과 benchmark CSV를 만들지 않는다.
다음 연결
runner의 AfterPlan·Benchmark phase가 fail-fast 문맥에서 전체 assert.sql native exit를 확인한다.
인덱스 gate
  1. 이름 한 건이면 열 순서도 맞다는 뜻이지?

  2. pg_indexes count 조건은 schema와 name만 제한한다.

  3. 정의 비교도 plan 확인도 아니다.

  4. 존재 검사의 좁은 범위를 표시할게요.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1정상 seed네 table count와 hot condition을 별도 SELECT로 집계한다.a=1000, t=100000, l=100000, h=90000동시 writer가 있으면 SELECT 사이 committed 상태가 달라질 수 있다.
2account 1 balance=89999ledger sum 90000과 비교해 mismatch를 센다.m=1이 되어 counts exception이 발생한다.어떤 ledger가 원인인지는 메시지에 나오지 않는다.
3index 삭제pg_indexes target count가 0이 된다.W13 index missing 예외로 marker 전에 중단된다.plan query 자체는 이 SQL에 없다.
marker
  1. 마지막 SELECT가 앞 검사를 다시 실행하나?

  2. 아니다. DO block이 끝난 뒤 고정 문자열 한 행을 내보낼 뿐이다.

  3. 예외가 났다면 marker까지 도달하지 못해.

  4. marker를 성공 신호로만 해석하겠습니다.

09

STEP 09 / 13

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

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

PL/pgSQL

DO block 변수가 같은 실행 안에서 count와 guard를 연결한다.

결과 변수를 외부 session에 남기지 않는다.
PostgreSQL aggregation

계좌별 signed_amount SUM과 account balance를 LEFT JOIN한다.

명시적 repeatable-read/serializable이나 writer lock이 없어 여러 SELECT가 하나의 snapshot을 공유한다고 보장하지 않는다.
System catalog

pg_indexes view에서 schema와 이름을 센다.

index definition이나 scan adoption은 보지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ 행 수가 맞으면 ledger도 올바르다.

왜 틀리나 10만 행이어도 signed_amount나 account 연결이 틀릴 수 있다.

바르게 읽기 m=0 대사와 필요한 별도 row-level 검사를 구분한다.

반례 두 ledger의 account_id를 바꾸되 합계가 우연히 같을 수도 있다.

❌ m=0이면 모든 금융 불변성이 증명된다.

왜 틀리나 이 gate는 balance와 계좌별 signed sum 하나만 비교한다.

바르게 읽기 현재 합성 schema의 직접 식만 주장한다.

반례 business_tx 상태나 entry_type이 틀려도 합계가 같으면 m은 0일 수 있다.

❌ pg_indexes count=1이면 target query가 index를 쓴다.

왜 틀리나 catalog 존재와 planner 선택은 다른 사실이다.

바르게 읽기 FindIndex로 JSON plan node를 확인한다.

반례 planner가 sequential scan을 골라도 catalog count는 1이다.

❌ DO block 안의 count는 모두 같은 snapshot이다.

왜 틀리나 여러 SELECT에 repeatable-read/serializable 선언이나 writer lock이 없다.

바르게 읽기 동시 변경 가능성이 있으면 비원자 집계라고 표시한다.

반례 첫 count 뒤 다른 transaction이 commit하면 다음 SELECT가 다른 committed 상태를 볼 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

plan

target query가 목표 index로 실행됨

이 책임을 맡는 곳: runner AfterPlan/Benchmark JSON plan gate
행 의미

모든 ledger entry_type·business_tx link·timestamp가 올바름

이 책임을 맡는 곳: additional row-level assertions
동시 snapshot

명시적 isolation·lock 없이 네 count와 mismatch count가 같은 committed 상태를 봄

이 책임을 맡는 곳: repeatable-read/serializable policy or writer lock plus concurrent test
12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

  • a·t·l·h·m의 뜻과 기대값을 순서대로 말한다.
  • LEFT JOIN·SUM·coalesce가 m을 만드는 흐름을 다시 쓴다.
  • catalog 존재와 execution plan 선택을 서로 다른 증거로 설명한다.

2단계 · 코드 조각 재조립

  1. 네 개 count
  2. 잔액 대사 m
  3. catalog gate

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

정본을 보지 않고 7줄 전체를 다시 쓰고 비공백 mapping·번역 7행과 source-local test 0개를 대조한다.

자가 점검
  • 비공백 원문 7줄이 mapping과 번역에 정확히 한 번씩 있는지 본다.
  • 각 줄의 비유·실제 의미·입력·직접 결과·한계가 서로 다른 역할을 하는지 읽는다.
  • 고정 수치·파일명·분기 조건을 canonical source와 다시 대조한다.
  • 직접 보장과 다음 layer 책임을 반례로 설명한다.
13

STEP 13 / 13

전체 원본 정답

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

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcesql/w13/assert.sqlSHA-256 f7601add027b9eb0a57fb5e8b14f0b23766f6443b2b7c7f50de2986d982718ea
assert.sql — fixture 숫자·잔액 대사·인덱스 존재를 막는 SQL gate 전체
DO $gate$ DECLARE a bigint;t bigint;l bigint;h bigint;m bigint; BEGIN
 SELECT count(*) INTO a FROM perf_w13.account; SELECT count(*) INTO t FROM perf_w13.business_tx; SELECT count(*) INTO l FROM perf_w13.ledger_entry; SELECT count(*) INTO h FROM perf_w13.ledger_entry WHERE account_id=1;
 SELECT count(*) INTO m FROM perf_w13.account x LEFT JOIN(SELECT account_id,sum(signed_amount) b FROM perf_w13.ledger_entry GROUP BY account_id)y ON y.account_id=x.id WHERE x.balance<>coalesce(y.b,0);
 IF a<>1000 OR t<>100000 OR l<>100000 OR h<>90000 OR m<>0 THEN RAISE EXCEPTION 'W13 counts a=% t=% l=% h=% m=%',a,t,l,h,m; END IF;
 IF (SELECT count(*) FROM pg_indexes WHERE schemaname='perf_w13' AND indexname='idx_ledger_account_created_id')<>1 THEN RAISE EXCEPTION 'W13 index missing'; END IF;
END $gate$;
SELECT 'W13_ASSERT 1000|100000|100000|90000|0';
06

run-w13-perf.ps1 — 폐기형 DB에서 계획·분포·전후 30회를 증거로 남기는 orchestrator

scripts/run-w13-perf.ps1

PowerShell 정본 · 정본 · W13-F06
143줄 연결143줄 번역16 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

같은 synthetic fixture와 top-N SQL을 phase별로 실행하고, 계획·count·30회 전후 시간·cleanup 상태를 fail-closed evidence 파일로 남긴다.

  1. Prepare와 네 db phase, D7 Report는 각각 무엇을 읽고 쓸까?
  2. IsPathRooted가 true면 입력이 fully-qualified absolute path일까?
  3. Quantile(.5)와 Quantile(.95)는 30개 중 정확히 몇 번째 값을 고를까?
  4. Benchmark의 AND 조건은 어떤 경우를 통과시킬까?
  5. try/finally와 owned flag는 언제 compose down -v를 실행할까?
이 파일에서 끝까지 다시 쓰는 값strict phases=Prepare/BeforePlan/AfterPlan/Selectivity/Benchmarkexcluded shared branch=Report (D7)samples=30 before then 30 afternearest-rank .5=index14/15th; .95=index28/29thBenchmark fails only when am>=bm AND ap>=bptarget index=idx_ledger_account_created_idSourceHashes=records 5 current digests; expected comparisons=0; compose excludedcleanup=compose down -v when owned
02

STEP 02 / 13

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

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

STARRY가 일회용 PostgreSQL 무대를 매 phase 새로 만들고 같은 top-N 곡을 인덱스 전후로 비교한다.

같은 무대를 전후로 재는 증거 진행표

Prepare는 포장과 Docker·compose 설정을 검사하고 다섯 파일의 현재 hash를 기록한다. expected digest 목록과 비교하지 않으므로 변경 bytes를 hash만으로 거절하지는 않는다.

가운데 값은 30개의 두 중앙 평균이 아니라 15번째 nearest-rank다. Benchmark는 median과 p95가 둘 다 개선되지 않을 때만 실패하므로, 통과해도 두 metric과 합성 lab 한계를 함께 읽어야 한다.

딱 여기까지만 공연 리허설 비유는 phase 순서와 evidence 이동을 설명할 뿐 warm-up·randomization·운영 latency·통계적 유의성을 추가하지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

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

phase 하나씩

한 invocation은 Prepare 또는 DB phase 하나만 실행하고 자기 evidence를 쓴다.

코드 연결
2줄·54~70줄·93~148줄
비유
하루에 결과표 한 종류만 만드는 진행표
비유의 끝
Report는 같은 source지만 D7로 strict 범위 밖이다.

rooted와 full path

IsPathRooted로 root 없는 값을 거른 뒤 GetFullPath가 accepted input을 전체 경로로 해석한다.

코드 연결
10~13줄
비유
root 표식을 본 뒤 정식 주소로 펼치기
비유의 끝
rooted가 fully-qualified를 뜻하지 않고 C:Documents 같은 값은 현재 drive 문맥에 의존할 수 있다.

30개 순위

before 30회 뒤 index를 만들고 after 30회를 재며 .5는 15번째, .95는 29번째 값을 고른다.

코드 연결
28~43줄·129~140줄
비유
서른 초시계 표를 작은 순서로 세워 15·29번 뽑기
비유의 끝
warm-up이나 교차 순서를 구현하지 않는다.

fail-closed 정리

native exit와 evidence sentinel을 검사하고 owned Compose는 성공·실패 모두 down -v한다.

코드 연결
95~148줄
비유
장비를 빌렸다면 결과와 무관하게 반납하기
비유의 끝
cleanup 오류가 원래 오류를 가릴 수 있고 Container mode는 소유하지 않는다.

Report 읽기 전용

D7 Report는 다섯 saved JSON을 읽어 perf-manifest를 만들 뿐 query를 다시 돌리지 않는다.

코드 연결
72~91줄
비유
지난 결과표 다섯 장을 요약표에 옮기기
비유의 끝
predecessor 파일의 현재 진실성을 다시 측정하지 않는다.
nearest-rank
  1. 30개면 median은 당연히 두 가운데 평균 아닌가?

  2. 이 함수는 Ceiling(30×.5)-1이라 index 14, 즉 15번째 하나를 반환한다.

  3. 함수 이름보다 실제 배열 식을 믿어.

  4. 1..30 반례에서 15라고 계산하겠습니다.

Benchmark AND
  1. 통과하면 두 지표가 모두 빨라진 거지?

  2. 아니다. 둘 다 개선되지 않았을 때만 throw하므로 하나만 좋아져도 통과한다.

  3. median과 p95 네 값을 보고서에 함께 남겨야 해.

  4. pass를 두 지표 개선으로 과장하지 않을게요.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 143줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.PowerShell 정본의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.143 / 143 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F06-L01 param( 니지카가 W13 성능 리허설 접수대를 열어 이번 실행에 필요한 신청 칸을 펼친다. param(이 Phase·Mode·Container·ComposeProject·EvidenceDir를 script 입력으로 선언하는 범위를 시작한다.
입력
PowerShell이 스크립트 호출 인수를 바인딩하기 직전 상태를 받는다.
결과·효과
뒤 2~6줄의 parameter metadata가 하나의 script parameter 목록에 속하게 된다.
비유의 한계
parameter 범위를 연 것만으로 Docker나 PostgreSQL은 실행되지 않는다.
2줄F06-L02 [Parameter(Mandatory=$true)][ValidateSet('Prepare','BeforePlan','AfterPlan','Selectivity','Benchmark','Report')][string]$Phase, 진행표에는 준비·전 계획·후 계획·분포·측정·보고 여섯 무대명만 적을 수 있고 빈칸은 허용하지 않는다. Mandatory string $Phase에 Prepare, BeforePlan, AfterPlan, Selectivity, Benchmark, Report ValidateSet을 적용한다.
입력
호출자가 넘긴 -Phase 문자열 또는 누락된 필수 인수를 받는다.
결과·효과
허용된 정확한 phase만 $Phase에 바인딩되고 그 밖의 값은 본문 전에 parameter 오류가 된다.
비유의 한계
ValidateSet에 Report가 있어도 엄격 Monday–Saturday owner 명령은 Prepare와 네 db phase뿐이며 Report는 D7 분기다.
3줄F06-L03 [ValidateSet('Compose','Container')][string]$Mode='Compose', 리허설 장비를 새로 빌리거나 이미 있는 장비를 쓰는 두 방식 중, 표시가 없으면 새 대여 방식을 고른다. $Mode를 Compose·Container 두 값으로 제한하고 caller가 생략하면 Compose 문자열을 넣는다.
입력
선택적 -Mode 인수 또는 인수가 없는 호출을 받는다.
결과·효과
$Mode가 허용값 하나로 정해져 뒤 startup 분기가 선택할 기준이 된다.
비유의 한계
Mode 선택은 Docker engine 가용성이나 Container 값의 유효성을 아직 확인하지 않는다.
4줄F06-L04 [string]$Container='', 이미 마련된 DB 장비표를 꽂을 칸을 만들되 새 Compose 장비를 쓸 때는 비워 둔다. 선택 parameter $Container의 기본값을 빈 문자열로 선언한다.
입력
호출자가 제공한 -Container 문자열 또는 생략 상태를 받는다.
결과·효과
$Container에 제공값이나 빈 문자열이 바인딩된다.
비유의 한계
이 선언은 컨테이너 존재·PostgreSQL 준비·Compose 모드와의 조합을 검증하지 않는다.
5줄F06-L05 [string]$ComposeProject='w13-perf-lab', 일회용 장비 세트에 다른 공연과 겹치지 않도록 w13-perf-lab이라는 대여표를 붙인다. $ComposeProject를 선택 문자열로 선언해 compose -p 인수의 기본 project name을 고정한다.
입력
호출자의 -ComposeProject 값 또는 생략 상태를 받는다.
결과·효과
Compose 시작·조회·정리 명령이 사용할 project 문자열이 정해진다.
비유의 한계
사용자가 같은 project 이름을 동시에 재사용하면 격리가 깨질 수 있으며 이 줄은 uniqueness를 검사하지 않는다.
6줄F06-L06 [Parameter(Mandatory=$true)][string]$EvidenceDir 각 리허설 결과를 학생이 소유한 증거함에 넣기 위해 비울 수 없는 주소 칸을 만든다. Mandatory $EvidenceDir가 phase JSON·plan·CSV가 저장될 caller-supplied 경로를 받도록 선언된다.
입력
호출자가 제공한 -EvidenceDir 문자열 또는 누락 상태를 받는다.
결과·효과
문자열이 제공되어야 본문으로 진입하며 이후 rooted 여부 검사와 GetFullPath 해석의 대상이 된다.
비유의 한계
Mandatory는 경로의 절대성·소유권·쓰기 권한을 자체 보장하지 않는다.
7줄F06-L07 ) 다섯 입력 칸의 테두리를 닫아 접수표 한 장을 완성한다. 닫는 괄호가 param 블록을 끝내고 실행 본문으로 제어를 넘긴다.
입력
2~6줄의 parameter 선언이 모두 구문상 완성된 상태를 받는다.
결과·효과
PowerShell parameter binder 다음에 8줄을 실행할 수 있는 구조가 된다.
비유의 한계
닫는 괄호 자체는 인수 값을 검증하거나 외부 상태를 바꾸지 않는다.
8줄F06-L08 $ErrorActionPreference='Stop' 작은 실수도 조용히 넘기지 않고 리허설을 멈추게 하는 빨간 중단 스위치를 켠다. $ErrorActionPreference='Stop'이 비종료 PowerShell error도 catch/finally로 흐를 수 있는 terminating error로 승격한다.
입력
새 PowerShell process의 기존 ErrorActionPreference 값을 받는다.
결과·효과
이후 cmdlet 오류의 기본 처리 정책이 Stop으로 바뀐다.
비유의 한계
native executable의 0이 아닌 종료값은 자동 예외가 아니므로 source가 $LASTEXITCODE를 별도로 검사해야 한다.
9줄F06-L09 $root=Split-Path -Parent $PSScriptRoot scripts 방에서 한 층 올라가 compose와 sql이 함께 있는 기준 무대 바닥을 찾는다. Split-Path -Parent가 $PSScriptRoot의 부모 디렉터리를 $root에 저장한다.
입력
현재 실행 중인 scripts/run-w13-perf.ps1의 $PSScriptRoot 값을 받는다.
결과·효과
$root가 compose.yaml과 sql/w13 파일을 찾는 기준 경로가 된다.
비유의 한계
부모가 올바른 package root라는 배치 계약에 의존하며 repository 구조를 탐색해 확인하지 않는다.
10줄F06-L10 if(-not [IO.Path]::IsPathRooted($EvidenceDir)){ 주소에 drive나 root 표시가 전혀 없으면 접수를 거절하되, 표시가 있다고 완전한 주소라고 단정하지 않는다. IsPathRooted 결과를 부정해 root 없는 경로인 경우에만 if 본문으로 진입한다.
입력
caller가 바인딩한 $EvidenceDir 문자열을 받는다.
결과·효과
rooted 값은 13줄로 가고 root 없는 값은 11줄 throw가 선택된다; Windows의 C:Documents나 \Documents 같은 fully-qualified가 아닌 값도 rooted일 수 있다.
비유의 한계
IsPathRooted는 fully-qualified/absolute 입력 판정이 아니며 직접 그 계약을 보려면 IsPathFullyQualified 같은 검사가 필요하다.
11줄F06-L11 throw 'EvidenceDir must be an absolute learner-owned path' root 표시가 없는 주소에는 ‘전체 주소가 필요하다’는 안내문을 보여 주지만 안내문 자체가 검사 강도를 높이지는 않는다. throw가 EvidenceDir must be an absolute learner-owned path 문자열을 내지만 실제 선행 predicate는 IsPathRooted 하나다.
입력
10줄의 -not IsPathRooted 조건이 true인 root 없는 입력을 받는다.
결과·효과
현재 스크립트가 외부 명령을 실행하기 전에 실패한다.
비유의 한계
오류 문구의 absolute·learner-owned 표현은 fully-qualified 검사나 ACL 소유권 확인 결과가 아니다.
12줄F06-L12 } root 표시가 전혀 없는 주소를 막는 검사문을 접고 통과값을 다음 해석 창구로 보낸다. 닫는 중괄호가 root 없는 경로 오류 분기의 lexical scope를 끝낸다.
입력
IsPathRooted가 true여서 throw하지 않은 제어 흐름을 받는다.
결과·효과
13줄의 canonical path 계산으로 실행 위치가 이동한다.
비유의 한계
scope 종료 시점에도 입력이 fully-qualified였다는 보장은 없고 경로 해석은 아직 수행되지 않았다.
13줄F06-L13 $e=[IO.Path]::GetFullPath($EvidenceDir) rooted이지만 덜 완전할 수 있는 주소를 현재 drive·directory 문맥으로 해석해 정식 전체 주소표로 펴 쓴다. IO.Path.GetFullPath가 rooted $EvidenceDir의 점·부모 구간을 해석한 문자열을 $e에 저장한다.
입력
IsPathRooted를 통과했지만 fully-qualified라고 단정할 수 없는 $EvidenceDir를 받는다.
결과·효과
$e가 이후 모든 증거 파일 Join-Path의 기준이 된다.
비유의 한계
GetFullPath는 현재 drive/directory에 의존해 일부 rooted 입력을 해석할 수 있으며 디렉터리 존재·소유권·reparse point 안전성은 확인하지 않는다.
14줄F06-L14 New-Item -ItemType Directory -Force $e|Out-Null 정식 주소에 증거함이 없으면 만들되 ‘만들었다’는 안내 전표는 조용히 치운다. New-Item -Directory -Force 결과 객체를 pipeline으로 Out-Null에 넘겨 $e 디렉터리를 보장한다.
입력
정규화된 $e 경로와 현재 파일시스템 상태를 받는다.
결과·효과
$e가 기존 디렉터리로 남거나 새로 만들어지고 success stream에는 객체가 남지 않는다.
비유의 한계
-Force는 쓰기 권한을 우회하지 않으며 기존 증거 파일을 지우지 않는다.
15줄F06-L15 $compose=Join-Path $root 'compose.yaml' 기준 무대 바닥에서 일회용 DB 장비 설명서의 고정 자리를 표시한다. Join-Path가 $root와 compose.yaml을 결합해 $compose에 저장한다.
입력
9줄에서 계산한 package $root를 받는다.
결과·효과
$compose가 Prepare config 검사와 db phase lifecycle 명령의 -f 인수가 된다.
비유의 한계
경로 계산은 파일 존재를 확인하지 않으며 SourceHashes 다섯 파일 목록에도 compose는 들어가지 않는다.
16줄F06-L16 $owned=$false 아직 빌린 장비가 없으니 ‘내가 반납해야 함’ 표찰을 false로 놓는다. $owned=$false가 finally에서 compose down을 실행할지 결정하는 ownership flag의 초기 상태를 만든다.
입력
스크립트가 DB startup 전인 제어 상태를 받는다.
결과·효과
$owned가 false라서 지금 예외가 나면 cleanup 명령을 호출하지 않는다.
비유의 한계
이 boolean은 실제 Docker resource 소유권을 조회한 값이 아니라 script가 관리하는 내부 약속이다.
17줄F06-L17 $dbPhases=@('BeforePlan','AfterPlan','Selectivity','Benchmark') 실제 장비를 켜야 하는 전 계획·후 계획·분포·측정 무대 네 장의 표를 따로 모은다. $dbPhases가 BeforePlan, AfterPlan, Selectivity, Benchmark 문자열 배열을 가진다.
입력
여섯 ValidateSet phase 중 Prepare와 Report를 제외할 분류 기준을 받는다.
결과·효과
93줄의 -notin guard가 비교할 정확한 네 값 집합이 준비된다.
비유의 한계
배열 선언은 각 phase의 DB 동작을 실행하지 않으며 Report가 D7이라는 범위를 자동 표시하지 않는다.
19줄F06-L19 function Write-JsonAtomic([string]$Name,$Value){ 니지카가 완성 전 봉투는 임시 칸에 쓰고 마지막에만 증거함 이름으로 바꾸는 포장대를 연다. function 선언이 $Name과 $Value를 받는 JSON evidence writer의 body 범위를 시작한다.
입력
뒤 phase들이 evidence object를 안전하게 파일로 남길 공통 기능을 필요로 하는 상태를 받는다.
결과·효과
Write-JsonAtomic이라는 호출 가능한 함수 정의가 만들어지기 시작한다.
비유의 한계
함수 선언 시점에는 파일을 쓰지 않으며 process crash까지 견디는 filesystem durability를 약속하지 않는다.
20줄F06-L20 $target=Join-Path $e $Name;$temporary="$target.tmp" 증거함 안 최종 칸과 바로 옆 임시 작성 칸을 한 번에 표시한다. Join-Path로 $e/$Name을 $target에 만들고 문자열 보간으로 $target.tmp를 $temporary에 둔다.
입력
함수 인수 $Name과 정규화된 evidence root $e를 받는다.
결과·효과
최종·임시 두 경로가 같은 디렉터리를 가리키도록 준비된다.
비유의 한계
Name의 경로 구간을 정화하지 않으므로 caller가 하위 경로나 특수 이름을 주면 그대로 결합된다.
21줄F06-L21 $Value|ConvertTo-Json -Depth 6|Set-Content -Encoding utf8 $temporary 확인표 값을 여섯 겹 안쪽까지 JSON으로 접어 임시 봉투에 먼저 적는다. pipeline이 $Value를 ConvertTo-Json -Depth 6에 보내고 그 문자열을 Set-Content로 $temporary에 기록한다.
입력
직렬화할 $Value와 쓰기 가능한 $temporary 경로를 받는다.
결과·효과
완성 후보 JSON이 임시 파일에 생기며 아직 최종 $target 이름은 바뀌지 않는다.
비유의 한계
Depth 6보다 깊은 객체는 잘릴 수 있고 Set-Content 완료가 disk flush까지 보장하지 않는다.
22줄F06-L22 Move-Item -LiteralPath $temporary -Destination $target -Force 봉투를 다 쓴 뒤 임시 꼬리표를 떼고 최종 증거 이름 칸에 한 번에 옮긴다. Move-Item -LiteralPath가 $temporary를 $target로 rename하며 기존 target이 있으면 -Force로 교체한다.
입력
21줄이 완성한 임시 파일과 계산된 최종 경로를 받는다.
결과·효과
호출이 성공하면 최종 evidence 파일이 새 JSON 내용으로 보인다.
비유의 한계
파일시스템·볼륨 구현에 따른 atomicity만 기대하며 동시 writer locking이나 backup은 제공하지 않는다.
23줄F06-L23 } 임시 작성과 최종 교체를 묶은 포장대의 셔터를 내린다. 닫는 중괄호가 JSON writer 정의를 완성한다.
입력
target 이동 문장까지 구문상 완성된 함수 body를 받는다.
결과·효과
이후 phase code가 Write-JsonAtomic 이름으로 함수를 호출할 수 있다.
비유의 한계
함수 정의 완료는 아직 어떤 evidence 파일도 생성하지 않는다.
24줄F06-L24 function PsqlFile([string]$Relative){ 료가 기준 악보함의 SQL 한 장을 DB 장비에 그대로 전달하는 연주 통로를 만든다. function PsqlFile이 $Relative 문자열을 입력으로 받는 native psql 실행 범위를 시작한다.
입력
seed·create-index·assert 같은 package-relative SQL을 공통 방식으로 실행할 필요를 받는다.
결과·효과
PsqlFile이라는 함수 정의가 뒤 25~26줄 동작을 소유한다.
비유의 한계
선언만으로 파일 존재·container 연결·SQL 성공은 확인되지 않는다.
25줄F06-L25 Get-Content -Raw -LiteralPath (Join-Path $root $Relative)|docker exec -i $Container psql -X -v ON_ERROR_STOP=1 -U app -d financial_core 악보 한 장의 전체 내용을 읽어 app 연주자가 쓰는 financial_core 무대로 끊김 없이 전달한다. Get-Content -Raw 출력이 pipeline을 통해 docker exec -i의 psql -X -v ON_ERROR_STOP=1 -U app -d financial_core 입력이 된다.
입력
$root와 $Relative가 가리키는 SQL 파일, 해석된 $Container를 받는다.
결과·효과
psql이 startup 파일 없이 SQL을 실행하고 SQL 오류 시 native exit code를 비정상으로 돌려준다.
비유의 한계
pipeline은 파일 텍스트만 전달하며 transaction wrapping·출력 캡처·timeout을 추가하지 않는다.
26줄F06-L26 if($LASTEXITCODE-ne0){throw "psql file failed: $Relative exit=$LASTEXITCODE"} DB 연주가 실패 표를 돌려주면 어떤 악보였는지와 번호를 읽고 즉시 리허설을 멈춘다. $LASTEXITCODE -ne 0 guard가 native psql 실패를 PowerShell terminating error로 바꾼다.
입력
25줄 docker exec/psql이 남긴 native 종료 코드와 $Relative를 받는다.
결과·효과
0이면 함수가 정상 종료되고 비zero면 psql file failed 메시지로 caller에 예외가 전파된다.
비유의 한계
$LASTEXITCODE는 바로 앞 native program에 의존하며 SQL 결과의 의미나 출력 marker를 검증하지 않는다.
27줄F06-L27 } SQL 전달과 종료표 확인을 묶은 연주 통로를 닫는다. 닫는 중괄호가 PsqlFile 정의의 범위를 끝낸다.
입력
SQL 실행과 exit guard 두 문장이 완성된 상태를 받는다.
결과·효과
뒤 phase가 상대 경로 하나로 이 함수를 부를 수 있다.
비유의 한계
함수 종료는 psql output을 구조화하거나 보존하지 않는다.
28줄F06-L28 function MeasurePlan([string]$Variant,[string]$Output){ 히토리가 전·후 이름표와 기록 파일을 받아 같은 곡을 서른 번 재는 초시계 책상을 편다. function MeasurePlan이 $Variant와 $Output을 입력으로 하는 sampling 범위를 시작한다.
입력
before 또는 after label과 raw CSV destination이 준비된 caller 상태를 받는다.
결과·효과
30회 실행시간 수집 로직이 MeasurePlan 이름 아래 정의된다.
비유의 한계
이 함수에는 warm-up·실행 순서 randomization·before/after interleaving이 없다.
29줄F06-L29 $rows=@() 서른 번의 초시계 기록을 넣을 빈 표 묶음을 책상 위에 놓는다. $rows=@()가 현재 MeasurePlan 호출만을 위한 빈 PowerShell array를 초기화한다.
입력
아직 measure.sql을 한 번도 실행하지 않은 함수 상태를 받는다.
결과·효과
$rows.Count가 0인 수집기가 준비된다.
비유의 한계
빈 배열은 용량이나 row type을 고정하지 않으며 측정 실패를 대체하지 않는다.
30줄F06-L30 for($sample=1;$sample-le30;$sample++){ 초시계 기록지의 1번 칸부터 30번 칸까지 차례대로 돌겠다는 반복선을 긋는다. for가 $sample=1에서 시작해 $sample -le 30인 동안 body를 실행하고 매회 증가시킨다.
입력
빈 $rows와 측정 명칭 $Variant를 받는다.
결과·효과
정상 경로에서는 31~34줄이 정확히 30회 순차 실행된다.
비유의 한계
반복은 warm-up을 버리거나 각 sample 사이를 쉬거나 순서를 섞지 않는다.
31줄F06-L31 $json=Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core 같은 top-N 악보를 DB에 연주시키고 EXPLAIN 시계표 전체를 한 장의 JSON으로 받아 든다. Get-Content -Raw 출력이 pipeline으로 docker exec -i psql -X -qAt에 들어가고 Execution Time을 품은 stdout JSON이 $json에 대입된다.
입력
$root/sql/w13/measure.sql, 현재 $Container, 현재 sample 반복 상태를 받는다.
결과·효과
$json이 이번 실행의 EXPLAIN (ANALYZE, FORMAT JSON) 결과 문자열을 담는다.
비유의 한계
측정은 고정 query를 실제 실행하지만 timeout·warm-up·cache 통제·동시 부하를 추가하지 않는다.
32줄F06-L32 if($LASTEXITCODE-ne0){throw "measure failed: phase=$Variant sample=$sample exit=$LASTEXITCODE"} 서른 칸 중 어느 연주가 실패했는지 전·후 이름과 칸 번호, 종료표 번호를 함께 적어 중단한다. $LASTEXITCODE guard가 직전 docker/psql 실패를 measure failed terminating error로 승격한다.
입력
31줄 native 실행이 남긴 code와 현재 $Variant·$sample을 받는다.
결과·효과
성공이면 JSON parse로 진행하고 실패면 남은 sample은 실행하지 않는다.
비유의 한계
0 종료는 JSON 구조나 Execution Time 필드 존재까지 보장하지 않는다.
33줄F06-L33 $object=$json|ConvertFrom-Json DB가 돌려준 시계표 봉투를 열어 안쪽 칸을 이름으로 꺼낼 수 있는 표로 바꾼다. $json pipeline이 ConvertFrom-Json으로 전달되어 parsed result가 $object에 저장된다.
입력
psql이 0으로 끝낸 현재 sample의 JSON 문자열을 받는다.
결과·효과
$object[0]에서 Plan과 Execution Time 속성을 읽을 수 있는 객체가 만들어진다.
비유의 한계
parser 성공은 plan의 특정 node나 시간값의 합리성을 검증하지 않는다.
34줄F06-L34 $rows+=[pscustomobject]@{variant=$Variant;sample=$sample;ms=[double]$object[0].'Execution Time'} 이번 초시계 표에 전·후 이름표, 몇 번째인지, 걸린 밀리초 세 칸을 적어 기록 묶음 뒤에 붙인다. pscustomobject가 $Variant, $sample, $object[0].'Execution Time'을 double로 저장되고 +=로 $rows에 append된다.
입력
parsed $object와 현재 반복의 variant·sample을 받는다.
결과·효과
$rows가 한 행 늘어나며 이번 실행시간이 ms field로 고정된다.
비유의 한계
+= 배열 append는 표본 독립성이나 시간 단위의 운영 대표성을 보장하지 않는다.
35줄F06-L35 } 현재 초시계 칸을 마치고 다음 번호로 이동하거나 30번 뒤 반복선을 끝낸다. 닫는 중괄호가 for body를 종료해 증분·조건 재평가로 제어를 돌린다.
입력
한 sample의 JSON parse와 row append가 끝난 상태를 받는다.
결과·효과
sample 30까지 성공하면 36줄 CSV export로 진행한다.
비유의 한계
반복 종료는 수집 row가 정확히 30인지 별도 assertion하지 않는다; 정상 loop 구조에 의존한다.
36줄F06-L36 $rows|Export-Csv -NoTypeInformation -Encoding utf8 -LiteralPath $Output 서른 장의 초시계 표를 전·후·번호·밀리초 열을 가진 한 파일로 묶어 증거함에 넣는다. $rows pipeline이 Export-Csv -NoTypeInformation -Encoding utf8 -LiteralPath $Output으로 전달된다.
입력
최대 30개 pscustomobject와 caller가 정한 CSV $Output을 받는다.
결과·효과
variant,sample,ms 열의 raw 측정 CSV가 생성된다.
비유의 한계
CSV 기록은 atomic writer를 쓰지 않으며 분산·median·p95를 계산하지 않는다.
37줄F06-L37 return $rows 파일에도 넣은 서른 장 표 묶음을 다음 계산대가 바로 쓰도록 손으로 다시 건넨다. return $rows가 MeasurePlan pipeline output으로 측정 객체 배열을 보낸다.
입력
CSV export까지 성공한 현재 $rows를 받는다.
결과·효과
Benchmark branch가 before/after array를 변수에 받아 Quantile에 전달할 수 있다.
비유의 한계
반환은 CSV durability나 인덱스 사용 여부를 추가 확인하지 않는다.
38줄F06-L38 } 서른 번 측정·파일 기록·표 반환을 묶은 초시계 책상을 접는다. 닫는 중괄호가 MeasurePlan 정의를 완성한다.
입력
sampling 함수의 모든 문장이 구문상 끝난 상태를 받는다.
결과·효과
뒤 Benchmark phase에서 함수를 두 번 호출할 수 있게 된다.
비유의 한계
정의 완료 시점에는 실제 sample이 아직 없다.
39줄F06-L39 function Quantile($Rows,[double]$P){ 료가 서른 초시계 기록과 0.5 또는 0.95 위치표를 받아 순위를 고르는 계산판을 편다. function Quantile이 $Rows와 double $P를 parameter로 하는 nearest-rank 계산 범위를 시작한다.
입력
MeasurePlan 결과와 요청 확률 값이 준비된 호출 상태를 받는다.
결과·효과
정렬·개수 guard·배열 index 계산이 Quantile 이름 아래 묶인다.
비유의 한계
함수 이름이 Quantile이어도 통계학의 모든 분위수 정의를 구현하지 않는다.
40줄F06-L40 $values=@($Rows.ms|Sort-Object) 서른 초시계 숫자를 작은 것부터 큰 것까지 한 줄로 다시 세운다. $Rows.ms가 Sort-Object pipeline을 지나고 @()가 1개여도 array인 $values를 만든다.
입력
ms 속성이 있는 입력 row 집합을 받는다.
결과·효과
$values가 오름차순 실행시간 배열이 된다.
비유의 한계
정렬은 NaN·null·단위 오류를 걸러내지 않고 원래 sample 순서를 잃는다.
41줄F06-L41 if($values.Count-ne30){throw "quantile requires 30 values, got $($values.Count)"} 순위표가 서른 칸이 아니면 몇 칸뿐인지 말하고 계산판을 닫는다. $values.Count -ne 30 guard가 quantile requires 30 values 예외를 발생시킨다.
입력
40줄에서 만든 정렬 배열의 실제 Count를 받는다.
결과·효과
정확히 30이면 index 식으로 가고 아니면 분위수 계산이 중단된다.
비유의 한계
개수만 검사하므로 값의 유한성·측정 성공의 독립성은 확인하지 않는다.
42줄F06-L42 return $values[[Math]::Min(29,[Math]::Ceiling(30*$P)-1)] 위치표 0.5면 서른 기록 중 15번째, 0.95면 29번째 표를 집어 caller에게 건넨다. 0-based index Min(29, Ceiling(30*$P)-1)로 nearest-rank 값을 골라 return한다.
입력
정확히 30개로 정렬된 $values와 보통 .5 또는 .95인 $P를 받는다.
결과·효과
P=.5는 index 14의 15번째 값, P=.95는 index 28의 29번째 값을 돌려준다.
비유의 한계
30개에서 .5 결과는 두 중앙값 평균이 아니므로 1..30 입력이면 15이며 관례적 median 15.5가 아니다; P<=0 범위도 방어하지 않는다.
43줄F06-L43 } 순위를 한 값으로 고르는 계산판을 접는다. 닫는 중괄호가 Quantile 정의의 범위를 끝낸다.
입력
개수 guard와 nearest-rank return이 완성된 상태를 받는다.
결과·효과
Benchmark가 같은 함수로 before/after의 .5와 .95를 계산할 수 있다.
비유의 한계
함수 종료는 이 구현을 일반적인 median 정의로 바꾸지 않는다.
44줄F06-L44 function FindIndex($Node){ 키타가 실행 계획 나무의 가지를 내려가며 첫 인덱스 명찰을 찾는 탐색대를 연다. function FindIndex가 하나의 JSON plan $Node를 입력으로 받는 depth-first 탐색 범위를 시작한다.
입력
ConvertFrom-Json으로 만든 Plan root 또는 child node를 받는다.
결과·효과
현재 node 검사와 Plans child 재귀가 FindIndex 이름 아래 묶인다.
비유의 한계
탐색은 첫 발견 인덱스만 반환하며 전체 plan의 모든 index node를 수집하지 않는다.
45줄F06-L45 if($Node.'Index Name'){return [pscustomobject]@{name=[string]$Node.'Index Name';type=[string]$Node.'Node Type'}} 현재 계획 상자에 인덱스 명찰이 붙어 있으면 이름과 연주 방식 두 칸만 베껴 탐색을 끝낸다. truthy $Node.'Index Name' guard가 pscustomobject{name,type}을 만들어 early return한다.
입력
현재 재귀 단계의 $Node 속성 Index Name과 Node Type을 받는다.
결과·효과
인덱스 node면 문자열 name·type을 가진 객체가 caller까지 바로 올라간다.
비유의 한계
현재 첫 인덱스의 이름·type만 보여 주며 predicate·열·비용·heap fetch는 보존하지 않는다.
46줄F06-L46 foreach($child in @($Node.Plans)){$found=FindIndex $child;if($found){return $found}} 명찰이 없으면 자식 상자를 차례로 열고, 어느 가지에서 찾자마자 나머지는 보지 않고 들고 나온다. @($Node.Plans) foreach가 각 child에 FindIndex를 재귀 호출하고 truthy $found에서 early return한다.
입력
현재 node에 직접 Index Name이 없고 0개 이상의 Plans child가 있는 상태를 받는다.
결과·효과
깊이 우선 순서에서 처음 찾은 인덱스 객체가 반환되거나 모든 child 뒤 다음 줄로 간다.
비유의 한계
여러 인덱스가 있는 계획에서는 목표가 아닌 첫 인덱스가 선택될 수 있고 recursion cycle 방어가 없다.
47줄F06-L47 return $null 모든 자식 상자를 열어도 명찰이 없으면 빈손이라는 표를 위 탐색자에게 보낸다. return $null이 current node와 descendants에서 Index Name을 찾지 못한 결과를 표시한다.
입력
45줄과 46줄이 아무 객체도 반환하지 않은 탐색 상태를 받는다.
결과·효과
caller는 falsy null로 인덱스 미발견을 판별한다.
비유의 한계
null은 왜 plan이 index를 쓰지 않았는지 설명하지 않는다.
48줄F06-L48 } 계획 나무에서 첫 인덱스 명찰을 찾는 탐색대를 닫는다. 닫는 중괄호가 recursive FindIndex 정의를 완성한다.
입력
현재 node·children·null 세 경로가 모두 구문상 끝난 상태를 받는다.
결과·효과
AfterPlan과 Benchmark가 parsed plan을 이 함수에 넘길 수 있다.
비유의 한계
정의 완료 자체는 어떤 plan도 검사하지 않는다.
49줄F06-L49 function SourceHashes(){ 증거에 붙일 악보 지문 다섯 개를 모으는 확인대를 편다. function SourceHashes가 고정 file list를 SHA-256 객체 배열로 바꾸는 범위를 시작한다.
입력
Prepare evidence가 source identity를 기록하려는 상태를 받는다.
결과·효과
SourceHashes라는 호출 가능한 함수 정의가 시작된다.
비유의 한계
함수 이름은 모든 dependency hash를 뜻하지 않고 compose.yaml도 목록 밖이며, expected digest와 비교하는 동작은 없다.
50줄F06-L50 $files=@('scripts/run-w13-perf.ps1','sql/w13/seed.sql','sql/w13/measure.sql','sql/w13/create-index.sql','sql/w13/assert.sql') 실행 대본 하나와 seed·measure·index·assert 악보 네 장만 지문 목록에 올린다. $files가 scripts/run-w13-perf.ps1 및 sql/w13의 seed, measure, create-index, assert 경로를 순서대로 가진다.
입력
$root 아래 packaged source closure를 받는다.
결과·효과
hash loop가 순회할 정확한 다섯 경로가 고정된다.
비유의 한계
Prepare와 DB phase가 직접 읽는 compose.yaml은 배열 밖이고 expected hash 목록도 없어, 이 다섯 경로 선언만으로 변경 bytes를 거절할 수 없다.
51줄F06-L51 return @($files|ForEach-Object{[pscustomobject]@{file=$_;sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $root $_)).Hash.ToLowerInvariant()}}) 지문 목록의 각 악보를 한 장씩 스캔해 경로와 소문자 지문을 짝지은 표로 돌려준다. $files pipeline의 ForEach-Object가 Get-FileHash SHA256을 실행하고 @()가 pscustomobject 배열을 보장한다.
입력
50줄의 상대 경로 다섯 개와 repository $root를 받는다.
결과·효과
호출자는 실행 시점의 file·sha256 필드가 있는 정확히 다섯 current hash row를 받아 evidence에 기록할 수 있다.
비유의 한계
expected baseline·equality 비교·mismatch throw가 전혀 없어 changed bytes도 hash 계산 자체로 거절하지 못하며 compose·runtime version도 고정하지 않는다.
52줄F06-L52 } 다섯 악보 지문 확인대를 접는다. 닫는 중괄호가 SourceHashes 정의를 완성한다.
입력
고정 목록과 hash projection 두 문장이 완성된 상태를 받는다.
결과·효과
Prepare body가 hashes=SourceHashes로 함수를 호출할 수 있다.
비유의 한계
함수 정의 완료만으로 hash evidence가 기록되지는 않는다.
54줄F06-L54 if($Phase-eq'Prepare'){ 장비를 켜지 않고 포장 상태만 검사하는 첫날 리허설이면 별도 점검표를 펼친다. $Phase -eq 'Prepare' 조건이 true인 호출만 55~69줄의 config-only 준비 절차로 보낸다.
입력
ValidateSet을 통과한 현재 $Phase 문자열을 받는다.
결과·효과
Prepare면 package·stale·Docker·compose contract를 검사하고 다른 phase면 72줄로 건너간다.
비유의 한계
이 branch는 database container를 시작하거나 seed SQL을 실행하지 않는다.
55줄F06-L55 if(!(Test-Path -LiteralPath $compose -PathType Leaf)){throw 'packaged compose.yaml missing'} 장비 설명서가 정해진 서랍에 실제 종이로 없으면 대여 절차를 시작하기 전에 중단한다. Test-Path -LiteralPath $compose -PathType Leaf를 부정해 missing compose dependency를 throw한다.
입력
15줄에서 계산한 compose.yaml 후보 경로를 받는다.
결과·효과
파일이 있으면 stale artifact 목록 작성으로 진행하고 없으면 packaged compose.yaml missing 오류가 난다.
비유의 한계
존재만 확인하며 compose bytes는 SourceHashes에 포함되지 않고 YAML 유효성은 62줄 config가 맡는다.
56줄F06-L56 $futureFiles=@('before-day.json','before-plan.txt','after-day.json','after-plan.txt','selectivity-day.json','benchmark-manifest.json','before-raw.csv','after-raw.csv','benchmark-after-plan.txt','perf-manifest.json') 아직 시작하지 않은 뒤 무대의 결과표 열 장 이름을 ‘미리 있으면 안 됨’ 목록에 적는다. $futureFiles가 before/after plan·day JSON, selectivity, benchmark CSV/manifest/plan, perf-manifest 이름을 가진다.
입력
깨끗한 EvidenceDir인지 검사할 Prepare phase 상태를 받는다.
결과·효과
뒤 stale 검색과 post-check가 같은 열 개 이름을 재사용할 수 있다.
비유의 한계
목록 밖의 임의 파일은 stale로 취급하지 않으며 prepare.json 자체도 검사 대상이 아니다.
57줄F06-L57 $stale=@($futureFiles|Where-Object{Test-Path -LiteralPath (Join-Path $e $_) -PathType Leaf}) 미래 결과표 목록을 증거함 칸과 대조해 이미 꽂힌 종이만 따로 꺼낸다. $futureFiles pipeline의 Where-Object가 Join-Path $e 경로에서 Leaf 존재를 확인하고 @()가 $stale 배열을 만든다.
입력
56줄 file names와 현재 $e 디렉터리 내용을 받는다.
결과·효과
$stale.Count와 존재 파일 이름 목록이 Prepare 선행조건 검사에 제공된다.
비유의 한계
디렉터리·symlink 대상·목록 밖 파일은 이 stale array의 의미에 포함되지 않는다.
58줄F06-L58 if($stale.Count-ne0){throw "stale W13 D2-D6 artifact exists before Prepare: $($stale -join ',')"} 뒤 무대 결과표가 한 장이라도 미리 꽂혀 있으면 그 이름을 모두 읽고 새 리허설과 섞이지 않게 멈춘다. $stale.Count -ne 0 guard가 D2-D6 artifact exists 메시지에 -join ',' 목록을 넣어 throw한다.
입력
57줄에서 찾은 실제 $stale 배열을 받는다.
결과·효과
0개면 Docker 설정 검사로 진행하고 1개 이상이면 기존 evidence를 보존한 채 종료된다.
비유의 한계
이 줄은 stale 파일을 삭제·격리하지 않으며 오류 문구가 D2-D6라고 해도 목록에는 D7 perf-manifest가 포함된다.
59줄F06-L59 if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w13-prepare-config-only'} 설명서를 해석할 때 빈 비밀번호 칸 때문에 멈추지 않도록 실제 접속용이 아닌 점검 전용 표식을 채운다. IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)일 때 w13-prepare-config-only를 현재 process environment에 대입한다.
입력
caller가 설정했거나 설정하지 않은 FCL_DB_PASSWORD 환경값을 받는다.
결과·효과
빈 경우 compose variable interpolation용 placeholder가 생기고 기존 nonblank 값은 유지된다.
비유의 한계
이 값은 Prepare에서 DB 로그인을 검증하지 않고 process 환경을 변경하며 보안 비밀번호 품질을 보장하지 않는다.
60줄F06-L60 $docker=& docker version --format '{{.Server.Version}}' 장비 대여소에 서버가 살아 있는지 묻고 버전 번호 한 줄을 받아 적는다. docker version --format '{{.Server.Version}}' stdout을 $docker에 저장한다.
입력
현재 PATH의 docker CLI와 접근 가능한 engine을 받는다.
결과·효과
$docker가 server version 출력 또는 빈/오류 상태를 담고 $LASTEXITCODE가 갱신된다.
비유의 한계
버전 조회는 compose service를 시작하지 않으며 특정 최소 Docker version을 비교하지 않는다.
61줄F06-L61 if($LASTEXITCODE-ne0-or[string]::IsNullOrWhiteSpace($docker)){throw 'Docker engine is unavailable'} 대여소 응답 번호가 실패이거나 버전표가 비어 있으면 장비가 없다고 판정한다. $LASTEXITCODE -ne 0 OR IsNullOrWhiteSpace($docker) 조건이 true면 terminating error를 낸다.
입력
60줄의 native exit code와 server version 문자열을 받는다.
결과·효과
둘 다 정상일 때 compose config 검사로 진행하고 아니면 Prepare가 중단된다.
비유의 한계
engine 응답만 확인하며 image pull·network·container startup 가능성은 증명하지 않는다.
62줄F06-L62 $services=@(& docker compose -f $compose config --services) 장비 설명서를 실제로 펼쳐 무대 장비 이름 목록만 뽑아 한 칸짜리 표인지 본다. docker compose -f $compose config --services의 각 output line을 @()로 감싸 $services 배열에 저장한다.
입력
존재하는 compose.yaml과 사용 가능한 Docker engine을 받는다.
결과·효과
$services와 직전 compose config의 $LASTEXITCODE가 63줄 contract 검사의 입력이 된다.
비유의 한계
config 렌더링은 컨테이너를 생성하지 않고 service health나 image 실행을 확인하지 않는다.
63줄F06-L63 if($LASTEXITCODE-ne0-or$services.Count-ne1-or$services[0]-cne'db'){throw "compose service contract mismatch: $services"} 장비 목록이 정확히 한 줄 ‘db’가 아니면 다른 장비가 섞인 포장으로 보고 거절한다. exit nonzero OR Count!=1 OR services[0] -cne 'db'이면 compose service contract mismatch를 throw한다.
입력
62줄의 native code와 service name 배열을 받는다.
결과·효과
정확히 db 하나일 때만 future artifact post-check로 넘어간다.
비유의 한계
service 이름 계약만 검사하며 port·volume·image·healthcheck 설정의 정확성을 별도로 단언하지 않는다.
64줄F06-L64 $futureCount=@($futureFiles|Where-Object{Test-Path -LiteralPath (Join-Path $e $_) -PathType Leaf}).Count 설명서 점검만 했는데 뒤 무대 결과표가 생기지 않았는지 증거함을 한 번 더 센다. $futureFiles를 다시 Where-Object로 필터하고 array Count를 $futureCount에 저장한다.
입력
Docker version·compose config까지 끝난 $e 상태와 열 개 파일 이름을 받는다.
결과·효과
Prepare 과정 후 존재하는 미래 evidence 개수가 정수로 고정된다.
비유의 한계
Prepare 시작 전·후 사이 동시 writer race를 잠그지 않으며 목록 밖 artifact는 세지 않는다.
65줄F06-L65 if($futureCount-ne0){throw "Prepare created a future W13 artifact: count=$futureCount"} 포장 점검만 했는데 결과표가 생겼다면 개수를 적고 config-only 약속 위반으로 중단한다. $futureCount -ne 0 guard가 Prepare created a future W13 artifact 예외를 낸다.
입력
64줄의 재검사 count를 받는다.
결과·효과
0이면 prepare evidence 작성으로 가고 nonzero면 성공 marker 전에 종료된다.
비유의 한계
어떤 process가 파일을 만들었는지 구분하지 않고 현재 존재만 Prepare 책임 위반으로 본다.
66줄F06-L66 $body=[ordered]@{phase='Prepare';docker_server=[string]$docker;compose_services=@($services);hashes=SourceHashes;native_exit=0;future_artifacts_created=$futureCount} 점검표에 준비 단계, 장비 버전, db 한 대, 악보 지문 다섯, 정상 종료, 미래표 0장을 정해진 순서로 적는다. ordered hashtable이 SourceHashes를 호출해 실행 시점의 다섯 current SHA-256을 prepare.json field로 기록할 객체를 만든다.
입력
검증된 $docker, $services, $futureCount와 읽을 수 있는 hash 대상 다섯 파일을 받는다.
결과·효과
$body가 Prepare 결과와 비교되지 않은 current hash 다섯 행을 포함한 직렬화 가능 객체가 된다.
비유의 한계
expected digest 비교나 mismatch branch가 없고 hashes에는 compose.yaml도 없으며 native_exit=0은 파일 쓰기 성공 전의 예정값이다.
67줄F06-L67 Write-JsonAtomic 'prepare.json' $body 완성한 준비 점검표를 임시 봉투에 쓴 뒤 prepare.json 이름으로 증거함에 넣는다. Write-JsonAtomic이 $body를 $e/prepare.json에 기록한다.
입력
66줄의 ordered evidence object와 정상 동작하는 JSON writer를 받는다.
결과·효과
성공하면 후속 Report가 읽을 prepare.json이 존재한다.
비유의 한계
이 호출은 DB query를 실행하지 않으며 쓰기 실패 시 68줄 marker로 가지 않는다.
68줄F06-L68 'W13_PREPARE_GREEN compose=db hashes=5 future_artifacts=0 native_exit=0' 키타가 db 하나·지문 다섯·미래표 0·종료 0을 초록 안내로 읽는다. literal string W13_PREPARE_GREEN compose=db hashes=5 future_artifacts=0 native_exit=0이 success stream에 나온다.
입력
prepare.json 원자 저장이 성공한 제어 흐름을 받는다.
결과·효과
owner command가 exact marker를 대조할 수 있는 stdout 한 줄이 생성된다.
비유의 한계
hashes=5는 current digest row 개수일 뿐 baseline 일치 판정이 아니며 marker에는 hash 내용·compose bytes·database 준비 완료가 없다.
69줄F06-L69 exit 0 준비 무대만 맡았으니 초록표를 낸 뒤 다른 phase로 내려가지 않고 공연장을 정상 퇴장한다. exit 0이 현재 PowerShell host의 남은 script 문장을 실행하지 않고 성공 code를 반환한다.
입력
68줄 marker까지 출력한 Prepare branch를 받는다.
결과·효과
72줄 이후 Report/DB phase 로직이 이번 invocation에서 실행되지 않는다.
비유의 한계
host를 종료하는 동작은 caller가 같은 process에서 후속 코드를 기대하면 영향을 줄 수 있다.
70줄F06-L70 } 준비 전용 점검표의 테두리를 닫아 다음 phase 분기와 구분한다. 닫는 중괄호가 if($Phase-eq'Prepare') lexical scope를 끝낸다.
입력
Prepare가 아니어서 54줄 조건을 건너온 흐름 또는 구문 분석 상태를 받는다.
결과·효과
다음 실행 문장은 72줄 Report 판별이다.
비유의 한계
Prepare true 경로는 69줄 exit 때문에 정상적으로 이 닫는 괄호 뒤를 실행하지 않는다.
72줄F06-L72 if($Phase-eq'Report'){ 이미 끝난 다섯 무대 결과표만 모아 요약하는 일요일 보고대라면 별도 서랍을 연다. $Phase -eq 'Report' 조건이 true인 호출을 73~90줄의 saved-evidence aggregation으로 보낸다.
입력
Prepare가 아닌 현재 $Phase 문자열을 받는다.
결과·효과
Report면 다섯 JSON을 읽고 perf-manifest를 만들며, 그 밖이면 DB phase guard로 간다.
비유의 한계
이 분기는 D7로 strict Monday–Saturday owner 범위 밖이며 성능 query를 다시 실행하지 않는다.
73줄F06-L73 $prepare=Get-Content -Raw (Join-Path $e 'prepare.json')|ConvertFrom-Json 보고대가 준비 무대 점검표 봉투를 열어 필드로 읽을 수 있게 편다. Get-Content -Raw와 ConvertFrom-Json pipeline이 $e/prepare.json을 $prepare 객체로 만든다.
입력
존재하고 유효한 prepare.json evidence 파일을 받는다.
결과·효과
$prepare.future_artifacts_created와 hashes 등 저장 필드가 메모리에 복원된다.
비유의 한계
파일 내용을 재검증하지 않고 읽을 뿐 SourceHashes를 다시 계산하거나 Prepare를 재실행하지 않는다.
74줄F06-L74 $before=Get-Content -Raw (Join-Path $e 'before-day.json')|ConvertFrom-Json 인덱스 전 계획 무대의 종료·정리표를 보고대 위에 펼친다. Raw file text가 ConvertFrom-Json을 거쳐 $before evidence object가 된다.
입력
$e/before-day.json의 기존 bytes를 받는다.
결과·효과
$before.cleanup 등 BeforePlan manifest 값이 이후 guard에 제공된다.
비유의 한계
before-plan query를 다시 실행하거나 plan_sha256 원본 파일과 대조하지 않는다.
75줄F06-L75 $after=Get-Content -Raw (Join-Path $e 'after-day.json')|ConvertFrom-Json 인덱스 후 계획 무대에서 남긴 정리표 봉투를 열어 보고대에 놓는다. Get-Content→ConvertFrom-Json pipeline이 after-day.json을 $after로 복원한다.
입력
AfterPlan phase가 앞서 쓴 JSON evidence를 받는다.
결과·효과
$after.cleanup과 chosen index 관련 저장값을 메모리에서 접근할 수 있다.
비유의 한계
after-plan.txt의 현재 hash나 실제 DB index 상태를 여기서 확인하지 않는다.
76줄F06-L76 $selectivity=Get-Content -Raw (Join-Path $e 'selectivity-day.json')|ConvertFrom-Json 계좌·거래·원장·hot 분포 숫자가 든 봉투를 다시 열어 요약 표의 재료로 둔다. selectivity-day.json raw JSON이 parsed $selectivity object로 변환된다.
입력
Selectivity phase의 기존 evidence 파일을 받는다.
결과·효과
accounts·transactions·ledger·hot과 cleanup 값을 뒤 body에서 재사용할 수 있다.
비유의 한계
현재 DB count를 재조회하지 않으며 파일이 변조되지 않았다는 hash도 검증하지 않는다.
77줄F06-L77 $benchmark=Get-Content -Raw (Join-Path $e 'benchmark-manifest.json')|ConvertFrom-Json 전·후 서른 번 결과와 선택 인덱스가 적힌 측정 봉투를 보고대에 펼친다. benchmark-manifest.json이 ConvertFrom-Json으로 $benchmark 객체가 된다.
입력
Benchmark phase가 앞서 저장한 manifest를 받는다.
결과·효과
row counts·median·p95·chosen index·cleanup 값이 보고서 조립에 제공된다.
비유의 한계
before/after CSV를 다시 계산하거나 측정 순서를 재현하지 않는다.
78줄F06-L78 if($prepare.future_artifacts_created-ne0-or$before.cleanup-ne1-or$after.cleanup-ne1-or$selectivity.cleanup-ne1-or$benchmark.cleanup-ne1){throw 'W13 predecessor phase/cleanup contract invalid'} 다섯 무대 표 가운데 미리 만든 결과가 있거나 장비 반납 도장이 빠진 표가 하나라도 있으면 보고서를 거절한다. OR로 연결된 saved fields가 expected sentinel과 다르면 predecessor phase/cleanup contract invalid를 throw한다.
입력
$prepare, $before, $after, $selectivity, $benchmark에서 읽은 future_artifacts_created·cleanup 값을 받는다.
결과·효과
모든 sentinel이 맞을 때 benchmark core contract 검사로 진행하고 아니면 perf-manifest를 쓰지 않는다.
비유의 한계
cleanup=1은 저장된 주장일 뿐 Report가 Docker resource 부재를 직접 조회한 증거가 아니다.
79줄F06-L79 if($benchmark.before_rows-ne30-or$benchmark.after_rows-ne30-or$benchmark.chosen_index-cne'idx_ledger_account_created_id'){throw 'W13 benchmark contract invalid'} 측정표에 전 30장·후 30장·세 겹 찾아보기 명찰이 정확히 적혀야 보고 요약을 허용한다. before_rows/after_rows -ne30 또는 chosen_index -cne target name이면 benchmark contract invalid를 throw한다.
입력
77줄에서 읽은 benchmark evidence fields를 받는다.
결과·효과
세 값이 일치하면 ordered report body 구성으로 가고 하나라도 다르면 중단된다.
비유의 한계
median·p95 개선 여부, plan node type, raw CSV row 내용은 이 guard가 재검증하지 않는다.
80줄F06-L80 $body=[ordered]@{ 검증된 다섯 봉투를 한 장의 최종 요약표에 정해진 순서로 옮길 큰 틀을 편다. $body=[ordered]@{가 perf-manifest 필드 순서를 보존하는 ordered hashtable literal을 연다.
입력
78~79줄 saved-evidence guard를 통과한 다섯 parsed 객체를 받는다.
결과·효과
81~86줄의 요약 필드가 하나의 $body 객체에 속하게 된다.
비유의 한계
ordered body 시작은 source evidence를 다시 측정하거나 검증하지 않는다.
81줄F06-L81 schema='perf_w13';query_kind='top-N';accounts=$selectivity.accounts;transactions=$selectivity.transactions;ledger=$selectivity.ledger;hot=$selectivity.hot 요약표 첫 줄에 실험 무대 이름, top-N 표찰, 네 장부 숫자를 그대로 옮겨 적는다. 고정 schema/query_kind와 $selectivity의 네 count를 ordered body fields에 할당한다.
입력
selectivity-day.json에서 복원한 accounts·transactions·ledger·hot 값을 받는다.
결과·효과
perf-manifest가 실험 대상과 synthetic fixture 규모를 표시하게 된다.
비유의 한계
이 복사는 값의 현재성이나 운영 데이터 대표성을 새로 증명하지 않는다.
82줄F06-L82 before_rows=$benchmark.before_rows;after_rows=$benchmark.after_rows 전·후 초시계 표가 각각 몇 장인지 최종 요약표의 두 칸에 베낀다. $benchmark.before_rows와 after_rows가 같은 이름의 $body fields에 저장된다.
입력
79줄에서 30으로 확인된 두 row count를 받는다.
결과·효과
최종 manifest가 before=30, after=30이라는 predecessor 기록을 보존한다.
비유의 한계
CSV 파일의 실제 row 개수를 다시 세지 않아 저장값과 raw evidence의 일치를 직접 증명하지 않는다.
83줄F06-L83 before_median_ms=$benchmark.before_median_ms;before_p95_ms=$benchmark.before_p95_ms;after_median_ms=$benchmark.after_median_ms;after_p95_ms=$benchmark.after_p95_ms 두 무대의 가운데 순위와 95% 순위 초시계 숫자를 네 칸에 나란히 적는다. $benchmark의 before_median_ms, before_p95_ms, after_median_ms, after_p95_ms를 $body에 할당한다.
입력
Benchmark가 Quantile 함수로 저장한 네 시간 값을 받는다.
결과·효과
perf-manifest가 비교할 네 metric을 모두 포함한다.
비유의 한계
Report는 수치를 재계산하지 않으며 median 이름은 source의 nearest-rank 구현 결과를 그대로 가리킨다.
84줄F06-L84 chosen_index=$benchmark.chosen_index;plan_node=$benchmark.plan_node 측정 무대가 찾은 찾아보기 명찰과 연주 방식 표찰을 최종표에 옮긴다. $benchmark.chosen_index와 plan_node가 ordered body의 같은 필드가 된다.
입력
saved Benchmark plan inspection 결과를 받는다.
결과·효과
최종 manifest에 idx_ledger_account_created_id와 node type이 기록될 수 있다.
비유의 한계
현재 PostgreSQL plan을 재실행하지 않고 predicate·cost·buffer 정보도 포함하지 않는다.
85줄F06-L85 verdict='relative improvement observed on this deterministic lab; no absolute latency claim';native_exit=0;cleanup=1;hashes=@($prepare.hashes) 최종표에 ‘이 리허설에서만 상대 개선, 절대 속도 약속 없음’을 크게 적고 정상·반납 도장과 다섯 지문을 붙인다. 고정 verdict, native_exit=0, cleanup=1, @($prepare.hashes)가 report body에 추가된다.
입력
앞 predecessor guard와 prepare.json에서 읽은 hash rows를 받는다.
결과·효과
보고서가 성능 일반화 한계와 저장된 source identity를 명시한다.
비유의 한계
상대 개선 문장은 운영 절대 latency·부하 확장성·재현성을 보장하지 않고 hashes에도 compose가 없다.
86줄F06-L86 phase_evidence=@('prepare.json','before-day.json','after-day.json','selectivity-day.json','benchmark-manifest.json') 요약표 맨 아래에 참고한 다섯 봉투의 이름을 목차처럼 붙인다. phase_evidence가 prepare, before-day, after-day, selectivity-day, benchmark-manifest 이름 배열을 가진다.
입력
Report가 실제로 읽은 다섯 file name 계약을 받는다.
결과·효과
perf-manifest 소비자가 predecessor 파일 목록을 알 수 있다.
비유의 한계
파일 이름만 기록하며 각 predecessor evidence의 bytes·SHA를 report 시점에 고정하지 않는다.
87줄F06-L87 } 최종 요약표의 모든 칸을 채운 뒤 테두리를 닫는다. 닫는 중괄호가 80줄에서 연 ordered hashtable을 완성한다.
입력
81~86줄 필드가 모두 계산된 상태를 받는다.
결과·효과
$body가 Write-JsonAtomic에 넘길 완성 report object가 된다.
비유의 한계
객체 완성은 파일 저장이나 predecessor 재실행을 뜻하지 않는다.
88줄F06-L88 Write-JsonAtomic 'perf-manifest.json' $body 완성한 최종 요약표를 임시 봉투를 거쳐 perf-manifest.json 칸에 넣는다. Write-JsonAtomic이 $body를 $e/perf-manifest.json으로 교체 기록한다.
입력
87줄의 ordered report object와 evidence directory를 받는다.
결과·효과
D7 consumer가 읽을 최종 manifest 파일이 생성된다.
비유의 한계
저장은 query를 재실행하지 않으며 이미 있던 perf-manifest를 -Force로 교체한다.
89줄F06-L89 "W13_REPORT_GREEN phases=5 before=30 after=30 index=$($body.chosen_index) hashes=5 cleanup=1 native_exit=0" 키타가 다섯 증거 봉투와 전후 30장, 선택 명찰, 지문·반납 도장을 한 줄로 발표한다. double-quoted string이 $body.chosen_index를 보간해 W13_REPORT_GREEN marker를 success stream에 보낸다.
입력
perf-manifest.json 저장이 성공하고 $body가 메모리에 남은 상태를 받는다.
결과·효과
caller가 exact report success contract를 stdout에서 확인할 수 있다.
비유의 한계
marker는 metric 값 자체를 표시하지 않으며 Monday–Saturday strict owner marker로 세지 않는다.
90줄F06-L90 exit 0 일요일 요약만 마쳤으므로 DB 장비 무대로 내려가지 않고 정상 퇴장한다. exit 0이 93줄 이후 db-phase lifecycle 실행을 차단한다.
입력
Report marker가 출력된 완료 상태를 받는다.
결과·효과
호출 process는 성공 code로 종료되고 DB container를 새로 만들지 않는다.
비유의 한계
Report 자체가 cleanup을 수행하지 않고 predecessor의 저장된 cleanup=1을 신뢰한다.
91줄F06-L91 } 저장된 결과만 읽는 보고대의 테두리를 닫는다. 닫는 중괄호가 if($Phase-eq'Report') lexical scope를 끝낸다.
입력
Report가 아니어서 72줄을 건너온 흐름을 받는다.
결과·효과
93줄에서 남은 phase가 dbPhases인지 검사하게 된다.
비유의 한계
true Report 경로는 90줄 exit 때문에 정상적으로 이 뒤를 실행하지 않는다.
93줄F06-L93 if($Phase-notin$dbPhases){throw "unsupported W13 phase=$Phase"} 준비도 보고도 아닌 표찰이 실제 장비 무대 네 장에도 없으면 알 수 없는 순서표로 거절한다. $Phase -notin $dbPhases guard가 unsupported W13 phase 메시지를 throw한다.
입력
Prepare·Report 분기를 통과하지 않은 $Phase와 17줄 네 값 배열을 받는다.
결과·효과
허용된 BeforePlan·AfterPlan·Selectivity·Benchmark만 body 초기화로 진행한다.
비유의 한계
ValidateSet이 먼저 막으므로 정상 parameter binding에서는 중복 방어에 가깝고 phase 동작 성공을 보장하지 않는다.
94줄F06-L94 $body=$null 이번 장비 무대가 결과표를 실제로 채웠는지 마지막에 확인하도록 빈 표찰로 시작한다. $body=$null이 db phase 분기 이전 sentinel 상태를 만든다.
입력
지원되는 db phase와 아직 evidence object가 없는 상태를 받는다.
결과·효과
145줄은 phase branch가 $body를 할당하지 못한 경우를 탐지할 수 있다.
비유의 한계
null 초기화는 이전 invocation의 파일을 지우거나 stale 여부를 검사하지 않는다.
95줄F06-L95 try{ 장비를 켜고 SQL을 연주하는 모든 행동을, 성공·실패 뒤 반드시 반납대로 가는 큰 상자 안에 넣는다. try 블록이 96~141줄의 startup·seed·phase logic을 142줄 finally와 연결한다.
입력
supported db phase, null $body, false $owned 초기 상태를 받는다.
결과·효과
본문에서 발생한 terminating error도 finally를 거친 뒤 전파된다.
비유의 한계
try/finally는 실패를 복구하거나 원래 오류와 cleanup 오류를 모두 보존한다고 보장하지 않는다.
96줄F06-L96 if($Mode-eq'Compose'){ 새 장비 대여 방식을 골랐으면 전용 project로 DB를 켜는 절차를 펼친다. $Mode -eq 'Compose' 조건이 true인 경우에만 97~104줄을 실행한다.
입력
ValidateSet을 통과한 $Mode 값을 받는다.
결과·효과
Compose면 script-owned lifecycle을 준비하고 Container면 105줄 요구조건으로 간다.
비유의 한계
문자열 선택만으로 project isolation이나 startup 성공은 보장되지 않는다.
97줄F06-L97 if($Container){throw 'Compose mode rejects -Container'} 새 장비를 빌리면서 기존 장비표도 내면 어느 것을 쓸지 모호하므로 접수를 거절한다. truthy $Container guard가 Compose mode rejects -Container terminating error를 낸다.
입력
Compose mode와 caller-supplied Container 문자열을 받는다.
결과·효과
빈 값일 때 password/setup으로 가고 nonblank 값이면 startup 전에 중단된다.
비유의 한계
공백만 있는 문자열은 PowerShell truthiness상 처리 차이가 있을 수 있으며 여기서는 IsNullOrWhiteSpace를 쓰지 않는다.
98줄F06-L98 if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w13-disposable-password'} 새로 빌릴 DB 장비의 빈 비밀번호 칸에 이 리허설 전용 w13-disposable-password를 채운다. blank FCL_DB_PASSWORD에 w13-disposable-password를 대입해 compose startup interpolation을 준비한다.
입력
Compose mode의 현재 환경변수 값을 받는다.
결과·효과
빈 경우 disposable password가 설정되고 caller의 nonblank 비밀번호는 유지된다.
비유의 한계
하드코딩 값은 폐기형 lab 전용이며 운영 secret 관리나 이미 실행 중인 DB credential을 검증하지 않는다.
99줄F06-L99 # The unique Compose project is ours before startup; a partial `up` 주석표가 ‘이 이름의 대여 장비는 켜기 전부터 우리 반납 책임’이라고 다음 줄의 이유를 알려 준다. 첫 comment line이 partial compose up 실패도 finally 정리 대상으로 삼는 ownership contract를 문서화한다.
입력
$ComposeProject를 이 invocation 전용으로 사용한다는 source 전제를 받는다.
결과·효과
독자는 $owned를 up 이전에 true로 바꾸는 설계를 이해하게 된다.
비유의 한계
주석은 project 이름의 실제 uniqueness를 검사하거나 runtime 동작을 만들지 않는다.
100줄F06-L100 # failure must still be cleaned by finally. 장비가 반쯤만 켜져도 반납대를 반드시 거친다는 안내문 둘째 줄이다. second comment line이 99줄의 설명을 이어 partial resource cleanup 의도를 명시한다.
입력
compose up가 일부 resource 생성 뒤 nonzero로 끝날 수 있는 failure model을 받는다.
결과·효과
101줄 ownership flag 설정의 이유가 source 안에 완결된다.
비유의 한계
comment 자체는 finally 실행이나 cleanup 성공을 보장하지 않는다.
101줄F06-L101 $owned=$true 장비가 아직 완전히 켜지지 않았어도 이 project 반납표를 먼저 자신의 이름으로 돌린다. $owned=$true가 이후 어떤 terminating error에서도 143줄 compose down 조건을 활성화한다.
입력
Compose mode이고 ambiguous Container가 없으며 password 준비가 끝난 상태를 받는다.
결과·효과
finally가 이번 invocation의 project를 정리해야 한다고 판단하게 된다.
비유의 한계
flag는 실제 project가 새것인지 확인하지 않아 이름 충돌 시 다른 resource에 영향을 줄 위험을 제거하지 않는다.
102줄F06-L102 & docker compose -f $compose -p $ComposeProject up -d --wait db 전용 이름으로 DB 장비를 켠 뒤 준비등이 켜질 때까지 대기한다. docker compose -f $compose -p $ComposeProject up -d --wait db를 native command로 실행한다.
입력
compose.yaml, project name, password environment, Docker engine을 받는다.
결과·효과
성공 시 db container가 실행·healthy 상태가 되고 $LASTEXITCODE가 0이 된다.
비유의 한계
startup은 timeout 값을 source에서 고정하지 않고 workload query 성공이나 fixture 상태를 아직 확인하지 않는다.
103줄F06-L103 if($LASTEXITCODE-ne0){throw "compose up failed: phase=$Phase exit=$LASTEXITCODE"} 장비 준비등이 실패 번호를 내면 어느 무대였는지 적어 중단하고 반납대로 이동한다. $LASTEXITCODE guard가 compose up failed terminating error를 발생시킨다.
입력
102줄 native command code와 현재 $Phase를 받는다.
결과·효과
0이면 container ID 조회로 가고 실패면 finally가 owned project 정리를 시도한다.
비유의 한계
성공 code만 검사하며 health 상세나 log 내용은 evidence에 저장하지 않는다.
104줄F06-L104 $Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim() 준비된 DB 장비의 실제 표찰 번호를 찾아 양끝 빈칸 없이 작업표에 적는다. docker compose ps -q db stdout에 Trim()을 적용해 $Container에 대입한다.
입력
성공한 project startup과 db service를 받는다.
결과·효과
$Container가 이후 docker exec 대상 container ID 문자열이 된다.
비유의 한계
이 줄 직후 native exit code를 따로 검사하지 않고 ID 공백 여부만 106줄에서 본다.
105줄F06-L105 }elseif([string]::IsNullOrWhiteSpace($Container)){throw 'Container mode requires -Container'} 기존 장비 사용 방식에서 실제 장비표를 내지 않으면 무엇에 연주할지 몰라 거절한다. Compose가 아닌 분기의 IsNullOrWhiteSpace($Container) guard가 Container mode requires -Container를 throw한다.
입력
$Mode='Container'와 caller-supplied $Container를 받는다.
결과·효과
nonblank container면 공통 resolution guard로 가고 blank면 seed 전에 종료된다.
비유의 한계
문자열 존재만 보며 그 이름의 container가 실행 중인지·PostgreSQL인지 확인하지 않는다.
106줄F06-L106 if([string]::IsNullOrWhiteSpace($Container)){throw 'PostgreSQL container resolution failed'} 새 장비든 기존 장비든 최종 표찰 번호가 빈칸이면 SQL 악보를 보내기 전에 멈춘다. 공통 IsNullOrWhiteSpace guard가 PostgreSQL container resolution failed 예외를 낸다.
입력
104줄 조회 결과 또는 caller의 Container mode 문자열을 받는다.
결과·효과
nonblank target만 seed 실행으로 진행한다.
비유의 한계
nonblank는 container 존재·DB ready·credential 성공을 증명하지 않는다.
107줄F06-L107 PsqlFile 'sql/w13/seed.sql'|Out-Null 매 무대마다 같은 10만 행 장부를 새로 만들고 연주 통로의 일반 출력표는 치운다. PsqlFile 'sql/w13/seed.sql' 호출 결과를 Out-Null로 보내 deterministic perf_w13 fixture를 재생성한다.
입력
해석된 PostgreSQL container와 packaged seed.sql을 받는다.
결과·효과
정상 시 account 1000·business_tx 100000·ledger 100000의 새 schema가 phase 분기 입력이 된다.
비유의 한계
합성 폐기형 fixture이며 운영 분포가 아니고 seed output은 evidence로 보존하지 않는다.
109줄F06-L109 if($Phase-eq'BeforePlan'){ 새 찾아보기를 붙이기 전, 기본 장부에서 top-N 악보가 어떻게 연주되는지 기록하는 무대를 연다. $Phase -eq 'BeforePlan' 조건이 110~114줄을 첫 db phase branch로 선택한다.
입력
방금 seed된 perf_w13 DB와 현재 Phase 문자열을 받는다.
결과·효과
BeforePlan 호출만 plan capture와 before-day body 생성을 수행한다.
비유의 한계
이 분기는 create-index.sql을 실행하지 않지만 PostgreSQL이 다른 기본 index를 쓰지 않는다고 미리 단정하지 않는다.
110줄F06-L110 $path=Join-Path $e 'before-plan.txt' 인덱스 전 실행 계획표를 증거함의 before-plan.txt 칸에 둘 주소를 적는다. Join-Path $e 'before-plan.txt' 결과를 $path에 저장한다.
입력
쓰기 가능한 evidence directory $e를 받는다.
결과·효과
$path가 psql stdout destination과 뒤 SHA 계산 대상이 된다.
비유의 한계
경로 계산은 기존 파일 stale 여부를 다시 검사하거나 원자 쓰기를 제공하지 않는다.
111줄F06-L111 Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $path top-N 악보 전체를 DB 장비에 건네고 돌아온 실행 계획 한 장을 인덱스 전 증거 칸에 바로 적는다. Get-Content -Raw | docker exec -i ... psql -qAt | Set-Content pipeline이 query 결과를 $path에 저장한다.
입력
seed된 $Container, packaged measure.sql, $path를 받는다.
결과·효과
정상 시 EXPLAIN ANALYZE JSON 텍스트가 before-plan.txt에 기록되고 native exit code가 갱신된다.
비유의 한계
Set-Content가 pipeline 끝에 있어 native failure와 파일 작성 상태의 결합이 복잡하며 query는 실제로 실행되지만 latency 일반화를 주지 않는다.
112줄F06-L112 if($LASTEXITCODE-ne0){throw 'before plan query failed'} 계획표를 받는 연주가 실패 번호를 남기면 인덱스 전 무대를 중단한다. $LASTEXITCODE -ne 0 guard가 before plan query failed를 throw한다.
입력
111줄 pipeline 뒤 남은 native exit code를 받는다.
결과·효과
0이면 saved plan parse로 가고 nonzero면 finally cleanup 뒤 실패가 전파된다.
비유의 한계
이 검사는 before-plan.txt가 완전한 JSON인지 확인하지 않으며 pipeline 뒤 cmdlet이 exit code 의미를 바꾸지 않았다는 source 동작에 의존한다.
113줄F06-L113 $plan=(Get-Content -Raw $path|ConvertFrom-Json)[0].Plan 인덱스 전 계획 봉투를 다시 열어 첫 실행 계획 나무만 책상 위에 놓는다. Get-Content -Raw→ConvertFrom-Json 결과의 [0].Plan을 $plan에 대입한다.
입력
111줄에서 쓴 parse 가능한 before-plan.txt를 받는다.
결과·효과
$plan이 top node·actual rows evidence를 만들 객체가 된다.
비유의 한계
첫 array element만 사용하며 plan 안의 index 유무나 모든 node를 이 줄에서 검사하지 않는다.
114줄F06-L114 $body=[ordered]@{phase='BeforePlan';top_node=[string]$plan.'Node Type';actual_rows=[int]$plan.'Actual Rows';plan_sha256=(Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant();native_exit=0;cleanup=1} 계획 나무 맨 위 연주 방식, 실제 나온 행, 원문 지문, 정상·반납 약속을 결과표에 적는다. $plan 속성과 $path SHA-256으로 BeforePlan evidence object를 구성한다.
입력
parsed $plan과 저장된 before-plan.txt를 받는다.
결과·효과
$body가 phase=BeforePlan, node/rows/hash, native_exit=0, cleanup=1 fields를 갖는다.
비유의 한계
cleanup=1은 finally 실행 전에 미리 적힌 기대 sentinel이며 인덱스 부재나 plan 품질을 직접 증명하지 않는다.
115줄F06-L115 }elseif($Phase-eq'AfterPlan'){ 앞 무대가 아니고 찾아보기를 붙인 뒤 계획을 볼 차례면 다음 검수대를 펼친다. elseif가 $Phase -eq 'AfterPlan'인 경우 116~123줄을 선택한다.
입력
BeforePlan 조건이 false이고 seed가 끝난 현재 phase를 받는다.
결과·효과
AfterPlan만 index 생성·plan capture·assert·FindIndex를 수행한다.
비유의 한계
분기 선택은 인덱스가 실제로 생성되거나 쓰인다는 보장이 아니다.
116줄F06-L116 PsqlFile 'sql/w13/create-index.sql'|Out-Null 계좌·최신 시각·큰 번호 순서의 세 겹 찾아보기를 장부에 붙인다. PsqlFile이 sql/w13/create-index.sql을 실행하고 pipeline 결과를 Out-Null로 보낸다.
입력
seed된 perf_w13 DB와 packaged create-index SQL을 받는다.
결과·효과
성공하면 target index와 갱신 통계가 PostgreSQL에 존재한다.
비유의 한계
이 함수 호출만으로 다음 query가 Index Scan을 선택한다고 증명하지 않는다.
117줄F06-L117 $path=Join-Path $e 'after-plan.txt' 찾아보기를 붙인 뒤 실행 계획표를 after-plan.txt 칸에 둘 주소를 정한다. Join-Path가 $e와 after-plan.txt를 결합해 $path에 대입한다.
입력
AfterPlan evidence directory를 받는다.
결과·효과
$path가 다음 pipeline의 출력 파일이 된다.
비유의 한계
기존 파일을 여기서 거부하지 않으며 쓰기 방식은 임시 rename이 아니다.
118줄F06-L118 Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $path 인덱스 전과 같은 top-N 악보를 다시 연주하고 새 계획 나무를 후 증거 칸에 기록한다. Get-Content→docker exec psql→Set-Content pipeline이 query stdout을 $path에 저장한다.
입력
인덱스가 생성된 같은 synthetic fixture와 동일 measure.sql을 받는다.
결과·효과
after-plan.txt에 인덱스 후 EXPLAIN ANALYZE JSON이 생기고 native code가 갱신된다.
비유의 한계
동일 source query이지만 실행 시점·cache 조건을 통제하거나 before와 교차 실행하지 않는다.
119줄F06-L119 if($LASTEXITCODE-ne0){throw 'after plan query failed'} 찾아보기 후 계획 연주가 실패 번호를 돌려주면 검수 전 즉시 멈춘다. $LASTEXITCODE guard가 after plan query failed terminating error를 발생시킨다.
입력
118줄 pipeline 뒤 native exit code를 받는다.
결과·효과
0이면 assert.sql로 진행하고 nonzero면 finally cleanup을 거쳐 실패한다.
비유의 한계
파일 JSON 완전성이나 plan 의미는 아직 확인하지 않는다.
120줄F06-L120 PsqlFile 'sql/w13/assert.sql'|Out-Null 계획을 읽기 전 네 장부 숫자·잔액 합계·찾아보기 명찰이 그대로인지 별도 검수표로 확인한다. PsqlFile sql/w13/assert.sql 실행이 SQL gate를 통과해야 하고 출력은 Out-Null로 버린다.
입력
AfterPlan query 실행 뒤의 perf_w13 tables와 catalog를 받는다.
결과·효과
성공이면 count 1000/100000/100000/90000, mismatch 0, index name 한 건 계약이 유지된다.
비유의 한계
assert SQL은 plan node를 읽지 않아 실제 index 선택을 증명하지 않는다; 그 증거는 121~122줄 FindIndex가 맡는다.
121줄F06-L121 $plan=(Get-Content -Raw $path|ConvertFrom-Json)[0].Plan;$chosen=FindIndex $plan 후 계획 봉투를 열어 나무를 펼친 뒤 첫 찾아보기 명찰을 가지 아래에서 찾는다. 한 줄에서 $plan=(...)[0].Plan을 만든 뒤 FindIndex $plan 결과를 $chosen에 대입한다.
입력
parse 가능한 after-plan.txt와 FindIndex 함수 정의를 받는다.
결과·효과
$plan은 전체 tree, $chosen은 첫 Index Name의 name·type 또는 null을 가진다.
비유의 한계
두 문장이 세미콜론으로 이어져 있으며 첫 발견만 보존하므로 복수 index plan의 전체 구조는 남지 않는다.
122줄F06-L122 if(!$chosen-or$chosen.name-cne'idx_ledger_account_created_id'-or$chosen.type-notin@('Index Scan','Index Only Scan')){throw "after plan index mismatch: $($chosen.name)/$($chosen.type)"} 찾은 명찰이 없거나 이름이 다르거나 연주 방식이 Index Scan 계열이 아니면 실제 두 표찰을 읽고 중단한다. !$chosen OR name -cne target OR type -notin Index Scan/Index Only Scan 조건이 after plan mismatch를 throw한다.
입력
121줄의 null 또는 name/type 객체를 받는다.
결과·효과
통과 시 정확한 index name과 허용 node type 하나가 직접 plan에서 관찰되었다.
비유의 한계
이 검사는 첫 발견 index만 보고 비용·buffer·heap fetch·지연 개선을 증명하지 않는다.
123줄F06-L123 $body=[ordered]@{phase='AfterPlan';chosen_index=$chosen.name;plan_node=$chosen.type;actual_rows=[int]$plan.'Actual Rows';plan_sha256=(Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant();native_exit=0;cleanup=1} 통과한 찾아보기 이름과 연주 방식, 실제 행 수, 계획표 지문, 정상·반납 도장을 결과표에 적는다. $chosen, $plan, $path hash로 AfterPlan evidence object를 구성한다.
입력
122줄 plan selection guard를 통과한 객체와 after-plan.txt를 받는다.
결과·효과
$body가 report와 final file writer가 사용할 AfterPlan fields를 가진다.
비유의 한계
cleanup=1은 finally 성공 전에 설정되고 plan SHA는 파일 identity이지 성능 개선 판정이 아니다.
124줄F06-L124 }elseif($Phase-eq'Selectivity'){ 앞 두 계획 무대가 아니고 hot 계좌 몰림을 셀 차례면 네 숫자 검수대를 펼친다. elseif가 $Phase -eq 'Selectivity'인 경우 125~128줄을 선택한다.
입력
BeforePlan·AfterPlan 조건이 false인 seed 완료 상태를 받는다.
결과·효과
Selectivity만 직접 count query와 exact value gate를 수행한다.
비유의 한계
분기명은 planner selectivity estimate나 운영 분포 대표성을 자동 측정하지 않는다.
125줄F06-L125 $raw=(& docker exec $Container psql -X -qAt -F '|' -v ON_ERROR_STOP=1 -U app -d financial_core -c "SELECT (SELECT count(*) FROM perf_w13.account),(SELECT count(*) FROM perf_w13.business_tx),(SELECT count(*) FROM perf_w13.ledger_entry),(SELECT count(*) FROM perf_w13.ledger_entry WHERE account_id=1)").Trim() 계좌·거래·원장·계좌1 원장 네 장부 수를 한 번에 세어 세로 막대 사이 한 줄 숫자표로 받는다. docker exec psql -qAt -F '|' -c SELECT의 stdout에 Trim()을 적용해 $raw에 저장한다.
입력
seed된 $Container와 perf_w13 네 count 대상 table/조건을 받는다.
결과·효과
$raw가 예상상 1000|100000|100000|90000 형태이고 native code가 갱신된다.
비유의 한계
한 snapshot의 합성 count만 반환하며 amount·plan·latency를 보지 않고 SQL 문자열은 별도 파일 hash 대상이 아니다.
126줄F06-L126 if($LASTEXITCODE-ne0){throw 'selectivity count query failed'} 네 숫자표 조회가 실패 번호를 내면 문자열 분해 전에 무대를 중단한다. $LASTEXITCODE -ne 0 guard가 selectivity count query failed를 throw한다.
입력
125줄 docker/psql 종료 코드를 받는다.
결과·효과
0이면 $raw.Split으로 진행하고 nonzero면 finally cleanup 뒤 실패한다.
비유의 한계
0 code는 field 개수와 숫자 값의 정확성을 보장하지 않아 127줄이 따로 확인한다.
127줄F06-L127 $values=$raw.Split('|');if($values.Count-ne4-or[int]$values[0]-ne1000-or[int]$values[1]-ne100000-or[int]$values[2]-ne100000-or[int]$values[3]-ne90000){throw "selectivity counts mismatch: $raw"} 세로 막대 숫자표를 네 칸으로 잘라 목표 숫자를 하나씩 대조하고 다르면 원문 표를 보여 준다. $raw.Split('|') 결과 Count와 int 변환 네 값을 OR guard로 검사해 mismatch 시 throw한다.
입력
125줄의 raw count 문자열과 고정 expected tuple을 받는다.
결과·효과
통과하면 accounts=1000, transactions=100000, ledger=100000, hot=90000이 직접 확인된다.
비유의 한계
hot_ratio .9를 이 줄에서 나눗셈으로 계산하지 않고 다른 account별 분포도 검사하지 않는다.
128줄F06-L128 $body=[ordered]@{phase='Selectivity';accounts=1000;transactions=100000;ledger=100000;hot=90000;hot_ratio=.9;native_exit=0;cleanup=1} 검증한 네 숫자와 9할 몰림 표찰, 정상·반납 도장을 결과표에 적는다. ordered hashtable이 phase Selectivity와 exact constants를 evidence fields로 만든다.
입력
127줄 exact tuple gate를 통과한 실행 상태를 받는다.
결과·효과
$body가 selectivity-day.json에 기록될 counts와 ratio를 가진다.
비유의 한계
hot_ratio=.9는 합성 fixture의 90000/100000 요약이며 운영 skew나 통계 추정 정확도로 일반화할 수 없다.
129줄F06-L129 }elseif($Phase-eq'Benchmark'){ 앞 세 무대가 아니고 찾아보기 전후 초시계를 비교할 차례면 측정대를 펼친다. elseif가 $Phase -eq 'Benchmark'일 때 130~140줄을 선택한다.
입력
지원되는 마지막 db phase와 seed 완료 상태를 받는다.
결과·효과
Benchmark 호출만 before 측정→index→after plan→after 측정 순서를 수행한다.
비유의 한계
before 30회를 모두 먼저 하고 after 30회를 나중에 하며 warm-up·randomization·interleaving이 없다.
130줄F06-L130 $before=MeasurePlan before (Join-Path $e 'before-raw.csv') 찾아보기 설치 전 같은 곡을 1번부터 30번까지 연속 재고 기록 묶음을 전 CSV 칸에 둔다. MeasurePlan before 호출 결과가 $before에 대입되고 raw output은 $e/before-raw.csv로 지정된다.
입력
seed 직후 아직 target index를 만들지 않은 DB와 MeasurePlan 함수를 받는다.
결과·효과
$before가 30개의 variant=before, sample, ms 객체 배열이 된다.
비유의 한계
첫 실행을 warm-up으로 버리지 않고 cache가 변할 수 있는 순차 측정이며 이것만으로 운영 baseline을 대표하지 않는다.
131줄F06-L131 PsqlFile 'sql/w13/create-index.sql'|Out-Null 전 기록 30장을 모두 적은 다음에야 세 겹 찾아보기를 장부에 붙인다. PsqlFile create-index.sql이 target index를 생성·ANALYZE하고 output은 버린다.
입력
before samples가 끝난 같은 seeded DB를 받는다.
결과·효과
뒤 after plan과 30회 측정이 인덱스가 있는 상태에서 실행된다.
비유의 한계
전후 사이에는 index 생성·ANALYZE·시간 경과가 함께 생겨 인덱스만의 causal effect를 완전히 분리하지 않는다.
132줄F06-L132 $afterPath=Join-Path $e 'benchmark-after-plan.txt' 측정 중 선택 인덱스를 확인할 별도 계획표를 benchmark-after-plan.txt 칸에 둘 주소를 정한다. Join-Path $e 'benchmark-after-plan.txt' 결과를 $afterPath에 저장한다.
입력
Benchmark evidence directory를 받는다.
결과·효과
$afterPath가 다음 query output과 FindIndex 입력 파일이 된다.
비유의 한계
경로 계산은 기존 파일을 거부하거나 임시 파일을 사용하지 않는다.
133줄F06-L133 Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $afterPath 후 초시계를 재기 전 같은 top-N 악보의 계획 나무를 한 번 꺼내 별도 증거표로 저장한다. Get-Content | docker exec psql | Set-Content pipeline이 JSON result를 $afterPath에 기록한다.
입력
target index가 생성된 DB, canonical measure.sql, output path를 받는다.
결과·효과
benchmark-after-plan.txt가 생기고 직전 native exit code가 갱신된다.
비유의 한계
이 단일 plan 실행은 뒤 30회와 별도 추가 실행이며 동일 plan 선택이 매 sample마다 유지된다고 증명하지 않는다.
134줄F06-L134 if($LASTEXITCODE-ne0){throw 'benchmark after plan failed'} 후 계획표를 받는 연주가 실패하면 후 30회 측정을 시작하지 않고 반납대로 간다. $LASTEXITCODE guard가 benchmark after plan failed terminating error를 발생시킨다.
입력
133줄 pipeline의 native 종료값을 받는다.
결과·효과
성공이면 after MeasurePlan으로 가고 실패면 finally cleanup을 수행한다.
비유의 한계
0 code만 보며 saved JSON의 구조와 목표 index 선택은 136~138줄에서 늦게 검사한다.
135줄F06-L135 $after=MeasurePlan after (Join-Path $e 'after-raw.csv');PsqlFile 'sql/w13/assert.sql'|Out-Null 찾아보기 후 같은 곡을 30번 연속 재고 후 기록 묶음을 저장한 다음 장부 숫자와 명찰 검수를 한다. MeasurePlan after 결과를 $after에 대입하고 세미콜론 뒤 PsqlFile assert.sql을 호출해 output을 버린다.
입력
인덱스가 있는 DB, after-raw.csv 경로, canonical assert SQL을 받는다.
결과·효과
$after의 30 rows가 생기고 fixture counts·balance reconciliation·index-name existence가 통과해야 다음으로 간다.
비유의 한계
두 명령은 한 줄이지만 assert는 plan 선택이나 30개 시간 분포를 증명하지 않고 after 측정은 warm-up을 분리하지 않는다.
136줄F06-L136 $plan=(Get-Content -Raw $afterPath|ConvertFrom-Json)[0].Plan;$chosen=FindIndex $plan 측정 전 보관한 계획 나무를 다시 펼쳐 첫 찾아보기 명찰을 재귀 탐색한다. $afterPath JSON의 [0].Plan을 $plan에 넣고 FindIndex 결과를 $chosen에 저장한다.
입력
parse 가능한 benchmark-after-plan.txt와 recursive finder를 받는다.
결과·효과
$chosen이 첫 index name/type 객체 또는 null이 된다.
비유의 한계
after 30개 각 sample의 plan이 아니라 단 한 번 따로 저장한 계획만 검사한다.
137줄F06-L137 $bm=Quantile $before .5;$bp=Quantile $before .95;$am=Quantile $after .5;$ap=Quantile $after .95 전 기록에서 15번째·29번째, 후 기록에서도 15번째·29번째 초시계 값을 각각 뽑는다. Quantile을 네 번 호출해 $bm, $bp, $am, $ap에 before median/p95와 after median/p95를 대입한다.
입력
정확히 30개인 $before·$after row와 Quantile nearest-rank 구현을 받는다.
결과·효과
네 비교 metric이 숫자로 준비되어 다음 index·improvement guard와 evidence body에 쓰인다.
비유의 한계
bm/am은 30개 두 중앙값 평균 15.5 방식이 아니라 각 15번째 값을 고르며 confidence interval이나 변동성 검정은 없다.
138줄F06-L138 if(!$chosen-or$chosen.name-cne'idx_ledger_account_created_id'){throw 'benchmark chosen index mismatch'} 측정 전 계획표에서 세 겹 찾아보기 명찰을 못 찾거나 다른 이름이면 성능 숫자를 합격으로 쓰지 않는다. !$chosen OR chosen.name -cne idx_ledger_account_created_id guard가 chosen index mismatch를 throw한다.
입력
136줄의 first index result와 목표 case-sensitive name을 받는다.
결과·효과
목표 이름이면 relative metric guard로 가고 아니면 finally 후 실패한다.
비유의 한계
AfterPlan과 달리 node type을 Index Scan/Index Only Scan으로 제한하지 않아 이름만 맞는 다른 node type은 통과할 수 있다.
139줄F06-L139 if($am-ge$bm-and$ap-ge$bp){throw "no relative improvement: median=$bm->$am p95=$bp->$ap"} 후 15번째와 29번째 초시계가 둘 다 전 기록보다 작아지지 않았을 때만 빨간 판정을 내린다. $am -ge $bm AND $ap -ge $bp 조건이 true인 경우 네 metric을 포함한 no relative improvement 오류를 낸다.
입력
137줄의 before/after nearest-rank median·p95 네 값을 받는다.
결과·효과
두 after metric 모두 개선되지 않으면 실패하고, 둘 중 하나라도 작아지면 이 guard는 통과한다.
비유의 한계
따라서 median이 악화되어도 p95만 개선되거나 그 반대여도 Benchmark는 통과할 수 있어 두 수치를 함께 봐야 하며 통계적 유의성은 없다.
140줄F06-L140 $body=[ordered]@{phase='Benchmark';before_rows=30;after_rows=30;before_median_ms=$bm;before_p95_ms=$bp;after_median_ms=$am;after_p95_ms=$ap;chosen_index=$chosen.name;plan_node=$chosen.type;native_exit=0;cleanup=1} 통과한 전후 표 수, 네 초시계 순위값, 찾아보기 이름·방식, 정상·반납 도장을 최종 측정표에 적는다. $bm/$bp/$am/$ap와 $chosen으로 benchmark-manifest fields를 구성한다.
입력
index name guard와 one-metric-at-least improvement condition을 통과한 현재 측정값을 받는다.
결과·효과
$body가 before_rows=30, after_rows=30 및 모든 비교 값을 보존한다.
비유의 한계
cleanup=1은 아직 finally 성공 전이고 body의 존재가 절대 latency 목표나 재현 가능한 운영 개선을 뜻하지 않는다.
141줄F06-L141 } 네 실제 장비 무대 중 선택된 결과표 작성을 마치고 분기판을 접는다. 닫는 중괄호가 마지막 elseif body를 종료해 try의 끝으로 제어를 보낸다.
입력
지원 db phase 하나가 정상 실행되어 $body가 채워진 상태를 받는다.
결과·효과
142줄 finally가 성공 경로에서도 반드시 실행된다.
비유의 한계
분기 종료 자체는 evidence 파일을 아직 쓰지 않는다.
142줄F06-L142 }finally{ 연주 결과와 상관없이 장비 반납대로 반드시 이어지는 마지막 문을 연다. }finally{ 구문이 try completion 또는 terminating error를 받아 cleanup scope로 전환한다.
입력
95~141줄의 성공 상태나 예외 상태와 현재 $owned flag를 받는다.
결과·효과
Compose-owned invocation은 143줄 down 시도를 거친 뒤 원래 흐름으로 돌아간다.
비유의 한계
finally는 cleanup 성공을 보장하지 않으며 그 안의 새 throw가 원래 오류 정보를 가릴 수 있다.
143줄F06-L143 if($owned){& docker compose -f $compose -p $ComposeProject down -v|Out-Null;if($LASTEXITCODE-ne0){throw "compose cleanup failed: phase=$Phase"}} 자신이 빌린 장비라면 DB와 저장 상자를 모두 반납하고, 반납표가 실패면 그 phase 이름으로 다시 중단한다. $owned guard 안에서 docker compose down -v output을 버리고 직후 $LASTEXITCODE nonzero를 throw한다.
입력
101줄에서 설정될 수 있는 ownership flag, compose path, project name, 현재 phase를 받는다.
결과·효과
성공 시 script-owned containers/network/volumes가 제거되고 Container mode는 아무 cleanup도 하지 않는다.
비유의 한계
-v는 데이터를 파괴하는 폐기형 lab 동작이며 cleanup throw가 앞 try 예외를 대체할 수 있고 project 이름 충돌 위험은 별도다.
144줄F06-L144 } 장비 반납대를 닫고 성공 결과 또는 실패를 바깥 흐름으로 돌려보낸다. 닫는 중괄호가 finally scope를 끝내며 pending exception이 있으면 전파한다.
입력
143줄 cleanup 시도 결과를 받는다.
결과·효과
정상 cleanup이면 145줄 body sentinel 검사로 진행한다.
비유의 한계
scope 종료가 cleanup=1 evidence field와 실제 Docker 상태를 교차 확인하지 않는다.
145줄F06-L145 if($null-eq$body){throw "W13 phase produced no evidence: $Phase"} 장비를 반납한 뒤 결과표가 빈 종이이면 어떤 phase였는지 적어 초록표 생성을 막는다. $null -eq $body guard가 W13 phase produced no evidence terminating error를 낸다.
입력
finally를 통과한 $body sentinel과 현재 $Phase를 받는다.
결과·효과
non-null일 때만 phase filename lookup과 JSON 저장으로 진행한다.
비유의 한계
객체가 non-null이라는 사실만 보며 필수 field completeness는 branch 작성에 의존한다.
146줄F06-L146 $file=@{BeforePlan='before-day.json';AfterPlan='after-day.json';Selectivity='selectivity-day.json';Benchmark='benchmark-manifest.json'}[$Phase] 전 계획·후 계획·분포·측정 표찰을 각자 정해진 증거함 파일명에 연결한다. hashtable literal을 $Phase로 index해 BeforePlan→before-day.json, AfterPlan→after-day.json, Selectivity→selectivity-day.json, Benchmark→benchmark-manifest.json을 $file에 저장한다.
입력
지원 phase와 non-null evidence $body를 받는다.
결과·효과
$file이 이번 invocation의 정확한 output filename이 된다.
비유의 한계
매핑 밖 key는 null이지만 93줄과 145줄이 정상 흐름에서 선행 방어한다; stale output 교체 여부는 writer가 결정한다.
147줄F06-L147 Write-JsonAtomic $file $body 완성된 결과표를 임시 봉투에 적고 현재 무대에 맞는 증거함 칸으로 옮긴다. Write-JsonAtomic $file $body가 evidence object를 $e 아래 최종 이름으로 기록한다.
입력
146줄 filename과 phase-specific ordered $body를 받는다.
결과·효과
성공 시 Report가 읽을 before-day, after-day, selectivity-day 또는 benchmark-manifest가 생긴다.
비유의 한계
DB cleanup 뒤 파일을 쓰므로 evidence write 실패 시 DB는 이미 제거되며 phase query를 자동 재실행하지 않는다.
148줄F06-L148 "W13_${Phase}_GREEN native_exit=0 cleanup=1 evidence=$file" 키타가 무대명·정상 종료·반납·결과표 파일명을 한 줄 초록 안내로 읽는다. double-quoted W13_${Phase}_GREEN 문자열이 $Phase와 $file을 보간해 success stream에 보낸다.
입력
phase JSON 원자 저장까지 성공한 실행 상태를 받는다.
결과·효과
strict owner가 phase별 native_exit=0 cleanup=1 evidence=<file> marker를 대조할 수 있다.
비유의 한계
marker는 median·p95·plan 상세를 보여 주지 않으며 caller가 exact exit code와 evidence 내용을 함께 확인해야 한다.
측정 순서
  1. 서른 번이면 cache 영향이 자동으로 사라져?

  2. before 30회를 전부 먼저 하고 after 30회를 나중에 한다. warm-up도 섞기도 없다.

  3. 표본 수와 실험 설계는 다른 문제야.

  4. 순차 측정 한계를 결과 옆에 쓰겠습니다.

05

STEP 05 / 13

원본 코드 조각

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

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

F06-C01 · parameters and shared paths1–18줄
1–18줄 원본
param(
 [Parameter(Mandatory=$true)][ValidateSet('Prepare','BeforePlan','AfterPlan','Selectivity','Benchmark','Report')][string]$Phase,
 [ValidateSet('Compose','Container')][string]$Mode='Compose',
 [string]$Container='',
 [string]$ComposeProject='w13-perf-lab',
 [Parameter(Mandatory=$true)][string]$EvidenceDir
)
$ErrorActionPreference='Stop'
$root=Split-Path -Parent $PSScriptRoot
if(-not [IO.Path]::IsPathRooted($EvidenceDir)){
 throw 'EvidenceDir must be an absolute learner-owned path'
}
$e=[IO.Path]::GetFullPath($EvidenceDir)
New-Item -ItemType Directory -Force $e|Out-Null
$compose=Join-Path $root 'compose.yaml'
$owned=$false
$dbPhases=@('BeforePlan','AfterPlan','Selectivity','Benchmark')
F06-C02 · atomic JSON writer19–23줄
19–23줄 원본
function Write-JsonAtomic([string]$Name,$Value){
 $target=Join-Path $e $Name;$temporary="$target.tmp"
 $Value|ConvertTo-Json -Depth 6|Set-Content -Encoding utf8 $temporary
 Move-Item -LiteralPath $temporary -Destination $target -Force
}
F06-C03 · psql file runner24–27줄
24–27줄 원본
function PsqlFile([string]$Relative){
 Get-Content -Raw -LiteralPath (Join-Path $root $Relative)|docker exec -i $Container psql -X -v ON_ERROR_STOP=1 -U app -d financial_core
 if($LASTEXITCODE-ne0){throw "psql file failed: $Relative exit=$LASTEXITCODE"}
}
F06-C04 · 30-sample plan measurement28–38줄
28–38줄 원본
function MeasurePlan([string]$Variant,[string]$Output){
 $rows=@()
 for($sample=1;$sample-le30;$sample++){
  $json=Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core
  if($LASTEXITCODE-ne0){throw "measure failed: phase=$Variant sample=$sample exit=$LASTEXITCODE"}
  $object=$json|ConvertFrom-Json
  $rows+=[pscustomobject]@{variant=$Variant;sample=$sample;ms=[double]$object[0].'Execution Time'}
 }
 $rows|Export-Csv -NoTypeInformation -Encoding utf8 -LiteralPath $Output
 return $rows
}
F06-C05 · nearest-rank Quantile implementation39–43줄
39–43줄 원본
function Quantile($Rows,[double]$P){
 $values=@($Rows.ms|Sort-Object)
 if($values.Count-ne30){throw "quantile requires 30 values, got $($values.Count)"}
 return $values[[Math]::Min(29,[Math]::Ceiling(30*$P)-1)]
}
F06-C06 · recursive plan index finder44–48줄
44–48줄 원본
function FindIndex($Node){
 if($Node.'Index Name'){return [pscustomobject]@{name=[string]$Node.'Index Name';type=[string]$Node.'Node Type'}}
 foreach($child in @($Node.Plans)){$found=FindIndex $child;if($found){return $found}}
 return $null
}
F06-C07 · five-file source hash list49–53줄
49–53줄 원본
function SourceHashes(){
 $files=@('scripts/run-w13-perf.ps1','sql/w13/seed.sql','sql/w13/measure.sql','sql/w13/create-index.sql','sql/w13/assert.sql')
 return @($files|ForEach-Object{[pscustomobject]@{file=$_;sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $root $_)).Hash.ToLowerInvariant()}})
}
F06-C08 · Prepare phase54–71줄
54–71줄 원본
if($Phase-eq'Prepare'){
 if(!(Test-Path -LiteralPath $compose -PathType Leaf)){throw 'packaged compose.yaml missing'}
 $futureFiles=@('before-day.json','before-plan.txt','after-day.json','after-plan.txt','selectivity-day.json','benchmark-manifest.json','before-raw.csv','after-raw.csv','benchmark-after-plan.txt','perf-manifest.json')
 $stale=@($futureFiles|Where-Object{Test-Path -LiteralPath (Join-Path $e $_) -PathType Leaf})
 if($stale.Count-ne0){throw "stale W13 D2-D6 artifact exists before Prepare: $($stale -join ',')"}
 if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w13-prepare-config-only'}
 $docker=& docker version --format '{{.Server.Version}}'
 if($LASTEXITCODE-ne0-or[string]::IsNullOrWhiteSpace($docker)){throw 'Docker engine is unavailable'}
 $services=@(& docker compose -f $compose config --services)
 if($LASTEXITCODE-ne0-or$services.Count-ne1-or$services[0]-cne'db'){throw "compose service contract mismatch: $services"}
 $futureCount=@($futureFiles|Where-Object{Test-Path -LiteralPath (Join-Path $e $_) -PathType Leaf}).Count
 if($futureCount-ne0){throw "Prepare created a future W13 artifact: count=$futureCount"}
 $body=[ordered]@{phase='Prepare';docker_server=[string]$docker;compose_services=@($services);hashes=SourceHashes;native_exit=0;future_artifacts_created=$futureCount}
 Write-JsonAtomic 'prepare.json' $body
 'W13_PREPARE_GREEN compose=db hashes=5 future_artifacts=0 native_exit=0'
 exit 0
}
F06-C09 · Report phase shared source, excluded from strict scope72–92줄
72–92줄 원본
if($Phase-eq'Report'){
 $prepare=Get-Content -Raw (Join-Path $e 'prepare.json')|ConvertFrom-Json
 $before=Get-Content -Raw (Join-Path $e 'before-day.json')|ConvertFrom-Json
 $after=Get-Content -Raw (Join-Path $e 'after-day.json')|ConvertFrom-Json
 $selectivity=Get-Content -Raw (Join-Path $e 'selectivity-day.json')|ConvertFrom-Json
 $benchmark=Get-Content -Raw (Join-Path $e 'benchmark-manifest.json')|ConvertFrom-Json
 if($prepare.future_artifacts_created-ne0-or$before.cleanup-ne1-or$after.cleanup-ne1-or$selectivity.cleanup-ne1-or$benchmark.cleanup-ne1){throw 'W13 predecessor phase/cleanup contract invalid'}
 if($benchmark.before_rows-ne30-or$benchmark.after_rows-ne30-or$benchmark.chosen_index-cne'idx_ledger_account_created_id'){throw 'W13 benchmark contract invalid'}
 $body=[ordered]@{
  schema='perf_w13';query_kind='top-N';accounts=$selectivity.accounts;transactions=$selectivity.transactions;ledger=$selectivity.ledger;hot=$selectivity.hot
  before_rows=$benchmark.before_rows;after_rows=$benchmark.after_rows
  before_median_ms=$benchmark.before_median_ms;before_p95_ms=$benchmark.before_p95_ms;after_median_ms=$benchmark.after_median_ms;after_p95_ms=$benchmark.after_p95_ms
  chosen_index=$benchmark.chosen_index;plan_node=$benchmark.plan_node
  verdict='relative improvement observed on this deterministic lab; no absolute latency claim';native_exit=0;cleanup=1;hashes=@($prepare.hashes)
  phase_evidence=@('prepare.json','before-day.json','after-day.json','selectivity-day.json','benchmark-manifest.json')
 }
 Write-JsonAtomic 'perf-manifest.json' $body
 "W13_REPORT_GREEN phases=5 before=30 after=30 index=$($body.chosen_index) hashes=5 cleanup=1 native_exit=0"
 exit 0
}
F06-C10 · database phase validation and disposable startup93–108줄
93–108줄 원본
if($Phase-notin$dbPhases){throw "unsupported W13 phase=$Phase"}
$body=$null
try{
 if($Mode-eq'Compose'){
  if($Container){throw 'Compose mode rejects -Container'}
  if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w13-disposable-password'}
  # The unique Compose project is ours before startup; a partial `up`
  # failure must still be cleaned by finally.
  $owned=$true
  & docker compose -f $compose -p $ComposeProject up -d --wait db
  if($LASTEXITCODE-ne0){throw "compose up failed: phase=$Phase exit=$LASTEXITCODE"}
  $Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()
 }elseif([string]::IsNullOrWhiteSpace($Container)){throw 'Container mode requires -Container'}
 if([string]::IsNullOrWhiteSpace($Container)){throw 'PostgreSQL container resolution failed'}
 PsqlFile 'sql/w13/seed.sql'|Out-Null
F06-C11 · BeforePlan phase109–114줄
109–114줄 원본
 if($Phase-eq'BeforePlan'){
  $path=Join-Path $e 'before-plan.txt'
  Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $path
  if($LASTEXITCODE-ne0){throw 'before plan query failed'}
  $plan=(Get-Content -Raw $path|ConvertFrom-Json)[0].Plan
  $body=[ordered]@{phase='BeforePlan';top_node=[string]$plan.'Node Type';actual_rows=[int]$plan.'Actual Rows';plan_sha256=(Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant();native_exit=0;cleanup=1}
F06-C12 · AfterPlan phase115–123줄
115–123줄 원본
 }elseif($Phase-eq'AfterPlan'){
  PsqlFile 'sql/w13/create-index.sql'|Out-Null
  $path=Join-Path $e 'after-plan.txt'
  Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $path
  if($LASTEXITCODE-ne0){throw 'after plan query failed'}
  PsqlFile 'sql/w13/assert.sql'|Out-Null
  $plan=(Get-Content -Raw $path|ConvertFrom-Json)[0].Plan;$chosen=FindIndex $plan
  if(!$chosen-or$chosen.name-cne'idx_ledger_account_created_id'-or$chosen.type-notin@('Index Scan','Index Only Scan')){throw "after plan index mismatch: $($chosen.name)/$($chosen.type)"}
  $body=[ordered]@{phase='AfterPlan';chosen_index=$chosen.name;plan_node=$chosen.type;actual_rows=[int]$plan.'Actual Rows';plan_sha256=(Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant();native_exit=0;cleanup=1}
F06-C13 · Selectivity phase124–128줄
124–128줄 원본
 }elseif($Phase-eq'Selectivity'){
  $raw=(& docker exec $Container psql -X -qAt -F '|' -v ON_ERROR_STOP=1 -U app -d financial_core -c "SELECT (SELECT count(*) FROM perf_w13.account),(SELECT count(*) FROM perf_w13.business_tx),(SELECT count(*) FROM perf_w13.ledger_entry),(SELECT count(*) FROM perf_w13.ledger_entry WHERE account_id=1)").Trim()
  if($LASTEXITCODE-ne0){throw 'selectivity count query failed'}
  $values=$raw.Split('|');if($values.Count-ne4-or[int]$values[0]-ne1000-or[int]$values[1]-ne100000-or[int]$values[2]-ne100000-or[int]$values[3]-ne90000){throw "selectivity counts mismatch: $raw"}
  $body=[ordered]@{phase='Selectivity';accounts=1000;transactions=100000;ledger=100000;hot=90000;hot_ratio=.9;native_exit=0;cleanup=1}
F06-C14 · Benchmark phase129–141줄
129–141줄 원본
 }elseif($Phase-eq'Benchmark'){
  $before=MeasurePlan before (Join-Path $e 'before-raw.csv')
  PsqlFile 'sql/w13/create-index.sql'|Out-Null
  $afterPath=Join-Path $e 'benchmark-after-plan.txt'
  Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $afterPath
  if($LASTEXITCODE-ne0){throw 'benchmark after plan failed'}
  $after=MeasurePlan after (Join-Path $e 'after-raw.csv');PsqlFile 'sql/w13/assert.sql'|Out-Null
  $plan=(Get-Content -Raw $afterPath|ConvertFrom-Json)[0].Plan;$chosen=FindIndex $plan
  $bm=Quantile $before .5;$bp=Quantile $before .95;$am=Quantile $after .5;$ap=Quantile $after .95
  if(!$chosen-or$chosen.name-cne'idx_ledger_account_created_id'){throw 'benchmark chosen index mismatch'}
  if($am-ge$bm-and$ap-ge$bp){throw "no relative improvement: median=$bm->$am p95=$bp->$ap"}
  $body=[ordered]@{phase='Benchmark';before_rows=30;after_rows=30;before_median_ms=$bm;before_p95_ms=$bp;after_median_ms=$am;after_p95_ms=$ap;chosen_index=$chosen.name;plan_node=$chosen.type;native_exit=0;cleanup=1}
 }
F06-C15 · finally cleanup142–144줄
142–144줄 원본
}finally{
 if($owned){& docker compose -f $compose -p $ComposeProject down -v|Out-Null;if($LASTEXITCODE-ne0){throw "compose cleanup failed: phase=$Phase"}}
}
F06-C16 · phase evidence file and Green marker145–148줄
145–148줄 원본
if($null-eq$body){throw "W13 phase produced no evidence: $Phase"}
$file=@{BeforePlan='before-day.json';AfterPlan='after-day.json';Selectivity='selectivity-day.json';Benchmark='benchmark-manifest.json'}[$Phase]
Write-JsonAtomic $file $body
"W13_${Phase}_GREEN native_exit=0 cleanup=1 evidence=$file"
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 143 / 143

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

준비·설명 줄 17개도 번역해서 보기
원본한국어 번역
1param(run-w13-perf 스크립트의 parameter 블록을 연다.
2 [Parameter(Mandatory=$true)][ValidateSet('Prepare','BeforePlan','AfterPlan','Selectivity','Benchmark','Report')][string]$Phase,필수 Phase를 여섯 문자열 중 하나로 제한해 받는다.
3 [ValidateSet('Compose','Container')][string]$Mode='Compose',Mode는 Compose 또는 Container만 받고 기본값은 Compose로 둔다.
4 [string]$Container='',외부 컨테이너 ID나 이름을 받을 Container 문자열을 빈 값으로 시작한다.
5 [string]$ComposeProject='w13-perf-lab',Compose project 이름을 받고 기본값을 w13-perf-lab으로 둔다.
6 [Parameter(Mandatory=$true)][string]$EvidenceDir증거 디렉터리 경로를 필수 문자열로 받는다.
7)script parameter 목록을 닫는다.
8$ErrorActionPreference='Stop'PowerShell 오류 기본 동작을 Stop으로 바꾼다.
9$root=Split-Path -Parent $PSScriptRoot스크립트 폴더의 부모를 repository root로 계산한다.
10if(-not [IO.Path]::IsPathRooted($EvidenceDir)){EvidenceDir에 root가 없으면 실패 분기를 연다.
11 throw 'EvidenceDir must be an absolute learner-owned path'root 없는 EvidenceDir 호출을 absolute learner-owned path라는 오류 문구로 중단한다.
12}EvidenceDir root 유무 guard의 본문을 닫는다.
13$e=[IO.Path]::GetFullPath($EvidenceDir)EvidenceDir를 정규화된 전체 경로 e로 바꾼다.
14New-Item -ItemType Directory -Force $e|Out-Null증거 디렉터리를 필요하면 만들고 생성 출력은 버린다.
15$compose=Join-Path $root 'compose.yaml'repository root 아래 compose.yaml 경로를 compose 변수에 만든다.
16$owned=$false현재 실행이 Compose DB를 소유하지 않았다고 초기화한다.
17$dbPhases=@('BeforePlan','AfterPlan','Selectivity','Benchmark')DB를 필요로 하는 네 phase 이름을 배열로 묶는다.
원본한국어 번역
19function Write-JsonAtomic([string]$Name,$Value){이름과 값을 받아 JSON을 원자 교체하는 Write-JsonAtomic 함수를 연다.
20 $target=Join-Path $e $Name;$temporary="$target.tmp"최종 target과 같은 경로의 .tmp 임시 파일 이름을 계산한다.
21 $Value|ConvertTo-Json -Depth 6|Set-Content -Encoding utf8 $temporaryValue를 깊이 6 JSON으로 바꿔 UTF-8 임시 파일에 쓴다.
22 Move-Item -LiteralPath $temporary -Destination $target -Force임시 JSON 파일을 최종 target으로 강제 이동한다.
23}Write-JsonAtomic 함수 본문을 닫는다.
24function PsqlFile([string]$Relative){repository 상대 SQL 파일을 psql로 실행하는 PsqlFile 함수를 연다.
25 Get-Content -Raw -LiteralPath (Join-Path $root $Relative)|docker exec -i $Container psql -X -v ON_ERROR_STOP=1 -U app -d financial_core상대 SQL 원문을 읽어 지정 container의 financial_core psql 표준입력으로 보낸다.
26 if($LASTEXITCODE-ne0){throw "psql file failed: $Relative exit=$LASTEXITCODE"}바로 앞 psql의 종료값이 0이 아니면 상대 경로와 code를 넣어 실패한다.
27}PsqlFile 함수 본문을 닫는다.
28function MeasurePlan([string]$Variant,[string]$Output){variant와 CSV 출력 경로를 받아 계획을 30번 재는 MeasurePlan 함수를 연다.
29 $rows=@()측정 row를 모을 빈 배열을 만든다.
30 for($sample=1;$sample-le30;$sample++){sample을 1부터 30까지 하나씩 늘리는 반복을 연다.
31 $json=Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_coremeasure.sql을 psql로 실행해 JSON plan 문자열을 json 변수에 받는다.
32 if($LASTEXITCODE-ne0){throw "measure failed: phase=$Variant sample=$sample exit=$LASTEXITCODE"}측정 psql 종료값이 비zero면 variant·sample·exit를 포함해 실패한다.
33 $object=$json|ConvertFrom-JsonJSON 측정 문자열을 PowerShell 객체로 바꾼다.
34 $rows+=[pscustomobject]@{variant=$Variant;sample=$sample;ms=[double]$object[0].'Execution Time'}variant, sample, Execution Time ms를 가진 custom row를 배열에 추가한다.
35 }30회 sample 반복 본문을 닫는다.
36 $rows|Export-Csv -NoTypeInformation -Encoding utf8 -LiteralPath $Output수집 row를 header가 있는 UTF-8 CSV로 Output 경로에 쓴다.
37 return $rows수집한 row 배열을 caller에게 반환한다.
38}MeasurePlan 함수 본문을 닫는다.
39function Quantile($Rows,[double]$P){30개 row와 확률 P를 받아 분위수를 구하는 Quantile 함수를 연다.
40 $values=@($Rows.ms|Sort-Object)Rows의 ms 값만 오름차순 정렬해 배열로 고정한다.
41 if($values.Count-ne30){throw "quantile requires 30 values, got $($values.Count)"}정렬값 수가 정확히 30이 아니면 실제 개수를 넣어 실패한다.
42 return $values[[Math]::Min(29,[Math]::Ceiling(30*$P)-1)]ceil(30×P)-1을 최대 29로 막은 배열 원소를 반환한다.
43}Quantile 함수 본문을 닫는다.
44function FindIndex($Node){plan node에서 사용 인덱스를 재귀 탐색하는 FindIndex 함수를 연다.
45 if($Node.'Index Name'){return [pscustomobject]@{name=[string]$Node.'Index Name';type=[string]$Node.'Node Type'}}현재 node에 Index Name이 있으면 name과 Node Type 객체를 즉시 반환한다.
46 foreach($child in @($Node.Plans)){$found=FindIndex $child;if($found){return $found}}Plans 자식들을 배열로 돌며 재귀 결과가 나오면 첫 값을 반환한다.
47 return $null현재 subtree에 인덱스가 없으면 null을 반환한다.
48}FindIndex 함수 본문을 닫는다.
49function SourceHashes(){source 파일 SHA 목록을 만드는 SourceHashes 함수를 연다.
50 $files=@('scripts/run-w13-perf.ps1','sql/w13/seed.sql','sql/w13/measure.sql','sql/w13/create-index.sql','sql/w13/assert.sql')runner와 W13 SQL 네 개의 상대 경로를 정확히 다섯 항목 배열로 만든다.
51 return @($files|ForEach-Object{[pscustomobject]@{file=$_;sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $root $_)).Hash.ToLowerInvariant()}})다섯 파일마다 소문자 SHA-256을 계산한 file·sha256 객체 배열을 반환한다.
52}SourceHashes 함수 본문을 닫는다.
54if($Phase-eq'Prepare'){Phase가 Prepare일 때 준비 전용 분기를 연다.
55 if(!(Test-Path -LiteralPath $compose -PathType Leaf)){throw 'packaged compose.yaml missing'}packaged compose.yaml이 일반 파일이 아니면 즉시 실패한다.
56 $futureFiles=@('before-day.json','before-plan.txt','after-day.json','after-plan.txt','selectivity-day.json','benchmark-manifest.json','before-raw.csv','after-raw.csv','benchmark-after-plan.txt','perf-manifest.json')Prepare 전에 없어야 할 D2~D7 미래 증거 파일 열 개를 배열로 고정한다.
57 $stale=@($futureFiles|Where-Object{Test-Path -LiteralPath (Join-Path $e $_) -PathType Leaf})열 개 이름 중 EvidenceDir에 이미 있는 일반 파일만 stale 배열로 모은다.
58 if($stale.Count-ne0){throw "stale W13 D2-D6 artifact exists before Prepare: $($stale -join ',')"}stale 파일이 하나라도 있으면 이름들을 쉼표로 묶어 실패한다.
59 if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w13-prepare-config-only'}DB password 환경변수가 비었으면 Prepare config 전용 기본 문자열을 넣는다.
60 $docker=& docker version --format '{{.Server.Version}}'Docker server version 문자열을 native command로 읽는다.
61 if($LASTEXITCODE-ne0-or[string]::IsNullOrWhiteSpace($docker)){throw 'Docker engine is unavailable'}Docker command가 실패했거나 version이 비면 engine unavailable로 실패한다.
62 $services=@(& docker compose -f $compose config --services)compose config가 선언한 service 이름들을 배열로 읽는다.
63 if($LASTEXITCODE-ne0-or$services.Count-ne1-or$services[0]-cne'db'){throw "compose service contract mismatch: $services"}compose config가 성공하고 service가 대소문자까지 db 하나인지 강제한다.
64 $futureCount=@($futureFiles|Where-Object{Test-Path -LiteralPath (Join-Path $e $_) -PathType Leaf}).CountPrepare 검사 중 미래 파일이 새로 생겼는지 같은 목록을 다시 세어 futureCount에 넣는다.
65 if($futureCount-ne0){throw "Prepare created a future W13 artifact: count=$futureCount"}futureCount가 0이 아니면 Prepare가 미래 증거를 만들었다고 실패한다.
66 $body=[ordered]@{phase='Prepare';docker_server=[string]$docker;compose_services=@($services);hashes=SourceHashes;native_exit=0;future_artifacts_created=$futureCount}Prepare phase·Docker version·db service·다섯 hash·exit 0·future 0을 ordered body에 담는다.
67 Write-JsonAtomic 'prepare.json' $bodyPrepare body를 prepare.json으로 원자 교체 저장한다.
68 'W13_PREPARE_GREEN compose=db hashes=5 future_artifacts=0 native_exit=0'Prepare 성공 계약을 고정 marker 한 줄로 출력한다.
69 exit 0Prepare 성공 후 process를 exit code 0으로 끝낸다.
70}Prepare 조건 블록을 닫는다.
72if($Phase-eq'Report'){Phase가 Report일 때 D7 보고 분기를 연다.
73 $prepare=Get-Content -Raw (Join-Path $e 'prepare.json')|ConvertFrom-Json저장된 prepare.json을 읽어 JSON 객체 prepare로 바꾼다.
74 $before=Get-Content -Raw (Join-Path $e 'before-day.json')|ConvertFrom-Json저장된 before-day.json을 before 객체로 읽는다.
75 $after=Get-Content -Raw (Join-Path $e 'after-day.json')|ConvertFrom-Json저장된 after-day.json을 after 객체로 읽는다.
76 $selectivity=Get-Content -Raw (Join-Path $e 'selectivity-day.json')|ConvertFrom-Json저장된 selectivity-day.json을 selectivity 객체로 읽는다.
77 $benchmark=Get-Content -Raw (Join-Path $e 'benchmark-manifest.json')|ConvertFrom-Json저장된 benchmark-manifest.json을 benchmark 객체로 읽는다.
78 if($prepare.future_artifacts_created-ne0-or$before.cleanup-ne1-or$after.cleanup-ne1-or$selectivity.cleanup-ne1-or$benchmark.cleanup-ne1){throw 'W13 predecessor phase/cleanup contract invalid'}Prepare future=0과 네 predecessor cleanup=1 중 하나라도 아니면 실패한다.
79 if($benchmark.before_rows-ne30-or$benchmark.after_rows-ne30-or$benchmark.chosen_index-cne'idx_ledger_account_created_id'){throw 'W13 benchmark contract invalid'}benchmark의 before/after row가 각각 30이고 index 이름이 정확한지 검사한다.
80 $body=[ordered]@{Report의 ordered body 작성을 시작한다.
81 schema='perf_w13';query_kind='top-N';accounts=$selectivity.accounts;transactions=$selectivity.transactions;ledger=$selectivity.ledger;hot=$selectivity.hotschema·top-N 종류와 selectivity의 계좌·거래·원장·hot 수를 report에 복사한다.
82 before_rows=$benchmark.before_rows;after_rows=$benchmark.after_rowsbefore_rows와 after_rows를 benchmark 저장값에서 report로 옮긴다.
83 before_median_ms=$benchmark.before_median_ms;before_p95_ms=$benchmark.before_p95_ms;after_median_ms=$benchmark.after_median_ms;after_p95_ms=$benchmark.after_p95_ms전·후 median과 p95 네 값을 benchmark에서 report로 복사한다.
84 chosen_index=$benchmark.chosen_index;plan_node=$benchmark.plan_node선택 인덱스 이름과 plan node type을 benchmark에서 report로 복사한다.
85 verdict='relative improvement observed on this deterministic lab; no absolute latency claim';native_exit=0;cleanup=1;hashes=@($prepare.hashes)합성 lab의 상대 개선만 관찰했다는 verdict와 exit·cleanup·prepare hashes를 담는다.
86 phase_evidence=@('prepare.json','before-day.json','after-day.json','selectivity-day.json','benchmark-manifest.json')보고서가 근거로 삼은 phase evidence 파일 다섯 이름을 배열로 적는다.
87 }Report ordered body literal을 닫는다.
88 Write-JsonAtomic 'perf-manifest.json' $bodyReport body를 perf-manifest.json으로 원자 저장한다.
89 "W13_REPORT_GREEN phases=5 before=30 after=30 index=$($body.chosen_index) hashes=5 cleanup=1 native_exit=0"Report 성공 marker에 phase 5개·30/30·index·hash 5·cleanup 1을 출력한다.
90 exit 0Report 성공 후 process를 exit code 0으로 끝낸다.
91}Report 조건 블록을 닫는다.
93if($Phase-notin$dbPhases){throw "unsupported W13 phase=$Phase"}Phase가 네 db phase에 없으면 unsupported 오류로 실패한다.
94$body=$nullphase evidence body를 null로 초기화한다.
95try{DB 실행 본문을 try로 열어 finally cleanup과 묶는다.
96 if($Mode-eq'Compose'){Mode가 Compose일 때 일회용 DB startup 분기를 연다.
97 if($Container){throw 'Compose mode rejects -Container'}Compose mode에서 Container 인수가 비어 있지 않으면 실패한다.
98 if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w13-disposable-password'}DB password가 비었으면 일회용 접속용 기본 문자열을 process 환경에 넣는다.
99 # The unique Compose project is ours before startup; a partial `up`고유 Compose project는 startup 전부터 이 script 소유라는 cleanup 의도를 설명한다.
100 # failure must still be cleaned by finally.부분 up 실패도 finally에서 정리해야 한다는 주석을 마친다.
101 $owned=$truecompose up 전에 owned를 true로 바꾼다.
102 & docker compose -f $compose -p $ComposeProject up -d --wait dbcompose project의 db service를 detached로 시작하고 health까지 기다린다.
103 if($LASTEXITCODE-ne0){throw "compose up failed: phase=$Phase exit=$LASTEXITCODE"}compose up 종료값이 nonzero면 phase와 exit를 넣어 실패한다.
104 $Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()Compose db service의 container ID를 조회해 공백을 제거한다.
105 }elseif([string]::IsNullOrWhiteSpace($Container)){throw 'Container mode requires -Container'}Container mode인데 Container가 비어 있으면 필수 인수 오류를 낸다.
106 if([string]::IsNullOrWhiteSpace($Container)){throw 'PostgreSQL container resolution failed'}두 mode를 거친 Container 값이 여전히 비면 resolution 실패로 중단한다.
107 PsqlFile 'sql/w13/seed.sql'|Out-Null모든 db phase 시작에 seed.sql을 실행하고 출력은 버린다.
109 if($Phase-eq'BeforePlan'){Phase가 BeforePlan이면 인덱스 생성 전 계획 증거 분기를 연다.
110 $path=Join-Path $e 'before-plan.txt'before plan JSON 원문을 저장할 evidence 경로를 만든다.
111 Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $pathmeasure.sql을 psql로 실행하고 JSON stdout을 UTF-8 before-plan.txt에 보낸다.
112 if($LASTEXITCODE-ne0){throw 'before plan query failed'}before plan native 실행이 실패했으면 오류를 낸다.
113 $plan=(Get-Content -Raw $path|ConvertFrom-Json)[0].Plan저장된 before plan JSON의 첫 원소 Plan을 꺼낸다.
114 $body=[ordered]@{phase='BeforePlan';top_node=[string]$plan.'Node Type';actual_rows=[int]$plan.'Actual Rows';plan_sha256=(Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant();native_exit=0;cleanup=1}BeforePlan top node·actual rows·plan SHA·exit 0·cleanup 1을 ordered body에 담는다.
115 }elseif($Phase-eq'AfterPlan'){아니고 Phase가 AfterPlan이면 인덱스 후 계획 분기를 연다.
116 PsqlFile 'sql/w13/create-index.sql'|Out-Nullcreate-index.sql을 실행해 복합 인덱스를 만들고 일반 출력은 버린다.
117 $path=Join-Path $e 'after-plan.txt'after plan JSON을 저장할 evidence 경로를 만든다.
118 Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $path같은 measure.sql을 실행해 JSON plan을 after-plan.txt에 쓴다.
119 if($LASTEXITCODE-ne0){throw 'after plan query failed'}after plan native 실행이 실패하면 오류를 낸다.
120 PsqlFile 'sql/w13/assert.sql'|Out-Nullassert.sql로 fixture count·잔액 대사·인덱스 이름 존재를 검사한다.
121 $plan=(Get-Content -Raw $path|ConvertFrom-Json)[0].Plan;$chosen=FindIndex $planafter-plan JSON에서 Plan을 읽고 첫 인덱스 node를 재귀 탐색한다.
122 if(!$chosen-or$chosen.name-cne'idx_ledger_account_created_id'-or$chosen.type-notin@('Index Scan','Index Only Scan')){throw "after plan index mismatch: $($chosen.name)/$($chosen.type)"}선택 index 이름과 node type이 목표 계약이 아니면 상세값으로 실패한다.
123 $body=[ordered]@{phase='AfterPlan';chosen_index=$chosen.name;plan_node=$chosen.type;actual_rows=[int]$plan.'Actual Rows';plan_sha256=(Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant();native_exit=0;cleanup=1}AfterPlan 선택 index·node·rows·plan SHA·exit·cleanup을 ordered body에 담는다.
124 }elseif($Phase-eq'Selectivity'){아니고 Phase가 Selectivity이면 합성 분포 검사 분기를 연다.
125 $raw=(& docker exec $Container psql -X -qAt -F '|' -v ON_ERROR_STOP=1 -U app -d financial_core -c "SELECT (SELECT count(*) FROM perf_w13.account),(SELECT count(*) FROM perf_w13.business_tx),(SELECT count(*) FROM perf_w13.ledger_entry),(SELECT count(*) FROM perf_w13.ledger_entry WHERE account_id=1)").Trim()네 count를 한 SQL로 조회해 | 구분 raw 문자열로 받고 양끝 공백을 제거한다.
126 if($LASTEXITCODE-ne0){throw 'selectivity count query failed'}selectivity count native 실행이 실패하면 오류를 낸다.
127 $values=$raw.Split('|');if($values.Count-ne4-or[int]$values[0]-ne1000-or[int]$values[1]-ne100000-or[int]$values[2]-ne100000-or[int]$values[3]-ne90000){throw "selectivity counts mismatch: $raw"}raw를 네 칸으로 나누고 1000·100000·100000·90000과 정확히 대조한다.
128 $body=[ordered]@{phase='Selectivity';accounts=1000;transactions=100000;ledger=100000;hot=90000;hot_ratio=.9;native_exit=0;cleanup=1}Selectivity 고정 count와 hot_ratio .9, exit·cleanup을 ordered body에 담는다.
129 }elseif($Phase-eq'Benchmark'){아니고 Phase가 Benchmark이면 전후 30회 비교 분기를 연다.
130 $before=MeasurePlan before (Join-Path $e 'before-raw.csv')인덱스 전 계획을 30번 재고 before-raw.csv에 쓰며 rows를 받는다.
131 PsqlFile 'sql/w13/create-index.sql'|Out-Nullbefore 측정 뒤 create-index.sql을 실행한다.
132 $afterPath=Join-Path $e 'benchmark-after-plan.txt'benchmark용 after plan JSON 출력 경로를 만든다.
133 Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $afterPath인덱스 후 measure.sql 계획을 실행해 benchmark after plan 파일에 쓴다.
134 if($LASTEXITCODE-ne0){throw 'benchmark after plan failed'}benchmark after plan native 실행이 실패하면 오류를 낸다.
135 $after=MeasurePlan after (Join-Path $e 'after-raw.csv');PsqlFile 'sql/w13/assert.sql'|Out-Null인덱스 후 계획을 30번 재고 after CSV를 만든 뒤 assert.sql을 실행한다.
136 $plan=(Get-Content -Raw $afterPath|ConvertFrom-Json)[0].Plan;$chosen=FindIndex $plan저장한 benchmark plan을 읽고 첫 인덱스 node를 찾는다.
137 $bm=Quantile $before .5;$bp=Quantile $before .95;$am=Quantile $after .5;$ap=Quantile $after .95before/after 각각 .5와 .95 nearest-rank 값을 네 변수로 계산한다.
138 if(!$chosen-or$chosen.name-cne'idx_ledger_account_created_id'){throw 'benchmark chosen index mismatch'}선택 인덱스가 없거나 목표 이름이 아니면 benchmark를 실패시킨다.
139 if($am-ge$bm-and$ap-ge$bp){throw "no relative improvement: median=$bm->$am p95=$bp->$ap"}after median과 p95가 각각 before 이상일 때만 no improvement로 실패한다.
140 $body=[ordered]@{phase='Benchmark';before_rows=30;after_rows=30;before_median_ms=$bm;before_p95_ms=$bp;after_median_ms=$am;after_p95_ms=$ap;chosen_index=$chosen.name;plan_node=$chosen.type;native_exit=0;cleanup=1}Benchmark 30/30 rows·네 metric·index·node·exit·cleanup을 ordered body에 담는다.
141 }BeforePlan부터 Benchmark까지 이어진 phase 조건 사슬을 닫는다.
142}finally{try가 끝난 뒤 성공·실패 모두 실행할 finally 블록을 연다.
143 if($owned){& docker compose -f $compose -p $ComposeProject down -v|Out-Null;if($LASTEXITCODE-ne0){throw "compose cleanup failed: phase=$Phase"}}owned이면 compose project와 volume을 내리고 cleanup 실패도 예외로 바꾼다.
144}finally cleanup 블록을 닫는다.
145if($null-eq$body){throw "W13 phase produced no evidence: $Phase"}body가 여전히 null이면 해당 phase가 evidence를 만들지 못했다고 실패한다.
146$file=@{BeforePlan='before-day.json';AfterPlan='after-day.json';Selectivity='selectivity-day.json';Benchmark='benchmark-manifest.json'}[$Phase]현재 db phase를 네 evidence JSON 파일 이름 중 하나로 매핑한다.
147Write-JsonAtomic $file $bodyphase body를 선택한 JSON 파일로 원자 저장한다.
148"W13_${Phase}_GREEN native_exit=0 cleanup=1 evidence=$file"현재 phase 이름과 evidence 파일을 넣은 고정 Green marker를 출력한다.
07

STEP 07 / 13

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

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

한 줄로 읽기

phase별 disposable lifecycle로 동일 fixture의 plan·count·전후 30회 nearest-rank 지표를 저장하고 owned Compose를 정리하지만 통과 조건과 측정 설계의 좁은 한계를 그대로 가진다.

문법 해부

  • PowerShell pipeline |은 왼쪽 success output을 오른쪽 command input으로 보낸다.
  • @(...)는 결과가 하나여도 배열로 고정하고 [ordered]@{...}는 JSON field 순서를 보존한다.
  • $LASTEXITCODE는 바로 앞 native executable의 종료 상태이므로 위치가 중요하다.
  • try/finally는 try가 성공하거나 예외를 던져도 finally를 실행한다.
  • double-quoted string은 $Phase 같은 변수를 보간하고 single-quoted string은 literal로 둔다.
  • elseif 사슬은 네 db phase 중 하나의 body만 채운다.

실행 순서

  1. parameter binding 뒤 IsPathRooted로 root 없는 EvidenceDir를 막고 GetFullPath가 통과값을 full path로 해석한다.
  2. Prepare면 compose config와 stale evidence를 검사하고 비교하지 않은 current hash 다섯 개를 prepare.json에 기록한 뒤 exit한다.
  3. Report면 saved JSON 다섯 개를 읽어 perf-manifest를 쓰고 exit한다.
  4. DB phase면 Compose를 소유해 켜거나 외부 Container를 받고 매번 seed.sql을 실행한다.
  5. 선택 phase가 plan·selectivity·benchmark body를 만든다.
  6. finally가 owned Compose project와 volume을 내린다.
  7. body를 phase JSON에 원자 저장하고 exact Green marker를 출력한다.

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

F06-C01 · parameters and shared paths
문법 해부
param이 Phase와 EvidenceDir 전달을 Mandatory로 요구하고 Phase ValidateSet에 Prepare·BeforePlan·AfterPlan·Selectivity·Benchmark·Report를 허용하며 dbPhases에는 네 DB phase만 둔다.
실제 값 추적
`C:\evidence\w13`처럼 rooted·fully-qualified 입력은 IsPathRooted를 통과하고 GetFullPath 결과가 $e에 들어간다.
정상 예
Windows의 `C:Documents`나 `\Documents`는 rooted이면서 fully qualified가 아니어도 IsPathRooted를 통과하며, 뒤 GetFullPath가 현재 drive/directory 문맥을 써 $e를 해석한다.
틀린 예·반례
`evidence\w13`처럼 root가 없는 입력은 L10에서 throw되어 L13 GetFullPath와 directory 생성에 도달하지 않는다.
착각 방지
Mandatory binding은 빈 문자열을 거부해도 absolute·fully-qualified를 증명하지 않으며, rooted 판정은 fully-qualified 판정과 다르므로 오류문구의 absolute 주장이 실제 predicate보다 강하다.
하지 않는 일
GetFullPath 결과는 호출 환경에 의존할 수 있고 이 입력 검사는 Docker/DB 상태, source hash 일치, evidence directory 소유권도 확인하지 않는다.
다음 연결
Write-JsonAtomic이 resolve된 $e 아래 phase 결과를 tmp 후 rename으로 저장한다.
F06-C02 · atomic JSON writer
문법 해부
함수는 target.tmp에 Depth 6 JSON을 쓴 뒤 Move-Item -Force로 최종 이름에 교체한다.
실제 값 추적
prepare.json 같은 evidence는 완성된 JSON 한 번으로 보이게 된다.
정상 예
process가 Set-Content 전 실패하면 최종 target을 부분 JSON으로 직접 덮지 않는다.
틀린 예·반례
temporary 이름 충돌이나 filesystem crash까지 완전한 durability를 보장하지 않는다.
착각 방지
JSON body의 업무 정확성은 호출 phase가 만들어야 한다.
하지 않는 일
source file hash 계산과 DB cleanup을 수행하지 않는다.
다음 연결
PsqlFile이 canonical SQL을 container psql에 전달한다.
F06-C03 · psql file runner
문법 해부
Get-Content -Raw 파이프와 docker exec psql ON_ERROR_STOP이 상대 SQL 파일을 실행하고 native exit를 검사한다.
실제 값 추적
seed.sql/create-index.sql/assert.sql 중 전달받은 Relative가 financial_core DB에서 실행된다.
정상 예
psql exit가 0이 아니면 파일명과 exit code를 넣어 즉시 throw한다.
틀린 예·반례
Container가 틀린 DB를 가리키면 canonical SQL도 잘못된 곳에서 실패할 수 있다.
착각 방지
함수는 SQL 결과의 업무 의미를 자체 파싱하지 않는다.
하지 않는 일
컨테이너 생성과 제거는 바깥 lifecycle이 맡는다.
다음 연결
MeasurePlan이 measure.sql을 정확히 30번 실행해 raw rows를 모은다.
F06-C04 · 30-sample plan measurement
문법 해부
for sample=1..30 loop가 measure.sql JSON을 실행·parse하고 variant/sample/ms 객체를 CSV와 배열로 만든다.
실제 값 추적
before 또는 after 각각 30개의 Execution Time double 값이 생긴다.
정상 예
17번째 실행 실패면 phase·sample=17·exit를 담은 예외로 전체 phase가 실패한다.
틀린 예·반례
30회는 독립 machine noise와 warmup bias를 제거하는 보편 표본 수가 아니다.
착각 방지
이 함수는 OFFSET/keyset이나 여러 account selectivity를 측정하지 않는다.
하지 않는 일
median/p95 계산과 relative-improvement gate는 뒤 Benchmark가 맡는다.
다음 연결
Quantile이 이 30개 ms 배열에서 위치 값을 고른다.
F06-C05 · nearest-rank Quantile implementation
문법 해부
ms를 정렬하고 Count=30을 검사한 뒤 Ceiling(30*P)-1 위치를 반환하는 nearest-rank 식이다.
실제 값 추적
값 1..30에서 P=.5는 index14의 15, P=.95는 index28의 29를 낸다.
정상 예
p95=29는 printed nearest-rank 예와 일치한다.
틀린 예·반례
짝수 표본 median 계약은 (15+16)/2=15.5인데 현재 함수는 15를 반환한다.
착각 방지
현재 before_median_ms/after_median_ms를 올바른 pair-average median이라고 가르치면 안 된다.
하지 않는 일
통계적 신뢰구간이나 outlier 정책은 계산하지 않는다.
다음 연결
FindIndex가 after plan tree에서 실제 선택 index를 찾는다.
F06-C06 · recursive plan index finder
문법 해부
현재 Node의 Index Name을 먼저 보고 없으면 Plans children을 재귀 순회해 첫 index node를 반환한다.
실제 값 추적
Index Scan 또는 Index Only Scan 아래에서 idx_ledger_account_created_id 이름과 Node Type을 찾는다.
정상 예
top node가 Sort여도 하위 plan에 index가 있으면 재귀가 발견한다.
틀린 예·반례
여러 index node가 있으면 첫 발견만 반환해 전체 plan 구조를 대표하지 못할 수 있다.
착각 방지
함수는 cost·buffers·actual time을 평가하지 않는다.
하지 않는 일
index가 query 의미를 보존하는지는 assert와 source 계약의 몫이다.
다음 연결
SourceHashes가 phase evidence에 묶을 다섯 파일을 열거한다.
F06-C07 · five-file source hash list
문법 해부
문자열 배열 다섯 경로를 ForEach-Object로 돌며 SHA256 소문자 객체를 만든다.
실제 값 추적
runner,seed,measure,create-index,assert의 file/hash 다섯 쌍이 prepare body에 들어간다.
정상 예
파일 한 byte가 바뀌면 해당 sha256 값이 달라진다.
틀린 예·반례
compose.yaml과 inline D5 gate는 이 배열에 없어 다섯 hash closure 밖이다.
착각 방지
다섯 hash를 전체 환경 sealed manifest라고 부르면 안 된다.
하지 않는 일
hash는 파일 내용의 올바름이나 Docker image digest를 증명하지 않는다.
다음 연결
Prepare가 compose shape와 stale future artifacts를 검사하고 이 hashes를 저장한다.
F06-C08 · Prepare phase
문법 해부
Prepare branch가 compose 존재·stale artifact 없음·Docker server/version·service=db·futureCount=0을 검사하고 prepare.json을 쓴다.
실제 값 추적
DB를 올리거나 seed하지 않고 compose config를 확인하며 다섯 source의 현재 SHA를 계산해 기록한다.
정상 예
future artifact가 하나라도 이미 있으면 stale 목록을 넣어 실패한다.
틀린 예·반례
source 한 byte를 Prepare 전에 바꿔도 새 hash를 그대로 기록할 뿐 known-good expected baseline과 equality/mismatch 비교가 없어 그 변경만으로는 실패하지 않는다.
착각 방지
현재 SHA 기록은 무결성 승인이나 compose.yaml 포함 전체 closure가 아니며 current runner 한 파일은 다섯 목록 안에 포함될 뿐이다.
하지 않는 일
D2-D6 결과의 current 실행을 대신 증명하지 않는다.
다음 연결
다음 Report branch는 같은 파일에 있지만 strict scope에서 호출되지 않는다.
F06-C09 · Report phase shared source, excluded from strict scope
문법 해부
Report branch가 다섯 predecessor JSON을 읽고 cleanup/count/index를 검사해 perf-manifest를 조립한다.
실제 값 추적
D7에서 hashes와 phase_evidence를 합쳐 W13_REPORT_GREEN marker를 낼 수 있다.
정상 예
strict Monday-Saturday는 Phase Report를 호출하지 않아 perf-manifest를 만들지 않는다.
틀린 예·반례
이 source가 파일에 존재한다는 이유로 D1-D6 결과에 final manifest가 있다고 주장하면 안 된다.
착각 방지
branch는 historical D2-D6 evidence를 current runner hash로 재실행하지 않는다.
하지 않는 일
compose.yaml과 inline gate hash를 SourceHashes에 추가하지 않는다.
다음 연결
strict DB phase validation이 BeforePlan/AfterPlan/Selectivity/Benchmark만 허용한다.
F06-C10 · database phase validation and disposable startup
문법 해부
dbPhases membership을 확인하고 try에서 지정 ComposeProject 소유 간주·비밀번호·up --wait·container 해석 또는 Container 입력을 fail-closed로 처리한다.
실제 값 추적
Compose mode는 기본 w13-perf-lab 또는 전달된 project를 이 run의 소유로 간주한 뒤 db를 띄우고 seed.sql을 실행한다.
정상 예
partial `up` 실패도 owned=true라 finally cleanup 대상이 된다.
틀린 예·반례
같은 ComposeProject를 다른 실행이 쓰고 있어도 uniqueness나 collision을 사전 검사하지 않는다.
착각 방지
startup 성공은 phase-specific evidence나 relative improvement를 아직 증명하지 않는다.
하지 않는 일
Container mode의 외부 container는 삭제하지 않지만 seed.sql이 perf_w13 schema를 재생성한다.
다음 연결
BeforePlan branch가 index 전 plan 한 번을 파일로 보존한다.
F06-C11 · BeforePlan phase
문법 해부
BeforePlan이 measure.sql을 한 번 실행해 before-plan.txt를 쓰고 JSON top node·actual rows·file hash를 body에 담는다.
실제 값 추적
seed.sql이 schema를 재생성한 직후 index 생성 전 plan 1개가 기록된다.
정상 예
psql native exit가 nonzero면 before plan query failed로 중단한다.
틀린 예·반례
이 한 plan은 30회 benchmark before raw CSV가 아니며 Container mode가 container 자체의 disposable ownership을 뜻하지도 않는다.
착각 방지
top_node만으로 전체 child plan과 성능 원인을 설명하지 않는다.
하지 않는 일
cleanup 성공은 finally 뒤에 확정되며 이 branch 내부 출력만으로 끝나지 않는다.
다음 연결
AfterPlan은 같은 fresh schema에 복합 index를 추가하고 selected node를 검증한다.
F06-C12 · AfterPlan phase
문법 해부
AfterPlan이 create-index, one measure plan, assert.sql, FindIndex를 순서대로 실행하고 exact name/type을 gate한다.
실제 값 추적
chosen index가 idx_ledger_account_created_id이며 Index Scan 계열이면 after-day body를 만든다.
정상 예
다른 index 이름이나 Seq Scan만 나오면 after plan index mismatch로 실패한다.
틀린 예·반례
one after plan은 30개 after latency 분포가 아니다.
착각 방지
index 선택이 모든 account 분포와 production query에 최적임을 보장하지 않는다.
하지 않는 일
median bug와 D5 proof gap을 교정하지 않는다.
다음 연결
Selectivity가 고정 fixture 네 count를 별도 fresh DB에서 확인한다.
F06-C13 · Selectivity phase
문법 해부
한 psql scalar query가 네 count를 |로 받고 정확히 1000/100000/100000/90000인지 검사해 hot_ratio=.9 body를 만든다.
실제 값 추적
fresh seed에서 account 1의 ledger 비율이 90%임을 확인한다.
정상 예
값 하나라도 다르거나 field가 4개가 아니면 selectivity counts mismatch가 난다.
틀린 예·반례
여러 account predicate나 histogram selectivity query를 실행하지 않는다.
착각 방지
90% synthetic hot ratio는 운영 분포가 아니다.
하지 않는 일
plan과 index latency를 이 phase에서 측정하지 않는다.
다음 연결
Benchmark가 한 disposable DB에서 before30과 after30을 이어 측정한다.
F06-C14 · Benchmark phase
문법 해부
Benchmark가 before 30회→index 생성과 ANALYZE→after plan 1회→after 30회→assert·FindIndex·Quantile·relative gate를 고정 순서로 실행한다.
실제 값 추적
예를 들어 before median/p95=10/20, after=9/25여도 median 하나가 좋아서 gate를 통과하고 네 통계 값과 chosen index가 body에 들어간다.
정상 예
after median과 p95가 둘 다 before 미만일 필요는 없고, 둘 중 하나만 작으면 다른 하나가 악화돼도 현재 OR 성격의 gate는 통과한다.
틀린 예·반례
after median=10, p95=21처럼 둘 다 before 이상이면 no relative improvement 예외가 나며, 현재 Quantile .5는 pair-average median도 아니다.
착각 방지
pass를 median·p95가 모두 개선됐다는 증거로 읽으면 실제 `-and` 실패 조건과 다르다.
하지 않는 일
warmup·순서 무작위화·before/after interleave가 없어 warm cache와 시간 경과 bias를 통제하지 않으며 absolute SLA, pagination, write/storage 비용도 측정하지 않는다.
다음 연결
finally가 owned로 간주한 Compose project와 volume을 성공·실패 모두에서 내린다.
F06-C15 · finally cleanup
문법 해부
finally가 owned일 때 지정 ComposeProject로 docker compose down -v를 호출하고 cleanup native exit도 검사한다.
실제 값 추적
Compose phase가 성공하거나 중간 throw해도 그 project의 container와 volume 제거를 시도한다.
정상 예
owned Compose phase에서 down -v exit가 0이면 finally를 빠져나와 뒤 evidence write 단계로 진행한다.
틀린 예·반례
같은 project 이름을 다른 실행이 공유하거나 down exit가 nonzero이면 자원 충돌 또는 cleanup failed가 생긴다.
착각 방지
owned=true는 project 이름의 실제 독점성을 검증한 결과가 아니라 이 run의 소유권 가정이다.
하지 않는 일
Container mode의 외부 container 비삭제, process hard-kill 뒤 cleanup, filesystem durability는 이 finally가 보장하지 않는다.
다음 연결
cleanup 뒤 phase body와 filename을 검사해 JSON과 Green marker를 쓴다.
F06-C16 · phase evidence file and Green marker
문법 해부
null body를 거부하고 phase→evidence filename map으로 Write-JsonAtomic 후 W13_${Phase}_GREEN 문자열을 출력한다.
실제 값 추적
BeforePlan은 before-day.json, Benchmark는 benchmark-manifest.json과 cleanup=1 marker를 남긴다.
정상 예
지원 phase가 body를 만들지 못하면 evidence 파일 없이 예외가 난다.
틀린 예·반례
Green 문자열만 복사해 붙여도 실제 JSON/hash/cleanup이 증명되지는 않는다.
착각 방지
이 끝부분은 Prepare와 Report branch의 earlier exit에는 도달하지 않는다.
하지 않는 일
strict 전체 final manifest와 predecessor index는 excluded D7 책임이다.
다음 연결
inline D5 gate가 measured query의 제한된 top-N source boundary를 별도로 기록한다.
SourceHashes
  1. hashes=5면 known-good 값과 비교까지 끝난 거야?

  2. 아니다. 다섯 current SHA를 계산해 기록할 뿐 expected 목록·비교·mismatch branch가 없다.

  3. compose도 목록 밖이라 changed bytes를 hashes=5만으로 거절 못 해.

  4. 기록과 baseline 검증을 분리해서 설명하겠습니다.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1Prepare + 깨끗한 evidence dircompose file·Docker engine·service db 하나·future file 0을 검사한다.prepare.json과 W13_PREPARE_GREEN이 생긴다.DB container나 seed는 실행하지 않는다.
2Benchmark + Composeseed→before 30→index/ANALYZE→after plan→after 30→assert 순서로 실행한다.before/after CSV와 benchmark-manifest body가 준비된다.warm-up·random order·interleaving이 없다.
3before=1..30, P=.5정렬 후 Ceiling(15)-1=index14를 고른다.bm은 15가 된다.통상 even-n median 15.5와 다르다.
4bm=10,bp=20,am=11,ap=19am>=bm은 true지만 ap>=bp는 false라 AND 전체가 false다.Benchmark guard를 통과한다.median이 악화돼도 p95 하나가 개선되면 pass 가능하다.
5Compose up 중간 실패up 전에 owned=true이고 terminating error가 try를 빠져나간다.finally가 compose down -v를 시도한다.cleanup 자체 실패가 원래 오류를 가릴 수 있다.
6Report저장된 prepare/before/after/selectivity/benchmark JSON만 읽는다.perf-manifest.json을 새로 조립한다.DB query와 raw CSV 계산을 다시 하지 않는다.
assert와 plan
  1. assert가 성공했으니 index scan도 확인된 거지?

  2. assert는 index 이름 한 건만 센다. FindIndex가 captured plan을 따로 본다.

  3. 존재와 사용을 섞으면 안 돼.

  4. AfterPlan의 두 gate를 각각 설명하겠습니다.

09

STEP 09 / 13

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

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

PowerShell process

rooted predicate·GetFullPath·array·ordered map·JSON/CSV writer와 branch control을 수행한다.

IsPathRooted는 fully-qualified/ownership 검사가 아니고 native failure는 각 $LASTEXITCODE guard가 있어야 예외가 된다.
Docker Compose

고유 project로 db를 up --wait하고 owned일 때 down -v한다.

project uniqueness는 caller 계약이며 이름 충돌을 조회하지 않는다.
PostgreSQL

psql이 seed·measure·index·assert SQL을 disposable schema에서 실행한다.

Container mode DB lifecycle과 데이터 삭제는 script가 소유하지 않는다.
Evidence filesystem

phase body JSON은 temp write 후 rename하고 raw CSV/plan은 직접 쓴다.

모든 artifact가 원자 저장되는 것은 아니며 concurrent writer lock도 없다.
Planner JSON

FindIndex가 Plans를 재귀 탐색해 첫 Index Name을 찾는다.

전체 node와 여러 index를 보존하지 않는다.
Quantile

정렬된 30개에서 Ceiling(30P)-1 nearest-rank index를 반환한다.

일반 median 정의·confidence interval·outlier policy가 아니다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ median은 30개 중 15번째와 16번째 평균이다.

왜 틀리나 source는 nearest-rank 배열 index 14를 직접 반환한다.

바르게 읽기 이 runner에서는 .5를 15번째 값이라고 읽는다.

반례 1..30 표본에서 결과는 15이지 15.5가 아니다.

❌ Benchmark 통과는 median과 p95가 둘 다 좋아졌다는 뜻이다.

왜 틀리나 throw 조건이 두 after 값 모두 before 이상일 때만 true다.

바르게 읽기 둘 중 하나라도 작아지면 통과할 수 있다고 표시한다.

반례 median 11>10, p95 19<20도 pass다.

❌ 30번 측정했으니 warm-up과 순서 bias가 제거됐다.

왜 틀리나 source는 before 30회를 먼저, index 뒤 after 30회를 나중에 순차 실행한다.

바르게 읽기 측정 설계를 그대로 공개하고 운영 결론을 좁힌다.

반례 cache가 뒤 실행에 유리해도 이를 분리할 randomization이 없다.

❌ SourceHashes가 다섯 파일을 known-good baseline과 비교해 고정한다.

왜 틀리나 expected digest 목록·equality 비교·mismatch throw가 없고 current value만 계산한다.

바르게 읽기 Prepare는 다섯 current hash를 기록할 뿐 변경 bytes를 거절하지 못하며 compose도 목록 밖이다.

반례 runner bytes가 바뀌어도 새 digest가 prepare.json에 기록되고 hash mismatch만으로 실패하지 않는다.

❌ IsPathRooted가 true면 fully-qualified absolute path다.

왜 틀리나 .NET에서는 rooted와 fully qualified가 다르고 C:Documents나 \Documents 같은 값이 rooted일 수 있다.

바르게 읽기 line 10은 root 없음만 거르고 line 13 GetFullPath가 current drive/directory 문맥으로 해석한다고 읽는다.

반례 직접 fully-qualified 입력을 요구하려면 IsPathFullyQualified 같은 별도 predicate가 필요하다.

❌ assert.sql이 index plan 사용을 증명한다.

왜 틀리나 assert는 pg_indexes 이름 count만 본다.

바르게 읽기 AfterPlan/Benchmark의 FindIndex 결과를 plan evidence로 사용한다.

반례 catalog에 index가 있어도 planner가 sequential scan을 고를 수 있다.

❌ Report가 최신 성능을 다시 측정한다.

왜 틀리나 Report는 saved JSON을 읽고 필드를 복사한다.

바르게 읽기 읽기 전용 aggregation으로 설명한다.

반례 raw CSV나 DB가 바뀌어도 Report는 query를 재실행하지 않는다.

Report 경계
  1. Report를 누르면 benchmark가 다시 도나?

  2. 아니다. saved JSON 다섯 개를 읽어 perf-manifest로 복사한다.

  3. D7 shared branch이고 strict 다섯 owner command 밖이야.

  4. 읽기 전용 요약이라고 표시하겠습니다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

통계 정의

관례적 even-n median이나 통계적 유의성

이 책임을 맡는 곳: separate statistical analysis with declared estimator
실험 설계

warm-up·randomization·interleaving·cache isolation

이 책임을 맡는 곳: expanded benchmark harness
합격 의미

median과 p95가 모두 개선됨

이 책임을 맡는 곳: stricter predicate if both improvements are required
source baseline

다섯 current hash가 expected digest와 일치하고 compose.yaml bytes도 검증됨

이 책임을 맡는 곳: freeze expected digests, compare/throw on mismatch, and include compose dependency
경로 계약

IsPathRooted 통과값이 fully-qualified이고 learner-owned임

이 책임을 맡는 곳: IsPathFullyQualified plus ownership/reparse-point policy
계획 증거

assert.sql 자체가 plan selection을 증명함

이 책임을 맡는 곳: FindIndex on captured EXPLAIN JSON
pagination

cursor 또는 OFFSET 성능과 의미론

이 책임을 맡는 곳: separate pagination benchmark and correctness tests
운영 성능

절대 latency·concurrency·hardware 일반화

이 책임을 맡는 곳: production-like load and observability
Report 최신성

Report 호출이 DB와 query를 재실행함

이 책임을 맡는 곳: rerun owner phases or add live verification
12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

  • 여섯 Phase 중 strict 다섯과 D7 Report를 구분한다.
  • MeasurePlan의 pipeline과 30회 loop를 값 흐름으로 다시 쓴다.
  • Quantile(.5/.95)의 exact 0-based index와 실제 순위를 계산한다.
  • Benchmark AND 조건의 통과·실패 반례를 각각 만든다.
  • try/finally와 owned flag의 partial-up cleanup 순서를 그린다.
  • Report가 읽는 다섯 파일과 쓰는 한 파일을 적고 재실행이 아님을 설명한다.

2단계 · 코드 조각 재조립

  1. phase 하나씩
  2. rooted와 full path
  3. 30개 순위
  4. fail-closed 정리
  5. Report 읽기 전용

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

정본을 보지 않고 148줄 전체를 다시 쓰고 비공백 mapping·번역 143행과 source-local test 0개를 대조한다.

자가 점검
  • 비공백 원문 143줄이 mapping과 번역에 정확히 한 번씩 있는지 본다.
  • 각 줄의 비유·실제 의미·입력·직접 결과·한계가 서로 다른 역할을 하는지 읽는다.
  • 고정 수치·파일명·분기 조건을 canonical source와 다시 대조한다.
  • 직접 보장과 다음 layer 책임을 반례로 설명한다.
13

STEP 13 / 13

전체 원본 정답

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

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourcescripts/run-w13-perf.ps1SHA-256 d7298bc7734177f5fe66362711d09c46e5f9e1bde4fc720a8a4c01ff78b8c223
run-w13-perf.ps1 — 폐기형 DB에서 계획·분포·전후 30회를 증거로 남기는 orchestrator 전체
param(
 [Parameter(Mandatory=$true)][ValidateSet('Prepare','BeforePlan','AfterPlan','Selectivity','Benchmark','Report')][string]$Phase,
 [ValidateSet('Compose','Container')][string]$Mode='Compose',
 [string]$Container='',
 [string]$ComposeProject='w13-perf-lab',
 [Parameter(Mandatory=$true)][string]$EvidenceDir
)
$ErrorActionPreference='Stop'
$root=Split-Path -Parent $PSScriptRoot
if(-not [IO.Path]::IsPathRooted($EvidenceDir)){
 throw 'EvidenceDir must be an absolute learner-owned path'
}
$e=[IO.Path]::GetFullPath($EvidenceDir)
New-Item -ItemType Directory -Force $e|Out-Null
$compose=Join-Path $root 'compose.yaml'
$owned=$false
$dbPhases=@('BeforePlan','AfterPlan','Selectivity','Benchmark')

function Write-JsonAtomic([string]$Name,$Value){
 $target=Join-Path $e $Name;$temporary="$target.tmp"
 $Value|ConvertTo-Json -Depth 6|Set-Content -Encoding utf8 $temporary
 Move-Item -LiteralPath $temporary -Destination $target -Force
}
function PsqlFile([string]$Relative){
 Get-Content -Raw -LiteralPath (Join-Path $root $Relative)|docker exec -i $Container psql -X -v ON_ERROR_STOP=1 -U app -d financial_core
 if($LASTEXITCODE-ne0){throw "psql file failed: $Relative exit=$LASTEXITCODE"}
}
function MeasurePlan([string]$Variant,[string]$Output){
 $rows=@()
 for($sample=1;$sample-le30;$sample++){
  $json=Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core
  if($LASTEXITCODE-ne0){throw "measure failed: phase=$Variant sample=$sample exit=$LASTEXITCODE"}
  $object=$json|ConvertFrom-Json
  $rows+=[pscustomobject]@{variant=$Variant;sample=$sample;ms=[double]$object[0].'Execution Time'}
 }
 $rows|Export-Csv -NoTypeInformation -Encoding utf8 -LiteralPath $Output
 return $rows
}
function Quantile($Rows,[double]$P){
 $values=@($Rows.ms|Sort-Object)
 if($values.Count-ne30){throw "quantile requires 30 values, got $($values.Count)"}
 return $values[[Math]::Min(29,[Math]::Ceiling(30*$P)-1)]
}
function FindIndex($Node){
 if($Node.'Index Name'){return [pscustomobject]@{name=[string]$Node.'Index Name';type=[string]$Node.'Node Type'}}
 foreach($child in @($Node.Plans)){$found=FindIndex $child;if($found){return $found}}
 return $null
}
function SourceHashes(){
 $files=@('scripts/run-w13-perf.ps1','sql/w13/seed.sql','sql/w13/measure.sql','sql/w13/create-index.sql','sql/w13/assert.sql')
 return @($files|ForEach-Object{[pscustomobject]@{file=$_;sha256=(Get-FileHash -Algorithm SHA256 (Join-Path $root $_)).Hash.ToLowerInvariant()}})
}

if($Phase-eq'Prepare'){
 if(!(Test-Path -LiteralPath $compose -PathType Leaf)){throw 'packaged compose.yaml missing'}
 $futureFiles=@('before-day.json','before-plan.txt','after-day.json','after-plan.txt','selectivity-day.json','benchmark-manifest.json','before-raw.csv','after-raw.csv','benchmark-after-plan.txt','perf-manifest.json')
 $stale=@($futureFiles|Where-Object{Test-Path -LiteralPath (Join-Path $e $_) -PathType Leaf})
 if($stale.Count-ne0){throw "stale W13 D2-D6 artifact exists before Prepare: $($stale -join ',')"}
 if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w13-prepare-config-only'}
 $docker=& docker version --format '{{.Server.Version}}'
 if($LASTEXITCODE-ne0-or[string]::IsNullOrWhiteSpace($docker)){throw 'Docker engine is unavailable'}
 $services=@(& docker compose -f $compose config --services)
 if($LASTEXITCODE-ne0-or$services.Count-ne1-or$services[0]-cne'db'){throw "compose service contract mismatch: $services"}
 $futureCount=@($futureFiles|Where-Object{Test-Path -LiteralPath (Join-Path $e $_) -PathType Leaf}).Count
 if($futureCount-ne0){throw "Prepare created a future W13 artifact: count=$futureCount"}
 $body=[ordered]@{phase='Prepare';docker_server=[string]$docker;compose_services=@($services);hashes=SourceHashes;native_exit=0;future_artifacts_created=$futureCount}
 Write-JsonAtomic 'prepare.json' $body
 'W13_PREPARE_GREEN compose=db hashes=5 future_artifacts=0 native_exit=0'
 exit 0
}

if($Phase-eq'Report'){
 $prepare=Get-Content -Raw (Join-Path $e 'prepare.json')|ConvertFrom-Json
 $before=Get-Content -Raw (Join-Path $e 'before-day.json')|ConvertFrom-Json
 $after=Get-Content -Raw (Join-Path $e 'after-day.json')|ConvertFrom-Json
 $selectivity=Get-Content -Raw (Join-Path $e 'selectivity-day.json')|ConvertFrom-Json
 $benchmark=Get-Content -Raw (Join-Path $e 'benchmark-manifest.json')|ConvertFrom-Json
 if($prepare.future_artifacts_created-ne0-or$before.cleanup-ne1-or$after.cleanup-ne1-or$selectivity.cleanup-ne1-or$benchmark.cleanup-ne1){throw 'W13 predecessor phase/cleanup contract invalid'}
 if($benchmark.before_rows-ne30-or$benchmark.after_rows-ne30-or$benchmark.chosen_index-cne'idx_ledger_account_created_id'){throw 'W13 benchmark contract invalid'}
 $body=[ordered]@{
  schema='perf_w13';query_kind='top-N';accounts=$selectivity.accounts;transactions=$selectivity.transactions;ledger=$selectivity.ledger;hot=$selectivity.hot
  before_rows=$benchmark.before_rows;after_rows=$benchmark.after_rows
  before_median_ms=$benchmark.before_median_ms;before_p95_ms=$benchmark.before_p95_ms;after_median_ms=$benchmark.after_median_ms;after_p95_ms=$benchmark.after_p95_ms
  chosen_index=$benchmark.chosen_index;plan_node=$benchmark.plan_node
  verdict='relative improvement observed on this deterministic lab; no absolute latency claim';native_exit=0;cleanup=1;hashes=@($prepare.hashes)
  phase_evidence=@('prepare.json','before-day.json','after-day.json','selectivity-day.json','benchmark-manifest.json')
 }
 Write-JsonAtomic 'perf-manifest.json' $body
 "W13_REPORT_GREEN phases=5 before=30 after=30 index=$($body.chosen_index) hashes=5 cleanup=1 native_exit=0"
 exit 0
}

if($Phase-notin$dbPhases){throw "unsupported W13 phase=$Phase"}
$body=$null
try{
 if($Mode-eq'Compose'){
  if($Container){throw 'Compose mode rejects -Container'}
  if([string]::IsNullOrWhiteSpace($env:FCL_DB_PASSWORD)){$env:FCL_DB_PASSWORD='w13-disposable-password'}
  # The unique Compose project is ours before startup; a partial `up`
  # failure must still be cleaned by finally.
  $owned=$true
  & docker compose -f $compose -p $ComposeProject up -d --wait db
  if($LASTEXITCODE-ne0){throw "compose up failed: phase=$Phase exit=$LASTEXITCODE"}
  $Container=(& docker compose -f $compose -p $ComposeProject ps -q db).Trim()
 }elseif([string]::IsNullOrWhiteSpace($Container)){throw 'Container mode requires -Container'}
 if([string]::IsNullOrWhiteSpace($Container)){throw 'PostgreSQL container resolution failed'}
 PsqlFile 'sql/w13/seed.sql'|Out-Null

 if($Phase-eq'BeforePlan'){
  $path=Join-Path $e 'before-plan.txt'
  Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $path
  if($LASTEXITCODE-ne0){throw 'before plan query failed'}
  $plan=(Get-Content -Raw $path|ConvertFrom-Json)[0].Plan
  $body=[ordered]@{phase='BeforePlan';top_node=[string]$plan.'Node Type';actual_rows=[int]$plan.'Actual Rows';plan_sha256=(Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant();native_exit=0;cleanup=1}
 }elseif($Phase-eq'AfterPlan'){
  PsqlFile 'sql/w13/create-index.sql'|Out-Null
  $path=Join-Path $e 'after-plan.txt'
  Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $path
  if($LASTEXITCODE-ne0){throw 'after plan query failed'}
  PsqlFile 'sql/w13/assert.sql'|Out-Null
  $plan=(Get-Content -Raw $path|ConvertFrom-Json)[0].Plan;$chosen=FindIndex $plan
  if(!$chosen-or$chosen.name-cne'idx_ledger_account_created_id'-or$chosen.type-notin@('Index Scan','Index Only Scan')){throw "after plan index mismatch: $($chosen.name)/$($chosen.type)"}
  $body=[ordered]@{phase='AfterPlan';chosen_index=$chosen.name;plan_node=$chosen.type;actual_rows=[int]$plan.'Actual Rows';plan_sha256=(Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant();native_exit=0;cleanup=1}
 }elseif($Phase-eq'Selectivity'){
  $raw=(& docker exec $Container psql -X -qAt -F '|' -v ON_ERROR_STOP=1 -U app -d financial_core -c "SELECT (SELECT count(*) FROM perf_w13.account),(SELECT count(*) FROM perf_w13.business_tx),(SELECT count(*) FROM perf_w13.ledger_entry),(SELECT count(*) FROM perf_w13.ledger_entry WHERE account_id=1)").Trim()
  if($LASTEXITCODE-ne0){throw 'selectivity count query failed'}
  $values=$raw.Split('|');if($values.Count-ne4-or[int]$values[0]-ne1000-or[int]$values[1]-ne100000-or[int]$values[2]-ne100000-or[int]$values[3]-ne90000){throw "selectivity counts mismatch: $raw"}
  $body=[ordered]@{phase='Selectivity';accounts=1000;transactions=100000;ledger=100000;hot=90000;hot_ratio=.9;native_exit=0;cleanup=1}
 }elseif($Phase-eq'Benchmark'){
  $before=MeasurePlan before (Join-Path $e 'before-raw.csv')
  PsqlFile 'sql/w13/create-index.sql'|Out-Null
  $afterPath=Join-Path $e 'benchmark-after-plan.txt'
  Get-Content -Raw (Join-Path $root 'sql/w13/measure.sql')|docker exec -i $Container psql -X -qAt -v ON_ERROR_STOP=1 -U app -d financial_core|Set-Content -Encoding utf8 $afterPath
  if($LASTEXITCODE-ne0){throw 'benchmark after plan failed'}
  $after=MeasurePlan after (Join-Path $e 'after-raw.csv');PsqlFile 'sql/w13/assert.sql'|Out-Null
  $plan=(Get-Content -Raw $afterPath|ConvertFrom-Json)[0].Plan;$chosen=FindIndex $plan
  $bm=Quantile $before .5;$bp=Quantile $before .95;$am=Quantile $after .5;$ap=Quantile $after .95
  if(!$chosen-or$chosen.name-cne'idx_ledger_account_created_id'){throw 'benchmark chosen index mismatch'}
  if($am-ge$bm-and$ap-ge$bp){throw "no relative improvement: median=$bm->$am p95=$bp->$ap"}
  $body=[ordered]@{phase='Benchmark';before_rows=30;after_rows=30;before_median_ms=$bm;before_p95_ms=$bp;after_median_ms=$am;after_p95_ms=$ap;chosen_index=$chosen.name;plan_node=$chosen.type;native_exit=0;cleanup=1}
 }
}finally{
 if($owned){& docker compose -f $compose -p $ComposeProject down -v|Out-Null;if($LASTEXITCODE-ne0){throw "compose cleanup failed: phase=$Phase"}}
}
if($null-eq$body){throw "W13 phase produced no evidence: $Phase"}
$file=@{BeforePlan='before-day.json';AfterPlan='after-day.json';Selectivity='selectivity-day.json';Benchmark='benchmark-manifest.json'}[$Phase]
Write-JsonAtomic $file $body
"W13_${Phase}_GREEN native_exit=0 cleanup=1 evidence=$file"
07

W13 D5 pagination scope gate — top-N 토큰만 확인하고 미측정 범위를 기록

inline/w13-d5-pagination-scope.ps1

PowerShell 인라인 Gate · 정본 · W13-F07
8줄 연결8줄 번역5 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

owner query가 latest top-N 모양인지 텍스트로 확인하고 OFFSET·cursor benchmark를 하지 않았다는 범위를 JSON과 marker로 명시한다.

  1. 이 gate가 찾는 세 필수 토큰은 무엇일까?
  2. OFFSET 부재 검사는 어떤 방식이며 무엇을 놓칠까?
  3. cursor_benchmark=false는 cursor가 느리다는 뜻일까?
  4. 이 코드는 SQL을 실제로 실행할까?
이 파일에서 끝까지 다시 쓰는 값tokens=ORDER BY created_at DESC | id DESC | LIMIT 50forbidden regex=(?i)\bOFFSET\bverified=top-N onlyoffset_benchmark=falsecursor_benchmark=falseoutput=evidence\w13\pagination-scope.json
02

STEP 02 / 13

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

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

STARRY는 top-N 악보를 확인했지만 pagination 전체를 실험한 것처럼 말하지 않으려 한다.

이번 실험의 범위표 붙이기

measure.sql 텍스트에서 created_at DESC, id DESC, LIMIT 50 세 토큰을 찾고 OFFSET 단어가 있으면 실패한다. 이 검사는 SQL parser나 DB 실행이 아니라 문자열 gate다.

성공하면 top-N만 검증했고 offset·cursor benchmark는 하지 않았다는 false 값을 source hash와 함께 JSON에 쓴다. 미측정은 실패나 성능 열세를 뜻하지 않는다.

딱 여기까지만 악보 단어 찾기 비유는 텍스트 포함 여부만 설명하며 cursor 의미론·결과 정확성·성능 실행을 증명하지 않는다.

03

STEP 03 / 13

초등학생도 이해하는 설명

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

세 토큰

정렬 두 조각과 LIMIT 50 문자열이 모두 있어야 통과한다.

코드 연결
3줄
비유
악보에서 세 지시문 찾기
비유의 끝
올바른 SQL 위치나 account filter는 검사하지 않는다.

OFFSET 금지

대소문자 무관 단어 경계 정규식으로 OFFSET을 찾으면 실패한다.

코드 연결
4줄
비유
금지 표찰 한 단어 찾기
비유의 끝
주석의 OFFSET도 잡을 수 있고 다른 pagination 문법은 모른다.

범위 JSON

검증한 top-N과 측정하지 않은 offset/cursor를 false로 기록한다.

코드 연결
5~7줄
비유
한 일과 안 한 일을 확인표에 나눠 적기
비유의 끝
false는 성능 판정이 아니다.
세 토큰
  1. ORDER BY 한 줄 전체를 exact match하는 거야?

  2. 아니다. created_at DESC, id DESC, LIMIT 50 세 부분 문자열을 각각 찾는다.

  3. 문장 구조가 아니라 토큰 포함 gate야.

  4. 검사 단위를 세 문자열로 적겠습니다.

OFFSET regex
  1. 소문자 offset이면 빠져나가지 않아?

  2. (?i)가 대소문자를 무시하고 word boundary가 단어를 찾는다.

  3. 대신 주석 속 단어도 잡을 수 있어.

  4. regex의 장점과 오탐 경계를 같이 쓰겠습니다.

04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 8줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.PowerShell 인라인 Gate의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.8 / 8 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F07-L01 $Sql=Join-Path $ReferenceRoot 'sql\w13\measure.sql' 히토리가 기준 악보함에서 이번 top-N 악보의 정확한 서랍 위치를 찾아 손에 든다. Join-Path가 외부에서 주어진 ReferenceRoot와 Windows 상대 경로를 결합해 검사 대상 measure.sql을 가리킨다.
입력
유효한 ReferenceRoot 값과 그 아래 sql\w13\measure.sql 배치를 받는다.
결과·효과
$Sql 변수에 검사할 SQL 파일 경로 문자열이 저장된다.
비유의 한계
경로를 만든 것만으로 파일 존재나 SQL 실행 가능성이 확인되지는 않는다.
2줄F07-L02 $Text=Get-Content -Raw $Sql 니지카가 top-N 악보를 줄마다 넘기지 않고 한 장의 긴 내용으로 펼친다. Get-Content -Raw가 $Sql 파일의 모든 문자를 읽어 정규식 검사용 $Text 문자열을 만든다.
입력
$Sql이 가리키는 읽을 수 있는 measure.sql 파일을 받는다.
결과·효과
$Text가 파일 원문 전체를 담는다.
비유의 한계
텍스트 읽기는 PostgreSQL parser를 호출하거나 쿼리를 실행하지 않는다.
3줄F07-L03 foreach($Token in @('ORDER BY created_at DESC','id DESC','LIMIT 50')){if($Text-cnotmatch[regex]::Escape($Token)){throw "measure.sql missing $Token"}} 료가 악보에서 ‘최신 시각부터’, ‘큰 번호부터’, ‘50장만’이라는 세 지시를 하나씩 찾아 없으면 즉시 중단한다. foreach가 세 고정 문자열을 순회하고 case-sensitive escaped regex로 $Text 포함 여부를 검사한다.
입력
$Text와 토큰 배열 ORDER BY created_at DESC, id DESC, LIMIT 50을 받는다.
결과·효과
세 토큰이 모두 있으면 다음 OFFSET 검사로 진행하고 하나라도 없으면 measure.sql missing 예외가 난다.
비유의 한계
부분 문자열 존재만 확인하므로 토큰의 SQL 문법 위치·account_id=1 조건·실행 결과를 증명하지 않는다.
4줄F07-L04 if($Text-cmatch'(?i)\bOFFSET\b'){throw 'owner query unexpectedly contains OFFSET'} 키타가 악보 어디든 ‘OFFSET’ 표찰이 보이면 이번 공연은 건너뛰기 실험이 아니라며 막는다. 단어 경계가 있는 (?i) OFFSET 정규식으로 $Text를 검사해 발견 시 throw한다.
입력
세 필수 토큰 검사를 통과한 measure.sql 전체 문자열을 받는다.
결과·효과
OFFSET이 없으면 증거 작성으로 가고, 있으면 owner query unexpectedly contains OFFSET으로 종료된다.
비유의 한계
OFFSET 부재는 cursor/keyset pagination 구현을 증명하지 않으며 주석·문자열 속 OFFSET도 탐지할 수 있다.
5줄F07-L05 $Out=Join-Path $LearnerRoot 'evidence\w13\pagination-scope.json' 검사 결과를 학생 증거함의 W13 칸에 둘 정확한 파일 자리를 정한다. Join-Path가 LearnerRoot와 evidence\w13\pagination-scope.json을 결합해 출력 대상을 계산한다.
입력
쓰기 가능한 LearnerRoot와 이미 존재한다고 가정한 evidence\w13 디렉터리를 받는다.
결과·효과
$Out에 scope JSON을 쓸 경로 문자열이 저장된다.
비유의 한계
이 줄은 부모 디렉터리를 만들지 않으며 경로가 안전한 learner-owned 위치인지 재검증하지 않는다.
6줄F07-L06 $Body=[ordered]@{verified='top-N only';offset_benchmark=$false;cursor_benchmark=$false;source_sha256=(Get-FileHash $Sql -Algorithm SHA256).Hash.ToLowerInvariant()} 니지카가 확인표에 ‘top-N만’, ‘OFFSET 측정 안 함’, ‘cursor 측정 안 함’과 악보 지문을 순서대로 적는다. ordered hashtable이 verified, 두 false 플래그, measure.sql의 소문자 SHA-256을 JSON 필드 순서대로 준비한다.
입력
$Sql 파일과 앞의 문자열 gate 성공 상태를 받는다.
결과·효과
$Body에 verified=top-N only, offset_benchmark=false, cursor_benchmark=false, source_sha256가 저장된다.
비유의 한계
false 플래그는 미실행 범위를 정직하게 기록할 뿐 OFFSET 또는 cursor 성능이 나쁘다는 결론이 아니다.
7줄F07-L07 $Body|ConvertTo-Json|Set-Content -Encoding utf8 $Out 료가 네 칸 확인표를 JSON 봉투로 접어 앞에서 정한 증거함 자리에 넣는다. ConvertTo-Json 출력이 pipeline으로 Set-Content에 전달되어 $Out 파일을 생성하거나 덮어쓴다.
입력
필드가 채워진 $Body와 쓰기 가능한 $Out 경로를 받는다.
결과·효과
UTF-8 JSON 증거 파일이 $Out에 저장된다.
비유의 한계
임시 파일 rename을 쓰지 않아 원자 저장이 아니며 DB 실행 증거도 포함하지 않는다.
8줄F07-L08 'W13_PAGINATION_SCOPE top-N-only offset=false cursor=false' 키타가 ‘이번 무대는 top-N뿐’이라는 짧은 초록 멘트를 마지막에 읽는다. PowerShell success stream에 W13_PAGINATION_SCOPE top-N-only offset=false cursor=false 문자열을 보낸다.
입력
JSON 쓰기가 예외 없이 끝난 실행 흐름을 받는다.
결과·효과
호출자가 읽을 고정 scope marker 한 줄이 출력된다.
비유의 한계
marker는 cursor 의미론이나 pagination 결과를 추가 검증하지 않고 exit code를 따로 설정하지도 않는다.
cursor false
  1. false니까 cursor 방식이 실패한 거지?

  2. 아니다. cursor benchmark를 하지 않았다는 기록이다.

  3. 미실행과 성능 실패는 전혀 달라.

  4. false를 측정 범위로 읽겠습니다.

05

STEP 05 / 13

원본 코드 조각

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

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

F07-C01 · locate and read measure.sql1–2줄
1–2줄 원본
$Sql=Join-Path $ReferenceRoot 'sql\w13\measure.sql'
$Text=Get-Content -Raw $Sql
F07-C02 · require three top-N tokens3–3줄
3–3줄 원본
foreach($Token in @('ORDER BY created_at DESC','id DESC','LIMIT 50')){if($Text-cnotmatch[regex]::Escape($Token)){throw "measure.sql missing $Token"}}
F07-C03 · reject OFFSET token4–4줄
4–4줄 원본
if($Text-cmatch'(?i)\bOFFSET\b'){throw 'owner query unexpectedly contains OFFSET'}
F07-C04 · write scope evidence and source hash5–7줄
5–7줄 원본
$Out=Join-Path $LearnerRoot 'evidence\w13\pagination-scope.json'
$Body=[ordered]@{verified='top-N only';offset_benchmark=$false;cursor_benchmark=$false;source_sha256=(Get-FileHash $Sql -Algorithm SHA256).Hash.ToLowerInvariant()}
$Body|ConvertTo-Json|Set-Content -Encoding utf8 $Out
F07-C05 · Green marker8–8줄
8–8줄 원본
'W13_PAGINATION_SCOPE top-N-only offset=false cursor=false'
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 8 / 8

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

원본한국어 번역
1$Sql=Join-Path $ReferenceRoot 'sql\w13\measure.sql'ReferenceRoot 아래 sql\w13\measure.sql의 절대 후보 경로를 Sql에 만든다.
2$Text=Get-Content -Raw $Sqlmeasure.sql 전체 텍스트를 한 문자열로 읽어 Text에 넣는다.
3foreach($Token in @('ORDER BY created_at DESC','id DESC','LIMIT 50')){if($Text-cnotmatch[regex]::Escape($Token)){throw "measure.sql missing $Token"}}ORDER BY created_at DESC, id DESC, LIMIT 50 세 토큰이 각각 없으면 해당 토큰 이름으로 실패한다.
4if($Text-cmatch'(?i)\bOFFSET\b'){throw 'owner query unexpectedly contains OFFSET'}대소문자와 무관하게 OFFSET 단어가 있으면 owner query 범위 위반으로 실패한다.
5$Out=Join-Path $LearnerRoot 'evidence\w13\pagination-scope.json'LearnerRoot 아래 evidence\w13\pagination-scope.json 출력 경로를 만든다.
6$Body=[ordered]@{verified='top-N only';offset_benchmark=$false;cursor_benchmark=$false;source_sha256=(Get-FileHash $Sql -Algorithm SHA256).Hash.ToLowerInvariant()}top-N만 검증했고 offset·cursor benchmark는 하지 않았다는 값과 SQL SHA-256을 ordered body에 담는다.
7$Body|ConvertTo-Json|Set-Content -Encoding utf8 $OutBody를 JSON으로 바꿔 UTF-8 pagination-scope.json에 기록한다.
8'W13_PAGINATION_SCOPE top-N-only offset=false cursor=false'top-N 전용이고 offset·cursor가 false인 고정 성공 marker를 출력한다.
07

STEP 07 / 13

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

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

한 줄로 읽기

measure.sql의 top-N 세 문자열과 OFFSET 부재만 정적으로 확인해 top-N-only JSON을 쓰며 cursor·OFFSET 의미론이나 DB 성능은 의도적으로 증명하지 않는다.

문법 해부

  • Get-Content -Raw는 파일 전체를 하나의 문자열로 읽는다.
  • foreach는 세 Token을 순서대로 검사한다.
  • [regex]::Escape는 token의 문자를 literal 정규식으로 만든다.
  • -cnotmatch는 대소문자를 구분하고 -cmatch의 (?i)는 해당 regex를 대소문자 무관으로 만든다.
  • [ordered]@{...}는 JSON field 순서를 안정적으로 유지한다.

실행 순서

  1. ReferenceRoot에서 measure.sql 경로를 만든다.
  2. 파일을 한 문자열로 읽는다.
  3. 세 case-sensitive token이 모두 있는지 검사한다.
  4. case-insensitive OFFSET word가 없는지 확인한다.
  5. source SHA와 세 scope field를 ordered body에 담는다.
  6. pagination-scope.json을 직접 쓰고 고정 marker를 출력한다.

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

F07-C01 · locate and read measure.sql
문법 해부
Join-Path로 canonical measure.sql을 찾고 Get-Content -Raw로 전체 text를 읽는다.
실제 값 추적
D5 gate가 검사할 $Sql과 $Text가 exact measured source를 가리킨다.
정상 예
measure.sql 한 byte 변경은 뒤 source_sha256에도 반영된다.
틀린 예·반례
ReferenceRoot가 잘못되면 파일 읽기에서 실패한다.
착각 방지
읽기만으로 query 실행·plan·rows는 확인되지 않는다.
하지 않는 일
account_id predicate 요구 여부는 다음 token gate가 결정해야 한다.
다음 연결
세 top-N token 존재 검사가 현재 source 모양을 확인한다.
F07-C02 · require three top-N tokens
문법 해부
foreach가 ORDER BY created_at DESC, id DESC, LIMIT 50 세 literal token을 case-sensitive regex escape로 요구한다.
실제 값 추적
현재 measure.sql은 세 문자열을 모두 포함해 loop를 통과한다.
정상 예
LIMIT 50을 LIMIT 500으로 바꾸면 missing LIMIT 50 예외가 난다.
틀린 예·반례
account_id=1을 삭제해도 세 token이 남으면 이 gate는 통과할 수 있다.
착각 방지
token 존재는 SQL parse tree나 query 실행 결과를 증명하지 않는다.
하지 않는 일
cursor/keyset predicate 부재를 검사하지 않는다.
다음 연결
OFFSET token rejection이 명시한 한 가지 금지 패턴을 확인한다.
F07-C03 · reject OFFSET token
문법 해부
case-insensitive word-boundary regex가 OFFSET 단어가 있으면 즉시 throw한다.
실제 값 추적
현재 exact measure.sql에는 OFFSET이 없어 통과한다.
정상 예
`OFFSET 50000`을 추가하면 owner query unexpectedly contains OFFSET이 난다.
틀린 예·반례
cursor predicate `(created_at,id)<(...)`는 OFFSET 없이도 들어갈 수 있어 잡히지 않는다.
착각 방지
OFFSET 부재를 pagination 전략 비교 benchmark로 해석하면 안 된다.
하지 않는 일
DB plan이나 page 안정성은 이 정적 검사 책임이 아니다.
다음 연결
evidence body가 제한된 검사 결과와 source hash를 JSON으로 쓴다.
F07-C04 · write scope evidence and source hash
문법 해부
Out 경로와 ordered body를 만들고 ConvertTo-Json/Set-Content로 pagination-scope.json을 저장한다.
실제 값 추적
verified=top-N only, offset_benchmark=false, cursor_benchmark=false와 measure source hash가 기록된다.
정상 예
LearnerRoot/evidence/w13 directory가 미리 있으면 현재 source에서 JSON 파일이 생성된다.
틀린 예·반례
부모 directory가 없으면 Set-Content가 실패하고 이 inline body는 directory를 만들어 주지 않는다.
착각 방지
cursor_benchmark=false는 cursor syntax를 검사한 결과가 아니라 hardcoded 값이다.
하지 않는 일
원자 tmp-rename, stale artifact 검사, account_id=1 요구는 이 세 줄에 없다.
다음 연결
마지막 문자열이 top-N-only Green marker를 표준 출력한다.
F07-C05 · Green marker
문법 해부
단일 문자열 expression이 W13_PAGINATION_SCOPE와 offset=false/cursor=false marker를 출력한다.
실제 값 추적
앞 token 검사와 JSON write가 성공했을 때 로그 마지막에 marker가 보인다.
정상 예
이 문자열만 수동 출력하면 source 검사 없이도 같은 글자가 나온다.
틀린 예·반례
marker의 cursor=false를 실제 keyset benchmark 결과로 읽으면 안 된다.
착각 방지
표준 출력은 evidence file hash나 실행 환경을 seal하지 않는다.
하지 않는 일
D5는 DB를 띄우거나 query latency를 측정하지 않는다.
다음 연결
Q15 illustrative SQL은 별도 workbook fixture 가정 아래 집계 예를 보여 준다.
실행 여부
  1. 이 code가 measure.sql을 DB에 보내나?

  2. Get-Content로 text와 hash만 읽고 JSON을 쓴다. docker나 psql 호출은 없다.

  3. 정적 gate이지 실행 test가 아니야.

  4. 성능 증거로 과장하지 않겠습니다.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1현재 canonical measure.sql세 token을 순서대로 찾고 OFFSET regex를 검사한다.verified=top-N only JSON과 Green marker가 나온다.DB query는 실행되지 않는다.
2LIMIT 25로 변경LIMIT 50 token이 case-sensitive match에 실패한다.measure.sql missing LIMIT 50 예외가 난다.다른 limit이 틀렸다는 일반 규칙이 아니라 owner contract 불일치다.
3SQL 주석에 OFFSET 추가(?i) word-boundary regex가 주석 문자열도 찾는다.owner query unexpectedly contains OFFSET으로 실패할 수 있다.SQL parser가 아니므로 comment context를 구분하지 않는다.
4cursor condition 추가세 token이 남고 OFFSET이 없으면 text gate를 통과할 수 있다.cursor_benchmark=false로 JSON이 기록된다.cursor correctness나 실제 사용 여부를 판별하지 않는다.
cursor 의미론
  1. OFFSET이 없고 id DESC가 있으면 keyset 완성 아닌가?

  2. cursor 값과 비교 predicate, 다음 page 결과를 전혀 검사하지 않는다.

  3. D5 gate가 보장하는 건 top-N scope뿐이야.

  4. 별도 correctness test 책임을 표시할게요.

09

STEP 09 / 13

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

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

PowerShell filesystem

두 root를 기준으로 source·output path를 만들고 text/JSON을 읽고 쓴다.

output parent directory 생성과 atomic rename이 없다.
Regex engine

escaped exact tokens와 OFFSET word-boundary pattern을 문자열에 적용한다.

SQL grammar와 comment/string literal을 이해하지 않는다.
Hash evidence

measure.sql bytes의 SHA-256을 lowercase로 저장한다.

ReferenceRoot 전체나 runner source는 hash하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ gate 통과는 cursor pagination이 구현됐다는 뜻이다.

왜 틀리나 필수 token 세 개와 OFFSET 부재만 검사한다.

바르게 읽기 verified 값을 top-N only로 읽는다.

반례 cursor predicate가 없어도 canonical file은 통과한다.

❌ cursor_benchmark=false는 cursor가 더 느리다는 결과다.

왜 틀리나 false는 benchmark를 하지 않았다는 범위 표지다.

바르게 읽기 미측정과 실패 판정을 구분한다.

반례 측정값 없이 어느 방식이 빠른지 비교할 수 없다.

❌ 텍스트 gate가 SQL 결과 정확성을 보장한다.

왜 틀리나 DB parser나 psql을 호출하지 않는다.

바르게 읽기 정적 scope check라고 표시한다.

반례 토큰을 주석에만 넣어도 포함 검사는 통과할 수 있다.

❌ OFFSET만 없으면 모든 keyset 의미론이 맞다.

왜 틀리나 account filter·cursor predicate·동률 경계·중복/누락을 검사하지 않는다.

바르게 읽기 별도 pagination correctness test가 필요하다.

반례 id DESC 문자열만 있어도 cursor parameter가 없을 수 있다.

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

SQL 실행

measure.sql이 PostgreSQL에서 성공하고 50행을 반환함

이 책임을 맡는 곳: runner psql execution and result assertions
cursor 의미론

next page가 중복·누락 없이 이어짐

이 책임을 맡는 곳: fixture-bound cursor pagination tests
OFFSET 성능

OFFSET이 cursor보다 느리거나 빠름

이 책임을 맡는 곳: separate paired benchmark
query 범위

account_id=1 predicate와 모든 ORDER BY 위치가 정확함

이 책임을 맡는 곳: SQL parser/AST or exact source assertion
원자성

pagination-scope.json이 partial write 없이 교체됨

이 책임을 맡는 곳: temporary file plus atomic rename writer
12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

  • 세 필수 token과 OFFSET regex를 정확히 적는다.
  • case-sensitive token 검사와 case-insensitive OFFSET 검사를 구분한다.
  • verified·offset_benchmark·cursor_benchmark 세 field의 뜻을 말한다.
  • text gate가 증명하지 않는 DB 실행·cursor correctness를 반례로 설명한다.

2단계 · 코드 조각 재조립

  1. 세 토큰
  2. OFFSET 금지
  3. 범위 JSON

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

정본을 보지 않고 8줄 전체를 다시 쓰고 비공백 mapping·번역 8행과 source-local test 0개를 대조한다.

자가 점검
  • 비공백 원문 8줄이 mapping과 번역에 정확히 한 번씩 있는지 본다.
  • 각 줄의 비유·실제 의미·입력·직접 결과·한계가 서로 다른 역할을 하는지 읽는다.
  • 고정 수치·파일명·분기 조건을 canonical source와 다시 대조한다.
  • 직접 보장과 다음 layer 책임을 반례로 설명한다.
13

STEP 13 / 13

전체 원본 정답

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

정본 전체 코드 확인하기
hash로 고정한 native 정본 sourceinline/w13-d5-pagination-scope.ps1SHA-256 9e21322931e9bc91c8f0cd7ddd3e29a54d2411c48e3a7912c0196fd2b4755a51
W13 D5 pagination scope gate — top-N 토큰만 확인하고 미측정 범위를 기록 전체
$Sql=Join-Path $ReferenceRoot 'sql\w13\measure.sql'
$Text=Get-Content -Raw $Sql
foreach($Token in @('ORDER BY created_at DESC','id DESC','LIMIT 50')){if($Text-cnotmatch[regex]::Escape($Token)){throw "measure.sql missing $Token"}}
if($Text-cmatch'(?i)\bOFFSET\b'){throw 'owner query unexpectedly contains OFFSET'}
$Out=Join-Path $LearnerRoot 'evidence\w13\pagination-scope.json'
$Body=[ordered]@{verified='top-N only';offset_benchmark=$false;cursor_benchmark=$false;source_sha256=(Get-FileHash $Sql -Algorithm SHA256).Hash.ToLowerInvariant()}
$Body|ConvertTo-Json|Set-Content -Encoding utf8 $Out
'W13_PAGINATION_SCOPE top-N-only offset=false cursor=false'
08

Q15 학습용 SQL — 계좌별 입금·출금 방향을 한 번 정하고 두 합계를 나란히 보기

illustrative/sql/W13-SQL-Q15.sql

SQL 학습용 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W13-F08
28줄 연결28줄 번역3 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

각 원장 행의 방향과 양수를 directed CTE에서 한 번 정한 뒤 계좌별 입금 합계와 출금 합계를 한 행에 놓는다.

  1. 왜 원장 방향을 CTE에서 한 번만 분류하나?
  2. 왜 account에서 LEFT JOIN해 원장 없는 계좌도 남기나?
  3. 왜 OTHER amount를 0으로 두고 두 FILTER에서 제외하나?
이 파일에서 끝까지 다시 쓰는 값DEPOSIT/TRANSFER_IN=INWITHDRAW/TRANSFER_OUT=OUTaccount101 totals=1000/1000fixture accounts=8
W13-SQL-Q15 · 방향을 한 번만 정하는 이유
  1. 입금 합계와 출금 합계를 바로 CASE 두 개로 더하면 안 되나요?

  2. 원장 한 행의 direction과 amount를 directed CTE에서 먼저 고정하면 두 합계가 같은 분류 규칙을 재사용해.

  3. OPENING과 REVERSAL을 OTHER·0으로 둔다는 가정은 공식 정책이 아니라는 표찰이 필요해.

  4. fixture account 101에서 DEPOSIT 1000은 IN·1000, WITHDRAW -500은 OUT·500, OPENING 10000은 OTHER·0으로 추적돼요.

02

STEP 02 / 13

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

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

스타리 영수증 분류대와 여덟 계좌 상자

Q15 학습용 SQL — 계좌별 입금·출금 방향을 한 번 정하고 두 합계를 나란히 보기

니지카는 원장 영수증마다 입금·출금·기타 도장을 한 번만 찍어 directed 바구니에 넣는다.

키타는 여덟 계좌 상자를 먼저 늘어놓고 각 바구니 금액을 따로 더해 빈 상자에도 0원 표를 붙인다.

딱 여기까지만 공식 정답이 아니라 명시한 분류 가정의 예시다

03

STEP 03 / 13

초등학생도 이해하는 설명

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

방향 한 번 정하기

fixture의 DEPOSIT 1000·WITHDRAW -500도 두 CASE가 IN·1000과 OUT·500으로 한 번 정한다.

코드 연결
8~21줄
비유
스타리 영수증 분류대와 여덟 계좌 상자에서 방향 한 번 정하기 순서표를 확인한다.
비유의 끝
이 조각은 fixture의 DEPOSIT 1000·WITHDRAW -500도 두 CASE가 IN·1000과 OUT·500으로 한 번 정한다. 그 밖의 정책과 정본성은 증명하지 않는다.

빈 계좌 살리기

account에서 LEFT JOIN하므로 원장 0건인 103·104도 0·0 행으로 남는다.

코드 연결
22~28줄
비유
스타리 영수증 분류대와 여덟 계좌 상자에서 빈 계좌 살리기 순서표를 확인한다.
비유의 끝
이 조각은 account에서 LEFT JOIN하므로 원장 0건인 103·104도 0·0 행으로 남는다. 그 밖의 정책과 정본성은 증명하지 않는다.

입금·출금 따로 더하기

account 101에서 FILTER가 IN 1000과 OUT 500·300·200을 서로 다른 SUM에 넣는다.

코드 연결
24~25줄
비유
스타리 영수증 분류대와 여덟 계좌 상자에서 입금·출금 따로 더하기 순서표를 확인한다.
비유의 끝
이 조각은 account 101에서 FILTER가 IN 1000과 OUT 500·300·200을 서로 다른 SUM에 넣는다. 그 밖의 정책과 정본성은 증명하지 않는다.
04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 28줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.SQL 학습용 예시 · 정본 답안 아님의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.28 / 28 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F08-L01 -- W13-SQL-Q15 illustrative example; canonical_answer=false Q15 연습 악보 겉면에 ‘학습용 편곡’ 도장을 찍는다. 이 SQL이 W13-SQL-Q15를 위해 새로 만든 illustrative example이며 canonical answer가 아님을 선언한다.
입력
독자가 이 파일의 답안 지위를 먼저 판단할 때
결과·효과
검토자는 실행 전에 이 파일을 비정본 Q15 예시로 분류할 근거를 얻는다.
비유의 한계
주석은 SQL 결과를 만들지 않고 출처만 밝힌다.
2줄F08-L02 -- Assumption: isolated workbook schema; OPENING and REVERSAL are not ordinary cash in/out. 공연 규칙판에 OPENING·REVERSAL 표는 평범한 입출금 표에서 빼겠다고 적는다. 격리 workbook schema를 쓰며 OPENING과 REVERSAL을 일반 현금 입출금으로 분류하지 않는다는 가정을 고정한다.
입력
원장 종류를 IN·OUT·OTHER로 나누기 전에
결과·효과
감사 기준에 OTHER로 보낼 종류와 현금 입출금에서 뺄 범위가 기록된다.
비유의 한계
이 분류는 예시의 가정이며 모든 금융 업무의 보편 규칙이 아니다.
3줄F08-L03 -- Expected cardinality: one row per account; the supplied fixture has 8 accounts. 좌석표에 계좌마다 한 자리씩, 준비된 무대에는 여덟 자리라고 써 둔다. 출력 cardinality를 계좌당 한 행으로 예상하고 제공 fixture에서는 8행이라고 밝힌다.
입력
집계 결과 행 수를 검토할 때
결과·효과
실행 결과를 8개 account 행과 대조할 명시적 기대값이 남는다.
비유의 한계
새 fixture나 다른 schema에서는 8행이라는 수가 달라질 수 있다.
4줄F08-L04 -- This is one auditable learning example, not the workbook's shipped answer. 연습용 악보 봉투에 ‘교재가 나눠 준 정답지 아님’이라고 한 번 더 봉인한다. 이 파일이 감사 가능한 학습 예시 하나일 뿐 workbook이 제공한 정답이 아님을 재확인한다.
입력
예시와 공식 답안을 혼동할 가능성을 막을 때
결과·효과
source package 답안과 이번에 만든 예시가 provenance상 분리된다.
비유의 한계
비공식이라는 표시는 query의 논리적 정확성을 자동 증명하지 않는다.
5줄F08-L05 \set ON_ERROR_STOP on 음 하나가 틀리면 리허설을 즉시 멈추는 psql 정지 스위치를 켠다. psql에서 뒤 명령 하나라도 실패하면 계속 진행하지 않도록 ON_ERROR_STOP을 켠다.
입력
이 파일을 psql로 실행하기 직전에
결과·효과
이후 statement 오류가 psql 성공 흐름으로 숨지 않고 nonzero 종료로 이어진다.
비유의 한계
이 설정은 성공한 query의 의미나 성능을 검증하지 않는다.
6줄F08-L06 SET search_path TO :"workbook_schema", public; 이번 합주가 사용할 격리 악보함을 workbook_schema, 그다음을 public 순서로 지정한다. psql 변수 workbook_schema가 가리키는 schema를 첫 search_path로 두고 public을 보조 경로로 둔다.
입력
테이블 이름을 schema 접두사 없이 찾을 때
결과·효과
접두사 없는 account와 ledger_entry가 격리 workbook schema에서 먼저 해석된다.
비유의 한계
변수가 없거나 잘못된 schema를 가리키면 이 줄만으로 복구하지 못한다.
8줄F08-L08 WITH directed AS ( 원장표를 한 번만 읽어 방향표를 붙일 directed 준비 무대를 연다. directed라는 CTE를 시작해 각 ledger_entry의 방향과 양수를 한 번 계산한다.
입력
계좌별 합계를 내기 전에 원장 행을 정규화할 때
결과·효과
SQL parser는 directed라는 임시 relation의 열 정의를 받는 상태로 들어간다.
비유의 한계
CTE 입구만으로 아직 계좌별 합계가 나오지 않는다.
9줄F08-L09 SELECT account_id, 각 영수증에 어느 계좌 상자에서 왔는지 account_id 꼬리표를 남긴다. directed CTE 결과에 원장 행의 account_id를 그대로 투영한다.
입력
뒤 LEFT JOIN이 계좌와 방향 행을 연결해야 할 때
결과·효과
각 정규화 행에 바깥 account와 다시 연결할 account_id가 남는다.
비유의 한계
이 열은 계좌 존재 여부를 검사하거나 중복을 제거하지 않는다.
10줄F08-L10 CASE 첫 번째 분류판을 펼쳐 영수증 종류를 IN·OUT·OTHER 중 하나로 보낼 준비를 한다. entry_type을 방향 문자열로 바꾸는 첫 CASE 표현식을 시작한다.
입력
원장 종류별 방향을 한 번 결정할 때
결과·효과
현재 row의 entry_type을 받을 첫 CASE branch 목록이 열린다.
비유의 한계
CASE 시작만으로 어느 branch도 아직 선택되지 않는다.
11줄F08-L11 WHEN entry_type IN ('DEPOSIT', 'TRANSFER_IN') THEN 'IN' DEPOSIT와 TRANSFER_IN 표를 받은 영수증을 입금함 IN으로 보낸다. entry_type이 DEPOSIT 또는 TRANSFER_IN이면 direction을 IN으로 만든다.
입력
현재 원장 행이 두 입금 종류 중 하나일 때
결과·효과
두 입금 종류 행의 direction 값이 문자열 IN으로 확정된다.
비유의 한계
OPENING이나 REVERSAL은 이 branch에 들어오지 않는다.
12줄F08-L12 WHEN entry_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN 'OUT' WITHDRAW와 TRANSFER_OUT 표는 출금함 OUT으로 보낸다. entry_type이 WITHDRAW 또는 TRANSFER_OUT이면 direction을 OUT으로 만든다.
입력
현재 원장 행이 두 출금 종류 중 하나일 때
결과·효과
두 출금 종류 행의 direction 값이 문자열 OUT으로 확정된다.
비유의 한계
입금 종류와 기타 종류의 금액은 이 branch가 다루지 않는다.
13줄F08-L13 ELSE 'OTHER' 앞의 네 표가 아니면 폐기하지 않고 OTHER 바구니에 따로 둔다. 앞 조건에 들지 않은 entry_type의 direction을 OTHER로 지정한다.
입력
OPENING·REVERSAL처럼 입출금 집계에서 뺄 행이 들어오면
결과·효과
앞 네 종류가 아닌 행은 버려지지 않고 direction OTHER를 가진다.
비유의 한계
OTHER라는 이름만 붙이며 그 종류의 업무 의미를 판정하지 않는다.
14줄F08-L14 END AS direction, 방향 분류판을 접고 결과 칸 이름을 direction으로 붙인다. 첫 CASE를 닫아 계산 열의 별칭을 direction으로 정한다.
입력
IN·OUT·OTHER branch가 모두 정의된 뒤
결과·효과
첫 CASE 결과가 directed relation의 direction 열로 노출된다.
비유의 한계
별칭 선언은 아직 금액의 부호를 바꾸지 않는다.
15줄F08-L15 CASE 두 번째 계산판을 펼쳐 출금도 양수 합계로 바꿀 준비를 한다. 집계용 amount를 만드는 두 번째 CASE 표현식을 시작한다.
입력
direction과 별도로 합산할 숫자를 정규화할 때
결과·효과
같은 원장 행에서 합산 숫자를 만들 두 번째 branch 목록이 열린다.
비유의 한계
CASE 입구는 signed_amount를 아직 선택하지 않는다.
16줄F08-L16 WHEN entry_type IN ('DEPOSIT', 'TRANSFER_IN') THEN signed_amount 입금 표의 숫자는 영수증에 적힌 signed_amount를 그대로 옮긴다. DEPOSIT 또는 TRANSFER_IN이면 signed_amount를 amount로 사용한다.
입력
입금 방향 원장 행의 합산 값을 만들 때
결과·효과
입금 행의 amount에는 원래 signed_amount 값이 들어간다.
비유의 한계
signed_amount가 올바른 양수라는 데이터 계약까지 검증하지 않는다.
17줄F08-L17 WHEN entry_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN -signed_amount 출금 영수증의 음수 표시는 방향 합계에서 양수로 세도록 부호를 뒤집는다. WITHDRAW 또는 TRANSFER_OUT이면 -signed_amount를 amount로 사용한다.
입력
출금 원장 값이 음수로 저장된다는 fixture 가정에서
결과·효과
출금 행의 amount에는 signed_amount의 반대 부호 값이 들어간다.
비유의 한계
잘못 양수로 저장된 출금 값까지 교정하는 일반 규칙은 아니다.
18줄F08-L18 ELSE 0 입출금이 아닌 표는 두 합계에 섞이지 않도록 금액표를 0으로 만든다. 기타 entry_type에는 amount 0을 부여한다.
입력
OPENING·REVERSAL 행이 directed CTE에 남아 있을 때
결과·효과
기타 종류 행은 두 FILTER SUM에 영향을 주지 않는 amount 0을 가진다.
비유의 한계
행 자체를 삭제하지 않으며 별도 OTHER 집계도 만들지 않는다.
19줄F08-L19 END AS amount 금액 계산판을 닫고 새 숫자 칸에 amount 이름표를 붙인다. 두 번째 CASE를 닫고 계산 결과 열을 amount라고 부른다.
입력
세 금액 branch가 모두 정의된 뒤
결과·효과
두 번째 CASE 결과가 directed relation의 amount 열로 노출된다.
비유의 한계
별칭만 정하며 아직 SUM이나 계좌별 그룹은 수행하지 않는다.
20줄F08-L20 FROM ledger_entry 방향표를 붙일 원본 영수증을 ledger_entry 상자에서 한 장씩 꺼낸다. directed CTE의 시작 테이블을 ledger_entry로 정한다.
입력
모든 원장 행에 두 CASE를 적용할 때
결과·효과
모든 ledger_entry 행이 두 계산 열을 만드는 입력 stream이 된다.
비유의 한계
account 테이블의 원장 없는 계좌는 이 단계에 나타나지 않는다.
21줄F08-L21 ) 원장 한 장당 방향·금액을 만든 directed 준비 무대를 닫는다. directed CTE의 SELECT 범위를 닫아 뒤 main query에서 사용할 relation을 완성한다.
입력
ledger_entry 스캔과 계산 열 정의가 끝난 뒤
결과·효과
main SELECT가 account_id·direction·amount 세 열의 directed relation을 참조할 수 있다.
비유의 한계
CTE를 닫아도 아직 결과 행이 클라이언트로 반환되지 않는다.
22줄F08-L22 SELECT a.account_id, 최종 좌석표의 첫 칸에 계좌 식별 번호를 놓는다. 최종 SELECT가 account 테이블의 account_id를 출력한다.
입력
계좌별 결과 행을 식별해야 할 때
결과·효과
최종 result row의 첫 식별 열에 a.account_id가 배치된다.
비유의 한계
account_id 하나만으로 입금·출금 합계는 설명되지 않는다.
23줄F08-L23 a.account_no, 두 번째 칸에는 사람이 알아볼 계좌 번호표 account_no를 나란히 둔다. 각 결과 행에 account_no를 추가로 출력한다.
입력
식별 ID와 표시용 계좌 번호를 함께 보여 줄 때
결과·효과
같은 result row에 사람이 읽을 a.account_no가 추가된다.
비유의 한계
계좌 번호의 유일성이나 마스킹 정책은 이 projection이 검증하지 않는다.
24줄F08-L24 COALESCE(SUM(d.amount) FILTER (WHERE d.direction = 'IN'), 0)::bigint AS total_deposit, IN 바구니 금액만 더하고 빈 계좌는 0원 표를 붙여 total_deposit 칸에 둔다. direction이 IN인 amount만 SUM하고 NULL이면 0으로 바꾼 뒤 bigint total_deposit으로 출력한다.
입력
한 계좌의 입금 방향 행이 0개 이상 모였을 때
결과·효과
각 group은 IN 금액 합계 또는 빈 경우 0인 bigint total_deposit을 얻는다.
비유의 한계
OTHER와 OUT 금액은 이 FILTER 합계에 포함되지 않는다.
25줄F08-L25 COALESCE(SUM(d.amount) FILTER (WHERE d.direction = 'OUT'), 0)::bigint AS total_withdraw OUT 바구니도 따로 더해 빈 합계를 0으로 바꾸고 total_withdraw 칸에 둔다. direction이 OUT인 amount만 SUM하고 NULL이면 0으로 바꾼 뒤 bigint total_withdraw로 출력한다.
입력
한 계좌의 출금 방향 행이 0개 이상 모였을 때
결과·효과
각 group은 OUT 금액 합계 또는 빈 경우 0인 bigint total_withdraw를 얻는다.
비유의 한계
입금 합계나 순증감액을 이 열에서 다시 계산하지 않는다.
26줄F08-L26 FROM account AS a 원장이 없는 계좌도 좌석표에 남도록 전체 account 명단에서 출발한다. 최종 query의 기준 relation을 account a로 정한다.
입력
모든 계좌를 결과에 보존해야 할 때
결과·효과
원장 유무와 관계없이 account의 여덟 행이 초기 후보 집합에 들어온다.
비유의 한계
고객 정보나 계좌 상태 조건은 이 시작 테이블만으로 추가되지 않는다.
27줄F08-L27 LEFT JOIN directed AS d ON d.account_id = a.account_id 계좌 번호가 같은 directed 영수증을 왼쪽 연결해 빈 상자도 떨어뜨리지 않는다. account_id가 같은 directed 행을 LEFT JOIN하여 원장 없는 account도 보존한다.
입력
계좌 명단에 0개 이상의 정규화 원장 행을 붙일 때
결과·효과
각 후보 계좌에 0개 이상의 directed 행이 붙고 없는 쪽은 NULL 확장행으로 남는다.
비유의 한계
잘못된 account_id나 중복 원장 자체를 제거하지 않는다.
28줄F08-L28 GROUP BY a.account_id, a.account_no 계좌 ID와 번호 한 쌍마다 합계 상자를 하나로 묶는다. account_id와 account_no를 GROUP BY하여 계좌당 한 결과 그룹을 만든다.
입력
FILTER SUM을 계좌 단위로 계산할 때
결과·효과
집계 입력이 account_id·account_no별 한 group으로 축약된다.
비유의 한계
다른 계좌 속성까지 선택하려면 그 열의 집계 규칙이 별도로 필요하다.
29줄F08-L29 ORDER BY a.account_id; 완성된 여덟 좌석표를 account_id가 작은 순서로 정렬한다. 최종 결과를 account_id 오름차순으로 정렬하고 query를 끝낸다.
입력
fixture 결과를 결정적인 순서로 비교할 때
결과·효과
클라이언트는 account_id 오름차순의 결정적 8행 결과를 받는다.
비유의 한계
정렬은 행 수나 합계 값을 바꾸지 않는다.
W13-SQL-Q15 · 여덟 계좌가 모두 남는 경로
  1. 원장이 하나도 없는 계좌는 어느 부분에서 살아남아요?

  2. account 여덟 행에서 시작해 directed를 LEFT JOIN하니 matching 원장이 없어도 계좌 row는 유지돼.

  3. INNER JOIN으로 바꾸면 원장 0개 계좌가 사라져 계좌당 한 행이라는 목적을 깨뜨려.

  4. 원장 0개 계좌의 두 SUM은 NULL이지만 최종 열은 COALESCE 때문에 0·0이고 행 자체는 남아요.

05

STEP 05 / 13

원본 코드 조각

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

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

F08-C01 · scope, assumptions, and workbook search path1–6줄
1–6줄 원본
-- W13-SQL-Q15 illustrative example; canonical_answer=false
-- Assumption: isolated workbook schema; OPENING and REVERSAL are not ordinary cash in/out.
-- Expected cardinality: one row per account; the supplied fixture has 8 accounts.
-- This is one auditable learning example, not the workbook's shipped answer.
\set ON_ERROR_STOP on
SET search_path TO :"workbook_schema", public;
F08-C02 · classify each ledger row once7–21줄
7–21줄 원본

WITH directed AS (
    SELECT account_id,
           CASE
               WHEN entry_type IN ('DEPOSIT', 'TRANSFER_IN') THEN 'IN'
               WHEN entry_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN 'OUT'
               ELSE 'OTHER'
           END AS direction,
           CASE
               WHEN entry_type IN ('DEPOSIT', 'TRANSFER_IN') THEN signed_amount
               WHEN entry_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN -signed_amount
               ELSE 0
           END AS amount
    FROM ledger_entry
)
F08-C03 · aggregate one output row per account22–29줄
22–29줄 원본
SELECT a.account_id,
       a.account_no,
       COALESCE(SUM(d.amount) FILTER (WHERE d.direction = 'IN'), 0)::bigint AS total_deposit,
       COALESCE(SUM(d.amount) FILTER (WHERE d.direction = 'OUT'), 0)::bigint AS total_withdraw
FROM account AS a
LEFT JOIN directed AS d ON d.account_id = a.account_id
GROUP BY a.account_id, a.account_no
ORDER BY a.account_id;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 28 / 28

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

원본한국어 번역
1-- W13-SQL-Q15 illustrative example; canonical_answer=false이 SQL이 W13-SQL-Q15를 위해 새로 만든 illustrative example이며 canonical answer가 아님을 선언한다.
2-- Assumption: isolated workbook schema; OPENING and REVERSAL are not ordinary cash in/out.격리 workbook schema를 쓰며 OPENING과 REVERSAL을 일반 현금 입출금으로 분류하지 않는다는 가정을 고정한다.
3-- Expected cardinality: one row per account; the supplied fixture has 8 accounts.출력 cardinality를 계좌당 한 행으로 예상하고 제공 fixture에서는 8행이라고 밝힌다.
4-- This is one auditable learning example, not the workbook's shipped answer.이 파일이 감사 가능한 학습 예시 하나일 뿐 workbook이 제공한 정답이 아님을 재확인한다.
5\set ON_ERROR_STOP onpsql에서 뒤 명령 하나라도 실패하면 계속 진행하지 않도록 ON_ERROR_STOP을 켠다.
6SET search_path TO :"workbook_schema", public;psql 변수 workbook_schema가 가리키는 schema를 첫 search_path로 두고 public을 보조 경로로 둔다.
8WITH directed AS (directed라는 CTE를 시작해 각 ledger_entry의 방향과 양수를 한 번 계산한다.
9 SELECT account_id,directed CTE 결과에 원장 행의 account_id를 그대로 투영한다.
10 CASEentry_type을 방향 문자열로 바꾸는 첫 CASE 표현식을 시작한다.
11 WHEN entry_type IN ('DEPOSIT', 'TRANSFER_IN') THEN 'IN'entry_type이 DEPOSIT 또는 TRANSFER_IN이면 direction을 IN으로 만든다.
12 WHEN entry_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN 'OUT'entry_type이 WITHDRAW 또는 TRANSFER_OUT이면 direction을 OUT으로 만든다.
13 ELSE 'OTHER'앞 조건에 들지 않은 entry_type의 direction을 OTHER로 지정한다.
14 END AS direction,첫 CASE를 닫아 계산 열의 별칭을 direction으로 정한다.
15 CASE집계용 amount를 만드는 두 번째 CASE 표현식을 시작한다.
16 WHEN entry_type IN ('DEPOSIT', 'TRANSFER_IN') THEN signed_amountDEPOSIT 또는 TRANSFER_IN이면 signed_amount를 amount로 사용한다.
17 WHEN entry_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN -signed_amountWITHDRAW 또는 TRANSFER_OUT이면 -signed_amount를 amount로 사용한다.
18 ELSE 0기타 entry_type에는 amount 0을 부여한다.
19 END AS amount두 번째 CASE를 닫고 계산 결과 열을 amount라고 부른다.
20 FROM ledger_entrydirected CTE의 시작 테이블을 ledger_entry로 정한다.
21)directed CTE의 SELECT 범위를 닫아 뒤 main query에서 사용할 relation을 완성한다.
22SELECT a.account_id,최종 SELECT가 account 테이블의 account_id를 출력한다.
23 a.account_no,각 결과 행에 account_no를 추가로 출력한다.
24 COALESCE(SUM(d.amount) FILTER (WHERE d.direction = 'IN'), 0)::bigint AS total_deposit,direction이 IN인 amount만 SUM하고 NULL이면 0으로 바꾼 뒤 bigint total_deposit으로 출력한다.
25 COALESCE(SUM(d.amount) FILTER (WHERE d.direction = 'OUT'), 0)::bigint AS total_withdrawdirection이 OUT인 amount만 SUM하고 NULL이면 0으로 바꾼 뒤 bigint total_withdraw로 출력한다.
26FROM account AS a최종 query의 기준 relation을 account a로 정한다.
27LEFT JOIN directed AS d ON d.account_id = a.account_idaccount_id가 같은 directed 행을 LEFT JOIN하여 원장 없는 account도 보존한다.
28GROUP BY a.account_id, a.account_noaccount_id와 account_no를 GROUP BY하여 계좌당 한 결과 그룹을 만든다.
29ORDER BY a.account_id;최종 결과를 account_id 오름차순으로 정렬하고 query를 끝낸다.
07

STEP 07 / 13

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

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

한 줄로 읽기

각 원장 행의 방향과 양수를 directed CTE에서 한 번 정한 뒤 계좌별 입금 합계와 출금 합계를 한 행에 놓는다.

문법 해부

  • fixture의 DEPOSIT 1000·WITHDRAW -500도 두 CASE가 IN·1000과 OUT·500으로 한 번 정한다.
  • account에서 LEFT JOIN하므로 원장 0건인 103·104도 0·0 행으로 남는다.
  • account 101에서 FILTER가 IN 1000과 OUT 500·300·200을 서로 다른 SUM에 넣는다.

실행 순서

  1. directed가 IN·1000으로 바꾼다
  2. 부호를 뒤집어 OUT·500으로 만든다
  3. LEFT JOIN의 NULL 방향은 두 FILTER를 모두 통과하지 못한다
  4. account_id로 묶어 방향별 FILTER SUM을 계산한다

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

F08-C01 · scope, assumptions, and workbook search path
문법 해부
네 provenance 주석, ON_ERROR_STOP, workbook search_path가 예시 지위·가정·행 수·실행 schema를 고정한다.
실제 값 추적
canonical_answer=false이고 OPENING/REVERSAL 제외, fixture 결과 8행을 전제로 한다.
정상 예
격리 schema 변수를 주고 psql로 실행하면 뒤 CTE가 workbook 표를 찾는다.
틀린 예·반례
이 여섯 줄을 빼면 query만 보고 공식 답안이나 perf_w13용으로 오해할 수 있다.
착각 방지
주석의 8행은 다른 fixture에서 유지되는 invariant가 아니다.
하지 않는 일
이 조각은 ledger 방향이나 합계를 아직 계산하지 않는다.
다음 연결
directed CTE가 원장 행마다 direction과 양수 amount를 한 번 만든다.
F08-C02 · classify each ledger row once
문법 해부
WITH directed의 두 CASE가 entry_type→IN/OUT/OTHER와 signed_amount→양수/0을 만들고 ledger_entry를 읽는다.
실제 값 추적
fixture account 101의 DEPOSIT 1000은 IN/1000, WITHDRAW -500은 OUT/500, OPENING 10000은 OTHER/0이 된다.
정상 예
같은 direction·amount 열을 뒤 두 FILTER SUM이 재사용한다.
틀린 예·반례
출금 signed_amount가 잘못 양수 500으로 저장되면 unary minus가 -500을 만들어 이 예시의 음수-outgoing 가정과 충돌한다.
착각 방지
CTE는 REVERSAL의 실제 회계 정책이나 잘못 저장된 부호 교정을 결정하지 않는다.
하지 않는 일
원장 없는 account를 보존하는 일은 main query가 맡는다.
다음 연결
account LEFT JOIN과 GROUP BY가 계좌당 두 합계를 만든다.
F08-C03 · aggregate one output row per account
문법 해부
account 기준 LEFT JOIN, 두 FILTER SUM+COALESCE, GROUP BY, ORDER BY가 계좌당 한 결과 행을 만든다.
실제 값 추적
fixture account 101은 total_deposit=1000, total_withdraw=1000이고 ledger 0건인 103·104는 0·0으로 남아 전체 8행이 된다.
정상 예
account 101의 IN 1000과 OUT 500·300·200은 서로 다른 bigint 합계로 나온다.
틀린 예·반례
ledger_entry에서 INNER JOIN으로 시작하면 원장 없는 account 103·104가 사라진다.
착각 방지
이 projection은 순잔액이나 고객별 총액을 계산하지 않는다.
하지 않는 일
공식 workbook 답안·운영 성능을 보장하지 않는다.
다음 연결
Q17 예시는 행 WHERE와 그룹 HAVING의 경계를 보여 준다.
W13-SQL-Q15 · fixture 출금 -500을 양수 500으로 바꾸기
  1. account 101의 WITHDRAW signed_amount가 -500인데 왜 다시 마이너스를 붙여요?

  2. 출금 총액을 양수 크기로 보여 주려는 예시라서 -signed_amount가 500을 만들어.

  3. 출금 값이 양수로 잘못 저장된 데이터까지 고치는 규칙은 아니라는 경계도 함께 적어야 해.

  4. 부호를 뒤집은 500은 total_withdraw에만 들어가고 total_deposit에는 합산되지 않아요.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1account 101 DEPOSIT, signed_amount=1000directed가 IN·1000으로 바꾼다total_deposit에 1000을 더할 후보가 된다OPENING 10000은 OTHER라 제외
2account 101 WITHDRAW, signed_amount=-500부호를 뒤집어 OUT·500으로 만든다total_withdraw에 500을 더할 후보가 된다잘못된 양수 출금은 교정하지 않음
3ledger_entry가 0건인 account 103LEFT JOIN의 NULL 방향은 두 FILTER를 모두 통과하지 못한다두 SUM의 NULL을 COALESCE해 0·0 행을 남긴다계좌가 account에 존재해야 보존됨
4account 101의 IN 1000과 OUT 500·300·200account_id로 묶어 방향별 FILTER SUM을 계산한다total_deposit=1000, total_withdraw=1000이 된다fixture 전체 출력은 account 8행
W13-SQL-Q15 · FILTER 두 개가 섞이지 않는지 확인
  1. IN과 OUT이 같은 group에 있으면 두 SUM이 섞이지 않나요?

  2. 각 SUM의 FILTER가 direction 값을 따로 골라 같은 account group 안에서도 서로 다른 행 집합을 더해.

  3. fixture 전체 결과는 account_id 순서의 8행이고 원장 없는 계좌는 0·0으로 보이는지 확인하면 돼.

  4. account 101은 IN 1000, OUT 500·300·200이므로 total_deposit=1000과 total_withdraw=1000이 따로 나와요.

09

STEP 09 / 13

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

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

psql

ON_ERROR_STOP과 격리 workbook search_path를 먼저 적용한다.

psql 변수와 fixture 준비는 실행 환경 책임이다.
PostgreSQL planner/executor

각 원장 행의 방향과 양수를 directed CTE에서 한 번 정한 뒤 계좌별 입금 합계와 출금 합계를 한 행에 놓는다.

이 예시는 EXPLAIN이나 성능 측정을 실행하지 않는다.
workbook fixture

예상값은 account101 totals=1000/1000, fixture accounts=8에 묶여 있다.

운영 데이터와 perf_w13 schema로 일반화하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ signed_amount를 그대로 모두 더하면 입금·출금 합계가 된다

왜 틀리나 출금은 음수이고 OTHER도 섞인다

바르게 읽기 방향과 양수를 먼저 정한다

반례 fixture WITHDRAW -500을 그대로 더하면 그 출금 기여가 -500으로 잘못 보인다

❌ ledger_entry에서 시작해도 모든 계좌가 나온다

왜 틀리나 원장 없는 계좌는 시작 집합에 없다

바르게 읽기 account에서 LEFT JOIN한다

반례 fixture account 103과 104가 결과에서 사라진다

❌ 이 SQL이 workbook 공식 정답이다

왜 틀리나 source package에는 답안이 없다

바르게 읽기 가정을 밝힌 illustrative example로만 읽는다

반례 REVERSAL 정책이 다르면 다른 풀이가 필요하다

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

정본성

공식 workbook 답안을 보장하지 않는다

이 책임을 맡는 곳: 학습자와 과제 채점 계약
업무 분류

REVERSAL의 실제 회계 방향을 결정하지 않는다

이 책임을 맡는 곳: 업무 정책
실행 환경

perf_w13 schema에서 그대로 실행됨을 보장하지 않는다

이 책임을 맡는 곳: 격리 workbook fixture
W13-SQL-Q15 · Q15 예시의 답안 지위
  1. 이 SQL을 Q15 정답으로 그대로 제출하면 되는 건가요?

  2. 아니야. prompt와 격리 fixture에 맞춘 감사 가능한 한 가지 학습 예시일 뿐이야.

  3. REVERSAL 분류나 운영 schema가 다르면 학습자가 가정을 다시 쓰고 다른 query를 만들 책임이 있어.

  4. 화면의 canonical_answer=false와 ‘정본 답안 아님’ 표지를 확인한 뒤 가정을 포함해 다시 써야 해요.

12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

  • 왜 원장 방향을 CTE에서 한 번만 분류하나?
  • 왜 account에서 LEFT JOIN해 원장 없는 계좌도 남기나?
  • 왜 OTHER amount를 0으로 두고 두 FILTER에서 제외하나?

2단계 · 코드 조각 재조립

  1. fixture의 DEPOSIT 1000·WITHDRAW -500도 두 CASE가 IN·1000과 OUT·500으로 한 번 정한다.
  2. account에서 LEFT JOIN하므로 원장 0건인 103·104도 0·0 행으로 남는다.
  3. account 101에서 FILTER가 IN 1000과 OUT 500·300·200을 서로 다른 SUM에 넣는다.

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

W13-SQL-Q15 prompt와 fixture 가정을 먼저 적고 전체 SQL을 다시 작성한다.

자가 점검
  • canonical_answer=false와 가정을 적었는가
  • 모든 비어 있지 않은 원본 줄을 보존했는가
  • 출력 cardinality와 반례를 설명했는가
13

STEP 13 / 13

전체 원본 정답

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

학습용 예시 전체 확인하기 · 정본 답안 아님
학습용 예시 · 정본 답안 아님 · fixture-bound illustrative SQLillustrative/sql/W13-SQL-Q15.sqlSHA-256 f30f8b1732c8f63760b0e75404b366ac83c8814426d3a5c66813ec4b954f7b54
Q15 학습용 SQL — 계좌별 입금·출금 방향을 한 번 정하고 두 합계를 나란히 보기 전체
-- W13-SQL-Q15 illustrative example; canonical_answer=false
-- Assumption: isolated workbook schema; OPENING and REVERSAL are not ordinary cash in/out.
-- Expected cardinality: one row per account; the supplied fixture has 8 accounts.
-- This is one auditable learning example, not the workbook's shipped answer.
\set ON_ERROR_STOP on
SET search_path TO :"workbook_schema", public;

WITH directed AS (
    SELECT account_id,
           CASE
               WHEN entry_type IN ('DEPOSIT', 'TRANSFER_IN') THEN 'IN'
               WHEN entry_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN 'OUT'
               ELSE 'OTHER'
           END AS direction,
           CASE
               WHEN entry_type IN ('DEPOSIT', 'TRANSFER_IN') THEN signed_amount
               WHEN entry_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN -signed_amount
               ELSE 0
           END AS amount
    FROM ledger_entry
)
SELECT a.account_id,
       a.account_no,
       COALESCE(SUM(d.amount) FILTER (WHERE d.direction = 'IN'), 0)::bigint AS total_deposit,
       COALESCE(SUM(d.amount) FILTER (WHERE d.direction = 'OUT'), 0)::bigint AS total_withdraw
FROM account AS a
LEFT JOIN directed AS d ON d.account_id = a.account_id
GROUP BY a.account_id, a.account_no
ORDER BY a.account_id;
09

Q17 학습용 SQL — 성공 거래를 고객별로 묶고 HAVING 100만원 경계 적용하기

illustrative/sql/W13-SQL-Q17.sql

SQL 학습용 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W13-F09
17줄 연결17줄 번역2 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

성공한 비개설·비취소 거래만 남기고 고객별 amount를 합친 뒤 HAVING으로 100만원 이상 그룹만 고른다.

  1. 왜 status와 tx_type은 WHERE에서 거르나?
  2. 왜 100만원 비교는 WHERE가 아니라 HAVING인가?
  3. 왜 고객에서 계좌와 거래를 차례로 JOIN하나?
이 파일에서 끝까지 다시 쓰는 값status=SUCCESSOPENING/REVERSAL excludedcustomer1 total=1201999fixture rows=1
W13-SQL-Q17 · WHERE와 HAVING의 서로 다른 시간
  1. 100만원 조건을 status 조건 옆 WHERE에 쓰면 더 간단하지 않나요?

  2. WHERE는 개별 거래를 먼저 거르고 HAVING은 customer group의 SUM이 생긴 뒤 1000000을 비교해.

  3. aggregate가 만들어지기 전에는 SUM threshold를 판정할 값 자체가 없다는 순서 반례가 생겨.

  4. fixture customer 1의 통과 거래 7건 합은 1201999라 WHERE 뒤 HAVING >= 1000000을 통과해요.

02

STEP 02 / 13

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

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

스타리 고객별 매출 봉투와 100만원 통과선

Q17 학습용 SQL — 성공 거래를 고객별로 묶고 HAVING 100만원 경계 적용하기

니지카는 성공 도장이 찍힌 일반 거래 영수증만 고객 봉투에 넣어 customer 1의 일곱 금액을 모은다.

료는 합계 1201999에 100만원 통과선을 대고, 정확히 1000000인 별도 fixture는 없다는 한계도 표시한다.

딱 여기까지만 총 거래액 정의는 예시 가정이며 공식 정답이 아니다

03

STEP 03 / 13

초등학생도 이해하는 설명

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

행 먼저 거르기

WHERE가 fixture의 FAILED 700과 OPENING·REVERSAL을 그룹 전에 뺀다.

코드 연결
14~15줄
비유
스타리 고객별 매출 봉투와 100만원 통과선에서 행 먼저 거르기 순서표를 확인한다.
비유의 끝
이 조각은 WHERE가 fixture의 FAILED 700과 OPENING·REVERSAL을 그룹 전에 뺀다. 그 밖의 정책과 정본성은 증명하지 않는다.

고객별 합치기

customer 1의 account 101·105 거래를 customer→account→business_tx 연결로 한 그룹에 모은다.

코드 연결
11~16줄
비유
스타리 고객별 매출 봉투와 100만원 통과선에서 고객별 합치기 순서표를 확인한다.
비유의 끝
이 조각은 customer 1의 account 101·105 거래를 customer→account→business_tx 연결로 한 그룹에 모은다. 그 밖의 정책과 정본성은 증명하지 않는다.

합계 뒤 거르기

HAVING이 customer 1의 실제 합계 1201999를 1000000과 비교해 한 행을 남긴다.

코드 연결
17줄
비유
스타리 고객별 매출 봉투와 100만원 통과선에서 합계 뒤 거르기 순서표를 확인한다.
비유의 끝
이 조각은 HAVING이 customer 1의 실제 합계 1201999를 1000000과 비교해 한 행을 남긴다. 그 밖의 정책과 정본성은 증명하지 않는다.
04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 17줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.SQL 학습용 예시 · 정본 답안 아님의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.17 / 17 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F09-L01 -- W13-SQL-Q17 illustrative example; canonical_answer=false Q17 집계 악보에 학습용 편곡이라는 붉은 표찰을 붙인다. 이 SQL이 W13-SQL-Q17용 illustrative example이고 canonical answer가 아님을 선언한다.
입력
파일의 답안 지위를 확인할 때
결과·효과
검토자는 이 source를 공식 답안이 아닌 Q17 분석 예로 먼저 구분한다.
비유의 한계
주석은 고객별 합계를 실행하지 않는다.
2줄F09-L02 -- Assumption: total means successful non-OPENING/non-REVERSAL business_tx.amount. 총 거래액 규칙판에 성공한 OPENING·REVERSAL 이외의 amount만 센다고 적는다. total의 의미를 성공한 비개설·비취소 business_tx.amount 합계로 가정한다.
입력
어떤 거래를 고객 합계에 넣을지 정하기 전에
결과·효과
합계에 들어갈 status와 tx_type 정책이 query 검토 기준으로 남는다.
비유의 한계
이 정의는 예시 가정이며 counterparty 귀속 같은 다른 정의를 배제하지 못한다.
3줄F09-L03 -- Expected cardinality: 1 row on the supplied workbook fixture. 제공 fixture에서는 기준을 넘는 고객 좌석이 하나라고 예상표를 붙인다. 격리 workbook fixture에서 예상 출력 cardinality가 1행임을 밝힌다.
입력
실행 결과 행 수를 검토할 때
결과·효과
현재 seed에서는 HAVING 통과 행을 1개로 대조할 수 있다.
비유의 한계
데이터가 바뀌면 1행이라는 기대도 함께 바뀐다.
4줄F09-L04 -- This is one auditable learning example, not the workbook's shipped answer. 공연장 배포 정답지가 아니라 감사 가능한 연습본이라는 봉인을 찍는다. 이 query가 workbook에 실려 있던 공식 답안이 아니라 새 학습 예시임을 재확인한다.
입력
교재 제공물과 새 예시를 구분할 때
결과·효과
패키지 제공물과 새 illustrative query 사이의 출처 경계가 명시된다.
비유의 한계
비공식 표시는 정확한 threshold fixture를 대신하지 않는다.
5줄F09-L05 \set ON_ERROR_STOP on 어느 SQL 음이 실패해도 다음 음으로 넘어가지 않는 psql 차단기를 올린다. psql ON_ERROR_STOP을 켜 실행 오류에서 즉시 중단한다.
입력
예시 파일을 psql에 넣을 때
결과·효과
뒤 JOIN이나 aggregate 오류가 발생하면 psql이 그 지점에서 실패한다.
비유의 한계
성공한 query의 업무 정의까지 검사하는 옵션은 아니다.
6줄F09-L06 SET search_path TO :"workbook_schema", public; 고객·계좌·거래표를 찾을 격리 workbook 악보함을 첫 경로로 지정한다. workbook_schema와 public 순서로 search_path를 설정한다.
입력
schema 접두사 없는 세 테이블을 해석할 때
결과·효과
customer·account·business_tx가 지정된 workbook namespace에서 해석된다.
비유의 한계
psql 변수 값이 올바르다는 보장은 이 줄 밖의 책임이다.
8줄F09-L08 SELECT c.customer_id, 고객별 결과표 첫 칸에 customer_id 좌석 번호를 놓는다. 최종 SELECT가 customer_id를 출력한다.
입력
집계 결과의 고객을 안정적으로 식별할 때
결과·효과
통과한 고객 row의 첫 열에 customer_id가 놓인다.
비유의 한계
고객 ID projection은 threshold를 적용하지 않는다.
9줄F09-L09 c.customer_name, 번호 옆에 관객이 읽을 customer_name 이름표를 붙인다. 각 고객 결과 행에 customer_name을 추가한다.
입력
고객 ID와 표시 이름을 함께 보여 줄 때
결과·효과
같은 고객 row에 표시 이름 customer_name이 놓인다.
비유의 한계
동명이인 여부나 이름 변경 이력은 다루지 않는다.
10줄F09-L10 SUM(t.amount)::bigint AS total_transaction_amount 남길 거래 금액을 모두 더해 bigint total_transaction_amount 표로 만든다. 그룹 안의 t.amount를 SUM하고 bigint로 변환해 total_transaction_amount로 출력한다.
입력
성공·종류 필터를 통과한 거래가 고객별로 모였을 때
결과·효과
필터를 지난 거래들의 합이 bigint total_transaction_amount 열로 계산된다.
비유의 한계
NULL 대체나 통화 단위 환산은 이 합계에 없다.
11줄F09-L11 FROM customer AS c 전체 고객 명단 customer c를 합계 여행의 출발점으로 삼는다. FROM relation을 customer c로 정한다.
입력
고객에서 계좌와 거래로 이동할 때
결과·효과
customer 행들이 join chain의 왼쪽 입력 집합이 된다.
비유의 한계
INNER JOIN이 이어지므로 거래 없는 고객은 최종 결과에 남지 않는다.
12줄F09-L12 JOIN account AS a ON a.customer_id = c.customer_id 각 고객 소유의 account 표를 customer_id 끈으로 연결한다. customer_id가 같은 account a를 INNER JOIN한다.
입력
한 고객의 모든 계좌를 펼칠 때
결과·효과
계좌 없는 고객은 빠지고 소유 account마다 고객 row가 펼쳐진다.
비유의 한계
계좌가 없는 고객을 보존하는 LEFT JOIN은 아니다.
13줄F09-L13 JOIN business_tx AS t ON t.account_id = a.account_id 각 계좌의 business_tx 영수증을 account_id로 이어 붙인다. account_id가 같은 business_tx t를 INNER JOIN한다.
입력
고객 계좌의 거래 금액을 합산 대상으로 가져올 때
결과·효과
각 account에 연결된 business_tx 행만 aggregate 입력으로 이어진다.
비유의 한계
원장 ledger_entry나 반대편 계좌의 거래를 추가로 연결하지 않는다.
14줄F09-L14 WHERE t.status = 'SUCCESS' 성공 도장이 찍힌 거래만 집계 줄에 세운다. t.status가 SUCCESS인 개별 거래 행만 WHERE 단계에서 남긴다.
입력
그룹을 만들기 전 실패 거래를 제외할 때
결과·효과
FAILED 등 비성공 거래가 group 생성 전에 제거된다.
비유의 한계
성공이라는 상태의 진실성이나 정산 완료를 검증하지 않는다.
15줄F09-L15 AND t.tx_type NOT IN ('OPENING', 'REVERSAL') 계좌 개설과 취소 표는 일반 거래 총액 바구니에서 뺀다. tx_type이 OPENING 또는 REVERSAL인 행을 WHERE 단계에서 제외한다.
입력
예시의 비개설·비취소 total 정의를 적용할 때
결과·효과
OPENING과 REVERSAL 행이 total 후보 집합에서 추가로 제거된다.
비유의 한계
다른 특수 tx_type을 자동으로 제외하는 포괄 규칙은 아니다.
16줄F09-L16 GROUP BY c.customer_id, c.customer_name 고객 ID와 이름별로 남은 거래 영수증을 한 묶음으로 묶는다. customer_id와 customer_name으로 GROUP BY하여 고객별 합계를 만든다.
입력
SUM(t.amount)를 고객 단위로 계산할 때
결과·효과
남은 거래들이 customer_id·customer_name별 하나의 aggregate group을 이룬다.
비유의 한계
계좌별 중간 합계나 날짜별 분할은 만들지 않는다.
17줄F09-L17 HAVING SUM(t.amount) >= 1000000 묶음 합계가 1,000,000 이상인 고객만 무대에 남긴다. GROUP BY 뒤 HAVING으로 SUM(t.amount)가 1000000 이상인 그룹만 통과시킨다.
입력
개별 행 필터 후 고객 합계 threshold를 판정할 때
결과·효과
합계가 1000000 이상인 group만 result set에 진입한다.
비유의 한계
정확히 경계값인 별도 반례 fixture는 이 source가 추가하지 않는다.
18줄F09-L18 ORDER BY c.customer_id; 통과한 고객표를 customer_id 오름차순으로 줄 세운다. 결과를 customer_id 오름차순으로 정렬하고 query를 끝낸다.
입력
한 행인 fixture와 여러 고객 결과를 결정적으로 비교할 때
결과·효과
통과 고객이 customer_id 오름차순으로 반환된다.
비유의 한계
ORDER BY는 HAVING 통과 여부나 총액을 바꾸지 않는다.
W13-SQL-Q17 · 고객에서 거래까지 이어지는 세 표
  1. business_tx만 보고 고객 이름을 바로 얻을 수는 없나요?

  2. 예시 fixture에서는 거래가 account_id를 갖고 account가 customer_id를 가지므로 customer→account→business_tx 순서로 연결해.

  3. INNER JOIN이라 계좌 없는 고객과 거래 없는 계좌는 결과에서 빠진다는 cardinality 손실을 숨기면 안 돼.

  4. customer_id와 customer_name으로 묶기 전 각 계좌의 거래 행이 고객 row에 연결되는지 확인해요.

05

STEP 05 / 13

원본 코드 조각

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

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

F09-C01 · scope, assumptions, and workbook search path1–6줄
1–6줄 원본
-- W13-SQL-Q17 illustrative example; canonical_answer=false
-- Assumption: total means successful non-OPENING/non-REVERSAL business_tx.amount.
-- Expected cardinality: 1 row on the supplied workbook fixture.
-- This is one auditable learning example, not the workbook's shipped answer.
\set ON_ERROR_STOP on
SET search_path TO :"workbook_schema", public;
F09-C02 · join, filter, group, and HAVING boundary7–18줄
7–18줄 원본

SELECT c.customer_id,
       c.customer_name,
       SUM(t.amount)::bigint AS total_transaction_amount
FROM customer AS c
JOIN account AS a ON a.customer_id = c.customer_id
JOIN business_tx AS t ON t.account_id = a.account_id
WHERE t.status = 'SUCCESS'
  AND t.tx_type NOT IN ('OPENING', 'REVERSAL')
GROUP BY c.customer_id, c.customer_name
HAVING SUM(t.amount) >= 1000000
ORDER BY c.customer_id;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 17 / 17

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

원본한국어 번역
1-- W13-SQL-Q17 illustrative example; canonical_answer=false이 SQL이 W13-SQL-Q17용 illustrative example이고 canonical answer가 아님을 선언한다.
2-- Assumption: total means successful non-OPENING/non-REVERSAL business_tx.amount.total의 의미를 성공한 비개설·비취소 business_tx.amount 합계로 가정한다.
3-- Expected cardinality: 1 row on the supplied workbook fixture.격리 workbook fixture에서 예상 출력 cardinality가 1행임을 밝힌다.
4-- This is one auditable learning example, not the workbook's shipped answer.이 query가 workbook에 실려 있던 공식 답안이 아니라 새 학습 예시임을 재확인한다.
5\set ON_ERROR_STOP onpsql ON_ERROR_STOP을 켜 실행 오류에서 즉시 중단한다.
6SET search_path TO :"workbook_schema", public;workbook_schema와 public 순서로 search_path를 설정한다.
8SELECT c.customer_id,최종 SELECT가 customer_id를 출력한다.
9 c.customer_name,각 고객 결과 행에 customer_name을 추가한다.
10 SUM(t.amount)::bigint AS total_transaction_amount그룹 안의 t.amount를 SUM하고 bigint로 변환해 total_transaction_amount로 출력한다.
11FROM customer AS cFROM relation을 customer c로 정한다.
12JOIN account AS a ON a.customer_id = c.customer_idcustomer_id가 같은 account a를 INNER JOIN한다.
13JOIN business_tx AS t ON t.account_id = a.account_idaccount_id가 같은 business_tx t를 INNER JOIN한다.
14WHERE t.status = 'SUCCESS't.status가 SUCCESS인 개별 거래 행만 WHERE 단계에서 남긴다.
15 AND t.tx_type NOT IN ('OPENING', 'REVERSAL')tx_type이 OPENING 또는 REVERSAL인 행을 WHERE 단계에서 제외한다.
16GROUP BY c.customer_id, c.customer_namecustomer_id와 customer_name으로 GROUP BY하여 고객별 합계를 만든다.
17HAVING SUM(t.amount) >= 1000000GROUP BY 뒤 HAVING으로 SUM(t.amount)가 1000000 이상인 그룹만 통과시킨다.
18ORDER BY c.customer_id;결과를 customer_id 오름차순으로 정렬하고 query를 끝낸다.
07

STEP 07 / 13

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

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

한 줄로 읽기

성공한 비개설·비취소 거래만 남기고 고객별 amount를 합친 뒤 HAVING으로 100만원 이상 그룹만 고른다.

문법 해부

  • WHERE가 fixture의 FAILED 700과 OPENING·REVERSAL을 그룹 전에 뺀다.
  • customer 1의 account 101·105 거래를 customer→account→business_tx 연결로 한 그룹에 모은다.
  • HAVING이 customer 1의 실제 합계 1201999를 1000000과 비교해 한 행을 남긴다.

실행 순서

  1. WHERE의 status와 tx_type 조건을 모두 통과시킨다
  2. status 조건에서 제외한다
  3. tx_type 조건에서 제외한다
  4. HAVING 1201999>=1000000을 평가한다

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

F09-C01 · scope, assumptions, and workbook search path
문법 해부
provenance 주석과 psql/search_path가 성공한 비개설·비취소 amount라는 total 정의와 fixture 1행 기대를 밝힌다.
실제 값 추적
canonical answer가 아닌 W13-SQL-Q17 예시로 격리 workbook에서 실행한다.
정상 예
source audit에 고정된 workbook fixture와 schema 변수를 준비하면 뒤 customer/account/business_tx 이름이 그 격리 schema에서 해석된다.
틀린 예·반례
가정 주석을 숨기면 transfer 금액 귀속과 OPENING 처리 정책이 불명확해진다.
착각 방지
1행 기대는 정확히 100만원인 별도 경계 fixture를 제공하지 않는다.
하지 않는 일
이 조각은 아직 JOIN·SUM·HAVING을 실행하지 않는다.
다음 연결
main SELECT가 고객별 거래 행을 묶고 합계 threshold를 적용한다.
F09-C02 · join, filter, group, and HAVING boundary
문법 해부
customer→account→business_tx INNER JOIN, row-level WHERE, customer GROUP BY, aggregate HAVING, ORDER BY 순서다.
실제 값 추적
fixture customer 1의 SUCCESS 일반 거래 1000·500·300·200·99999·100000·1000000 합계 1201999가 threshold를 넘어 한 행으로 남는다.
정상 예
가상 고객 그룹의 합계가 정확히 1000000이어도 >= 조건이라 통과한다.
틀린 예·반례
SUM 조건을 WHERE에 두면 그룹 값이 생기기 전이라 올바른 단계가 아니다.
착각 방지
fixture의 FAILED WITHDRAW 700, OPENING 10000·100000, REVERSAL 600·600은 WHERE에서 제외되며 데이터 자체가 없다는 뜻은 아니다.
하지 않는 일
현재 fixture 1행과 1201999를 production cardinality·금액으로 일반화하거나 counterparty·통화 정책으로 쓰지 않는다.
다음 연결
Q18 예시는 correlated EXISTS/NOT EXISTS로 존재·부재를 함께 판정한다.
W13-SQL-Q17 · 총 거래액에서 제외되는 두 종류
  1. SUCCESS면 OPENING과 REVERSAL도 합계에 넣어야 하지 않아요?

  2. 이 예시는 일반 거래 총액이라는 가정을 택해 성공 여부와 별개로 두 tx_type을 제외했어.

  3. 다른 총액 정의가 필요하면 WHERE 목록을 바꾸고 그 정책을 provenance에 새로 밝혀야 해.

  4. fixture의 FAILED WITHDRAW 700과 SUCCESS OPENING 10000·100000, REVERSAL 600·600은 total_transaction_amount를 올리지 않아요.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1customer 1의 SUCCESS 일반 거래 1000·500·300·200·99999·100000·1000000WHERE의 status와 tx_type 조건을 모두 통과시킨다일곱 amount의 합계 후보가 1201999가 된다통화 단위와 counterparty 귀속은 fixture 가정
2customer 1의 FAILED WITHDRAW 700status 조건에서 제외한다customer 1 합계는 1201999에서 변하지 않는다실패 정의는 status 값에 의존
3SUCCESS OPENING 10000·100000과 REVERSAL 600·600tx_type 조건에서 제외한다네 금액은 total_transaction_amount에 들어가지 않는다다른 특수 종류는 포함될 수 있음
4customer 1의 남은 합계=1201999HAVING 1201999>=1000000을 평가한다customer_id=1 한 행과 total_transaction_amount=1201999가 남는다정확히 1000000인 경계 fixture는 별도 보강 필요
W13-SQL-Q17 · 정확히 100만원인 그룹
  1. 고객 합계가 딱 1000000이면 남나요?

  2. HAVING 연산자가 >=라서 경계값도 통과하고 결과 열에는 같은 합계가 bigint로 나와.

  3. 다만 제공 seed에 정확한 경계 전용 고객이 없으니 그 반례 fixture까지 증명했다고 말하면 안 돼.

  4. 현재 fixture의 1행 기대와 별도로 exactly-1000000 반례 seed가 없다는 점을 체크리스트에 남겨요.

09

STEP 09 / 13

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

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

psql

ON_ERROR_STOP과 격리 workbook search_path를 먼저 적용한다.

psql 변수와 fixture 준비는 실행 환경 책임이다.
PostgreSQL planner/executor

성공한 비개설·비취소 거래만 남기고 고객별 amount를 합친 뒤 HAVING으로 100만원 이상 그룹만 고른다.

이 예시는 EXPLAIN이나 성능 측정을 실행하지 않는다.
workbook fixture

예상값은 customer1 total=1201999, fixture rows=1에 묶여 있다.

운영 데이터와 perf_w13 schema로 일반화하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ SUM 조건을 WHERE에 쓰면 된다

왜 틀리나 WHERE는 그룹 합계가 생기기 전 단계다

바르게 읽기 SUM threshold는 HAVING에 둔다

반례 WHERE SUM(t.amount)는 일반 SQL에서 유효하지 않다

❌ 모든 business_tx가 총 거래액이다

왜 틀리나 실패·OPENING·REVERSAL을 예시 정의에서 뺀다

바르게 읽기 WHERE 조건을 total 정의로 읽는다

반례 fixture의 FAILED 700이나 OPENING 10000을 섞으면 예시 정의와 다른 합계가 된다

❌ 1행 결과가 언제나 보장된다

왜 틀리나 1행은 제공 fixture의 기대일 뿐이다

바르게 읽기 데이터에 따라 0..N행으로 본다

반례 모든 고객 합계가 100만원 미만이면 0행이다

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

총액 정의

counterparty 귀속이나 중복 회계 정책을 정하지 않는다

이 책임을 맡는 곳: 업무 집계 정책
경계 증명

정확히 100만원인 별도 fixture를 제공하지 않는다

이 책임을 맡는 곳: 학습자 반례 fixture
정본성

공식 workbook 답안이나 운영 query를 보장하지 않는다

이 책임을 맡는 곳: 과제 계약
W13-SQL-Q17 · fixture 1행의 한계
  1. 지금 결과가 한 명이면 실제 데이터에서도 늘 한 명인가요?

  2. 1행은 supplied workbook fixture의 예상 cardinality라 데이터가 달라지면 0행이나 여러 행이 될 수 있어.

  3. 이 illustrative query는 counterparty 귀속·통화 환산·운영 성능을 결정하는 정본이 아니다.

  4. 현재 결과는 customer 1 한 행과 total_transaction_amount=1201999인지 확인하고, 데이터가 바뀌면 0..N행임을 유지해요.

12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

  • 왜 status와 tx_type은 WHERE에서 거르나?
  • 왜 100만원 비교는 WHERE가 아니라 HAVING인가?
  • 왜 고객에서 계좌와 거래를 차례로 JOIN하나?

2단계 · 코드 조각 재조립

  1. WHERE가 fixture의 FAILED 700과 OPENING·REVERSAL을 그룹 전에 뺀다.
  2. customer 1의 account 101·105 거래를 customer→account→business_tx 연결로 한 그룹에 모은다.
  3. HAVING이 customer 1의 실제 합계 1201999를 1000000과 비교해 한 행을 남긴다.

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

W13-SQL-Q17 prompt와 fixture 가정을 먼저 적고 전체 SQL을 다시 작성한다.

자가 점검
  • canonical_answer=false와 가정을 적었는가
  • 모든 비어 있지 않은 원본 줄을 보존했는가
  • 출력 cardinality와 반례를 설명했는가
13

STEP 13 / 13

전체 원본 정답

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

학습용 예시 전체 확인하기 · 정본 답안 아님
학습용 예시 · 정본 답안 아님 · fixture-bound illustrative SQLillustrative/sql/W13-SQL-Q17.sqlSHA-256 4dfb90b401fe2a80282e9ab73f90b222c80511b651d5fe78656175bbdbff62c8
Q17 학습용 SQL — 성공 거래를 고객별로 묶고 HAVING 100만원 경계 적용하기 전체
-- W13-SQL-Q17 illustrative example; canonical_answer=false
-- Assumption: total means successful non-OPENING/non-REVERSAL business_tx.amount.
-- Expected cardinality: 1 row on the supplied workbook fixture.
-- This is one auditable learning example, not the workbook's shipped answer.
\set ON_ERROR_STOP on
SET search_path TO :"workbook_schema", public;

SELECT c.customer_id,
       c.customer_name,
       SUM(t.amount)::bigint AS total_transaction_amount
FROM customer AS c
JOIN account AS a ON a.customer_id = c.customer_id
JOIN business_tx AS t ON t.account_id = a.account_id
WHERE t.status = 'SUCCESS'
  AND t.tx_type NOT IN ('OPENING', 'REVERSAL')
GROUP BY c.customer_id, c.customer_name
HAVING SUM(t.amount) >= 1000000
ORDER BY c.customer_id;
10

Q18 학습용 SQL — 입금 EXISTS와 출금 NOT EXISTS로 계좌 찾기

illustrative/sql/W13-SQL-Q18.sql

SQL 학습용 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W13-F10
22줄 연결22줄 번역3 chunks
01

STEP 01 / 13

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

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

오늘의 한 문장

각 계좌에 입금 방향 원장이 하나 이상 있고 출금 방향 원장은 하나도 없는지 correlated EXISTS 두 개로 판정한다.

  1. 왜 입금은 EXISTS이고 출금은 NOT EXISTS인가?
  2. 왜 SELECT 1로도 존재 여부를 확인할 수 있나?
  3. 왜 NOT IN 대신 NOT EXISTS가 NULL 함정을 피하나?
이 파일에서 끝까지 다시 쓰는 값EXISTS DEPOSIT/TRANSFER_INNOT EXISTS WITHDRAW/TRANSFER_OUTfixture accounts=102,105rows=2
W13-SQL-Q18 · 입금 존재와 출금 부재를 함께 묻기
  1. 입금 행만 찾으면 문제 조건이 끝나는 것 아닌가요?

  2. 첫 EXISTS가 incoming 하나 이상을 요구하고 둘째 NOT EXISTS가 outgoing 0개를 요구해야 두 조건이 동시에 맞아.

  3. account 101은 DEPOSIT 1000이 있어도 WITHDRAW -500과 TRANSFER_OUT 두 건이 있어 첫 조건만 쓰면 잘못 통과해.

  4. account 101은 DEPOSIT 1000으로 EXISTS=true지만 WITHDRAW -500과 TRANSFER_OUT -300·-200 때문에 NOT EXISTS=false라 탈락해요.

02

STEP 02 / 13

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

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

스타리 계좌별 입장·퇴장 도장 검사대

Q18 학습용 SQL — 입금 EXISTS와 출금 NOT EXISTS로 계좌 찾기

히토리는 account 102의 TRANSFER_IN 300처럼 입장 도장이 한 장이라도 있는지 첫 창구에서 확인한다.

료는 101의 WITHDRAW·TRANSFER_OUT은 거부하고 outgoing 0건인 102와 105만 남겨 NULL 목록 비교를 쓰지 않는다.

딱 여기까지만 OPENING과 REVERSAL을 입출금에서 제외한 예시 가정이다

03

STEP 03 / 13

초등학생도 이해하는 설명

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

입금 하나 이상

첫 EXISTS가 102의 TRANSFER_IN 300이나 105의 DEPOSIT처럼 같은 account_id의 incoming 행을 찾는다.

코드 연결
11~16줄
비유
스타리 계좌별 입장·퇴장 도장 검사대에서 입금 하나 이상 순서표를 확인한다.
비유의 끝
이 조각은 첫 EXISTS가 102의 TRANSFER_IN 300이나 105의 DEPOSIT처럼 같은 account_id의 incoming 행을 찾는다. 그 밖의 정책과 정본성은 증명하지 않는다.

출금 하나도 없음

NOT EXISTS가 101의 WITHDRAW -500·TRANSFER_OUT -300·-200 같은 outgoing 행이 있는 계좌를 거부한다.

코드 연결
17~22줄
비유
스타리 계좌별 입장·퇴장 도장 검사대에서 출금 하나도 없음 순서표를 확인한다.
비유의 끝
이 조각은 NOT EXISTS가 101의 WITHDRAW -500·TRANSFER_OUT -300·-200 같은 outgoing 행이 있는 계좌를 거부한다. 그 밖의 정책과 정본성은 증명하지 않는다.

값 대신 존재

SELECT 1은 금액이 아니라 행 존재를 뜻하므로 DEPOSIT 1000도 EXISTS를 만족한다.

코드 연결
12·18줄
비유
스타리 계좌별 입장·퇴장 도장 검사대에서 값 대신 존재 순서표를 확인한다.
비유의 끝
이 조각은 SELECT 1은 금액이 아니라 행 존재를 뜻하므로 DEPOSIT 1000도 EXISTS를 만족한다. 그 밖의 정책과 정본성은 증명하지 않는다.
04

STEP 04 / 13

비유 ↔ 코드 전체 연결표

비어 있지 않은 원본 22줄을 빠짐없이 연결합니다.

단어 몇 개만 뽑은 표가 아닙니다.SQL 학습용 예시 · 정본 답안 아님의 비어 있지 않은 모든 줄을 원본 줄 번호 그대로 연결했습니다.22 / 22 연결
정확한 원본 줄STARRY 비유실제 뜻·입력·결과·한계
1줄F10-L01 -- W13-SQL-Q18 illustrative example; canonical_answer=false Q18 탐색 악보에 공식 답안이 아닌 학습용 예시 표찰을 단다. 이 SQL이 W13-SQL-Q18용 illustrative example이며 canonical answer가 아님을 선언한다.
입력
query의 출처와 지위를 판단할 때
결과·효과
검토자는 이 파일을 비정본 Q18 존재성 풀이로 식별한다.
비유의 한계
주석 자체는 EXISTS 조건을 실행하지 않는다.
2줄F10-L02 -- Assumption: DEPOSIT/TRANSFER_IN are cash in; WITHDRAW/TRANSFER_OUT are cash out. 입장 도장은 DEPOSIT·TRANSFER_IN, 퇴장 도장은 WITHDRAW·TRANSFER_OUT이라고 규칙판에 쓴다. 입금과 출금으로 볼 네 entry_type을 예시 가정으로 고정한다.
입력
원장 행의 방향 존재 여부를 검사하기 전에
결과·효과
두 EXISTS가 사용할 incoming·outgoing 종류 목록이 문서 계약에 남는다.
비유의 한계
OPENING·REVERSAL을 제외하는 정책은 모든 업무에 보편적이지 않다.
3줄F10-L03 -- Expected cardinality: 2 rows on the supplied workbook fixture (accounts 102 and 105). 제공 fixture의 합격 좌석은 102와 105 두 개라고 예상표를 붙인다. 제공 workbook fixture에서 예상 결과가 account 102와 105의 2행임을 밝힌다.
입력
query 결과 cardinality와 식별값을 대조할 때
결과·효과
실행 결과를 account 102와 105 두 행에 맞춰 확인할 수 있다.
비유의 한계
다른 fixture에서는 두 계좌와 2행 기대가 유지되지 않는다.
4줄F10-L04 -- This is one auditable learning example, not the workbook's shipped answer. 교재가 배포한 정답지가 아니라 감사 가능한 한 가지 풀이임을 봉투에 새긴다. 이 파일이 workbook의 shipped answer가 아닌 새 학습 예시임을 재확인한다.
입력
학습자가 자신의 SQL과 비교할 때
결과·효과
다른 정당한 풀이와 이번 fixture-bound 예시의 경계가 드러난다.
비유의 한계
한 가지 예시는 다른 올바른 EXISTS 풀이를 배제하지 않는다.
5줄F10-L05 \set ON_ERROR_STOP on 부분 실행으로 잘못된 결과를 보지 않도록 psql 오류 정지 장치를 켠다. psql ON_ERROR_STOP을 활성화한다.
입력
예시 SQL을 순서대로 실행할 때
결과·효과
subquery나 schema 오류 뒤의 statement가 계속 실행되지 않는다.
비유의 한계
옵션은 정상 종료 query의 의미를 검증하지 않는다.
6줄F10-L06 SET search_path TO :"workbook_schema", public; 격리된 workbook 악보함을 먼저, public을 다음 탐색 경로로 둔다. workbook_schema와 public을 search_path로 지정한다.
입력
account와 ledger_entry 이름을 해석할 때
결과·효과
상관 subquery의 두 테이블 이름이 격리 workbook schema에 결속된다.
비유의 한계
잘못된 schema 변수나 fixture 누락은 별도 실행 환경이 잡아야 한다.
8줄F10-L08 SELECT a.account_id, 합격표 첫 칸에 계좌의 account_id 번호를 적는다. 최종 SELECT가 account_id를 출력한다.
입력
조건을 만족한 계좌를 식별할 때
결과·효과
합격 계좌 row의 첫 출력 열에 account_id가 배치된다.
비유의 한계
ID projection만으로 입금·출금 존재 조건은 만들어지지 않는다.
9줄F10-L09 a.account_no 두 번째 칸에 사람이 확인할 account_no를 함께 적는다. 각 결과 행에 account_no를 추가한다.
입력
계좌 식별 번호와 표시 번호를 같이 보여 줄 때
결과·효과
같은 출력 row에 account_no가 추가된다.
비유의 한계
계좌 번호 마스킹이나 고객 소유권은 확인하지 않는다.
10줄F10-L10 FROM account AS a 모든 계좌 명단 account a에서 후보를 한 장씩 꺼낸다. query의 기준 relation을 account a로 정한다.
입력
각 계좌에 상관 EXISTS 두 개를 적용할 때
결과·효과
모든 account가 두 존재 조건을 평가받을 바깥 후보가 된다.
비유의 한계
계좌 상태나 고객 조건은 이 FROM에 포함되지 않는다.
11줄F10-L11 WHERE EXISTS ( 후보 계좌에 입장 도장이 하나라도 있는지 보는 첫 검사창을 연다. 현재 account와 연관된 입금 방향 원장 존재를 요구하는 EXISTS를 시작한다.
입력
계좌별로 하나 이상의 incoming 행을 확인할 때
결과·효과
현재 account마다 incoming match를 찾는 boolean 평가가 시작된다.
비유의 한계
EXISTS 입구만으로 어떤 entry_type도 아직 제한하지 않는다.
12줄F10-L12 SELECT 1 입장 기록의 값은 필요 없으니 존재 신호 1만 고른다. EXISTS subquery에서 행의 존재만 필요해 상수 1을 SELECT한다.
입력
matching ledger_entry가 있는지만 판정할 때
결과·효과
matching row의 실제 열을 읽지 않고 존재 여부만 executor에 요청한다.
비유의 한계
상수 1이 원장 금액 1원이나 행 수 1을 뜻하지 않는다.
13줄F10-L13 FROM ledger_entry AS incoming 입장 기록 후보를 ledger_entry incoming 상자에서 찾는다. 첫 subquery의 원본을 ledger_entry incoming으로 정한다.
입력
입금 방향 행을 별칭으로 구분할 때
결과·효과
첫 subquery가 ledger_entry를 incoming 별칭으로 스캔한다.
비유의 한계
incoming이라는 별칭만으로 실제 입금 종류가 필터되지 않는다.
14줄F10-L14 WHERE incoming.account_id = a.account_id 바깥 후보와 같은 account_id 영수증만 첫 검사창에 넣는다. incoming.account_id를 바깥 a.account_id와 같게 해 correlated subquery를 만든다.
입력
현재 계좌의 원장만 검사해야 할 때
결과·효과
첫 subquery 후보가 현재 바깥 account의 원장으로 제한된다.
비유의 한계
다른 계좌 행과의 혼합은 막지만 entry_type 조건은 다음 줄이 맡는다.
15줄F10-L15 AND incoming.entry_type IN ('DEPOSIT', 'TRANSFER_IN') DEPOSIT 또는 TRANSFER_IN 도장이 찍힌 영수증만 입장 증거로 인정한다. incoming.entry_type을 DEPOSIT과 TRANSFER_IN으로 제한한다.
입력
현재 계좌의 입금 방향 존재를 확정할 때
결과·효과
그 후보 중 DEPOSIT·TRANSFER_IN이 하나라도 있으면 첫 EXISTS가 true가 된다.
비유의 한계
OPENING·REVERSAL이나 다른 credit 종류는 입금 증거로 세지 않는다.
16줄F10-L16 ) 입장 증거가 하나라도 있어야 한다는 EXISTS 검사창을 닫는다. 첫 correlated EXISTS 조건을 닫는다.
입력
account_id와 입금 종류 조건이 완성된 뒤
결과·효과
입금 match가 없는 account는 이 지점에서 최종 후보에서 탈락한다.
비유의 한계
이 조건만으로 출금이 없는지는 아직 알 수 없다.
17줄F10-L17 AND NOT EXISTS ( 이번에는 퇴장 도장이 단 하나도 없어야 하는 NOT EXISTS 검사창을 연다. 현재 account에 출금 방향 원장이 없음을 요구하는 NOT EXISTS를 시작한다.
입력
입금 존재 조건과 함께 anti condition을 적용할 때
결과·효과
남은 account마다 outgoing match가 없어야 하는 anti 평가가 시작된다.
비유의 한계
NOT EXISTS 입구는 outgoing 종류를 아직 지정하지 않는다.
18줄F10-L18 SELECT 1 퇴장 기록도 값 대신 존재 신호 1만 조회한다. NOT EXISTS subquery가 matching 행의 존재 여부만 보도록 상수 1을 SELECT한다.
입력
출금 행이 한 건이라도 있는지 판정할 때
결과·효과
둘째 subquery도 행 값 대신 match 존재 신호만 계산한다.
비유의 한계
SELECT 1은 결과로 사용자에게 반환되는 열이 아니다.
19줄F10-L19 FROM ledger_entry AS outgoing 퇴장 기록 후보를 같은 ledger_entry에서 outgoing 이름으로 꺼낸다. 둘째 subquery의 원본을 ledger_entry outgoing으로 정한다.
입력
입금 탐색과 출금 탐색 별칭을 분리할 때
결과·효과
같은 ledger_entry가 이번에는 outgoing 별칭의 탐색 입력이 된다.
비유의 한계
별칭 변경이 별도 테이블이나 복사본을 만들지는 않는다.
20줄F10-L20 WHERE outgoing.account_id = a.account_id 바깥 후보 계좌와 account_id가 같은 퇴장 영수증만 살핀다. outgoing.account_id를 바깥 a.account_id와 연결해 둘째 correlated subquery를 만든다.
입력
현재 계좌에 속한 출금 행만 거부할 때
결과·효과
출금 탐색 범위가 현재 바깥 account_id로 좁혀진다.
비유의 한계
계좌 연결만으로 출금 종류가 정해지지는 않는다.
21줄F10-L21 AND outgoing.entry_type IN ('WITHDRAW', 'TRANSFER_OUT') WITHDRAW 또는 TRANSFER_OUT 도장이 하나라도 보이면 후보를 탈락시킨다. outgoing.entry_type을 WITHDRAW와 TRANSFER_OUT으로 제한한다.
입력
NOT EXISTS가 금지할 출금 방향 집합을 정할 때
결과·효과
WITHDRAW·TRANSFER_OUT match가 하나라도 있으면 NOT EXISTS가 false가 된다.
비유의 한계
다른 debit 성격의 새 entry_type은 이 두 값 목록에 자동 포함되지 않는다.
22줄F10-L22 ) 출금 증거가 없을 때만 통과시키는 NOT EXISTS 검사창을 닫는다. 둘째 correlated NOT EXISTS 조건을 닫는다.
입력
계좌 연결과 출금 종류 조건이 완성된 뒤
결과·효과
입금은 있고 출금 match는 없는 account만 최종 result 후보로 남는다.
비유의 한계
원장 관점만 검사하며 business_tx status는 확인하지 않는다.
23줄F10-L23 ORDER BY a.account_id; 합격한 102·105 계좌표를 account_id 오름차순으로 정렬한다. 최종 결과를 account_id 오름차순으로 정렬하고 query를 끝낸다.
입력
두 fixture 계좌를 결정적인 순서로 대조할 때
결과·효과
account 102와 105가 ID 오름차순으로 반환된다.
비유의 한계
정렬은 EXISTS의 NULL-safe anti semantics나 행 수를 바꾸지 않는다.
W13-SQL-Q18 · SELECT 1의 실제 의미
  1. subquery의 SELECT 1은 금액이 1인 원장을 뜻하나요?

  2. 아니야. EXISTS는 projection 값을 반환하지 않고 matching row가 있는지만 보므로 상수 1이면 충분해.

  3. SELECT 1을 금액 필터로 읽으면 signed_amount가 1000인 실제 DEPOSIT도 놓친다는 잘못된 해석이 돼.

  4. SELECT 1을 SELECT incoming.signed_amount로 바꿔도 matching 행 집합이 같으면 EXISTS boolean은 같아요.

05

STEP 05 / 13

원본 코드 조각

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

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

F10-C01 · scope, assumptions, and workbook search path1–6줄
1–6줄 원본
-- W13-SQL-Q18 illustrative example; canonical_answer=false
-- Assumption: DEPOSIT/TRANSFER_IN are cash in; WITHDRAW/TRANSFER_OUT are cash out.
-- Expected cardinality: 2 rows on the supplied workbook fixture (accounts 102 and 105).
-- This is one auditable learning example, not the workbook's shipped answer.
\set ON_ERROR_STOP on
SET search_path TO :"workbook_schema", public;
F10-C02 · require at least one incoming row7–15줄
7–15줄 원본

SELECT a.account_id,
       a.account_no
FROM account AS a
WHERE EXISTS (
    SELECT 1
    FROM ledger_entry AS incoming
    WHERE incoming.account_id = a.account_id
      AND incoming.entry_type IN ('DEPOSIT', 'TRANSFER_IN')
F10-C03 · reject every outgoing row and sort16–23줄
16–23줄 원본
)
  AND NOT EXISTS (
    SELECT 1
    FROM ledger_entry AS outgoing
    WHERE outgoing.account_id = a.account_id
      AND outgoing.entry_type IN ('WITHDRAW', 'TRANSFER_OUT')
)
ORDER BY a.account_id;
06

STEP 06 / 13

코드 한 줄씩 한국어로 번역

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

전체 번역 22 / 22

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

원본한국어 번역
1-- W13-SQL-Q18 illustrative example; canonical_answer=false이 SQL이 W13-SQL-Q18용 illustrative example이며 canonical answer가 아님을 선언한다.
2-- Assumption: DEPOSIT/TRANSFER_IN are cash in; WITHDRAW/TRANSFER_OUT are cash out.입금과 출금으로 볼 네 entry_type을 예시 가정으로 고정한다.
3-- Expected cardinality: 2 rows on the supplied workbook fixture (accounts 102 and 105).제공 workbook fixture에서 예상 결과가 account 102와 105의 2행임을 밝힌다.
4-- This is one auditable learning example, not the workbook's shipped answer.이 파일이 workbook의 shipped answer가 아닌 새 학습 예시임을 재확인한다.
5\set ON_ERROR_STOP onpsql ON_ERROR_STOP을 활성화한다.
6SET search_path TO :"workbook_schema", public;workbook_schema와 public을 search_path로 지정한다.
8SELECT a.account_id,최종 SELECT가 account_id를 출력한다.
9 a.account_no각 결과 행에 account_no를 추가한다.
10FROM account AS aquery의 기준 relation을 account a로 정한다.
11WHERE EXISTS (현재 account와 연관된 입금 방향 원장 존재를 요구하는 EXISTS를 시작한다.
12 SELECT 1EXISTS subquery에서 행의 존재만 필요해 상수 1을 SELECT한다.
13 FROM ledger_entry AS incoming첫 subquery의 원본을 ledger_entry incoming으로 정한다.
14 WHERE incoming.account_id = a.account_idincoming.account_id를 바깥 a.account_id와 같게 해 correlated subquery를 만든다.
15 AND incoming.entry_type IN ('DEPOSIT', 'TRANSFER_IN')incoming.entry_type을 DEPOSIT과 TRANSFER_IN으로 제한한다.
16)첫 correlated EXISTS 조건을 닫는다.
17 AND NOT EXISTS (현재 account에 출금 방향 원장이 없음을 요구하는 NOT EXISTS를 시작한다.
18 SELECT 1NOT EXISTS subquery가 matching 행의 존재 여부만 보도록 상수 1을 SELECT한다.
19 FROM ledger_entry AS outgoing둘째 subquery의 원본을 ledger_entry outgoing으로 정한다.
20 WHERE outgoing.account_id = a.account_idoutgoing.account_id를 바깥 a.account_id와 연결해 둘째 correlated subquery를 만든다.
21 AND outgoing.entry_type IN ('WITHDRAW', 'TRANSFER_OUT')outgoing.entry_type을 WITHDRAW와 TRANSFER_OUT으로 제한한다.
22)둘째 correlated NOT EXISTS 조건을 닫는다.
23ORDER BY a.account_id;최종 결과를 account_id 오름차순으로 정렬하고 query를 끝낸다.
07

STEP 07 / 13

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

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

한 줄로 읽기

각 계좌에 입금 방향 원장이 하나 이상 있고 출금 방향 원장은 하나도 없는지 correlated EXISTS 두 개로 판정한다.

문법 해부

  • 첫 EXISTS가 102의 TRANSFER_IN 300이나 105의 DEPOSIT처럼 같은 account_id의 incoming 행을 찾는다.
  • NOT EXISTS가 101의 WITHDRAW -500·TRANSFER_OUT -300·-200 같은 outgoing 행이 있는 계좌를 거부한다.
  • SELECT 1은 금액이 아니라 행 존재를 뜻하므로 DEPOSIT 1000도 EXISTS를 만족한다.

실행 순서

  1. incoming 종류와 account_id 상관 조건을 만족시킨다
  2. outgoing subquery가 matching row를 찾지 못한다
  3. incoming EXISTS=true지만 outgoing EXISTS도 true로 만든다
  4. 두 상관 subquery를 각 account_id에 적용하고 정렬한다

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

F10-C01 · scope, assumptions, and workbook search path
문법 해부
네 provenance 주석과 psql/search_path가 네 입출금 종류, fixture 계좌 102·105, 비정본 지위를 고정한다.
실제 값 추적
격리 workbook에서 결과 2행을 기대하되 canonical_answer=false로 읽는다.
정상 예
schema 변수가 준비되면 account와 ledger_entry를 뒤 correlated subquery가 찾는다.
틀린 예·반례
perf_w13에 그대로 실행하면 workbook column/type 계약이 달라 실패할 수 있다.
착각 방지
OPENING·REVERSAL 제외는 예시 정책일 뿐이다.
하지 않는 일
이 범위는 아직 어떤 account도 선택하지 않는다.
다음 연결
첫 EXISTS가 현재 계좌에 입금 방향 행이 하나 이상인지 요구한다.
F10-C02 · require at least one incoming row
문법 해부
account 기준 SELECT와 correlated EXISTS가 같은 account_id의 DEPOSIT/TRANSFER_IN 행 존재를 검사한다.
실제 값 추적
fixture account 102의 TRANSFER_IN 300 한 건이 첫 EXISTS를 true로 만든다.
정상 예
SELECT 1은 금액이 아니라 matching row 존재만 요청하므로 account 101의 DEPOSIT 1000도 incoming match가 된다.
틀린 예·반례
입금 금액이 1이어야 한다고 해석하면 EXISTS projection을 오해한 것이다.
착각 방지
첫 EXISTS만으로 출금이 없다는 결론은 내릴 수 없다.
하지 않는 일
business_tx status와 원장 금액 크기는 검사하지 않는다.
다음 연결
NOT EXISTS가 같은 계좌의 출금 방향 행을 하나도 허용하지 않는다.
F10-C03 · reject every outgoing row and sort
문법 해부
correlated NOT EXISTS가 WITHDRAW/TRANSFER_OUT 부재를 요구하고 마지막 ORDER BY가 account_id 순서를 고정한다.
실제 값 추적
account 102는 outgoing 0건이라 통과하고, account 101은 WITHDRAW -500과 TRANSFER_OUT -300·-200이 있어 탈락하며 최종 결과는 102·105다.
정상 예
account 102처럼 incoming은 있고 outgoing은 없는 계좌가 valid 결과가 된다.
틀린 예·반례
ledger가 0건인 account 103·104는 첫 EXISTS=false라서 outgoing 부재만으로는 통과하지 못한다.
착각 방지
상관 NOT EXISTS는 NULL 목록 비교 함정을 피하지만 새 출금 종류를 두 값 목록에 자동 포함하지 않는다.
하지 않는 일
운영 query 최적성, business_tx status, 공식 workbook 답안을 증명하지 않는다.
다음 연결
이 illustrative 결과와 W13 native performance evidence는 서로 다른 fixture·책임으로 남는다.
W13-SQL-Q18 · 상관 조건이 바깥 계좌를 붙잡는 법
  1. incoming과 outgoing은 어떻게 지금 검사 중인 계좌를 알아요?

  2. 각 subquery의 account_id를 바깥 a.account_id와 같게 둬 account마다 별도의 존재 검사를 실행해.

  3. 그 상관식을 빼면 다른 계좌의 입출금 한 건이 모든 후보 판정에 섞이는 심각한 반례가 생긴다.

  4. 두 subquery 모두 `...account_id = a.account_id`가 있어야 102의 기록만 102 판정에 사용돼요.

08

STEP 08 / 13

실제 값 따라가기

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

순서들어온 값코드가 하는 일나온 값·상태경계
1account 102의 TRANSFER_IN 300 한 건incoming 종류와 account_id 상관 조건을 만족시킨다첫 EXISTS=true가 되어 입금 조건을 통과한다금액 크기와 business_tx status는 보지 않음
2account 102의 WITHDRAW·TRANSFER_OUT 0건outgoing subquery가 matching row를 찾지 못한다NOT EXISTS=true가 되어 account 102가 최종 통과한다새 outgoing entry_type은 목록에 자동 포함되지 않음
3account 101의 DEPOSIT 1000, WITHDRAW -500, TRANSFER_OUT -300·-200incoming EXISTS=true지만 outgoing EXISTS도 true로 만든다NOT EXISTS=false라 account 101은 탈락한다한 outgoing 행만 있어도 탈락
4fixture의 account 8개두 상관 subquery를 각 account_id에 적용하고 정렬한다account 102와 105 두 행이 ID 오름차순으로 남는다account 103·104는 ledger 0건이라 첫 EXISTS=false
W13-SQL-Q18 · 102는 통과하고 101은 탈락
  1. fixture 계좌 102와 101은 각각 어떤 boolean 값을 얻나요?

  2. 102는 TRANSFER_IN 300 때문에 incoming=true이고 outgoing 0건이라 남지만, 101은 입금과 출금 방향이 모두 있어 NOT EXISTS=false가 돼.

  3. 최종 ORDER BY 뒤에는 조건을 모두 만족한 102와 105 두 행만 ID 순서로 보인다.

  4. fixture 결과는 account_id 102, 105 순서의 정확히 두 행인지 확인해요.

09

STEP 09 / 13

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

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

psql

ON_ERROR_STOP과 격리 workbook search_path를 먼저 적용한다.

psql 변수와 fixture 준비는 실행 환경 책임이다.
PostgreSQL planner/executor

각 계좌에 입금 방향 원장이 하나 이상 있고 출금 방향 원장은 하나도 없는지 correlated EXISTS 두 개로 판정한다.

이 예시는 EXPLAIN이나 성능 측정을 실행하지 않는다.
workbook fixture

예상값은 fixture accounts=102,105, rows=2에 묶여 있다.

운영 데이터와 perf_w13 schema로 일반화하지 않는다.
10

STEP 10 / 13

흔한 착각과 틀린 예

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

❌ SELECT 1은 금액 1인 행만 찾는다

왜 틀리나 EXISTS 안의 projection 값은 존재 판정에 쓰이지 않는다

바르게 읽기 SELECT 1을 존재 신호로 읽는다

반례 fixture의 금액 1000인 DEPOSIT도 EXISTS를 만족한다

❌ NOT IN도 언제나 같은 결과다

왜 틀리나 subquery에 NULL이 끼면 UNKNOWN 때문에 결과가 달라질 수 있다

바르게 읽기 상관 NOT EXISTS로 anti condition을 표현한다

반례 NULL 하나가 NOT IN 후보를 전부 막을 수 있다

❌ 출금 없음은 업무거래 실패까지 확인한다

왜 틀리나 이 query는 ledger_entry 종류만 본다

바르게 읽기 원장 관점의 예시라고 한정한다

반례 business_tx status가 필요하면 별도 JOIN 조건이 필요하다

11

STEP 11 / 13

이 코드가 보장하지 않는 것

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

방향 정책

OPENING·REVERSAL의 입출금 취급을 결정하지 않는다

이 책임을 맡는 곳: 업무 정책
거래 상태

business_tx 성공 여부를 검사하지 않는다

이 책임을 맡는 곳: 업무거래 조회
정본성

공식 workbook 답안이나 운영 최적 query를 보장하지 않는다

이 책임을 맡는 곳: 학습 과제 계약
W13-SQL-Q18 · NULL 함정과 원장 관점
  1. NOT EXISTS를 썼으니 거래 상태까지 안전하게 확인한 건가요?

  2. NULL 목록 비교 함정은 피하지만 이 source는 ledger_entry 종류만 보고 business_tx status는 전혀 JOIN하지 않아.

  3. 공식 workbook 답안이나 모든 새 entry_type을 아우르는 운영 정책으로 일반화하면 안 된다.

  4. business_tx 상태 조건이 필요하다면 현재 23줄 밖에 JOIN과 정책을 새로 설계해야 해요.

12

STEP 12 / 13

직접 다시 써보기

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

1단계 · 뜻부터 복원

  • 왜 입금은 EXISTS이고 출금은 NOT EXISTS인가?
  • 왜 SELECT 1로도 존재 여부를 확인할 수 있나?
  • 왜 NOT IN 대신 NOT EXISTS가 NULL 함정을 피하나?

2단계 · 코드 조각 재조립

  1. 첫 EXISTS가 102의 TRANSFER_IN 300이나 105의 DEPOSIT처럼 같은 account_id의 incoming 행을 찾는다.
  2. NOT EXISTS가 101의 WITHDRAW -500·TRANSFER_OUT -300·-200 같은 outgoing 행이 있는 계좌를 거부한다.
  3. SELECT 1은 금액이 아니라 행 존재를 뜻하므로 DEPOSIT 1000도 EXISTS를 만족한다.

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

W13-SQL-Q18 prompt와 fixture 가정을 먼저 적고 전체 SQL을 다시 작성한다.

자가 점검
  • canonical_answer=false와 가정을 적었는가
  • 모든 비어 있지 않은 원본 줄을 보존했는가
  • 출력 cardinality와 반례를 설명했는가
13

STEP 13 / 13

전체 원본 정답

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

학습용 예시 전체 확인하기 · 정본 답안 아님
학습용 예시 · 정본 답안 아님 · fixture-bound illustrative SQLillustrative/sql/W13-SQL-Q18.sqlSHA-256 e413064af01640c52f001b2f0ac93d9ee2c8ce1bd83868b2c50e779d24f2c1ca
Q18 학습용 SQL — 입금 EXISTS와 출금 NOT EXISTS로 계좌 찾기 전체
-- W13-SQL-Q18 illustrative example; canonical_answer=false
-- Assumption: DEPOSIT/TRANSFER_IN are cash in; WITHDRAW/TRANSFER_OUT are cash out.
-- Expected cardinality: 2 rows on the supplied workbook fixture (accounts 102 and 105).
-- This is one auditable learning example, not the workbook's shipped answer.
\set ON_ERROR_STOP on
SET search_path TO :"workbook_schema", public;

SELECT a.account_id,
       a.account_no
FROM account AS a
WHERE EXISTS (
    SELECT 1
    FROM ledger_entry AS incoming
    WHERE incoming.account_id = a.account_id
      AND incoming.entry_type IN ('DEPOSIT', 'TRANSFER_IN')
)
  AND NOT EXISTS (
    SELECT 1
    FROM ledger_entry AS outgoing
    WHERE outgoing.account_id = a.account_id
      AND outgoing.entry_type IN ('WITHDRAW', 'TRANSFER_OUT')
)
ORDER BY a.account_id;