DB·SQL 실무 가이드 · Part 8

인덱스를 못 타는 쿼리 패턴

인덱스를 만들어 뒀는데 왜 안 타는지 진단하기

작성 기준2026년 7월버전과 지원 현황은 이후 달라질 수 있으니 공식 문서를 함께 확인하세요.

이 파트에서 다루는 내용

컬럼 가공암묵적 형변환LIKE와 와일드카드OR 조건부정 조건정리
01

컬럼을 가공하면 인덱스를 못 씁니다

인덱스에는 컬럼의 원래 값이 정렬돼 있습니다. 그 값을 가공한 결과로 찾으려 하면 인덱스에서는 찾을 수 없습니다. 사전에서 단어를 뒤집은 형태로 찾을 수 없는 것과 같습니다.

이게 인덱스가 안 타는 가장 흔한 원인입니다. 해결 방향은 두 가지입니다. 조건을 바꿔 컬럼을 그대로 두거나, 가공한 형태로 함수 기반 인덱스를 만드는 것입니다.

조건을 바꿔 컬럼을 살립니다sql
-- 인덱스 못 씀
WHERE SUBSTR(customer_no, 1, 3) = '010'
WHERE TO_CHAR(order_dt, 'YYYYMMDD') = '20260729'
WHERE amount * 1.1 > 100000
WHERE UPPER(customer_nm) = 'HONG'

-- 인덱스 사용 가능
WHERE customer_no LIKE '010%'
WHERE order_dt >= TO_DATE('20260729','YYYYMMDD')
AND   order_dt <  TO_DATE('20260729','YYYYMMDD') + 1
WHERE amount > 100000 / 1.1
WHERE customer_nm = 'HONG'  -- 저장 시 대문자로 통일했다면

연산을 반대편으로 넘기는 것이 요령입니다. 컬럼 쪽은 손대지 않고 상수 쪽에서 계산합니다.

함수 기반 인덱스로 해결sql
-- 대소문자 구분 없는 검색이 정말 필요하다면
CREATE INDEX idx_customer_upper_nm ON customer (UPPER(customer_nm));

-- 이제 이 조건이 인덱스를 씁니다
SELECT * FROM customer WHERE UPPER(customer_nm) = 'HONG';

함수 기반 인덱스는 쿼리에 쓴 표현식과 정확히 같아야 동작합니다. 인덱스는 UPPER(customer_nm) 인데 쿼리에서 LOWER 를 쓰면 안 탑니다.

02

암묵적 형변환은 눈에 안 보이는 컬럼 가공입니다

Part 2에서 다룬 내용이 성능 관점에서 다시 나옵니다. 타입이 다르면 Oracle이 자동으로 맞춰 주는데, 문자 컬럼과 숫자를 비교하면 컬럼 쪽에 TO_NUMBER 가 걸립니다.

쿼리에는 함수가 안 보이니까 원인을 찾기 어렵습니다. 실행계획의 Predicate Information 을 보면 숨어 있던 변환이 드러납니다.

실행계획에서 드러나는 숨은 변환text
SELECT * FROM orders WHERE status_cd = 1;

Predicate Information:
   2 - filter(TO_NUMBER("STATUS_CD")=1)
                ^^^^^^^^^^^^^^^^^^^^^^
                쿼리에 없던 함수가 컬럼에 걸려 있습니다

애플리케이션에서도 같은 일이 생깁니다. MyBatis에서 문자 컬럼에 숫자 파라미터를 넘기거나, JPA에서 타입이 어긋나면 동일한 증상이 나옵니다. 운영에서만 느린 쿼리를 만나면 실제 바인딩 타입을 확인합니다.

03

LIKE는 앞부분이 고정돼야 인덱스를 씁니다

인덱스는 앞에서부터 정렬돼 있습니다. 그래서 앞이 고정된 검색은 시작 지점을 찾아갈 수 있지만, 앞에 와일드카드가 오면 전체를 뒤져야 합니다.

'포함' 검색이 정말 필요하면 인덱스로는 한계가 있습니다. Oracle Text 같은 전문 검색 기능을 검토하거나, 검색 전용 저장소를 두는 설계를 고민할 시점입니다.

'ABC%'
인덱스 사용

앞이 고정돼 있어 인덱스 범위 스캔이 됩니다. 가장 좋은 형태입니다.

'%ABC'
못 씀

뒤에서 찾아야 하므로 전체 스캔입니다. 뒤에서 찾는 일이 잦으면 뒤집은 값을 함수 기반 인덱스로 만드는 방법이 있습니다.

'%ABC%'
못 씀

포함 검색은 인덱스로 처리할 수 없습니다. 데이터가 커지면 반드시 문제가 됩니다.

바인딩 주의
함정

화면에서 검색어를 안 넣으면 '%%'가 되어 전체 스캔이 되는 구조가 흔합니다. 검색 조건이 비었을 때의 동작을 반드시 정합니다.

04

OR와 부정 조건은 인덱스에 불리합니다

OR로 연결된 조건은 서로 다른 컬럼을 가리키면 하나의 인덱스로 처리하기 어렵습니다. 옵티마이저가 각각 인덱스로 찾아 합치는 방식을 고를 때도 있지만, 대개는 전체 스캔으로 갑니다.

부정 조건도 마찬가지입니다. '이것이 아닌 것'은 인덱스에서 범위로 좁힐 수가 없습니다. 대부분의 행이 대상이 되기 때문에 전체 스캔이 합리적이기도 합니다.

OR를 UNION ALL로 나누기sql
-- 서로 다른 컬럼의 OR
SELECT * FROM orders
WHERE  customer_id = 100 OR order_no = 'A-2026-001';

-- 각각 인덱스를 타도록 나눕니다
SELECT * FROM orders WHERE customer_id = 100
UNION ALL
SELECT * FROM orders WHERE order_no = 'A-2026-001'
AND    customer_id <> 100;   -- 중복 방지

나누는 편이 항상 빠른 것은 아닙니다. 각 조건의 선택도가 좋을 때만 이득입니다. 바꾸기 전후로 블록 읽기 수를 비교해 판단합니다.

부정 조건을 긍정으로sql
-- 상태가 3종류뿐이라면
WHERE status_cd <> 'CANCEL'

-- 긍정 목록으로 바꾸면 인덱스를 쓸 수 있습니다
WHERE status_cd IN ('DONE', 'READY')

값의 종류가 적고 고정적일 때만 쓸 수 있는 방법입니다. 나중에 상태가 추가되면 이 쿼리가 조용히 누락을 만듭니다. 코드 값이 늘어날 여지가 있으면 쓰지 않습니다.

05

한 장으로 정리

컬럼 가공
SUBSTR, TO_CHAR, 연산

조건을 반대편으로 옮기거나 함수 기반 인덱스를 만듭니다.

암묵적 형변환
타입 불일치

비교 값의 타입을 컬럼에 맞춥니다. 애플리케이션 바인딩 타입까지 확인합니다.

앞 와일드카드 LIKE
'%ABC'

구조적으로 인덱스가 안 됩니다. 검색 요구사항 자체를 재설계합니다.

선두 컬럼 누락
복합 인덱스

인덱스 컬럼 순서를 바꾸거나 별도 인덱스를 검토합니다.

OR 조건
다른 컬럼

UNION ALL로 나누는 것을 검토하되 전후를 측정합니다.

IS NULL
단일 인덱스

Oracle은 NULL을 인덱스에 저장하지 않습니다. 복합 인덱스나 함수 기반 인덱스로 우회합니다.

확인 순서

인덱스가 있는데 안 탄다면 이 순서로 봅니다. 첫째 컬럼이 가공됐는가, 둘째 타입이 맞는가, 셋째 복합 인덱스의 선두 컬럼이 조건에 있는가, 넷째 통계가 최신인가. 여기까지 확인하면 대부분 원인이 나옵니다.

체크

이 파트 완료 기준