stack-analysis
CHAPTER 96 / 120
읽기 약 2분
FUNCTION
데이터베이스 쿼리 최적화: N+1 + 인덱스
핵심 개념
N+1 검출·include·DataLoader·EXPLAIN·인덱스 — 쿼리 100배 가속.
본문
N+1 문제
// ❌ N+1 — 100명 사용자 → 101 쿼리
const users = await db.user.findMany({ take: 100 });
for (const user of users) {
const posts = await db.post.findMany({ // N번
where: { authorId: user.id },
});
user.posts = posts;
}
// ✅ JOIN으로 1 쿼리
const users = await db.user.findMany({
take: 100,
include: {
posts: { take: 5, orderBy: { createdAt: 'desc' } },
},
});DataLoader (GraphQL)
import DataLoader from 'dataloader';
const userLoader = new DataLoader<string, User>(async (ids) => {
// 한 번에 모든 ID 조회
const users = await db.user.findMany({
where: { id: { in: [...ids] } },
});
// 입력 순서대로 반환
return ids.map(id => users.find(u => u.id === id));
});
// 사용 — 자동 배치
const author1 = await userLoader.load(post1.authorId);
const author2 = await userLoader.load(post2.authorId);
const author3 = await userLoader.load(post3.authorId);
// → 단 1번의 SQL (IN 절)
// GraphQL Resolver
const resolvers = {
Post: {
author: (post, args, ctx) => ctx.userLoader.load(post.authorId),
},
};EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS)
SELECT u.*, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id
ORDER BY post_count DESC
LIMIT 10;
-- 결과 분석
HashAggregate (cost=15234.50..15234.60 rows=10 width=72)
(actual time=234ms rows=10)
-> Hash Right Join (actual time=12ms rows=8765)
-> Seq Scan on posts p ⚠️ -- Full scan!
-> Hash
-> Index Scan on users u ✅
Planning Time: 0.234 ms
Execution Time: 234 ms
-- 인덱스 추가
CREATE INDEX idx_posts_author_id ON posts(author_id);
ANALYZE posts;
-- → Index Scan, 5ms인덱스 전략
-- 1. 단일 컬럼
CREATE INDEX idx_users_email ON users(email);
-- 2. 복합 (순서 중요!)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC);
-- WHERE user_id = ? AND created_at > ? ORDER BY created_at DESC
-- → 활용 ✅
-- 3. 부분 인덱스 (조건부)
CREATE INDEX idx_orders_pending ON orders(user_id)
WHERE status = 'pending';
-- → status='pending'인 행만 인덱스 → 작고 빠름
-- 4. 표현식 인덱스
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
-- WHERE LOWER(email) = LOWER('Alice@x.com') → 활용 ✅
-- 5. 커버링 인덱스 (Index Only Scan)
CREATE INDEX idx_posts_covering ON posts(author_id, status)
INCLUDE (title, created_at);
-- → 인덱스만으로 답할 수 있음 (테이블 안 봄)Cursor 페이지네이션
// ❌ OFFSET — 큰 값에서 느림
SELECT * FROM posts ORDER BY id OFFSET 100000 LIMIT 20;
// ✅ Cursor — 일정 속도
SELECT * FROM posts
WHERE (created_at, id) < ('2026-04-29', 'abc') -- 마지막 cursor
ORDER BY created_at DESC, id DESC
LIMIT 20;
// Prisma
const posts = await db.post.findMany({
take: 20,
...(cursor && {
cursor: { id: cursor },
skip: 1,
}),
orderBy: { createdAt: 'desc' },
});SELECT 최소화
// ❌ 모든 컬럼
const users = await db.user.findMany();
// ✅ 필요한 것만
const users = await db.user.findMany({
select: {
id: true,
name: true,
avatarUrl: true,
},
});
// → 데이터 전송량 80% 감소
// → 메모리 사용 감소
// → JSON 직렬화 빠름Connection Pool
// Prisma 자동 — 환경변수
DATABASE_URL="postgresql://...?connection_limit=20&pool_timeout=20"
// 권장:
// - 1개 인스턴스: 10~20
// - 서버리스 (Vercel): 5
// - 모놀리스: 50~100
// pgBouncer로 효율 향상
DATABASE_URL="postgresql://localhost:6432/myapp"
DIRECT_URL="postgresql://localhost:5432/myapp" # 마이그레이션용슬로우 쿼리 모니터링
-- pg_stat_statements
CREATE EXTENSION pg_stat_statements;
-- TOP 10 느린 쿼리
SELECT
substring(query, 1, 100) as query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- postgresql.conf
log_min_duration_statement = 1000 -- 1초+ 자동 로깅Read Replica (스케일)
// 읽기 전용 복제본 활용
const writeDb = new PrismaClient({ datasources: { db: { url: WRITE_URL } } });
const readDb = new PrismaClient({ datasources: { db: { url: READ_URL } } });
// 쓰기는 primary
await writeDb.post.create({ data: {...} });
// 읽기는 replica (지연 허용)
const posts = await readDb.post.findMany({ where: { ... } });
// 단, replication lag 고려
// 본인이 방금 쓴 데이터를 즉시 읽을 때는 primary
const myPost = await writeDb.post.findUnique({ where: { id } });다음 챕터
CH.97 "메모리 관리: 프로파일링 + 누수 탐지".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 코드의 DB 쿼리 최적화 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
DB 쿼리 최적화 관련 베스트 프랙티스 5가지를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 DB 쿼리 최적화 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 시장의 DB 쿼리 최적화 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
데이터베이스 쿼리 최적화: N+1 + 인덱스는 이 3가지만 확실히 잡으세요
1.N+1은 include/JOIN 또는 DataLoader로 해결
2.복합 인덱스 순서 = 자주 필터되는 컬럼 먼저
3.OFFSET 큰 값은 cursor로 대체 — 일정 속도
공유하기
진행도 96 / 120