docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
---
|
||||
id: db-postgres-explain
|
||||
title: Postgres EXPLAIN — Plan / Index / Cost 분석
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [database, postgres, explain, vibe-coding]
|
||||
tech_stack: { language: "SQL / Postgres", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [EXPLAIN, EXPLAIN ANALYZE, query plan, sequential scan, index scan, nested loop, hash join]
|
||||
---
|
||||
|
||||
# Postgres EXPLAIN
|
||||
|
||||
> 느린 query = plan 부터. **`EXPLAIN ANALYZE BUFFERS`** 가 표준. Sequential scan / index scan / join type / actual time 분석. **explain.dalibo.com** 또는 pgMustard 가 시각화.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- EXPLAIN: plan 만 (실행 X).
|
||||
- EXPLAIN ANALYZE: 실제 실행 + 시간.
|
||||
- BUFFERS: 디스크 vs cache hit.
|
||||
- VERBOSE: 상세 정보.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### 기본
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM orders WHERE user_id = 'u1' ORDER BY created_at DESC LIMIT 10;
|
||||
```
|
||||
|
||||
```
|
||||
Limit (cost=0.42..1.20 rows=10 width=128) (actual time=0.05..0.15 rows=10 loops=1)
|
||||
-> Index Scan using orders_user_created on orders (cost=0.42..123.45 rows=1500 width=128) (actual time=0.04..0.14 rows=10 loops=1)
|
||||
Index Cond: (user_id = 'u1')
|
||||
Planning Time: 0.12 ms
|
||||
Execution Time: 0.18 ms
|
||||
```
|
||||
|
||||
→ Index Scan + Index Cond + actual rows 적음 = OK.
|
||||
|
||||
### Sequential scan = warning
|
||||
```
|
||||
Seq Scan on orders (cost=0.00..50000.00 rows=1000000 width=128)
|
||||
Filter: (user_id = 'u1')
|
||||
Rows Removed by Filter: 999999
|
||||
```
|
||||
|
||||
→ 100만 row 모두 읽음. Index 필요.
|
||||
|
||||
### Buffers 확인
|
||||
```sql
|
||||
EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT ...;
|
||||
|
||||
# Output:
|
||||
Buffers: shared hit=42 read=8
|
||||
# hit = cache, read = 디스크
|
||||
# 큰 read = 디스크 I/O bottleneck
|
||||
```
|
||||
|
||||
### Index 추가 + 재실행
|
||||
```sql
|
||||
CREATE INDEX orders_user_created ON orders (user_id, created_at DESC);
|
||||
|
||||
EXPLAIN ANALYZE SELECT ... -- 다시
|
||||
-- Index Scan 으로 변환
|
||||
```
|
||||
|
||||
### 자주 쓰는 plan node
|
||||
```
|
||||
Seq Scan: 풀 스캔 — 큰 테이블 = 느림
|
||||
Index Scan: index 로 row lookup — 빠름
|
||||
Index Only Scan: index 만 — heap 접근 X (covering index)
|
||||
Bitmap Heap Scan: 여러 row, bitmap 으로 batch
|
||||
Hash Join: 양쪽 hash 후 조인 — 큰 join 빠름
|
||||
Nested Loop: 각 row 마다 다른 쪽 lookup — 작은 한쪽
|
||||
Merge Join: 양쪽 sort 후 merge
|
||||
Sort: sort 작업 — work_mem 초과 시 디스크
|
||||
Aggregate: group by / sum
|
||||
HashAggregate: hash 기반 aggregate
|
||||
```
|
||||
|
||||
### Statistics — planner 가 잘못 추측
|
||||
```
|
||||
rows=1000 (실제 100만)
|
||||
→ planner 가 잘못 — Seq Scan 선택할 수 있음
|
||||
```
|
||||
|
||||
```sql
|
||||
ANALYZE orders; -- statistics update
|
||||
```
|
||||
|
||||
→ 자주 쓰는 큰 table = autovacuum 외 명시.
|
||||
|
||||
### Index 사용 안 하는 경우
|
||||
```sql
|
||||
-- ❌ 함수 / 변환
|
||||
SELECT * FROM users WHERE LOWER(email) = 'a@b.com';
|
||||
-- email 인덱스 못 씀
|
||||
|
||||
-- ✅ Functional index
|
||||
CREATE INDEX users_email_lower ON users ((LOWER(email)));
|
||||
|
||||
-- ❌ LIKE leading %
|
||||
WHERE email LIKE '%@gmail.com'
|
||||
|
||||
-- ❌ Type cast
|
||||
WHERE id::text = '42'
|
||||
|
||||
-- ❌ OR with different columns
|
||||
WHERE name = 'a' OR email = 'b'
|
||||
-- → UNION 으로 분리
|
||||
```
|
||||
|
||||
### Composite index 순서
|
||||
```sql
|
||||
-- (user_id, created_at) 인덱스
|
||||
SELECT ... WHERE user_id = $1 -- ✅ 사용
|
||||
SELECT ... WHERE user_id = $1 AND created_at > $2 -- ✅
|
||||
SELECT ... WHERE created_at > $2 -- ❌ leading 안 맞음
|
||||
```
|
||||
|
||||
### LIMIT + ORDER BY = matching index
|
||||
```sql
|
||||
SELECT * FROM events ORDER BY ts DESC LIMIT 10;
|
||||
-- (ts DESC) index = O(log n)
|
||||
|
||||
-- 또는 (user_id, ts DESC)
|
||||
WHERE user_id = $1 ORDER BY ts DESC LIMIT 10;
|
||||
```
|
||||
|
||||
### EXPLAIN 시각화
|
||||
```
|
||||
explain.dalibo.com
|
||||
explain.depesz.com
|
||||
pgMustard (paid)
|
||||
```
|
||||
|
||||
→ 큰 plan tree 가 직관적.
|
||||
|
||||
### auto_explain (slow query log)
|
||||
```sql
|
||||
-- postgresql.conf
|
||||
shared_preload_libraries = 'auto_explain'
|
||||
auto_explain.log_min_duration = '500ms'
|
||||
auto_explain.log_analyze = on
|
||||
auto_explain.log_buffers = on
|
||||
```
|
||||
|
||||
→ 500ms+ query 자동 plan log.
|
||||
|
||||
### pg_stat_statements
|
||||
```sql
|
||||
CREATE EXTENSION pg_stat_statements;
|
||||
|
||||
SELECT query, calls, total_exec_time, mean_exec_time, rows
|
||||
FROM pg_stat_statements
|
||||
ORDER BY total_exec_time DESC LIMIT 20;
|
||||
```
|
||||
|
||||
→ "어떤 query 가 가장 비싸"
|
||||
|
||||
### Common 문제 + fix
|
||||
```
|
||||
1. Seq Scan + 큰 table → CREATE INDEX
|
||||
2. Sort 가 외부 디스크 (Sort Method: external merge) → work_mem 증가
|
||||
3. Nested Loop + 큰 양쪽 → ANALYZE 후 hash join 으로
|
||||
4. 큰 IN list → 임시 테이블 또는 ANY array
|
||||
5. Subquery 가 매 row 실행 → JOIN 또는 LATERAL
|
||||
```
|
||||
|
||||
### work_mem
|
||||
```sql
|
||||
SET work_mem = '64MB'; -- 세션
|
||||
-- 또는 query
|
||||
SELECT /*+ work_mem='64MB' */ ...; -- pg_hint_plan
|
||||
```
|
||||
|
||||
→ Sort / Hash 가 메모리 안 들어가면 외부 디스크.
|
||||
|
||||
### Index size 검사
|
||||
```sql
|
||||
SELECT
|
||||
schemaname, tablename, indexname,
|
||||
pg_size_pretty(pg_relation_size(indexrelid)) AS size
|
||||
FROM pg_indexes
|
||||
JOIN pg_class ON pg_class.relname = indexname
|
||||
ORDER BY pg_relation_size(indexrelid) DESC LIMIT 10;
|
||||
```
|
||||
|
||||
### Unused index
|
||||
```sql
|
||||
SELECT
|
||||
schemaname, relname, indexrelname,
|
||||
idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE idx_scan = 0
|
||||
AND indexrelname NOT LIKE '%_pkey'
|
||||
ORDER BY pg_relation_size(indexrelid) DESC;
|
||||
```
|
||||
|
||||
→ 사용 안 되는 index = drop.
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 증상 | 액션 |
|
||||
|---|---|
|
||||
| 느린 query | EXPLAIN ANALYZE |
|
||||
| Seq Scan 큰 table | Index 추가 |
|
||||
| Sort 외부 디스크 | work_mem 또는 인덱스 |
|
||||
| Wrong rows estimate | ANALYZE |
|
||||
| 매번 느림 | pg_stat_statements |
|
||||
| Unused index | DROP |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **EXPLAIN 만 + ANALYZE 안 함**: 실제 시간 모름.
|
||||
- **Plan 만 보고 OK 가정**: row estimate 가 wrong 일 수 있음.
|
||||
- **SELECT ***: column 다 read.
|
||||
- **함수 / cast index 컬럼**: index 못 씀.
|
||||
- **Statistics 안 update**: planner 잘못.
|
||||
- **Unused index 방치**: 디스크 + INSERT 비용.
|
||||
- **Production 에 ANALYZE 큰 table**: lock. CONCURRENTLY.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- `EXPLAIN (ANALYZE, BUFFERS)` 표준.
|
||||
- explain.dalibo.com 시각화.
|
||||
- pg_stat_statements + auto_explain prod.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[DB_Index_Strategy]]
|
||||
- [[DB_Query_Optimization]]
|
||||
- [[DB_Vacuum_Autovacuum]]
|
||||
Reference in New Issue
Block a user