{"slug":"db-expert","title":"db-expert","summary":"관계형 스키마를 설계·검토하거나 인덱스·쿼리 튜닝·트랜잭션·마이그레이션을 다룰 때, 그리고 doksam pig 의 공유 PostgreSQL 클러스터를 운영할 때 사용한다. SQLite 고유 주제는 sqlite-expert 를 쓴다.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-24T15:42:49.34439Z","repo":{"url":"https://github.com/LeeYudok/doksam-skills","stars":12,"forks":2,"license":"MIT","updatedAt":"2026-09-24T05:35:27Z"},"bodyHtml":"<hr>\n<h2>name: db-expert\ndescription: 관계형 스키마를 설계·검토하거나 인덱스·쿼리 튜닝·트랜잭션·마이그레이션을 다룰 때, 그리고 doksam pig 의 공유 PostgreSQL 클러스터를 운영할 때 사용한다. SQLite 고유 주제는 sqlite-expert 를 쓴다.</h2>\n<h1>db-expert</h1>\n<p>관계형 설계 일반 + PostgreSQL 운영이 대상이다. SQLite 파일을 직접 다루는 문제는\n<code>sqlite-expert</code>, 애플리케이션 코드는 각 언어 스킬이 맡는다.</p>\n<h2>1. 스키마 설계 — 판단 기준</h2>\n<p>정규화는 목적이 아니라 <strong>이상현상(anomaly)을 없애는 수단</strong>이다. 3NF 를 기본으로 두고,\n역정규화는 <strong>측정된 병목</strong>이 있을 때만, 그리고 <strong>갱신 경로를 하나로 유지</strong>할 수 있을 때만.</p>\n<p>읽기 전에 스스로 답한다:</p>\n<ol>\n<li><strong>이 테이블의 한 행은 무엇 하나인가</strong> — 한 문장으로 안 되면 쪼갤 신호다.</li>\n<li><strong>자연키인가 대리키인가</strong> — 사업자번호·사번처럼 외부가 소유한 값은 바뀐다.\n대리키(식별자)를 두고 자연키에는 유니크 제약을 건다.</li>\n<li><strong>이 컬럼이 NULL 일 수 있는 실제 상황은 무엇인가</strong> — 답이 없으면 <code>NOT NULL</code>.\nNULL 은 \"모름\"이지 \"없음\"이나 \"0\"이 아니다.</li>\n<li><strong>삭제하면 무엇이 같이 사라져야 하는가</strong> — FK 의 <code>ON DELETE</code> 를 의도적으로 정한다.\n기본값에 맡기지 않는다.</li>\n</ol>\n<h3>제약은 애플리케이션이 아니라 DB 에 건다</h3>\n<p><code>NOT NULL</code>·<code>UNIQUE</code>·<code>CHECK</code>·<code>FOREIGN KEY</code> 는 마지막 방어선이다. 애플리케이션 검증은\n사용자 경험용이고, 데이터 무결성은 DB 가 보장한다. <strong>버그·수동 작업·다른 클라이언트</strong>는\n애플리케이션을 우회한다.</p>\n<h3>시간과 통화</h3>\n<ul>\n<li>타임스탬프는 <code>timestamptz</code>. <code>timestamp</code>(무TZ)는 서버·클라이언트 타임존이 갈리는 순간 깨진다.</li>\n<li>저장은 UTC, 표시에서 변환. 사용자 표기는 <code>YYYY-MM-DD HH:MM:SS.mmm</code> (KST 가정).</li>\n<li>돈은 <code>numeric</code>. 부동소수점 금지.</li>\n</ul>\n<h3>소프트 삭제</h3>\n<p><code>deleted_at</code> 을 도입하면 <strong>모든 조회에 조건이 붙는다.</strong> 빠뜨린 한 곳이 사고가 된다.\n정말 필요하면 뷰나 RLS 로 강제하고, 아니면 이력 테이블로 옮기는 편이 낫다.</p>\n<h2>2. 인덱스</h2>\n<ul>\n<li><strong>WHERE·JOIN·ORDER BY 에 쓰이는 컬럼</strong>이 후보다. 전부 만들지 않는다 — 인덱스는\n쓰기 비용과 저장공간을 먹는다.</li>\n<li>복합 인덱스는 <strong>앞 컬럼부터</strong> 쓰인다. 카디널리티가 높은 것 또는 등호 조건이 앞이다.</li>\n<li>부분 인덱스로 크기를 줄인다: <code>WHERE status = 'pending'</code> 처럼 대부분이 제외되는 경우.</li>\n<li>FK 컬럼에 인덱스가 없으면 부모 삭제가 풀스캔이 된다. PostgreSQL 은 자동 생성하지 않는다.</li>\n<li><strong>확인은 추측이 아니라 실행계획으로.</strong> <code>EXPLAIN (ANALYZE, BUFFERS) &lt;쿼리&gt;</code>.\n<code>Seq Scan</code> 이 큰 테이블에 보이면 원인을 찾는다.</li>\n</ul>\n<p>인덱스를 추가하기 전에 <strong>쿼리를 고칠 수 있는지</strong> 먼저 본다. 함수를 씌운 컬럼\n(<code>WHERE lower(name) = ...</code>)은 인덱스를 못 타므로, 표현식 인덱스를 만들거나 쿼리를 바꾼다.</p>\n<h2>3. 쿼리</h2>\n<ul>\n<li><code>SELECT *</code> 를 애플리케이션 쿼리에 쓰지 않는다. 컬럼이 늘면 전송량이 늘고,\n의도치 않은 필드가 새어나간다.</li>\n<li><strong>N+1 을 의심한다.</strong> 목록을 돌면서 건마다 조회하는 코드는 조인이나 <code>IN</code> 한 번으로 바꾼다.</li>\n<li>페이징은 큰 오프셋에서 느려진다. 정렬 키 기준 커서(<code>WHERE seq &gt; ?</code>)를 쓴다.</li>\n<li>문자열 조립 금지. <strong>값은 언제나 플레이스홀더.</strong> 식별자를 동적으로 넣어야 하면\n화이트리스트로 검증하고 인용한다.</li>\n</ul>\n<h2>4. 트랜잭션</h2>\n<ul>\n<li><strong>경계를 명시적으로 정한다.</strong> \"이 작업들이 전부 되거나 전부 안 돼야 한다\"가 기준이다.</li>\n<li>트랜잭션 안에서 <strong>외부 호출(HTTP·메일)을 하지 않는다.</strong> 락을 잡은 채 네트워크를 기다린다.</li>\n<li>격리수준은 기본(Read Committed)으로 두고, 필요한 경우에만 올린다. 올릴 때는\n<strong>직렬화 실패 시 재시도</strong>가 짝이다.</li>\n<li>락 순서를 일정하게 유지해 교착을 피한다.</li>\n<li>긴 트랜잭션은 VACUUM 을 막아 테이블을 부풀린다. 배치는 잘라서 커밋한다.</li>\n</ul>\n<h2>5. 마이그레이션</h2>\n<ul>\n<li><strong>되돌릴 수 있게</strong> 쓴다. 되돌릴 수 없으면(데이터 삭제) PR 본문에 명시한다.</li>\n<li>운영 중 스키마 변경은 <strong>잠금 시간</strong>이 관건이다. PostgreSQL 에서\n컬럼 추가(기본값 없는 NULL 허용)는 즉시지만, 타입 변경·<code>NOT NULL</code> 추가는 테이블을 다시 쓴다.\n큰 테이블이면 단계를 나눈다: 컬럼 추가 → 백필(배치) → 제약 추가 → 구 컬럼 제거.</li>\n<li>인덱스는 <code>CREATE INDEX CONCURRENTLY</code> 로 만든다. 일반 생성은 쓰기를 막는다.</li>\n<li>적용 전 <strong>백업 또는 되돌릴 계획</strong>을 확인한다.</li>\n</ul>\n<h2>6. doksam PostgreSQL 운영</h2>\n<p>pig 의 단일 클러스터를 여러 서비스가 공유한다 — gitlab·doksamlabs·srope·sonarqube 등.\n<strong>내 서비스 하나가 클러스터 전체를 마비시킬 수 있다는 전제</strong>로 다룬다.</p>\n<ul>\n<li>접속은 <code>yd_pg</code> MCP(<code>mcp__yd_pg__*</code>). 새로 등록할 때도 이름은 <code>yd_pg</code> 로 통일한다.</li>\n<li><strong><code>max_connections=200</code> 을 여럿이 나눠 쓴다.</strong> 커넥션 풀 상한을 정하지 않은 서비스는\n다른 서비스의 접속을 굶긴다. 애플리케이션마다 상한을 명시한다.</li>\n<li>컨테이너에서는 <code>host.docker.internal</code>(host-gateway)로 접근한다.\n호스트에서 공개 도메인으로 붙으면 NAT hairpin 으로 로컬 PG 에 떨어지므로 내부 IP 를 쓴다.</li>\n<li>계정·비밀번호는 <code>gimje/infra</code> 레포 <code>pig/PG.md</code>. <strong>값을 채팅·로그·이슈에 노출하지 않는다.</strong></li>\n</ul>\n<h3>쓰기 작업 규율</h3>\n<ul>\n<li>조회는 자유롭게. <strong>INSERT/UPDATE/DELETE·DDL 은 사용자의 명시 실행 신호 후에만</strong> 한다.</li>\n<li>대량 변경 전에 <strong>영향 행 수를 먼저 센다.</strong> <code>SELECT count(*)</code> 로 확인하고 보고한 뒤 실행한다.</li>\n<li><code>UPDATE</code>/<code>DELETE</code> 에 <code>WHERE</code> 가 없으면 실행하지 않는다. 예외 없다.</li>\n<li>운영 데이터 이동·삭제는 범위가 확정되지 않으면 시작하지 않는다.</li>\n</ul>\n<h3>진단 시작점</h3>\n<pre><code>-- 지금 무엇이 돌고 있는가 (오래된 것부터)\nSELECT pid, now() - query_start AS dur, state, left(query, 80)\nFROM pg_stat_activity WHERE state &lt;&gt; 'idle' ORDER BY dur DESC LIMIT 20;\n\n-- 커넥션을 누가 쓰고 있는가\nSELECT datname, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;\n\n-- 테이블 부풀림·죽은 튜플\nSELECT relname, n_live_tup, n_dead_tup, last_autovacuum\nFROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;\n</code></pre>\n<p><code>idle in transaction</code> 이 오래 떠 있으면 애플리케이션이 커밋을 안 하고 있는 것이다 —\n락과 VACUUM 을 동시에 막으므로 우선 처리한다.</p>\n<h2>7. 완료 조건</h2>\n<ul>\n<li>새 테이블·컬럼에 적절한 제약(<code>NOT NULL</code>·FK·<code>UNIQUE</code>)이 있고, NULL 허용은 근거가 있음</li>\n<li>조회 조건에 인덱스가 있고, 느린 쿼리는 <code>EXPLAIN (ANALYZE)</code> 로 확인함</li>\n<li>마이그레이션이 되돌릴 수 있거나, 불가능함을 명시함</li>\n<li>운영 클러스터를 만졌으면: 영향 범위를 먼저 세어 보고했고, 커넥션 상한을 확인함</li>\n<li>시크릿이 출력·로그·이슈에 노출되지 않음</li>\n</ul>\n","files":[{"path":"agents/antigravity.md","sizeBytes":730,"isText":true},{"path":"agents/claude.md","sizeBytes":501,"isText":true},{"path":"agents/codex.toml","sizeBytes":466,"isText":true},{"path":"agents/openai.yaml","sizeBytes":269,"isText":true},{"path":"SKILL.md","sizeBytes":7396,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-24T15:43:14.585545Z","sha256":"1D61D00E42DE5B3810987BC2871D930EBB7CCA4E109C5ADCDF8389CF0DF90099","sizeBytes":5757},"review":null,"source":{"repositoryUrl":"https://github.com/LeeYudok/doksam-skills","path":"skills/db-expert","license":"MIT","commit":"841cccdaa8b607e9c5d0adc9c95151c286a8f229","subtreeSha":"0AEE78B1A2E6614CD926A78D8B2661627F5E756EBD512D2C499DC14E4B8DBDA5","lastSyncedAt":"2026-09-24T15:42:49.340415Z"},"reviewedAt":"2026-09-24T15:44:38.341816Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/LeeYudok/doksam-skills/tree/main/skills/db-expert"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install leeyudok-doksam-skills@llmmart"},{"target":"git","command":"git clone https://github.com/LeeYudok/doksam-skills.git"}]}