본문 바로가기

데이터베이스 쿼리 최적화

요청 모양과 실제 SQL을 연결해 N+1·인덱스·projection·pagination 후보를 고르고 실행 계획과 canary 지표로 검증합니다.

NestJS 애플리케이션의 응답 지연은 컨트롤러보다 데이터베이스 왕복, 읽은 row 수, 정렬·조인 비용에서 시작되는 경우가 많습니다. 최적화는 먼저 느린 endpoint와 SQL을 연결하고, 가장 작은 변경을 적용한 뒤 같은 조건의 전후 지표로 채택 여부를 결정하는 작업입니다.

아래 다이어그램은 같은 목록 요청이 TypeORM을 거쳐 PostgreSQL에서 1 + N SQL이 되는 경로와, join 또는 batch로 왕복 수를 제한하는 경로를 비교합니다.

단계별 보기

N+1과 bounded 관계 조회의 paired sequence

bounded batch 발췌 한 경로에서 부모 조회→빈 목록/IN 판정→원 반환 자료를 추적합니다. SQL 수는 원문 전제의 조건부 1·2이고 그룹화·DTO 결합은 추가 구현이며 N+1·join은 독립 비교안으로 원 표에 보존합니다.

batch의 부모 목록 조회

bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다.

batch의 부모 목록 조회

상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

batch의 부모 목록 조회bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 선택 경로: bounded batch 발췌bounded batch 실행선택 경로: bounded batch 발췌SQL 수·반환 범위: 첫 조회: 부모 SQL 1개 계산 전제: 각 repository 호출이 SQL 하나, 추가 eager 조회 없음SQL 수·반환 범위첫 조회: 부모 SQL 1개계산 전제: 각 repository 호출이 SQL하나, 추가 eager 조회 없음
batch의 부모 목록 조회bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 선택 경로: bounded batch 발췌boundedbatch 실행선택 경로: boundedbatch 발췌SQL 수·반환 범위: 첫 조회: 부모 SQL 1개 계산 전제: 각 repository 호출이 SQL 하나, 추가 eager 조회 없음SQL 수·반환범위첫 조회: 부모 SQL1개계산 전제: 각repository 호출이SQL 하나, 추가eager 조회 없음

bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

선택 경로
bounded batch 발췌
첫 조회
부모 SQL 1개
계산 전제
각 repository 호출이 SQL 하나, 추가 eager 조회 없음

이번 단계에서 바뀐 점

  • 부모 결과의 길이가 관계 SQL을 실행할지 결정합니다. N+1과 join은 이 실행의 앞뒤 단계가 아니라 원 표의 별도 비교안입니다.

전체보기
같은 사용자 목록 요청에서 항목마다 관계 SQL을 반복하는 1 더하기 N 경로와 join 또는 IN batch로 왕복 수를 제한하는 경로를 비교하고, 증상에 따라 인덱스·projection·pagination을 선택하는 기준

Nest · TypeORM query comparison

목록 조회의 N+1과 bounded batch

목록 조회 뒤 항목마다 관계를 읽는 경로와 IN batch 경로를 비교합니다. join은 목록·관계를 함께 조회하는 별도 선택이며, 실제 SQL 왕복 수와 행 증폭은 로그·대표 데이터로 확인합니다.

N+1과 bounded 관계 조회 비교 시퀀스 HTTP 목록 요청을 TypeORM이 SQL로 바꾸고 PostgreSQL이 실행한다. 관계를 사용자마다 조회하면 N개의 추가 SQL이 반복되고, 필요한 관계를 join 또는 IN batch로 묶으면 왕복 수가 제한된다. ALT [relation을 항목마다 접근] [관계를 묶어서 조회] GET /users · include orders · take 20 SELECT users · take 20 users ≤ 20 N × SELECT orders WHERE user_id = ? rows × N · 1 + N WHERE user_id IN (...) bounded round trips page DTO HTTP 요청 TypeORM PostgreSQL planner · executor 관계 접근 횟수가 데이터 개수와 함께 늘어난다 join의 행 증폭과 batch의 추가 왕복을 실제 데이터로 비교한다
증상에서 시작해 가장 작은 조회 변경을 고르는 기준
관측한 증상먼저 검토할 선택반드시 확인할 근거
같은 relation SQL 반복명시적 join 또는 IN batch왕복 수와 join 행 증폭
filter·sort의 읽기 비용 증가실제 predicate와 정렬을 반영한 index 후보실행 계획, 데이터 분포, 쓰기 비용
넓은 row와 큰 응답 payloadselect projection필드 계약과 전송량
무제한 목록 또는 불안정한 페이지take와 고유 tie-breaker 정렬페이지 중복·누락과 deep offset 비용
같은 목록의 반복 SQL과 bounded batch 동일HTTP목록요청과TypeORM부모조회1개뒤사용자별관계조회또는INbatch를선택한다. 왼쪽은N개의관계SQL이추가되는1+N,오른쪽은비어있지않은목록에서관계SQL을1개로묶는조건의2왕복이다. 빈목록은batch관계조회전에반환한다. join은별도대안이며pagination등에따른실제SQL수와행증폭은로그로검증한다. 결과결합은본문batch발췌에서추가로구현할단계이다. 1+N batch HTTP 목록 요청 관계 필요 · take 20 TypeORM → DB 부모 목록 SQL 1개 관계 조회 선택 개별 접근 / 묶기 개별 조회 N개 SQL 합계1+N N=20:21 IN batch 관계1개 합계2개 빈목록1개 목록 결과 구성 관계 그룹화 · DTO 실제 로그 비교 왕복 · 행증폭 · 지연
요청 GET /users · relation 포함 · page limit
1 + N

항목마다 relation SQL 반복

목록 SQL 한 번 뒤 사용자마다 주문 SQL을 실행하면 데이터 개수에 따라 DB 왕복이 늘어납니다.

bounded

join 또는 IN batch

관계가 필요한 범위를 묶되 join 행 증폭과 batch 추가 왕복을 실제 데이터로 비교합니다.

index

filter·sort 비용

predicate, 정렬, 데이터 분포에 맞는 후보를 실행 계획과 쓰기 비용으로 검증합니다.

select

넓은 row·payload

필요한 필드만 projection하고 응답 계약과 전송량을 확인합니다.

page

무제한·불안정 목록

limit과 고유 tie-breaker 정렬을 두고 중복·누락과 deep offset 비용을 확인합니다.

목록 항목마다 반복되는 관계 SQL은 N+1의 증거입니다. index, projection, pagination은 서로 대체재가 아니라 각각 scan 비용, row 폭, 결과 집합 크기를 줄이는 선택이며 전후 측정으로 채택합니다.

이 예제의 가정

  • bounded batch 발췌 한 경로에서 부모 조회→빈 목록/IN 판정→원 반환 자료를 추적합니다. SQL 수는 원문 전제의 조건부 1·2이고 그룹화·DTO 결합은 추가 구현이며 N+1·join은 독립 비교안으로 원 표에 보존합니다.
  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • HTTP 목록 요청을 TypeORM이 SQL로 바꾸고 PostgreSQL이 실행한다. 관계를 사용자마다 조회하면 N개의 추가 SQL이 반복되고, 필요한 관계를 join 또는 IN batch로 묶으면 왕복 수가 제한된다.
  • 동일 HTTP 목록 요청과 TypeORM 부모 조회 1개 뒤 사용자별 관계 조회 또는 IN batch를 선택한다. 왼쪽은 N개의 관계 SQL이 추가되는 1+N, 오른쪽은 비어 있지 않은 목록에서 관계 SQL을 1개로 묶는 조건의 2왕복이다. 빈 목록은 batch 관계 조회 전에 반환한다. join은 별도 대안이며 pagination 등에 따른 실제 SQL 수와 행 증폭은 로그로 검증한다. 결과 결합은 본문 batch 발췌에서 추가로 구현할 단계다.
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1N+1과 bounded 관계 조회의 paired sequence
2같은 사용자 목록 요청에서 항목마다 관계 SQL을 반복하는 1 더하기 N 경로와 join 또는 IN batch로 왕복 수를 제한하는 경로를 비교하고, 증상에 따라 인덱스·projection·pagination을 선택하는 기준
3Nest · TypeORM query comparison
4목록 조회의 N+1과 bounded batch
5목록 조회 뒤 항목마다 관계를 읽는 경로와
6IN
7batch 경로를 비교합니다. join은 목록·관계를 함께 조회하는 별도 선택이며, 실제 SQL 왕복 수와 행 증폭은 로그·대표 데이터로 확인합니다.
8N+1과 bounded 관계 조회 비교 시퀀스
9HTTP 목록 요청을 TypeORM이 SQL로 바꾸고 PostgreSQL이 실행한다. 관계를 사용자마다 조회하면 N개의 추가 SQL이 반복되고, 필요한 관계를 join 또는 IN batch로 묶으면 왕복 수가 제한된다.
10ALT
11[relation을 항목마다 접근]
12[관계를 묶어서 조회]
13GET /users · include orders · take 20
14SELECT users · take 20
15users ≤ 20
16N × SELECT orders WHERE user_id = ?
17rows × N · 1 + N
18WHERE user_id IN (...)
19bounded round trips
20page DTO
21HTTP 요청
22TypeORM
23PostgreSQL
24planner · executor
25관계 접근 횟수가 데이터 개수와 함께 늘어난다
26join의 행 증폭과 batch의 추가 왕복을 실제 데이터로 비교한다
27증상에서 시작해 가장 작은 조회 변경을 고르는 기준
28관측한 증상
29먼저 검토할 선택
30반드시 확인할 근거
31같은 relation SQL 반복
32명시적 join 또는
33batch
34왕복 수와 join 행 증폭
35filter·sort의 읽기 비용 증가
36실제 predicate와 정렬을 반영한 index 후보
37실행 계획, 데이터 분포, 쓰기 비용
38넓은 row와 큰 응답 payload
39select
40projection
41필드 계약과 전송량
42무제한 목록 또는 불안정한 페이지
43take
44와 고유 tie-breaker 정렬
45페이지 중복·누락과 deep offset 비용
46같은 목록의 반복 SQL과 bounded batch
47동일 HTTP 목록 요청과 TypeORM 부모 조회 1개 뒤 사용자별 관계 조회 또는 IN batch를 선택한다. 왼쪽은 N개의 관계 SQL이 추가되는 1+N, 오른쪽은 비어 있지 않은 목록에서 관계 SQL을 1개로 묶는 조건의 2왕복이다. 빈 목록은 batch 관계 조회 전에 반환한다. join은 별도 대안이며 pagination 등에 따른 실제 SQL 수와 행 증폭은 로그로 검증한다. 결과 결합은 본문 batch 발췌에서 추가로 구현할 단계다.
481+N
49HTTP 목록 요청
50관계 필요 · take 20
51TypeORM → DB
52부모 목록 SQL 1개
53관계 조회 선택
54개별 접근 / 묶기
55개별 조회
56N개 SQL
57합계 1+N
58N=20:21
59IN batch
60관계 1개
61합계 2개
62빈 목록 1개
63목록 결과 구성
64관계 그룹화 · DTO
65실제 로그 비교
66왕복 · 행 증폭 · 지연
67요청
68GET /users
69· relation 포함 · page limit
701 + N
71항목마다 relation SQL 반복
72목록 SQL 한 번 뒤 사용자마다 주문 SQL을 실행하면 데이터 개수에 따라 DB 왕복이 늘어납니다.
73bounded
74join 또는
75관계가 필요한 범위를 묶되 join 행 증폭과 batch 추가 왕복을 실제 데이터로
76비교합니다.
77index
78filter·sort 비용
79predicate, 정렬, 데이터 분포에 맞는 후보를 실행 계획과
80쓰기 비용으로
81검증합니다.
82넓은 row·payload
83필요한 필드만 projection하고 응답 계약과 전송량을
84확인합니다.
85page
86무제한·불안정 목록
87limit과 고유 tie-breaker 정렬을 두고 중복·누락과 deep offset 비용을 검증합니다.
88목록 항목마다 반복되는 관계 SQL은 N+1의 증거입니다. index, projection,
89pagination은
90서로 대체재가 아니라 각각 scan 비용, row 폭, 결과 집합 크기를 줄이는 선택이며 전후 측정으로 채택합니다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
batch의 부모 목록 조회선택 경로: bounded batch 발췌 · 첫 조회: 부모 SQL 1개 · 계산 전제: 각 repository 호출이 SQL 하나, 추가 eager 조회 없음부모 결과의 길이가 관계 SQL을 실행할지 결정합니다. N+1과 join은 이 실행의 앞뒤 단계가 아니라 원 표의 별도 비교안입니다.
빈 목록과 IN 조회 판정부모 없음: 관계 SQL 생략·총 1개 · 부모 있음: IN 주문 SQL 1개·총 2개 · N+1 비교: 원래 조건에서 1+N, 20명은 21개같은 판정 지점에서 빈 목록은 종료하고 비어 있지 않은 목록만 관계 조회로 진행합니다. join의 실제 SQL 수는 별도 로그 확인 대상입니다.
발췌의 반환 범위빈 목록 결과: [] · 비어 있지 않은 결과: 별도 users·orders 자료 · 추가 구현: userId 그룹화·사용자 DTO 결합 · join 경계: 행 증폭·ORM pagination 조건을 따로 확인두 자료를 얻은 상태까지가 발췌의 산출물입니다. 사용자별 관계 DTO를 이미 결합했다고 확대하지 않습니다.
  1. 1 / 3단계

    batch의 부모 목록 조회

    bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다.

    batch의 부모 목록 조회

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    batch의 부모 목록 조회bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 선택 경로: bounded batch 발췌bounded batch 실행선택 경로: bounded batch 발췌SQL 수·반환 범위: 첫 조회: 부모 SQL 1개 계산 전제: 각 repository 호출이 SQL 하나, 추가 eager 조회 없음SQL 수·반환 범위첫 조회: 부모 SQL 1개계산 전제: 각 repository 호출이 SQL하나, 추가 eager 조회 없음
    batch의 부모 목록 조회bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 선택 경로: bounded batch 발췌boundedbatch 실행선택 경로: boundedbatch 발췌SQL 수·반환 범위: 첫 조회: 부모 SQL 1개 계산 전제: 각 repository 호출이 SQL 하나, 추가 eager 조회 없음SQL 수·반환범위첫 조회: 부모 SQL1개계산 전제: 각repository 호출이SQL 하나, 추가eager 조회 없음

    bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    선택 경로
    bounded batch 발췌
    첫 조회
    부모 SQL 1개
    계산 전제
    각 repository 호출이 SQL 하나, 추가 eager 조회 없음

    이번 단계에서 바뀐 점

    • 부모 결과의 길이가 관계 SQL을 실행할지 결정합니다. N+1과 join은 이 실행의 앞뒤 단계가 아니라 원 표의 별도 비교안입니다.
  2. 2 / 3단계

    빈 목록과 IN 조회 판정

    부모가 비면 즉시 []로 끝내고, 부모가 있으면 userId IN 조건으로 주문을 한 번 읽습니다.

    빈 목록과 IN 조회 판정

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    빈 목록과 IN 조회 판정부모가 비면 즉시 []로 끝내고, 부모가 있으면 userId IN 조건으로 주문을 한 번 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 부모 없음: 관계 SQL 생략·총 1개bounded batch 실행부모 없음: 관계 SQL 생략·총 1개SQL 수·반환 범위: 부모 있음: IN 주문 SQL 1개·총 2개 N+1 비교: 원래 조건에서 1+N, 20명은 21개SQL 수·반환 범위부모 있음: IN 주문 SQL 1개·총 2개N+1 비교: 원래 조건에서 1+N, 20명은 21개
    빈 목록과 IN 조회 판정부모가 비면 즉시 []로 끝내고, 부모가 있으면 userId IN 조건으로 주문을 한 번 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 부모 없음: 관계 SQL 생략·총 1개boundedbatch 실행부모 없음: 관계 SQL생략·총 1개SQL 수·반환 범위: 부모 있음: IN 주문 SQL 1개·총 2개 N+1 비교: 원래 조건에서 1+N, 20명은 21개SQL 수·반환범위부모 있음: IN 주문SQL 1개·총 2개N+1 비교: 원래조건에서 1+N,20명은 21개

    부모가 비면 즉시 []로 끝내고, 부모가 있으면 userId IN 조건으로 주문을 한 번 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    부모 없음
    관계 SQL 생략·총 1개
    부모 있음
    IN 주문 SQL 1개·총 2개
    N+1 비교
    원래 조건에서 1+N, 20명은 21개

    이번 단계에서 바뀐 점

    • 같은 판정 지점에서 빈 목록은 종료하고 비어 있지 않은 목록만 관계 조회로 진행합니다. join의 실제 SQL 수는 별도 로그 확인 대상입니다.
  3. 3 / 3단계

    발췌의 반환 범위

    빈 목록은 []를, 비어 있지 않은 목록은 { users, orders }를 반환합니다.

    발췌의 반환 범위

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    발췌의 반환 범위빈 목록은 []를, 비어 있지 않은 목록은 { users, orders }를 반환합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 빈 목록 결과: []bounded batch 실행빈 목록 결과: []SQL 수·반환 범위: 비어 있지 않은 결과: 별도 users·orders 자료 추가 구현: userId 그룹화·사용자 DTO 결합 join 경계: 행 증폭·ORM pagination 조건을 따로 확인SQL 수·반환 범위비어 있지 않은 결과: 별도 users·orders자료추가 구현: userId 그룹화·사용자 DTO 결합join 경계: 행 증폭·ORM pagination조건을 따로 확인
    발췌의 반환 범위빈 목록은 []를, 비어 있지 않은 목록은 { users, orders }를 반환합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 빈 목록 결과: []boundedbatch 실행빈 목록 결과: []SQL 수·반환 범위: 비어 있지 않은 결과: 별도 users·orders 자료 추가 구현: userId 그룹화·사용자 DTO 결합 join 경계: 행 증폭·ORM pagination 조건을 따로 확인SQL 수·반환범위비어 있지 않은 결과:별도 users·orders 자료추가 구현: userId그룹화·사용자 DTO결합join 경계: 행증폭·ORM pagination 조건을 따로 확인

    빈 목록은 []를, 비어 있지 않은 목록은 { users, orders }를 반환합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    빈 목록 결과
    []
    비어 있지 않은 결과
    별도 users·orders 자료
    추가 구현
    userId 그룹화·사용자 DTO 결합
    join 경계
    행 증폭·ORM pagination 조건을 따로 확인

    이번 단계에서 바뀐 점

    • 두 자료를 얻은 상태까지가 발췌의 산출물입니다. 사용자별 관계 DTO를 이미 결합했다고 확대하지 않습니다.

N+1과 bounded 관계 조회의 paired sequence

bounded batch 발췌 한 경로에서 부모 조회→빈 목록/IN 판정→원 반환 자료를 추적합니다. SQL 수는 원문 전제의 조건부 1·2이고 그룹화·DTO 결합은 추가 구현이며 N+1·join은 독립 비교안으로 원 표에 보존합니다.

이 예제의 가정

  • bounded batch 발췌 한 경로에서 부모 조회→빈 목록/IN 판정→원 반환 자료를 추적합니다. SQL 수는 원문 전제의 조건부 1·2이고 그룹화·DTO 결합은 추가 구현이며 N+1·join은 독립 비교안으로 원 표에 보존합니다.
  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • HTTP 목록 요청을 TypeORM이 SQL로 바꾸고 PostgreSQL이 실행한다. 관계를 사용자마다 조회하면 N개의 추가 SQL이 반복되고, 필요한 관계를 join 또는 IN batch로 묶으면 왕복 수가 제한된다.
  • 동일 HTTP 목록 요청과 TypeORM 부모 조회 1개 뒤 사용자별 관계 조회 또는 IN batch를 선택한다. 왼쪽은 N개의 관계 SQL이 추가되는 1+N, 오른쪽은 비어 있지 않은 목록에서 관계 SQL을 1개로 묶는 조건의 2왕복이다. 빈 목록은 batch 관계 조회 전에 반환한다. join은 별도 대안이며 pagination 등에 따른 실제 SQL 수와 행 증폭은 로그로 검증한다. 결과 결합은 본문 batch 발췌에서 추가로 구현할 단계다.
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1N+1과 bounded 관계 조회의 paired sequence
2같은 사용자 목록 요청에서 항목마다 관계 SQL을 반복하는 1 더하기 N 경로와 join 또는 IN batch로 왕복 수를 제한하는 경로를 비교하고, 증상에 따라 인덱스·projection·pagination을 선택하는 기준
3Nest · TypeORM query comparison
4목록 조회의 N+1과 bounded batch
5목록 조회 뒤 항목마다 관계를 읽는 경로와
6IN
7batch 경로를 비교합니다. join은 목록·관계를 함께 조회하는 별도 선택이며, 실제 SQL 왕복 수와 행 증폭은 로그·대표 데이터로 확인합니다.
8N+1과 bounded 관계 조회 비교 시퀀스
9HTTP 목록 요청을 TypeORM이 SQL로 바꾸고 PostgreSQL이 실행한다. 관계를 사용자마다 조회하면 N개의 추가 SQL이 반복되고, 필요한 관계를 join 또는 IN batch로 묶으면 왕복 수가 제한된다.
10ALT
11[relation을 항목마다 접근]
12[관계를 묶어서 조회]
13GET /users · include orders · take 20
14SELECT users · take 20
15users ≤ 20
16N × SELECT orders WHERE user_id = ?
17rows × N · 1 + N
18WHERE user_id IN (...)
19bounded round trips
20page DTO
21HTTP 요청
22TypeORM
23PostgreSQL
24planner · executor
25관계 접근 횟수가 데이터 개수와 함께 늘어난다
26join의 행 증폭과 batch의 추가 왕복을 실제 데이터로 비교한다
27증상에서 시작해 가장 작은 조회 변경을 고르는 기준
28관측한 증상
29먼저 검토할 선택
30반드시 확인할 근거
31같은 relation SQL 반복
32명시적 join 또는
33batch
34왕복 수와 join 행 증폭
35filter·sort의 읽기 비용 증가
36실제 predicate와 정렬을 반영한 index 후보
37실행 계획, 데이터 분포, 쓰기 비용
38넓은 row와 큰 응답 payload
39select
40projection
41필드 계약과 전송량
42무제한 목록 또는 불안정한 페이지
43take
44와 고유 tie-breaker 정렬
45페이지 중복·누락과 deep offset 비용
46같은 목록의 반복 SQL과 bounded batch
47동일 HTTP 목록 요청과 TypeORM 부모 조회 1개 뒤 사용자별 관계 조회 또는 IN batch를 선택한다. 왼쪽은 N개의 관계 SQL이 추가되는 1+N, 오른쪽은 비어 있지 않은 목록에서 관계 SQL을 1개로 묶는 조건의 2왕복이다. 빈 목록은 batch 관계 조회 전에 반환한다. join은 별도 대안이며 pagination 등에 따른 실제 SQL 수와 행 증폭은 로그로 검증한다. 결과 결합은 본문 batch 발췌에서 추가로 구현할 단계다.
481+N
49HTTP 목록 요청
50관계 필요 · take 20
51TypeORM → DB
52부모 목록 SQL 1개
53관계 조회 선택
54개별 접근 / 묶기
55개별 조회
56N개 SQL
57합계 1+N
58N=20:21
59IN batch
60관계 1개
61합계 2개
62빈 목록 1개
63목록 결과 구성
64관계 그룹화 · DTO
65실제 로그 비교
66왕복 · 행 증폭 · 지연
67요청
68GET /users
69· relation 포함 · page limit
701 + N
71항목마다 relation SQL 반복
72목록 SQL 한 번 뒤 사용자마다 주문 SQL을 실행하면 데이터 개수에 따라 DB 왕복이 늘어납니다.
73bounded
74join 또는
75관계가 필요한 범위를 묶되 join 행 증폭과 batch 추가 왕복을 실제 데이터로
76비교합니다.
77index
78filter·sort 비용
79predicate, 정렬, 데이터 분포에 맞는 후보를 실행 계획과
80쓰기 비용으로
81검증합니다.
82넓은 row·payload
83필요한 필드만 projection하고 응답 계약과 전송량을
84확인합니다.
85page
86무제한·불안정 목록
87limit과 고유 tie-breaker 정렬을 두고 중복·누락과 deep offset 비용을 검증합니다.
88목록 항목마다 반복되는 관계 SQL은 N+1의 증거입니다. index, projection,
89pagination은
90서로 대체재가 아니라 각각 scan 비용, row 폭, 결과 집합 크기를 줄이는 선택이며 전후 측정으로 채택합니다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
batch의 부모 목록 조회선택 경로: bounded batch 발췌 · 첫 조회: 부모 SQL 1개 · 계산 전제: 각 repository 호출이 SQL 하나, 추가 eager 조회 없음부모 결과의 길이가 관계 SQL을 실행할지 결정합니다. N+1과 join은 이 실행의 앞뒤 단계가 아니라 원 표의 별도 비교안입니다.
빈 목록과 IN 조회 판정부모 없음: 관계 SQL 생략·총 1개 · 부모 있음: IN 주문 SQL 1개·총 2개 · N+1 비교: 원래 조건에서 1+N, 20명은 21개같은 판정 지점에서 빈 목록은 종료하고 비어 있지 않은 목록만 관계 조회로 진행합니다. join의 실제 SQL 수는 별도 로그 확인 대상입니다.
발췌의 반환 범위빈 목록 결과: [] · 비어 있지 않은 결과: 별도 users·orders 자료 · 추가 구현: userId 그룹화·사용자 DTO 결합 · join 경계: 행 증폭·ORM pagination 조건을 따로 확인두 자료를 얻은 상태까지가 발췌의 산출물입니다. 사용자별 관계 DTO를 이미 결합했다고 확대하지 않습니다.
  1. 1 / 3단계

    batch의 부모 목록 조회

    bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다.

    batch의 부모 목록 조회

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    batch의 부모 목록 조회bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 선택 경로: bounded batch 발췌bounded batch 실행선택 경로: bounded batch 발췌SQL 수·반환 범위: 첫 조회: 부모 SQL 1개 계산 전제: 각 repository 호출이 SQL 하나, 추가 eager 조회 없음SQL 수·반환 범위첫 조회: 부모 SQL 1개계산 전제: 각 repository 호출이 SQL하나, 추가 eager 조회 없음
    batch의 부모 목록 조회bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 선택 경로: bounded batch 발췌boundedbatch 실행선택 경로: boundedbatch 발췌SQL 수·반환 범위: 첫 조회: 부모 SQL 1개 계산 전제: 각 repository 호출이 SQL 하나, 추가 eager 조회 없음SQL 수·반환범위첫 조회: 부모 SQL1개계산 전제: 각repository 호출이SQL 하나, 추가eager 조회 없음

    bounded batch 발췌가 take 20과 id 오름차순으로 사용자 목록을 먼저 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    선택 경로
    bounded batch 발췌
    첫 조회
    부모 SQL 1개
    계산 전제
    각 repository 호출이 SQL 하나, 추가 eager 조회 없음

    이번 단계에서 바뀐 점

    • 부모 결과의 길이가 관계 SQL을 실행할지 결정합니다. N+1과 join은 이 실행의 앞뒤 단계가 아니라 원 표의 별도 비교안입니다.
  2. 2 / 3단계

    빈 목록과 IN 조회 판정

    부모가 비면 즉시 []로 끝내고, 부모가 있으면 userId IN 조건으로 주문을 한 번 읽습니다.

    빈 목록과 IN 조회 판정

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    빈 목록과 IN 조회 판정부모가 비면 즉시 []로 끝내고, 부모가 있으면 userId IN 조건으로 주문을 한 번 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 부모 없음: 관계 SQL 생략·총 1개bounded batch 실행부모 없음: 관계 SQL 생략·총 1개SQL 수·반환 범위: 부모 있음: IN 주문 SQL 1개·총 2개 N+1 비교: 원래 조건에서 1+N, 20명은 21개SQL 수·반환 범위부모 있음: IN 주문 SQL 1개·총 2개N+1 비교: 원래 조건에서 1+N, 20명은 21개
    빈 목록과 IN 조회 판정부모가 비면 즉시 []로 끝내고, 부모가 있으면 userId IN 조건으로 주문을 한 번 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 부모 없음: 관계 SQL 생략·총 1개boundedbatch 실행부모 없음: 관계 SQL생략·총 1개SQL 수·반환 범위: 부모 있음: IN 주문 SQL 1개·총 2개 N+1 비교: 원래 조건에서 1+N, 20명은 21개SQL 수·반환범위부모 있음: IN 주문SQL 1개·총 2개N+1 비교: 원래조건에서 1+N,20명은 21개

    부모가 비면 즉시 []로 끝내고, 부모가 있으면 userId IN 조건으로 주문을 한 번 읽습니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    부모 없음
    관계 SQL 생략·총 1개
    부모 있음
    IN 주문 SQL 1개·총 2개
    N+1 비교
    원래 조건에서 1+N, 20명은 21개

    이번 단계에서 바뀐 점

    • 같은 판정 지점에서 빈 목록은 종료하고 비어 있지 않은 목록만 관계 조회로 진행합니다. join의 실제 SQL 수는 별도 로그 확인 대상입니다.
  3. 3 / 3단계

    발췌의 반환 범위

    빈 목록은 []를, 비어 있지 않은 목록은 { users, orders }를 반환합니다.

    발췌의 반환 범위

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    발췌의 반환 범위빈 목록은 []를, 비어 있지 않은 목록은 { users, orders }를 반환합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 빈 목록 결과: []bounded batch 실행빈 목록 결과: []SQL 수·반환 범위: 비어 있지 않은 결과: 별도 users·orders 자료 추가 구현: userId 그룹화·사용자 DTO 결합 join 경계: 행 증폭·ORM pagination 조건을 따로 확인SQL 수·반환 범위비어 있지 않은 결과: 별도 users·orders자료추가 구현: userId 그룹화·사용자 DTO 결합join 경계: 행 증폭·ORM pagination조건을 따로 확인
    발췌의 반환 범위빈 목록은 []를, 비어 있지 않은 목록은 { users, orders }를 반환합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.bounded batch 실행: 빈 목록 결과: []boundedbatch 실행빈 목록 결과: []SQL 수·반환 범위: 비어 있지 않은 결과: 별도 users·orders 자료 추가 구현: userId 그룹화·사용자 DTO 결합 join 경계: 행 증폭·ORM pagination 조건을 따로 확인SQL 수·반환범위비어 있지 않은 결과:별도 users·orders 자료추가 구현: userId그룹화·사용자 DTO결합join 경계: 행증폭·ORM pagination 조건을 따로 확인

    빈 목록은 []를, 비어 있지 않은 목록은 { users, orders }를 반환합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    빈 목록 결과
    []
    비어 있지 않은 결과
    별도 users·orders 자료
    추가 구현
    userId 그룹화·사용자 DTO 결합
    join 경계
    행 증폭·ORM pagination 조건을 따로 확인

    이번 단계에서 바뀐 점

    • 두 자료를 얻은 상태까지가 발췌의 산출물입니다. 사용자별 관계 DTO를 이미 결합했다고 확대하지 않습니다.

요청 모양과 SQL을 먼저 연결하기

다음 항목을 같은 관측 구간에 남깁니다.

  • endpoint와 주요 query parameter
  • p50·p95·p99 응답 시간
  • 요청당 SQL 개수와 SQL fingerprint
  • 반환 row와 읽은 row의 차이
  • DB CPU·I/O·lock wait·connection wait

ORM 로그만 보면 어떤 사용자 요청이 SQL을 만들었는지 놓치기 쉽고, endpoint 지표만 보면 어느 SQL이 느린지 알기 어렵습니다. 요청 ID나 trace ID로 두 기록을 연결하면 최적화 대상을 구체적으로 고를 수 있습니다.

N+1: 데이터 개수와 함께 늘어나는 왕복

목록 SQL 한 번 뒤 각 사용자마다 관계 SQL을 실행하면 요청당 쿼리 수는 1 + N이 됩니다.

src/user/users.service.ts
// 설명을 위한 N+1 예시
async function getUsersWithNPlusOne() {
  const users = await this.userRepository.find({
    take: 20,
    order: { id: 'ASC' },
  });

  return Promise.all(
    users.map(async user => ({
      ...user,
      orders: await this.orderRepository.find({
        where: { userId: user.id },
      }),
    })),
  );
}

예시 동시에 실행해도 SQL 개수는 줄어들지 않는다

다른 eager 조회나 후속 쿼리가 없고 각 repository 호출이 SQL 하나를 실행한다고 가정합니다. 목록이 20명을 반환하면 첫 SQL 1개와 사용자별 주문 SQL 20개를 더해 총 21개입니다. Promise.all()은 주문 요청을 함께 시작할 수 있게 하지만, 20개의 요청을 한 SQL로 합치지는 않습니다.

해결 목표는 무조건 SQL 한 번이 아니라 요청 크기가 늘어도 왕복 횟수가 제한되도록 만드는 것입니다.

관계가 항상 필요한 화면: 명시적 join

async function getUsersWithOrders() {
  return this.userRepository
    .createQueryBuilder('user')
    .leftJoinAndSelect('user.orders', 'order')
    .orderBy('user.id', 'ASC')
    .take(20)
    .getMany();
}

join은 왕복을 줄이지만 일대다 관계가 크면 같은 부모 row가 반복되는 행 증폭과 메모리 사용이 커질 수 있습니다. 실제 관계 cardinality와 실행 계획을 보고 선택합니다.

관계가 선택적이거나 큰 화면: bounded batch

import { In } from 'typeorm';

async function getUsersWithOrderBatch() {
  const users = await this.userRepository.find({
    take: 20,
    order: { id: 'ASC' },
  });

  if (users.length === 0) {
    return [];
  }

  const orders = await this.orderRepository.findBy({
    userId: In(users.map(user => user.id)),
  });

  // 실제 코드에서는 userId로 orders를 그룹화해 사용자 DTO에 결합합니다.
  return { users, orders };
}

이 방식은 부모 조회와 관계 조회를 분리하지만 요청당 왕복 수를 두 번처럼 제한할 수 있습니다. join의 행 증폭과 batch의 추가 왕복 중 어느 쪽이 나은지는 데이터 분포와 화면 계약으로 판단합니다.

직접 판단하기 목록이 5명을 반환했고 각 repository 호출은 위의 조건처럼 SQL 한 개라고 가정합니다. N+1 코드와 bounded batch 코드의 SQL 수는 각각 몇 개일까요? 목록이 비어 있으면 batch 코드가 주문 SQL도 실행할까요? 위의 join 코드는 같은 숫자를 항상 보장한다고 말할 수 있을까요?

호출 흐름으로 계산하기

N+1은 1 + 5 = 6개입니다. batch는 사용자 목록 1개와 IN 주문 조회 1개로 2개입니다. 빈 목록이면 users.length === 0에서 반환하므로 목록 SQL 1개만 실행합니다. join 코드는 ORM의 pagination 처리·관계 설정·DB 드라이버에 따라 추가 SQL이 생길 수 있으므로 실제 로그를 확인해야 합니다. 이 계산은 주어진 repository 호출 조건의 비교이며 모든 ORM 호출의 보편적 SQL 수가 아닙니다.

TypeORM 관계 로딩의 현재 의미

  • relations: { orders: true }는 find* 계열 조회에서 필요한 관계를 명시합니다.
  • eager: true 관계는 find* 계열에서 자동으로 로드됩니다. QueryBuilder에서는 eager 관계에 기대지 말고 leftJoinAndSelect처럼 필요한 join을 명시합니다.
  • eager: false는 lazy loading을 뜻하지 않습니다. TypeORM의 lazy relation은 Promise 타입을 사용하는 별도 설정이며 공식 문서에서도 실험적 기법으로 안내됩니다.
class User {
  @OneToMany(() => Order, order => order.user, { lazy: true })
  orders: Promise<Order[]>;
}

lazy relation을 반복문에서 await하면 N+1을 만들기 쉬우므로 SQL 로그에서 실제 왕복을 확인합니다.

인덱스: 쿼리와 데이터에서 후보를 만든다

인덱스는 WHERE, JOIN, ORDER BY에 자주 쓰인다는 이유만으로 추가하지 않습니다. predicate, 정렬 방향, 데이터 분포, 읽기·쓰기 비율을 함께 보고 실행 계획으로 검증합니다.

src/user/user.entity.ts
import {
  Column,
  Entity,
  Index,
  PrimaryGeneratedColumn,
} from 'typeorm';

@Entity()
@Index('IDX_user_last_first', ['lastName', 'firstName'])
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ unique: true })
  email: string;
}

이 예시는 email의 unique 선언을 컬럼에 한 번만 둡니다. 같은 unique 의미를 @Index(..., { unique: true })와 @Column({ unique: true })로 중복 선언하지 않습니다.

복합 인덱스의 컬럼 순서는 절대 규칙으로 정할 수 없습니다. leading column을 단독으로 쓰는 조회, equality·range 조건의 조합, 정렬, 선택도와 DB planner가 실제로 만든 계획을 함께 봅니다.

Projection: 현재 FindOptions 객체 형태 사용

필요한 필드만 반환하면 row 폭, 네트워크 전송, 직렬화 비용을 줄일 수 있습니다. 현재 TypeORM FindOptions의 select는 필드별 boolean 객체 형태로 작성할 수 있습니다.

const users = await this.userRepository.find({
  select: {
    id: true,
    firstName: true,
    lastName: true,
    email: true,
  },
  order: {
    lastName: 'ASC',
    id: 'ASC',
  },
  take: 20,
});

projection은 API 응답 계약과 함께 관리합니다. 뒤에서 relation이나 serializer가 선택하지 않은 필드가 당연히 존재한다고 가정하지 않도록 테스트합니다.

Pagination: 크기 제한과 고유 tie-breaker

offset pagination은 skip과 take로 표현할 수 있습니다. 결과가 같은 정렬 값에서 흔들리지 않도록 id처럼 고유한 tie-breaker를 마지막에 둡니다.

async function getUsersPage(page: number, pageSize: number) {
  const skip = Math.max(0, page - 1) * pageSize;

  const [rows, total] = await this.userRepository.findAndCount({
    select: {
      id: true,
      firstName: true,
      lastName: true,
      email: true,
    },
    order: {
      lastName: 'ASC',
      id: 'ASC',
    },
    skip,
    take: pageSize,
  });

  return { rows, total };
}

이 함수는 HTTP 입력 검증을 마친 양의 정수 page와 제한된 양의 정수 pageSize를 받는 부분 예제입니다. Math.max()는 음수 offset을 막을 뿐 소수·0 크기·지나치게 큰 pageSize를 거절하는 DTO가 아닙니다. controller 경계에서 정수 여부, page >= 1, 팀이 정한 최대 pageSize를 확인해야 합니다.

findAndCount는 page rows와 count를 함께 반환하는 편의 API입니다. 두 결과가 항상 하나의 동시 snapshot이라고 가정하지 않습니다. 정확히 같은 snapshot이 업무 요구라면 사용하는 데이터베이스와 isolation level에 맞는 transaction 경계를 별도로 설계합니다.

deep offset 비용이나 목록 변경 중 중복·누락이 중요하면 고유 정렬 키를 기준으로 cursor pagination을 검토합니다.

Transaction은 필요한 DB 작업만 짧게

transaction은 일관성을 제공하지만 외부 API 호출, 긴 계산, 사용자 대기를 안에 넣으면 lock 보유 시간이 늘어날 수 있습니다. isolation level과 lock 동작은 데이터베이스와 driver 설정에 따라 다르므로 기본값을 추측하지 말고 실제 구성을 확인합니다.

실행 계획과 전후 지표로 검증하기

TypeORM logging은 개발·검증 환경에서 SQL과 반복 패턴을 찾는 데 유용합니다.

src/app.module.ts
TypeOrmModule.forRoot({
  // ...
  logging: ['query', 'error'],
});

PostgreSQL의 EXPLAIN은 planner의 추정 계획을 보여 줍니다.

EXPLAIN
SELECT id, email
FROM users
WHERE email = '[email protected]';

실제 row와 buffer를 확인하려면 안전한 검증 환경에서 다음처럼 실행할 수 있습니다.

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, email
FROM users
WHERE email = '[email protected]';

ANALYZE 옵션은 문장을 실제로 실행하고 측정 overhead도 추가합니다. INSERT, UPDATE, DELETE 같은 변경 문장은 부작용이 발생하므로 운영에서 무심코 실행하지 않습니다. 필요한 경우 검증용 transaction과 ROLLBACK, 대표 데이터, 충분한 권한 통제를 사용합니다.

아래 루프는 endpoint·SQL 측정부터 ORM 로그와 실행 계획, 최소 변경, canary 전후 비교, keep·rollback·iterate 결정까지를 하나의 측정 기록에 누적합니다.

단계별 보기

측정 근거가 누적되는 쿼리 최적화 루프

기준 측정·SQL 식별·실행 계획·최소 변경·canary 비교·결정 기록의 근거가 쌓인다.

기준과 SQL 근거

기준 측정·SQL 식별·실행 계획을 기록합니다.

기준과 SQL 근거

상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

기준과 SQL 근거기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 근거: baseline·SQL·plan쿼리 개선근거: baseline·SQL·planbaseline·plan·결정: 기록: 중앙baseline·plan·결정기록: 중앙
기준과 SQL 근거기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 근거: baseline·SQL·plan쿼리 개선근거: baseline·SQL·planbaseline·plan·결정: 기록: 중앙baseline·plan·결정기록: 중앙

기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

근거
baseline·SQL·plan
기록
중앙

이번 단계에서 바뀐 점

  • 바꿀 대상을 증거로 좁힙니다.

전체보기
endpoint와 SQL 기준 측정, ORM 로그, 실행 계획, 최소 변경, canary 전후 비교, keep 또는 rollback 결정을 반복하며 SQL fingerprint와 기준선과 결정을 중앙 측정 기록에 누적하는 최적화 루프

Nest · Query optimization loop

측정에서 시작하는 쿼리 최적화

같은 부하와 데이터 조건에서 최소 변경의 전후를 비교합니다. SQL·실행 계획·측정값·채택 또는 rollback 결정을 기록해 다음 측정의 기준선으로 씁니다.

측정 기록을 공유하는 여섯 단계 쿼리 최적화 루프 기준 측정부터 SQL 식별, 실행 계획 확인, 최소 변경, canary 비교, 결정 기록까지 시계 방향으로 반복한다. 각 단계의 증거는 중앙 기록에 쌓이고 마지막 결정의 기준선이 다음 측정을 시작하게 한다. 1. 기준 측정 p95 · SQL 수 2. SQL 식별 ORM log · fingerprint 3. 계획 확인 EXPLAIN · rows · scan 4. 최소 변경 index · query shape 5. Canary 비교 동일 부하 · 전후 6. 결정 기록 keep · rollback · iterate 측정 근거 SQL · plan · metrics baseline · decision 6의 결정과 기준선이 1의 다음 회귀 측정을 시작한다
결정이 다음 기준선으로 돌아가는 최적화 루프 여섯단계는기준측정→SQL식별→계획확인→최소변경→같은조건canary비교→keep/rollback결정의작업순서다. 마지막결정은왼쪽되먹임으로다음기준측정을시작한다. 각단계의오른쪽점선은SQL/plan/metrics/decision을공유기록에누적하는근거관계다. ANALYZE는실제실행하므로안전한검증환경조건을유지한다. 1. 기준 측정 p95 · SQL 수 · 자원 2. SQL 식별 trace · fingerprint 3. 계획 확인 EXPLAIN · rows 4. 최소 변경 index · query shape 5. Canary 비교 같은 부하 · 데이터 6. 결정 기록 keep / rollback 공유 측정 근거 SQL · plan · metrics baseline · decision
공유 기록 SQL fingerprint · plan · before/after · decision
  1. 기준 측정

    endpoint p95, query count, 읽은 row와 DB 자원을 같은 구간에서 남깁니다.

  2. SQL 식별

    ORM log로 반복 SQL과 SQL fingerprint를 측정 기록에 연결합니다.

  3. 계획 확인

    EXPLAIN의 scan과 추정 row를 읽고, ANALYZE로 실제 row를 확인할 때는 안전한 환경에서만 실행 측정을 더합니다.

  4. 최소 변경

    index나 query shape 가운데 관측한 병목을 겨냥한 한 가지 변경을 준비합니다.

  5. Canary 전후 비교

    같은 부하와 데이터 조건에서 before/after 지표를 비교합니다.

  6. Keep · rollback · iterate

    결정과 승인 기준선을 기록합니다. 다음 회귀나 후보 SQL은 이 기준선에서 1번 측정을 다시 시작합니다.

실선 고리는 작업 순서, 안쪽 점선은 각 단계가 중앙 기록에 쓰는 근거입니다. 마지막 결정이 다음 기준선을 만들기 때문에 이 경로는 끝나는 체크리스트가 아니라 실제 운영 루프입니다.

이 예제의 가정

  • 기준 측정·SQL 식별·실행 계획·최소 변경·canary 비교·결정 기록의 근거가 쌓인다.
  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 기준 측정부터 SQL 식별, 실행 계획 확인, 최소 변경, canary 비교, 결정 기록까지 시계 방향으로 반복한다. 각 단계의 증거는 중앙 기록에 쌓이고 마지막 결정의 기준선이 다음 측정을 시작하게 한다.
  • 여섯 단계는 기준 측정→SQL 식별→계획 확인→최소 변경→같은 조건의 canary 비교→keep/rollback 결정의 작업 순서다. 마지막 결정은 왼쪽 되먹임으로 다음 기준 측정을 시작한다. 각 단계의 오른쪽 점선은 SQL/plan/metrics/decision을 공유 기록에 누적하는 근거 관계다. ANALYZE는 실제 실행하므로 안전한 검증 환경 조건을 유지한다.
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1측정 근거가 누적되는 쿼리 최적화 루프
2endpoint와 SQL 기준 측정, ORM 로그, 실행 계획, 최소 변경, canary 전후 비교, keep 또는 rollback 결정을 반복하며 SQL fingerprint와 기준선과 결정을 중앙 측정 기록에 누적하는 최적화 루프
3Nest · Query optimization loop
4측정에서 시작하는 쿼리 최적화
5같은 부하와 데이터 조건에서 최소 변경의 전후를 비교합니다. SQL·실행 계획·측정값·채택 또는 rollback 결정을 기록해 다음 측정의 기준선으로 씁니다.
6측정 기록을 공유하는 여섯 단계 쿼리 최적화 루프
7기준 측정부터 SQL 식별, 실행 계획 확인, 최소 변경, canary 비교, 결정 기록까지 시계 방향으로 반복한다. 각 단계의 증거는 중앙 기록에 쌓이고 마지막 결정의 기준선이 다음 측정을 시작하게 한다.
81. 기준 측정
9p95 · SQL 수
102. SQL 식별
11ORM log ·
12fingerprint
133. 계획 확인
14EXPLAIN · rows ·
15scan
164. 최소 변경
17index · query shape
185. Canary 비교
19동일 부하 · 전후
206. 결정 기록
21keep · rollback ·
22iterate
23측정 근거
24SQL · plan · metrics
25baseline · decision
266의 결정과 기준선이 1의 다음 회귀 측정을 시작한다
27결정이 다음 기준선으로 돌아가는 최적화 루프
28여섯 단계는 기준 측정→SQL 식별→계획 확인→최소 변경→같은 조건의 canary 비교→keep/rollback 결정의 작업 순서다. 마지막 결정은 왼쪽 되먹임으로 다음 기준 측정을 시작한다. 각 단계의 오른쪽 점선은 SQL/plan/metrics/decision을 공유 기록에 누적하는 근거 관계다. ANALYZE는 실제 실행하므로 안전한 검증 환경 조건을 유지한다.
29p95 · SQL 수 · 자원
30trace · fingerprint
31EXPLAIN · rows
32같은 부하 · 데이터
33keep / rollback
34공유 측정 근거
35공유 기록
36SQL fingerprint · plan · before/after · decision
37기준 측정
38endpoint p95, query count, 읽은 row와 DB 자원을 같은 구간에서 남깁니다.
39SQL 식별
40ORM log로 반복 SQL과 SQL fingerprint를 측정 기록에
41연결합니다.
42계획 확인
43EXPLAIN
44의 scan과 추정 row를 읽고, ANALYZE로 실제 row를 확인할 때는 안전한
45환경에서만
46실행 측정을 더합니다.
47최소 변경
48index나 query shape 가운데 관측한 병목을 겨냥한 한 가지 변경을 준비합니다.
49Canary 전후 비교
50같은 부하와 데이터 조건에서 before/after 지표를
51비교합니다.
52Keep · rollback · iterate
53결정과 승인 기준선을 기록합니다. 다음 회귀나 후보 SQL은 이 기준선에서 1번 측정을 다시 시작합니다.
54실선 고리는 작업 순서, 안쪽 점선은 각 단계가 중앙 기록에 쓰는
55근거입니다.
56마지막 결정이 다음 기준선을 만들기 때문에 이 경로는 끝나는
57체크리스트가
58아니라 실제 운영 루프입니다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
기준과 SQL 근거근거: baseline·SQL·plan · 기록: 중앙바꿀 대상을 증거로 좁힙니다.
최소 변경·canary변경: 최소 · 검증: canary 비교변경 효과의 근거를 얻습니다.
결정과 다음 기준산출물: 결정·기준선 · 다음: 측정관측 결과가 후속 판단의 입력이 됩니다.
  1. 1 / 3단계

    기준과 SQL 근거

    기준 측정·SQL 식별·실행 계획을 기록합니다.

    기준과 SQL 근거

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    기준과 SQL 근거기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 근거: baseline·SQL·plan쿼리 개선근거: baseline·SQL·planbaseline·plan·결정: 기록: 중앙baseline·plan·결정기록: 중앙
    기준과 SQL 근거기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 근거: baseline·SQL·plan쿼리 개선근거: baseline·SQL·planbaseline·plan·결정: 기록: 중앙baseline·plan·결정기록: 중앙

    기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    근거
    baseline·SQL·plan
    기록
    중앙

    이번 단계에서 바뀐 점

    • 바꿀 대상을 증거로 좁힙니다.
  2. 2 / 3단계

    최소 변경·canary

    최소 변경을 canary에서 같은 기준과 비교합니다.

    최소 변경·canary

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    최소 변경·canary최소 변경을 canary에서 같은 기준과 비교합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 변경: 최소쿼리 개선변경: 최소baseline·plan·결정: 검증: canary 비교baseline·plan·결정검증: canary 비교
    최소 변경·canary최소 변경을 canary에서 같은 기준과 비교합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 변경: 최소쿼리 개선변경: 최소baseline·plan·결정: 검증: canary 비교baseline·plan·결정검증: canary 비교

    최소 변경을 canary에서 같은 기준과 비교합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    변경
    최소
    검증
    canary 비교

    이번 단계에서 바뀐 점

    • 변경 효과의 근거를 얻습니다.
  3. 3 / 3단계

    결정과 다음 기준

    결정을 기록하고 새 기준을 다음 측정에 사용합니다.

    결정과 다음 기준

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    결정과 다음 기준결정을 기록하고 새 기준을 다음 측정에 사용합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 산출물: 결정·기준선쿼리 개선산출물: 결정·기준선baseline·plan·결정: 다음: 측정baseline·plan·결정다음: 측정
    결정과 다음 기준결정을 기록하고 새 기준을 다음 측정에 사용합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 산출물: 결정·기준선쿼리 개선산출물: 결정·기준선baseline·plan·결정: 다음: 측정baseline·plan·결정다음: 측정

    결정을 기록하고 새 기준을 다음 측정에 사용합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    산출물
    결정·기준선
    다음
    측정

    이번 단계에서 바뀐 점

    • 관측 결과가 후속 판단의 입력이 됩니다.

측정 근거가 누적되는 쿼리 최적화 루프

기준 측정·SQL 식별·실행 계획·최소 변경·canary 비교·결정 기록의 근거가 쌓인다.

이 예제의 가정

  • 기준 측정·SQL 식별·실행 계획·최소 변경·canary 비교·결정 기록의 근거가 쌓인다.
  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 기준 측정부터 SQL 식별, 실행 계획 확인, 최소 변경, canary 비교, 결정 기록까지 시계 방향으로 반복한다. 각 단계의 증거는 중앙 기록에 쌓이고 마지막 결정의 기준선이 다음 측정을 시작하게 한다.
  • 여섯 단계는 기준 측정→SQL 식별→계획 확인→최소 변경→같은 조건의 canary 비교→keep/rollback 결정의 작업 순서다. 마지막 결정은 왼쪽 되먹임으로 다음 기준 측정을 시작한다. 각 단계의 오른쪽 점선은 SQL/plan/metrics/decision을 공유 기록에 누적하는 근거 관계다. ANALYZE는 실제 실행하므로 안전한 검증 환경 조건을 유지한다.
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1측정 근거가 누적되는 쿼리 최적화 루프
2endpoint와 SQL 기준 측정, ORM 로그, 실행 계획, 최소 변경, canary 전후 비교, keep 또는 rollback 결정을 반복하며 SQL fingerprint와 기준선과 결정을 중앙 측정 기록에 누적하는 최적화 루프
3Nest · Query optimization loop
4측정에서 시작하는 쿼리 최적화
5같은 부하와 데이터 조건에서 최소 변경의 전후를 비교합니다. SQL·실행 계획·측정값·채택 또는 rollback 결정을 기록해 다음 측정의 기준선으로 씁니다.
6측정 기록을 공유하는 여섯 단계 쿼리 최적화 루프
7기준 측정부터 SQL 식별, 실행 계획 확인, 최소 변경, canary 비교, 결정 기록까지 시계 방향으로 반복한다. 각 단계의 증거는 중앙 기록에 쌓이고 마지막 결정의 기준선이 다음 측정을 시작하게 한다.
81. 기준 측정
9p95 · SQL 수
102. SQL 식별
11ORM log ·
12fingerprint
133. 계획 확인
14EXPLAIN · rows ·
15scan
164. 최소 변경
17index · query shape
185. Canary 비교
19동일 부하 · 전후
206. 결정 기록
21keep · rollback ·
22iterate
23측정 근거
24SQL · plan · metrics
25baseline · decision
266의 결정과 기준선이 1의 다음 회귀 측정을 시작한다
27결정이 다음 기준선으로 돌아가는 최적화 루프
28여섯 단계는 기준 측정→SQL 식별→계획 확인→최소 변경→같은 조건의 canary 비교→keep/rollback 결정의 작업 순서다. 마지막 결정은 왼쪽 되먹임으로 다음 기준 측정을 시작한다. 각 단계의 오른쪽 점선은 SQL/plan/metrics/decision을 공유 기록에 누적하는 근거 관계다. ANALYZE는 실제 실행하므로 안전한 검증 환경 조건을 유지한다.
29p95 · SQL 수 · 자원
30trace · fingerprint
31EXPLAIN · rows
32같은 부하 · 데이터
33keep / rollback
34공유 측정 근거
35공유 기록
36SQL fingerprint · plan · before/after · decision
37기준 측정
38endpoint p95, query count, 읽은 row와 DB 자원을 같은 구간에서 남깁니다.
39SQL 식별
40ORM log로 반복 SQL과 SQL fingerprint를 측정 기록에
41연결합니다.
42계획 확인
43EXPLAIN
44의 scan과 추정 row를 읽고, ANALYZE로 실제 row를 확인할 때는 안전한
45환경에서만
46실행 측정을 더합니다.
47최소 변경
48index나 query shape 가운데 관측한 병목을 겨냥한 한 가지 변경을 준비합니다.
49Canary 전후 비교
50같은 부하와 데이터 조건에서 before/after 지표를
51비교합니다.
52Keep · rollback · iterate
53결정과 승인 기준선을 기록합니다. 다음 회귀나 후보 SQL은 이 기준선에서 1번 측정을 다시 시작합니다.
54실선 고리는 작업 순서, 안쪽 점선은 각 단계가 중앙 기록에 쓰는
55근거입니다.
56마지막 결정이 다음 기준선을 만들기 때문에 이 경로는 끝나는
57체크리스트가
58아니라 실제 운영 루프입니다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
기준과 SQL 근거근거: baseline·SQL·plan · 기록: 중앙바꿀 대상을 증거로 좁힙니다.
최소 변경·canary변경: 최소 · 검증: canary 비교변경 효과의 근거를 얻습니다.
결정과 다음 기준산출물: 결정·기준선 · 다음: 측정관측 결과가 후속 판단의 입력이 됩니다.
  1. 1 / 3단계

    기준과 SQL 근거

    기준 측정·SQL 식별·실행 계획을 기록합니다.

    기준과 SQL 근거

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    기준과 SQL 근거기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 근거: baseline·SQL·plan쿼리 개선근거: baseline·SQL·planbaseline·plan·결정: 기록: 중앙baseline·plan·결정기록: 중앙
    기준과 SQL 근거기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 근거: baseline·SQL·plan쿼리 개선근거: baseline·SQL·planbaseline·plan·결정: 기록: 중앙baseline·plan·결정기록: 중앙

    기준 측정·SQL 식별·실행 계획을 기록합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    근거
    baseline·SQL·plan
    기록
    중앙

    이번 단계에서 바뀐 점

    • 바꿀 대상을 증거로 좁힙니다.
  2. 2 / 3단계

    최소 변경·canary

    최소 변경을 canary에서 같은 기준과 비교합니다.

    최소 변경·canary

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    최소 변경·canary최소 변경을 canary에서 같은 기준과 비교합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 변경: 최소쿼리 개선변경: 최소baseline·plan·결정: 검증: canary 비교baseline·plan·결정검증: canary 비교
    최소 변경·canary최소 변경을 canary에서 같은 기준과 비교합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 변경: 최소쿼리 개선변경: 최소baseline·plan·결정: 검증: canary 비교baseline·plan·결정검증: canary 비교

    최소 변경을 canary에서 같은 기준과 비교합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    변경
    최소
    검증
    canary 비교

    이번 단계에서 바뀐 점

    • 변경 효과의 근거를 얻습니다.
  3. 3 / 3단계

    결정과 다음 기준

    결정을 기록하고 새 기준을 다음 측정에 사용합니다.

    결정과 다음 기준

    상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

    결정과 다음 기준결정을 기록하고 새 기준을 다음 측정에 사용합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 산출물: 결정·기준선쿼리 개선산출물: 결정·기준선baseline·plan·결정: 다음: 측정baseline·plan·결정다음: 측정
    결정과 다음 기준결정을 기록하고 새 기준을 다음 측정에 사용합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.쿼리 개선: 산출물: 결정·기준선쿼리 개선산출물: 결정·기준선baseline·plan·결정: 다음: 측정baseline·plan·결정다음: 측정

    결정을 기록하고 새 기준을 다음 측정에 사용합니다. 두 역할의 위치는 고정하며 관찰 값과 실제 전환 이유를 비교합니다.

    산출물
    결정·기준선
    다음
    측정

    이번 단계에서 바뀐 점

    • 관측 결과가 후속 판단의 입력이 됩니다.

기준선 기록: endpoint p95, 요청당 query count, rows read, DB 자원을 같은 구간에서 기록합니다.

SQL 식별: ORM log와 trace를 사용해 반복 SQL과 SQL fingerprint를 대상 endpoint에 연결합니다.

계획 확인: scan 방식, 추정 row와 실제 row의 차이, sort·join·buffer 비용을 읽습니다.

최소 변경: index, join·batch, projection, pagination 가운데 관측한 병목을 겨냥한 한 가지를 바꿉니다.

Canary 비교: 같은 부하와 데이터 조건에서 before/after를 비교합니다.

결정 기록: 좋아졌으면 keep, 나빠졌거나 부작용이 크면 rollback하고 다음 가설을 반복합니다.

마지막 결정은 승인된 기준선을 갱신합니다. 다음 회귀나 다음 후보 SQL은 그 기록을 시작점으로 다시 측정하므로, 쿼리 최적화는 끝나는 체크리스트가 아니라 근거가 누적되는 운영 루프입니다.

이것으로 9장 성능 최적화와 스케일링의 두 번째 절을 마칩니다. 다음 절에서는 로드 밸런싱과 애플리케이션 확장을 다룹니다.