SQL은 원하는 결과를 말한다. status = 'ready'인 행을 달라고 할 뿐, 테이블을 처음부터 읽을지 인덱스로 찾을지, 두 테이블 중 어디부터 읽을지는 지정하지 않는 경우가 많다. 데이터베이스는 가능한 경로의 비용을 예상하고 실제 실행 순서를 고른다. 이것이 쿼리 실행 계획이다.
실행 계획을 이해하면 “인덱스가 있는데 왜 느릴까”라는 질문을 더 정확하게 바꿀 수 있다. 데이터베이스가 몇 행을 찾을 것으로 예상했고, 실제로는 얼마나 읽었으며, 어느 단계에서 작업량이 크게 늘었는지를 확인하게 된다.
같은 결과를 만드는 읽기 방법은 여러 개다
백만 행에서 status = 'ready'를 찾는다고 하자. 데이터베이스에는 적어도 두 경로가 있다.
경로 A: 테이블 전체를 차례로 읽고 status를 확인
경로 B: status 인덱스에서 ready 위치를 찾고 본문 행을 읽음
인덱스가 있으면 B가 항상 좋아 보인다. 하지만 인덱스는 조건에 맞는 위치를 찾은 뒤 필요한 본문 페이지를 다시 읽을 수 있다. 맞는 행이 거의 전부이라면 여기저기 찾아가는 것보다 전체를 한 번 순서대로 읽는 편이 나을 수 있다.
실행 계획은 인덱스의 존재보다 이 경로에서 실제로 얼마나 적은 데이터를 읽을 수 있는지를 판단한다.
선택도는 조건이 얼마나 많이 걸러 내는지를 말한다
백만 행 중 ready가 천 행이면 조건은 전체의 0.1%만 남긴다. 인덱스로 좁히는 효과가 크다. 반대로 ready가 80만 행이라면 조건은 대부분을 남긴다.
0.1% 일치 → 인덱스가 소수의 위치만 찾을 수 있음
80% 일치 → 인덱스를 따라가도 대부분의 행을 다시 읽음
이 차이를 선택도라고 생각할 수 있다. 같은 SQL과 같은 인덱스라도 데이터 분포가 달라지면 더 싼 경로도 달라진다. 운영 데이터가 쌓인 뒤 실행 계획이 바뀌는 이유다.
실행 계획은 통계로 행 수를 예상한다
데이터베이스는 쿼리를 실행하기 전에 모든 행을 직접 세어 볼 수 없다. 테이블 크기, 값의 분포, 서로 다른 값의 수 같은 통계를 이용해 각 단계가 몇 행을 만들지 예상한다.
status 조건 예상: 1,000행
실제 결과: 300,000행
예상보다 훨씬 많은 행이 다음 단계로 넘어가면, 그 예상에 맞춰 고른 조인과 정렬 방식도 비싸질 수 있다. 통계가 오래됐거나 특정 값에 데이터가 몰려 있는데 단순한 평균으로 판단하면 이런 차이가 생긴다.
그래서 계획을 볼 때는 노드 이름을 외우기보다 예상 행 수와 실제 행 수가 어디에서 크게 갈리는지 먼저 확인한다.
조인은 어느 테이블을 먼저 읽는지에 따라 일이 달라진다
주문 백만 건과 사용자 천 명을 조인하면서 특정 사용자 한 명만 찾는다고 하자. 사용자를 먼저 한 명으로 줄인 뒤 그 사람의 주문을 찾으면 작업량이 작다. 주문을 넓게 읽은 뒤 사용자 조건을 적용하면 훨씬 많은 중간 행이 생길 수 있다.
사용자 1명 찾기 → 그 사용자의 주문 찾기
주문 다수 읽기 → 사용자와 연결 → 마지막에 1명만 남기기
어느 순서가 좋은지는 조건, 인덱스, 실제 데이터 분포에 따라 달라진다. 작은 테이블을 항상 먼저 읽는다는 규칙만으로는 부족하다. 먼저 읽었을 때 다음 단계로 전달되는 행 수가 얼마나 줄어드는지를 봐야 한다.
정렬과 LIMIT도 읽기량을 바꾼다
ORDER BY created_at DESC LIMIT 20은 결과가 20개라는 뜻이지 반드시 20행만 읽는다는 뜻은 아니다. 원하는 순서의 인덱스가 없다면 조건에 맞는 행을 모아 정렬한 뒤 20개를 골라야 할 수 있다.
반대로 조건과 정렬 순서에 맞는 인덱스가 있다면, 가장 최근 위치부터 읽어 20개를 채운 뒤 멈출 수 있다. 복합 인덱스에서 열 순서가 중요한 이유도 여기에 있다. 어떤 조건으로 범위를 좁히고 어떤 순서로 결과를 꺼낼지와 인덱스 순서가 맞아야 한다.
인덱스를 추가하기 전에 실제 병목을 확인한다
느린 쿼리를 보면 곧바로 인덱스를 추가하고 싶어진다. 하지만 먼저 실제 실행 계획에서 다음을 따라가야 한다.
- 어느 단계가 가장 많은 행을 읽었는가.
- 예상 행 수와 실제 행 수가 어디에서 크게 달랐는가.
- 정렬이나 조인이 큰 중간 결과를 만들었는가.
- 애플리케이션이 필요 이상의 열과 행을 요청했는가.
인덱스는 읽기 경로를 하나 더 제공하지만, 삽입·수정·삭제 때 함께 갱신해야 한다. 문제를 찾지 않고 인덱스만 늘리면 쓰기 비용과 저장 공간은 늘고, 정작 실행 계획은 새 인덱스를 고르지 않을 수 있다.
이해 확인 질문과 답변
인덱스가 있는데 전체 스캔을 하면 잘못된 계획일까?
반드시 그렇지는 않다. 조건에 맞는 행이 대부분이면 인덱스에서 위치를 찾고 본문을 다시 읽는 것보다 전체를 순서대로 읽는 편이 더 쌀 수 있다.
같은 SQL의 실행 계획이 바뀔 수 있을까?
바뀔 수 있다. 데이터 양과 값의 분포, 통계, 사용 가능한 인덱스가 달라지면 데이터베이스가 예상하는 비용도 바뀐다.
예상 행 수가 중요한 이유는 무엇일까?
데이터베이스는 예상 행 수를 바탕으로 조인 순서와 방식, 정렬 방법을 고른다. 예상이 크게 틀리면 앞 단계뿐 아니라 뒤 단계의 선택도 비싸질 수 있다.
LIMIT 20이면 항상 20행만 읽을까?
아니다. 원하는 조건과 정렬을 만족하는 20행을 찾기 위해 더 많은 행을 읽고 정렬할 수 있다. 적절한 인덱스가 있을 때만 일찍 멈출 가능성이 커진다.
AI에게 이렇게 요청할 수 있다
백만 행에서 status 조건이 0.1%와 80%를 남기는 경우를 비교해 줘. 인덱스 스캔과 전체 스캔, 예상 행 수와 실제 행 수, 조인 순서와 LIMIT 정렬이 실행 계획을 어떻게 바꾸는지 설명해 줘.
쿼리 실행 계획을 이해했다면 SQL 문장만 보고 빠르다고 판단하지 않고, 데이터베이스가 어떤 순서로 몇 행을 읽으며 그 예상이 실제 데이터와 맞는지를 확인할 수 있다.
'배움과 성장 > 백엔드·데이터' 카테고리의 다른 글
| 메시지 큐와 재시도는 오래 걸리는 작업을 어떻게 나눌까? 확인 응답과 중복 처리 (0) | 2026.09.08 |
|---|---|
| 캐시는 왜 빠르게 만들면서도 틀린 값을 돌려줄까? 적중과 무효화 (0) | 2026.09.07 |
| 동시에 바꾸는 작업은 어떻게 서로를 방해하지 않을까? 잠금과 격리 (0) | 2026.09.01 |
| 트랜잭션은 여러 변경을 어떻게 하나의 작업으로 만들까? 커밋과 롤백 (0) | 2026.08.31 |
| B-트리는 왜 디스크 인덱스에 어울릴까? 노드와 범위 조회 (0) | 2026.08.30 |
댓글