외주 SI 프로젝트에서 잘 돌던 집계가 어느 날 끝나지 않게 됐습니다. 결과는 전과 똑같았습니다. 시간만 300배 늘었습니다.
바꾼 건 조건 한 줄이었습니다
두 컬럼으로 걸러야 하는 조건이 있었습니다. 원래는 이랬습니다.
WHERE EXISTS (
SELECT 1 FROM t_sub s
WHERE s.k1 = t.a
AND s.k2 = t.b
)
읽기 좋게 정리한다며 이렇게 바꿨습니다.
WHERE (t.a, t.b) IN (
SELECT k1, k2 || '' FROM t_sub
)
결과는 같았습니다. 테스트도 통과했습니다. 그리고 운영에서 멈췄습니다.
왜 이게 300배가 되나
차이는 || '' 하나입니다. 붙이는 문자열이 비어 있으니 값은 안 변합니다. 그런데 옵티마이저에게는 완전히 다른 이야기입니다.
- 컬럼을 그대로 비교하면 인덱스를 쓸 수 있습니다.
- 컬럼에 무언가를 붙이면 그건 더 이상 그 컬럼이 아닙니다. 인덱스를 못 씁니다.
여러 컬럼을 묶은 IN 안에서 이런 표현식이 나오면, 옵티마이저는 조인으로 푸는 걸 포기합니다. 실행계획에 이렇게 나옵니다.
FILTER
바깥 결과 한 건마다 안쪽 서브쿼리를 처음부터 다시 돌립니다. 바깥이 수십만 건이면 수십만 번입니다.
반대로 EXISTS 로 쓰면 이렇게 나옵니다.
NESTED LOOPS SEMI
이게 조인입니다. 한 번 훑고 끝납니다.
왜 안 걸렸나
세 가지 때문입니다.
- 결과가 같습니다. 기능 테스트로는 절대 안 잡힙니다.
- 개발 DB는 데이터가 적습니다. 수만 건에서는 둘 다 1초 안에 끝납니다. 차이는 데이터가 커져야 벌어집니다.
- 읽기 좋아 보였습니다. 오히려 개선처럼 보였습니다.
★성능 퇴행은 대체로 "더 깔끔해 보이는 코드"로 들어옵니다. 지저분한 코드는 의심하지만, 깔끔한 코드는 그냥 넘어갑니다.
그래서 만든 규칙
1. 비교하는 컬럼에는 손대지 않는다
-- 못 씀
WHERE SUBSTR(code, 1, 2) = '01'
WHERE code || '' = :p
WHERE TO_CHAR(reg_date, 'YYYYMM') = :ym
-- 씀
WHERE code LIKE '01%'
WHERE code = :p
WHERE reg_date >= :from AND reg_date < :to
왼쪽에 함수가 붙는 순간 인덱스가 죽습니다. 조건을 오른쪽으로 옮기세요.
2. 다중 컬럼 IN 은 쓰지 않는다
표현식이 하나만 섞여도 실행계획이 통째로 무너집니다. EXISTS 는 그런 함정이 없습니다.
3. 무거운 쿼리는 실행계획을 같이 본다
리뷰에서 SQL만 보면 이 변경은 통과합니다. 계획을 붙여서 보면 FILTER 한 낱말로 끝납니다. 지금은 무거운 쿼리를 고칠 때 계획을 같이 올립니다.
4. 데이터가 적은 곳에서 성능을 판단하지 않는다
개발 DB가 운영의 1/100이면, 300배 퇴행이 3배로 보입니다. 3배는 "원래 좀 느리네"로 넘어갑니다.
덤으로 알게 된 것
고치는 김에 인덱스를 하나 더 만들자는 얘기가 나왔습니다. 재보니 1% 개선이었습니다.
안 만들었습니다. 인덱스는 조회를 조금 빠르게 하고 입력·수정을 계속 느리게 합니다. 1%는 그 값을 못 합니다.
진짜 원인이 실행계획일 때, 인덱스를 더 만드는 건 대체로 헛수고입니다.
가져갈 것
|| '',SUBSTR,TO_CHAR\— 비교하는 컬럼 쪽에 붙으면 인덱스가 죽습니다.- 다중 컬럼
IN대신EXISTS. - 실행계획에
FILTER가 보이면 반복 실행입니다.NESTED LOOPS SEMI가 목표입니다. - 결과가 같은 변경도 퇴행입니다. 기능 테스트로는 안 잡힙니다.
- 데이터가 적은 환경에서 성능을 판단하지 마세요.
이 일로 배운 건, 리팩터링이 기능에는 안전해도 성능에는 안전하지 않다는 겁니다. 둘은 다른 종류의 안전입니다.
만든 것들
'Coding, Testing, Challenge' 카테고리의 다른 글
| API 9개가 404였는데, 서버 로그에는 아무것도 없었습니다 (1) | 2026.09.19 |
|---|---|
| 화면이 멈춘 게 아니라, 세 가지가 각각 달랐습니다 (0) | 2026.09.17 |
| 저장은 성공했다고 나오는데, 데이터가 없습니다 (0) | 2026.09.13 |
| 타임아웃을 30초로 걸었는데 410초를 모두 돌았다. (0) | 2026.09.11 |
| 토큰을 숨기는 명령어로 토큰을 유출했습니다 (0) | 2026.09.08 |