요약
- OR 연산은 인덱스의 연속 스캔(range scan)을 방해하여 비효율을 유발한다
- 특히 서로 다른 컬럼 간의 OR 조건이 문제가 된다
본문
연속 스캔이란
B+Tree 인덱스의 leaf node들은 키 정렬 순서로 linked list로 연결돼 있다. 시작점만 트리 탐색으로 찾으면, 이후는 한 방향으로 leaf를 따라가며 읽으면 된다.
[루트] → [중간] → [leaf₁] ↔ [leaf₂] ↔ [leaf₃] ↔ ...
└─ 여기부터 →→→→ 쭉 읽기
빠른 이유:
- 랜덤 I/O 1회 + 시퀀셜 I/O 다회: 시작 위치 탐색(트리 하강)만 랜덤 I/O, 이후는 인접 페이지 순차 read
- OS/디스크의 read-ahead 활용: 순차 접근 패턴이면 prefetch가 잘 먹어 throughput이 올라감
- 정렬 결과 무료: leaf가 이미 키 순서대로 정렬돼 있어
ORDER BY별도 비용 없음 - clustered index(PK) 기반이면 row data 자체도 인접 위치라 추가 룩업까지 시퀀셜
연속 스캔이 잘 되는 쿼리:
WHERE age BETWEEN 20 AND 30 -- 시작~끝 범위
WHERE name LIKE 'kim%' -- prefix가 정해진 범위
WHERE created_at > '2024-01-01' ORDER BY created_at -- 정렬까지 공짜
연속 스캔이 깨지는 패턴
매칭 행이 leaf 상에서 흩어져 있으면 여러 시작점을 각각 따로 찾아야 해서 시퀀셜 I/O 이점이 사라진다:
OR(서로 다른 컬럼) — 두 위치를 따로 찾아 합쳐야 함 → Index MergeIN (대량 값)— 값마다 leaf 위치가 다름LIKE '%kim'— leading wildcard라 시작점을 못 잡음- 함수 적용된 컬럼:
WHERE LOWER(name) = ...— 인덱스 정렬 순서와 무관 <>(부등호) — 매칭 영역이 "전체 - 일부"라 거의 풀 스캔- 타입 캐스팅:
WHERE id = '123'(id가 int인데 문자열) — 인덱스 미사용 가능
옵티마이저가 "어차피 흩어진 행을 랜덤 I/O로 읽을 거면 차라리 풀 테이블 스캔하자"라고 판단해 인덱스를 포기하기도 한다. EXPLAIN의 type: ALL 또는 Using where + rows 큰 값이 신호.
OR이 연속 스캔을 어떻게 방해하는가
A OR B는 의미상 "A를 만족하는 행들의 leaf 영역" + "B를 만족하는 영역"의 합집합이다. 두 영역이 인덱스 상 인접하지 않으면:
- A 조건으로 leaf를 한 번 스캔
- B 조건으로 leaf를 다시 한 번 스캔(또는 풀 스캔)
- 두 결과 합치고 중복 제거 (Index Merge)
각 단계가 시퀀셜이긴 해도, 두 번의 랜덤 I/O 시작점 + 합치는 비용이 누적된다. 동일 컬럼 OR(=IN)은 옵티마이저가 단일 leaf 영역으로 정리할 수 있지만, 다른 컬럼 OR은 그게 안 된다.
문제가 되는 케이스: 서로 다른 컬럼 간 OR
SELECT * FROM users
WHERE name = 'kim' OR email = 'test@example.com'
- 두 컬럼 모두 인덱스가 있어도 각각 스캔 후 합집합(Index Merge) 연산 필요
- Index Merge 비용이 높아 Full Scan이 더 나을 수 있음
괜찮은 케이스: 동일 컬럼의 OR
SELECT * FROM orders
WHERE status = 'PENDING' OR status = 'PROCESSING'
- 옵티마이저가
IN ('PENDING', 'PROCESSING')으로 자동 변환 - 인덱스 활용 가능
우회 방법
1. UNION ALL로 분리
SELECT * FROM users WHERE name = 'kim'
UNION ALL
SELECT * FROM users WHERE email = 'test@example.com' AND name != 'kim'
- 주의: UNION ALL은 중복 허용 → 조건이 상호 배타적인지 확인 필요
2. 동일 컬럼이면 IN 사용
WHERE status IN ('active', 'pending', 'review')
실무 팁
- EXPLAIN으로 실제 실행 계획 확인 필수
- OR 조건이 있는 느린 쿼리 발견 시 위 우회 방법 검토
참고
Zetelkasten-develop/복합 인덱스 설계 원칙 - 동등 조건 컬럼은 왼쪽에, 범위(BETWEEN, <, > 등) 조건 컬럼은 오른쪽에- B+Tree는 디스크 접근을 최소화하기 위해 설계된 균형 트리 자료구조다
- 선택도와 카디날리티 차이
- pt-query-digest로 슬로우 쿼리에 대한 정보를 자세히 볼 수 있다.