OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 96 / 120
stack-analysis
CHAPTER 96 / 120
읽기 약 2
FUNCTION

데이터베이스 쿼리 최적화: N+1 + 인덱스


핵심 개념

N+1 검출·include·DataLoader·EXPLAIN·인덱스 — 쿼리 100배 가속.

본문

N+1 문제

TYPESCRIPT📋 코드 (18줄)
// ❌ 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)

TYPESCRIPT📋 코드 (26줄)
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

SQL📋 코드 (25줄)
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

인덱스 전략

SQL📋 코드 (27줄)
-- 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 페이지네이션

TYPESCRIPT📋 코드 (20줄)
// ❌ 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 최소화

TYPESCRIPT📋 코드 (17줄)
// ❌ 모든 컬럼
const users = await db.user.findMany();


// ✅ 필요한 것만
const users = await db.user.findMany({
  select: {
    id: true,
    name: true,
    avatarUrl: true,
  },
});


// → 데이터 전송량 80% 감소
// → 메모리 사용 감소
// → JSON 직렬화 빠름

Connection Pool

TYPESCRIPT📋 코드 (13줄)
// 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"  # 마이그레이션용

슬로우 쿼리 모니터링

SQL📋 코드 (18줄)
-- 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 (스케일)

TYPESCRIPT📋 코드 (16줄)
// 읽기 전용 복제본 활용
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