#11 - EXPLAIN ANALYZE(큰 테이블이 작은 테이블을 만났을 때, DB는 어떻게 행동할까?)
중년개발자
@loxo
8일 전

실행 계획을 이해하는 감각 만들기
이제부터는 EXPLAIN ANALYZE를 왜 이렇게 나왔는지 납득하는 단계로 들어간다. 숫자와 용어를 외우는 게 아니라, 데이터 크기와 접근 방식의 이야기로 이해하면 훨씬 편하다.
Seq Scan이 선택되는 진짜 이유
Seq Scan은 말 그대로 테이블을 처음부터 끝까지 읽는 방식이다.
많이들 이렇게 생각한다.
Seq Scan = 인덱스를 못 써서 생긴 문제
하지만 PostgreSQL 입장에서는 전혀 다르다.
PostgreSQL이 Seq Scan을 고르는 논리
DB는 항상 이렇게 계산한다.
-
인덱스를 타면
- 인덱스 페이지 읽고
- 테이블 페이지 다시 읽고
- 랜덤 I/O가 많이 발생
-
Seq Scan을 하면
- 테이블 페이지를 순서대로 쭉 읽음
- 디스크, 캐시 입장에서 매우 효율적
그래서:
- 전체 row 중 많은 비율을 읽어야 한다면
- 인덱스가 있어도 Seq Scan이 더 빠를 수 있다
실생활 비유
책에서 단어 하나 찾기:
- 단어가 2~3개만 필요 → 목차(인덱스) 사용
- 책의 절반 이상을 봐야 함 → 처음부터 쭉 읽는 게 빠름
DB도 똑같다.
Index Scan vs Bitmap Scan 차이
둘 다 인덱스를 쓰지만, 데이터를 가져오는 전략이 다르다.
Index Scan
- 인덱스에서 row 위치 하나 찾음
- 바로 테이블 페이지로 이동
- 다시 인덱스로 돌아감
특징:
- 조건에 맞는 row가 아주 적을 때 최고
- 랜덤 I/O가 많아질 수 있음
Bitmap Scan
- 인덱스를 먼저 쭉 스캔
- 필요한 row들의 위치를 비트맵(목록) 으로 모음
- 테이블 페이지를 묶어서 한 번에 읽음
특징:
- 조건에 맞는 row가 어느 정도 많을 때 유리
- 페이지 단위 접근이라 효율적
실생활 비유
- Index Scan → 필요한 물건 하나하나 직접 집으러 다님
- Bitmap Scan → 쇼핑 리스트 먼저 만들고, 동선 최적화해서 한 번에 이동
PostgreSQL은 row 수와 페이지 분포를 보고 둘 중 하나를 고른다.
Nested Loop / Hash Join 감각 잡기
조인은 row 수의 곱셈 문제다. 이 감각이 없으면 실행 계획이 안 보인다.
Nested Loop
구조:
- 바깥 테이블 row 하나
- 안쪽 테이블을 매번 다시 탐색
그래서:
- 바깥 row가 적고
- 안쪽에 인덱스가 있을 때 매우 빠르다
하지만:
- 바깥 10만 row × 안쪽 탐색
- → 순식간에 재앙
Hash Join
구조:
- 작은 테이블을 메모리에 올려 해시 테이블 생성
- 큰 테이블을 한 번 훑으며 매칭
그래서:
- 대량 데이터에 안정적
- 인덱스가 없어도 동작
단점:
- 메모리(work_mem) 부족하면 느려짐
직관적인 기준
- 소량 × 인덱스 → Nested Loop
- 대량 × 대량 → Hash Join
실행 계획이 갑자기 바뀌는 이유
어제까지 빠르던 쿼리가 오늘 느려지는 이유는 대부분 이것이다.
- 데이터 양이 크게 변함
- 특정 값으로 데이터가 몰림
- 통계 정보가 오래됨
- 파라미터 값이 달라짐
PostgreSQL은 통계 기반 DB다. 통계가 현실과 어긋나면, 잘못된 선택을 한다.
그래서 ANALYZE가 중요하다.
actual time vs Execution Time 차이
- actual time → 각 노드가 실제로 걸린 시간
- Execution Time → 쿼리 전체가 끝난 시간
주의할 점:
- actual time은 노드별로 중복되어 보일 수 있다
- 성능 판단 기준은 항상 Execution Time
loops 값이 의미하는 것
loops는:
이 노드가 몇 번 실행되었는지
Nested Loop에서 특히 중요하다.
예:
- loops=1 → 한 번 실행
- loops=10000 → 만 번 반복
이때 actual time이 작아 보여도:
- time × loops = 실제 비용
그래서 loops는 숨겨진 폭탄이다.
EXPLAIN ANALYZE 결과 실제 예제 해부
EXPLAIN ANALYZE
SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.country = 'KR';해석 흐름:
-
users 테이블에서 country='KR' 조건
- row 수 확인
- Index Scan or Seq Scan 확인
-
해당 users row를 기준으로
- orders를 어떻게 조인하는지 확인
-
Nested Loop이면
- users row 수 × orders 접근 횟수 확인
-
loops 값과 actual rows를 함께 본다
이 순서로 보면 실행 계획이 문장처럼 읽히기 시작한다.
핵심 정리
- 실행 계획은 숫자가 아니라 이야기다
- row 수가 모든 선택의 출발점
- 인덱스는 page 단위로 움직인다
- loops는 곱셈이다
- DB는 항상 "가장 싸 보이는 길"을 고른다
EXPLAIN ANALYZE를 읽는다는 것은 PostgreSQL의 판단을 이해하는 것이다