[Oracle] 오라클 쿼리 튜닝 회고(TOP-N 방식 페이징 처리)
TOP-N과 인덱스 스캔이왜 그렇게 동작하는지까지
사내에서 페이징처리를 위해서 고전 방식의 쿼리를 이용한 TOP-N 방식을 사용한다.
예시
SELECT *
FROM
(
SELECT ROWNUM as row_num, T.*
FROM
(
SELECT *
FROM MY_TABLE
WHERE :조건
ORDER BY :인덱스_컬럼
) T
WHERE ROWNUM <= :pageNum * :pageCount + 1
)
WHERE row_num > (:pageNum - 1) * :pageCount
pageNum: 페이지 번호
pageCount: 한 페이지에 표기할 아이템 수
위와 같은 규칙에 맞게 작성되어있는 페이징을 처리하는 서비스의 테스트를 진행했다.
처음은 단순하게 생각했다.
PK정렬 -> 인덱스 존재 -> 인덱스로 정렬 없이 상위 N개를 먼저 읽어온다.
하지만 실제 개발서버에서 진행한 오라클의 실행계획(plan)은 예상과 달랐다.
- PK 인덱스가 있음에도 TABLE ACCESS FULL
- ORDER BY 정렬 후 WINDOW SORT
- 힌트를 사용할 경우, 비용(cost)증가
- CARDINALITY(card) 값이 실제 데이터 개수와 다름
- 조건에 따라 인덱스를 타거나 풀스캔을 진행하거나
위 경우를 하나씩 파고들어가며 Oracle 옵티마이저의 핵심 철학과 TOP-N 최적화의 내부 동작 원리를 이해할 수 있었다.
1. 페이징 쿼리와 인라인뷰의 오해
인라인뷰는 모든 데이터를 정렬하고 ROWNUM으로 자를까?
SELECT *
FROM (
SELECT *
FROM MY_TABLE
ORDER BY MY_TABLE_COL_PK
)
WHERE ROWNUM <= 1000
인라인뷰에 ORDER BY 절이 있으니 Oracle은 모든 행을 정렬한 뒤, 1000개만 자를까?
1. MY_TABLE 전체를 읽기(FULL SCAN)
2. 메모리 / 디스크 정렬 (WINDOW SORT)
3. 상위 1000개만 자르기
이것은 부분적으로만 맞다.
정렬 대상 컬럼에 적절한 인덱스가 있다면 Oracle은 이렇게 동작하지 않는다.
2. TOP-N이란?
TOP-N은 정렬된 결과의 상위 N개만을 가져오는 방식이다.
즉, 정렬 전체를 다 만들지 않고 상위 N개를 판별할 수 있는 순간 STOPKEY로 즉시 중단하는 최적화이다.
이 최적화가 발생할 경우 플랜
COUNT STOPKEY
WINDOW NOSORT STOPKEY
STOPKEY가 등장한다면 Oracle은 중간에 읽기를 끊었다는 신호
3. TOP-N이 효율적으로 작동하는 조건
TOP-N 최적화는 모든 상황에서 효율적인 것은 아니다.
핵심: ORDER BY 컬럼이 인덱스 정렬 순서와 동일한 형태로 존재해야 한다.
예시
| 컬럼 | 인덱스 | ORDER BY | TOP-N 최적화 |
| PK(a) | a ASC | ORDER BY a ASC | ✅ 매우 빠름 |
| PK(a) | a ASC | ORDER BY a DESC | ✅ 빠름 혹은 매우빠름 |
| PK(a) | a ASC | ORDER BY b | ❌ 느림 |
| PK(a), b | 복한 인덱스 (a, b) | ORDER BY b | ❌ 느림 |
즉, 정렬 가능한 인덱스가 있을 때만 TOP-N 최적화가 제대로 동작할 수 있다.
4. 적절한 인덱스 혹은 PK가 있는데도 WINDOW SORT / FULL SCAN이 나온 이유
실제 발생한 플랜
SELECT STATEMENT optimizer=all_rows (cost=113 card=1000 bytes=220000)
VIEW (cost=113 card=1000 bytes=220000)
WINDOW SORT PUSHED RANK (cost=113 card=221 bytes=18564)
TABLE ACCESS FULL OF 'MY_TABLE' (TABLE) (cost=112 card=221 bytes=18564)
"SELECT *" 를 사용
인덱스를 정렬 기준으로 사용할 수 있어도 쿼리에 SELECT * 문법을 사용하면 모든 컬럼을 테이블에서 찾아야한다.
즉, 인덱스를 따라 N개의 ROWID를 찾음 -> ROWID로 테이블을 다시 랜덤 엑세스(N번의 랜덤 I/O)
Oracle은 “랜덤 I/O 1000번” vs “풀스캔 1번”을 비교해
풀스캔이 더 쌀 것 같으면 TABLE ACCESS FULL 을 선택한다.
그 결과:
→ 인덱스는 정렬 순서를 제공하나
→ 결과 행의 모든 컬럼을 얻기 위해 결국 테이블 재접근이 필요
→ 랜덤 액세스 비용이 비싸면 옵티마이저가 풀스캔 선택
5. 힌트로 강제로 인덱스 스캔 → 더 안 좋은 COST?
인덱스 정렬을 위한 힌트 사용
--인덱스 컬럼정렬 힌트 사용
SELECT /*+ index_asc(t MY_TABLE_COL_PK) */*
FROM (
SELECT *
FROM MY_TABLE t
ORDER BY MY_TABLE_COL_PK
)
WHERE ROWNUM <= 1000
플랜결과
SELECT STATEMENT optimizer=all_rows (cost=409 card=1000 bytes=220000)
VIEW (cost=409 card=1000 bytes=220000)
WINDOW NOSORT STOPKEY (cost=409 card=221 bytes=18564)
TABLE ACCESS BY INDEX ROWID OF 'MY_TABLE' (TABLE) (cost=409 card=221 bytes=18564)
INDEX FULL SCAN OF 'MY_TABLE_COL_PK' (INDEX (UNIQUE)) (cost=8 card=221)
하지만 COST가 더 올라감.
왜일까?
이유는 매우 단순하다.
인덱스 FULL SCAN 방식 = 전체 풀스캔보다 더 많은 페이지 랜덤 액세스
인덱스는 보통:
- 블록이 더 작음
- 페이지 수가 더 많음
- 물리적 순서가 데이터 순서와 다름
즉, 인덱스 FULL SCAN은 테이블 FULL SCAN보다 오히려 더 많은 I/O가 발생할 수 있다.
그 다음엔 ROWID로 테이블 랜덤 액세스
그러니 힌트로 인덱스를 타게 만들면
“불필요하게 인덱스를 훑고 + 테이블을 다시 랜덤 액세스하는 비효율”
이 될 수도 있다.
6. 플랜에서 인덱스 스캔을 타는 경우
SELECT *
FROM (
SELECT *
FROM MY_TABLE
ORDER BY MY_TABLE_COL_PK
)
WHERE ROWNUM <= 50
--혹은
SELECT *
FROM MY_TABLE
ORDER BY MY_TABLE_COL_PK
FETCH FIRST 50 ROWS ONLY;
where 절의 ROWNUM을 50으로 제한해보자
이런 경우 옵티마이저가 “50개만 읽으면 되니까?” 하고 TOP-N 최적화를 자동으로 적용할 때가 있다.
왜 자동으로 적용되었을까?
- 테이블이 충분히 크고
- PK 인덱스가 정렬순서와 같고
- 상위 소량의 행(50개)만 필요할 때
이런 경우 Oracle은 다음 과정을 선택한다:
- 인덱스를 DESC로 스캔해 상위 50개 ROWID 추출
- 50개의 ROWID만 테이블에서 랜덤 액세스
- STOPKEY로 종료
이 작업은 풀스캔보다 훨씬 빠를 수 있다.
즉, 데이터량과 TOP-N의 N 값의 상대적 크기가 최적화 방향을 결정한다.
7. 왜 인라인 뷰의 card수가 실제 테이블의 row 수와 다를까
실제 테이블 rowcount = 1000+
그런데 실행계획에서: card = 221이렇게 나왔다.
이유
cardinality는 실제 접근 행수가 아니라
옵티마이저가 통계 기반으로 “예상”한 행 수이다.
따라서:
- 통계가 오래됨
- 히스토그램 없음
- 인덱스 컬럼 분포가 균등하다고 오해
- WHERE 조건의 선택도가 다르게 해석됨
이런 이유로 실제와 다르게 보인다.
8. 그럼 지금 사용 중인 인라인 페이징이 나쁜가?
결론
❌ 인라인 뷰 + ROWNUM 페이징 자체가 나쁜 것이 아니다.
⭕ ORDER BY 컬럼 인덱스 유무가 성능을 좌우한다.
ORDER BY 인덱스가 없다면
→ WINDOW SORT + FULL SCAN은 정상적이며 불가피하다.
ORDER BY 인덱스가 있다면
→ TOP-N 최적화가 적용되어 인덱스 스캔 + STOPKEY 가 가능해진다.
9. TOP-N 최적화가 발휘되는 데이터량 기준
정확한 기준 값은 없지만 경험적으로 다음과 같다.
| 테이블 크기 | TOP-N이 유리한 조건 |
| 수천 건 | 거의 모든 경우 인덱스 스캔이 유리 |
| 수만 건 | TOP-N이 1% 이하(상위 100개 등)일 때 유리 |
| 수십만~수백만 | 거의 항상 유리. STOPKEY 효과가 극대화 |
| 수백만~수천만 | 인덱스 스캔만으로 성능 유지 (테이블 풀스캔은 비쌈) |
즉, 데이터가 많아질수록 TOP-N 최적화는 더 큰 힘을 발휘한다.
🏁 마무리
- 인라인뷰의 정렬 가능한 인덱스 유무가 중요하다.
- STOPKEY는 상위 N개에서 “조기 종료”를 가능하게 한다.
- 인덱스 스캔이 항상 더 빠른 것이 아니며, SELECT * 이라면 테이블 랜덤 액세스 비용 때문에 FULL SCAN이 더 빠를 수도 있다.
- Cardinality(card)는 실제 행 수가 아닌 통계 기반 추정치이다.
- PK 인덱스는 정렬에 유리하나, 모든 상황에서 인덱스를 타는 것은 아니다.
- TOP-N은 데이터가 많을수록 효과가 크다.
Oracle 실행계획은 단순히 “풀스캔이면 느린 쿼리다”가 아니라 왜 풀스캔이 선택되었는지를 이해하는 것이 중요했다.
TOP-N, STOPKEY, 인덱스 스캔의 상관관계를 확실히 이해하기 좋은시간이었습니다.
'Programming > DB' 카테고리의 다른 글
| [ORACLE] 자주 사용되는 숫자 제어 함수 (0) | 2025.08.02 |
|---|---|
| [쿼리 튜닝] Oracle DB 쿼리 튜닝 회고(ft. LPAD) (0) | 2024.08.29 |
| [MySQL] JOIN 문법, left join, left outer join, inner join (1) | 2022.05.29 |
| postgresql Homebrew로 설치부터 DBeaver 연결까지 (0) | 2022.03.22 |
| [DBeaver / MySQL] 무한 로딩(dead lock) 해결 (2) | 2021.12.29 |





















