쿼리가 결과를 찾는 대신 테이블 전체를 훑기 시작하면, 데이터베이스의 성능 문제는 데이터가 커진 뒤에야 모습을 드러낼 수 있어.
한 줄로 말하면
전체 테이블 스캔은 쿼리가 조건에 맞는 인덱스를 이용하지 못해 테이블의 행을 넓게 읽으며 결과를 찾는 실행 방식이야. 작은 테이블에서는 눈에 띄지 않을 수 있지만, 행이 많아지면 읽어야 할 데이터와 CPU 작업이 함께 커질 수 있어.
비유로 이해하기
도서관에서 제목이나 저자별 색인이 있는데도 서가의 책을 처음부터 끝까지 한 권씩 확인하는 장면을 떠올리면 돼. 책이 열 권이면 금방 끝나지만, 책이 수십만 권이면 원하는 한 권을 찾는 일 자체가 큰 작업이 되지.
여기까지가 이해를 돕는 비유야. 실제 쿼리에서는 조건, 데이터의 분포, 실행 계획, 읽어야 하는 열과 행의 양이 함께 작용해. 인덱스가 있다고 해서 모든 쿼리가 자동으로 인덱스를 쓰는 것도 아니야.
정확한 정의
데이터베이스는 쿼리를 실행할 때 어떤 순서와 방법으로 테이블을 읽을지 정해. 조건에 맞는 인덱스를 활용하면 후보 행을 좁힌 뒤 필요한 행을 읽을 수 있지만, 인덱스가 없거나 쿼리 조건과 맞지 않거나 다른 방식이 더 낫다고 판단하면 테이블 스캔을 선택할 수 있어.
전체 테이블 스캔의 핵심은 결과의 개수가 아니라 찾는 과정에서 얼마나 넓은 범위를 읽었는가야. 결과가 한 행이어도 그 한 행을 찾기 위해 많은 행을 확인했다면 비용이 커질 수 있어. 반대로 작은 테이블을 한 번 읽는 상황에서는 전체 스캔이 실제 문제로 이어지지 않을 수도 있지.
왜 중요한가
성능 문제는 기능 테스트만으로 놓치기 쉬워. 개발 환경의 데이터가 작으면 전체 테이블 스캔이 발생해도 응답 시간이 짧게 보일 수 있고, 실제 크기의 데이터와 트래픽이 들어온 뒤에야 CPU 사용량과 지연으로 나타날 수 있어.
lobste.rs의 MariaDB에서 SQLite로 전환한 사례에서는 첫 배포가 읽기 전용 트래픽만으로 모든 CPU를 100% 사용해 롤백됐어. 뒤늦게 확인한 병목 가운데에는 대형 테이블에서 두 쿼리가 일으킨 전체 테이블 스캔이 있었고, 별도의 N+1 문제도 함께 발견됐지.1
이 사례가 보여주는 건 “인덱스를 추가하면 무조건 빨라진다”는 공식이 아니야. 어떤 쿼리가 어떤 데이터 규모에서 어떤 실행 방식을 택하는지 확인해야 한다는 뜻이야. 인덱스가 많아지면 쓰기와 저장 공간의 비용도 생길 수 있으므로, 개선 전후의 실행 계획과 실제 지연을 함께 봐야 해.
실제 예시
게시글 목록에서 특정 작성자나 상태를 찾는 쿼리가 있다고 해 보자. 해당 조건으로 좁힐 수 있는 인덱스를 쓰면 후보를 빠르게 줄일 여지가 있지만, 조건과 인덱스의 순서가 맞지 않거나 쿼리의 형태가 인덱스 활용에 적합하지 않으면 많은 행을 확인하는 실행 방식이 될 수 있어.
그래서 테스트에는 행이 몇 개뿐인 샘플만 넣지 않는 편이 좋아. 실제와 비슷한 데이터 규모와 값의 분포를 준비하고, 실행 계획이나 느린 쿼리 로그를 확인하며, 전체 테이블 스캔이 발생하면 실패하도록 만드는 기준을 둘 수 있어. lobste.rs 회고에서도 테스트 중 전체 테이블 스캔을 실패로 처리했다면 첫 배포의 병목을 더 일찍 발견했을 것이라는 교훈이 남았어.1
헷갈리지 말아야 할 점
- 전체 테이블 스캔은 곧바로 버그라는 뜻이 아니야. 작은 테이블을 읽거나 결과 대부분이 필요한 쿼리에서는 넓게 읽는 방식이 합리적일 수 있어.
- 인덱스가 있다고 항상 스캔이 사라지는 건 아니야. 쿼리의 조건과 데이터 분포, 실행 계획에 따라 데이터베이스가 다른 방식을 선택할 수 있어.
- N+1 쿼리와는 다른 병목이야. N+1은 항목별 조회가 반복되는 문제고, 전체 테이블 스캔은 한 번의 쿼리가 넓은 범위를 읽는 문제야. 한 요청에서 둘이 동시에 나타날 수도 있어.
- 기능이 맞는 것과 성능이 맞는 것은 달라. 작은 데이터에서 결과가 정확해도 실제 크기에서 지연과 자원 사용이 감당되지 않을 수 있어.
관련 문서
- 실제 크기의 데이터로 성능 병목을 드러내는 조건: 현실 크기 테스트 데이터
- 항목별 조회가 반복되는 다른 병목: N+1 쿼리와 애플리케이션 병목
- 데이터베이스 교체 과정에서 전체 테이블 스캔이 드러난 사례: lobste.rs의 MariaDB→SQLite 전환 정리
남은 질문들
- 주요 데이터베이스와 ORM은 전체 테이블 스캔을 테스트 실패로 바꾸는 기능을 어떻게 제공할까?
- 조건과 인덱스의 관계가 실행 계획에 미치는 영향을 작은 재현 사례로 어떻게 확인할까?
- 전체 테이블 스캔을 줄이는 것과 인덱스 유지 비용 사이의 균형은 데이터 쓰기 패턴에 따라 어떻게 달라질까?
댓글