yd-db-expert · git:20261004.14d5e02 · 2026-10-04 · sha256 520c684498783120
yd-db-expert git:20261004.14d5e02A
Immutable. This exact content never changes and is served at /api/v1/blob/520c684498783120.
--- name: yd-db-expert description: 엔진과 무관한 관계형 설계를 다룰 때 사용한다 — 스키마 정규화·키·NULL 의미·DB 제약·소프트 삭제·인덱스 선택 원칙·쿼리 규칙(N+1·플레이스홀더·커서 페이징)·트랜잭션 경계·마이그레이션 단계. PostgreSQL 고유 주제는 yd-pg-expert, Oracle 고유 주제는 yd-oracle-expert, SQLite 고유 주제는 yd-sqlite-expert 를 쓴다. --- # yd-db-expert 엔진과 무관한 관계형 설계가 대상이다. 잠금·테이블 재작성·실행계획 읽는 법·타입 세부처럼 엔진마다 다른 동작은 엔진 스킬이 맡는다 — PostgreSQL 은 `yd-pg-expert`, Oracle 은 `yd-oracle-expert`, SQLite 파일은 `yd-sqlite-expert`. 애플리케이션 코드는 각 언어 스킬이 맡는다. 아래 단언 가운데 SQL 의미론에 관한 것은 `scripts/verify_db_claims.py` 가 SQLite 로 재현한다(8장). 같은 의미론은 표준 SQL 이라 PostgreSQL·Oracle 에서도 같다. 엔진마다 갈리는 지점은 본문에 적었다. ## 1. 스키마 설계 — 판단 기준 정규화는 목적이 아니라 **이상현상(anomaly)을 없애는 수단**이다. 3NF 를 기본으로 두고, 역정규화는 **측정된 병목**이 있을 때만, 그리고 **갱신 경로를 하나로 유지**할 수 있을 때만. 설계하기 전에 스스로 답한다: 1. **이 테이블의 한 행은 무엇 하나인가** — 한 문장으로 안 되면 쪼갤 신호다. 2. **자연키인가 대리키인가** — 사업자번호·사번처럼 외부가 소유한 값은 바뀐다. 대리키(식별자)를 두고 자연키에는 유니크 제약을 건다. 3. **이 컬럼이 NULL 일 수 있는 실제 상황은 무엇인가** — 답이 없으면 `NOT NULL`. NULL 은 "모름"이지 "없음"이나 "0"이 아니다. 4. **삭제하면 무엇이 같이 사라져야 하는가** — FK 의 `ON DELETE` 를 의도적으로 정한다. 기본값에 맡기지 않는다. ### 제약은 애플리케이션이 아니라 DB 에 건다 `NOT NULL`·`UNIQUE`·`CHECK`·`FOREIGN KEY` 는 마지막 방어선이다. 애플리케이션 검증은 사용자 경험용이고, 데이터 무결성은 DB 가 보장한다. **버그·수동 작업·다른 클라이언트는 애플리케이션을 우회한다.** 제약이 없는 테이블에 수동 SQL 세 줄을 넣으면 NULL·중복·음수 잔액이 그대로 들어가고, 제약이 있는 테이블은 셋 다 거부한다. ### NULL 의 의미론 - **`NULL = NULL` 은 참이 아니라 NULL 이다.** `WHERE col = NULL` 은 아무 행도 고르지 못한다. `IS NULL` 을 쓴다. `col <> 'x'` 도 NULL 인 행을 고르지 않는다. - **`NOT IN` 서브쿼리에 NULL 이 하나라도 있으면 결과가 통째로 빈다.** 오류도 경고도 없다. 서브쿼리 컬럼이 NULL 을 허용하면 `NOT EXISTS` 를 쓴다. - **`UNIQUE` 는 NULL 끼리 같다고 보지 않는다.** NULL 행이 여러 개여도 통과한다. "값이 하나뿐" 이 규칙이면 `NOT NULL` 을 함께 건다. NULL 도 하나만 허용하는 방법은 엔진마다 다르다. ### 시간과 통화 - 시각은 타임존이 붙은 형식으로 저장한다. 저장은 UTC, 표시에서 변환한다. 엔진별 타입 선택은 엔진 스킬이 맡는다. 사용자 표기는 `YYYY-MM-DD HH:MM:SS.mmm` (KST 가정). - 돈은 고정소수점(`numeric`/`decimal`)이나 최소 단위 정수(원·센트)로 저장한다. 부동소수점 금지. ### 소프트 삭제 `deleted_at` 을 도입하면 **모든 조회에 조건이 붙는다.** 빠뜨린 한 곳이 사고가 된다. 정말 필요하면 뷰나 행 수준 보안으로 강제하고, 아니면 이력 테이블로 옮기는 편이 낫다. **일반 `UNIQUE` 는 탈퇴한 이메일의 재가입을 막는다.** 삭제 표시만 한 행이 값을 쥐고 있어서다. 살아 있는 행에만 거는 부분 유니크 인덱스(`WHERE deleted_at IS NULL`)는 재가입을 허용하고 활성 중복은 막는다. 부분 인덱스가 없는 엔진은 엔진 스킬에서 대안을 찾는다. ## 2. 인덱스 - **WHERE·JOIN·ORDER BY 에 쓰이는 컬럼**이 후보다. 전부 만들지 않는다 — 인덱스는 쓰기 비용과 저장공간을 먹는다. - 복합 인덱스는 앞 컬럼부터 쓰인다. 등호 조건 컬럼을 앞에, 범위 조건 컬럼을 뒤에 둔다. - **FK 를 선언해도 자식 컬럼 인덱스는 생기지 않는다** (PostgreSQL·SQLite 실측). 부모를 지우거나 부모 기준으로 자식을 찾을 때 자식 테이블 전체를 훑으므로 직접 만든다. - **함수를 씌운 조건은 컬럼 인덱스를 못 탄다.** `WHERE lower(name) = ...` 는 `name` 인덱스를 쓰지 않는다. 쿼리를 바꾸거나 표현식 인덱스를 만든다. - 인덱스를 추가하기 전에 **쿼리를 고칠 수 있는지** 먼저 본다. 판단은 추측이 아니라 실행계획으로 하고, 실행계획 읽는 법은 엔진 스킬을 따른다. ## 3. 쿼리 - `SELECT *` 를 애플리케이션 쿼리에 쓰지 않는다. 컬럼이 늘면 전송량이 늘고, 의도치 않은 필드가 새어나간다. - **N+1 을 의심한다.** 고객 50명의 주문 수를 건마다 세면 문이 51개 나간다. `IN` 한 번이면 2개로 끝나고 결과는 같다. 목록을 돌며 조회하는 코드는 조인이나 `IN` 으로 바꾼다. - **오프셋 페이징은 페이지 사이에 행이 들어오면 같은 행을 두 번 보여준다.** 큰 오프셋에서는 느리기도 하다. 정렬 키 기준 커서(`WHERE id < ?`)를 쓴다. - **커서의 정렬 키가 유일하지 않으면 동률 행을 건너뛴다.** 시각처럼 겹칠 수 있는 키는 `(at, id) > (?, ?)` 처럼 유일한 키를 덧붙여 비교한다. - 문자열 조립 금지. **값은 언제나 플레이스홀더.** 이어 붙인 `x' OR '1'='1` 은 전 행을 돌려주고, `O'Brien` 같은 정상 입력도 문법 오류를 낸다. - **플레이스홀더는 값 자리에만 쓸 수 있다.** 테이블·컬럼 이름 자리에 넣으면 문법 오류다. 식별자를 동적으로 넣어야 하면 화이트리스트로 검증하고 인용한다. ## 4. 트랜잭션 - **경계를 명시적으로 정한다.** "이 작업들이 전부 되거나 전부 안 돼야 한다"가 기준이다. 이체의 입금과 출금을 따로 커밋하면 출금이 실패할 때 입금만 남아 돈이 생긴다. - 트랜잭션 안에서 **외부 호출(HTTP·메일)을 하지 않는다.** 락을 잡은 채 네트워크를 기다린다. - 격리수준은 기본으로 두고, 필요한 경우에만 올린다. 올리면 직렬화 실패 재시도가 짝이다. 엔진마다 기본값과 오류 코드가 다르므로 엔진 스킬을 따른다. - 락 순서를 일정하게 유지해 교착을 피한다. - 긴 트랜잭션을 만들지 않는다. 배치는 잘라서 커밋한다. 엔진마다 대가는 다르다 — 정리 작업 지연, 잠금 대기, 언두 공간 부족. ## 5. 마이그레이션 - **되돌릴 수 있게** 쓴다. 되돌릴 수 없으면(데이터 삭제) PR 본문에 명시한다. - 운영 중 스키마 변경은 **잠금 시간**이 관건이다. 어떤 변경이 테이블을 다시 쓰고 무엇을 막는지는 엔진과 버전마다 다르므로 엔진 스킬의 표로 확인한다. 기억으로 판단하지 않는다. - 큰 테이블은 단계를 나눈다: 컬럼 추가 → 백필(배치) → 제약 추가 → 구 컬럼 제거. 각 단계가 따로 배포되어도 애플리케이션이 동작해야 한다. - 적용 전 **백업 또는 되돌릴 계획**을 확인한다. ## 6. 하지 않는 것 - 특정 엔진의 운영(접속·커넥션 상한·진단 쿼리)은 다루지 않는다. doksam db-host 공유 PostgreSQL 클러스터 운영은 `yd-pg-expert` 가 맡는다. - 엔진별 타입·잠금·실행계획 세부를 여기에 적지 않는다. 적으면 엔진 스킬과 어긋난다. ## 7. 완료 조건 - 새 테이블·컬럼에 적절한 제약(`NOT NULL`·FK·`UNIQUE`)이 있고, NULL 허용은 근거가 있다. - NULL 을 허용하는 컬럼을 `NOT IN`·`=` 로 비교하는 곳이 없다. - FK 컬럼과 조회 조건에 인덱스가 있고, 느린 쿼리는 엔진 스킬대로 실행계획을 확인했다. - 목록 조회에 N+1 이 없고, 페이징은 유일한 키 기준 커서다. - 마이그레이션이 되돌릴 수 있거나, 불가능함을 명시했다. 잠금 영향은 엔진 스킬로 확인했다. - 시크릿이 출력·로그·이슈에 노출되지 않았다. ## 8. 이 문서의 단언 재현 ```bash python3 skills/yd-db-expert/scripts/verify_db_claims.py ``` NULL 비교, `NOT IN`, `UNIQUE` 와 NULL, DB 제약, FK 인덱스, 함수 조건, N+1, 오프셋·커서 페이징, 원자성, 플레이스홀더, 소프트 삭제 유니크를 stdlib `sqlite3` 로 재현한다. 하나라도 문서와 다르면 exit 1 이다. 단언과 테스트의 대응은 `tests/claims.json` 이 갖는다.