쿼리 성능 개선 작업을 진행하면서 행의 값의 존재 유무를 표기하는데, EXISTS와 COUNT로 혼용하여 쿼리 가 되어 있는 부분이 있어 정리하면서 글로 남겨보았습니다.
결론부터 이야기 하면 행의 값의 존재 유무를 표기하는데 COUNT보다는 EXISTS가 성능이 더 좋습니다.
왜 그런지 비교와 예시를 들어 이유를 정리해 보겠습니다.
1. EXISTS vs COUNT 성능 비교
항목 | EXISTS | COUNT |
목적 | 레코드 존재 여부 확인 | 데이터의 건수 확인 |
동작 방식 | 첫 번째 행 발견 시 즉시 중단 | 모든 조건 만족 행을 전부 탐색 |
성능 | 빠름 (항상 일정하게 빠름) | 느림 (데이터 많을수록 느려짐) |
용도 | 데이터 존재여부 판단 | 정확한 레코드 수가 필요할 때만 |
이론상 성능 비교만 봐도 존재 여부만을 생각할때 COUNT보다는 EXISTS가 빠르다는걸 알 수 있습니다.
EXISTS는 값의 존재 유무에따라 즉각 중단되지만, COUNT는 전제 데이터를 스캔하는 것을 알 수 있습니다.
실제 오라클에서 100만건의 더미 데이터를 통해 성능 확인을 해보겠습니다.
-- 테이블 생성
CREATE TABLE TB_BOARD (
SEQ NUMBER PRIMARY KEY,
TITLE VARCHAR2(200),
CONTENT CLOB,
WRITER VARCHAR2(100)
);
-- 시퀀스 생성
CREATE SEQUENCE SQ_BOARD
START WITH 1
INCREMENT BY 1
NOCACHE
NOCYCLE;
-- 테스트 더미 데이터 100만건 입력
BEGIN
FOR i IN 1..1000000 LOOP
INSERT INTO TB_BOARD (
SEQ, TITLE, CONTENT, WRITER
) VALUES (
SQ_BOARD.NEXTVAL,
'제목 ' || i,
'내용입니다. 이건 테스트용 더미 데이터입니다. 번호: ' || i,
'작성자' || MOD(i, 10)
);
-- 커밋 주기를 정해서 성능 최적화
IF MOD(i, 10000) = 0 THEN
COMMIT;
END IF;
END LOOP;
COMMIT;
END;
위와 같이 테이블을 생성하고 100만건의 데이터를 입력 하겠습니다.
2. EXISTS와 COUNT 예시 쿼리 및 실행 계획 비교
-- 실행 속도 : 0.003초
SELECT 'Y'
FROM dual
WHERE EXISTS (
SELECT 1
FROM TB_BOARD
WHERE WRITER = '작성자1'
);
-- 실행 속도 0.053초
SELECT COUNT(*)
FROM TB_BOARD
WHERE WRITER = '작성자1';
둘다 쿼리를 실행 했을때는 1초 미만으로 쿼리가 빠르긴 하지만, 실행 속도와 실행 계획에서 COST를 보면 성능이 차이난다는 것을 알 수 있습니다. 그럼 여기서 성능을 더 높이기 위해서 WRITER행에 INDEX를 걸어 성능을 높여 보겠습니다.
-- 인덱스 생성
CREATE INDEX IDX_TB_BOARD_WRITER ON TB_BOARD(WRITER);
-- 인덱스 삭제
DROP INDEX IDX_TB_BOARD_WRITER;
인덱스 후 결과는 COUNT는 성능이 향상 되었다는걸 알 수 있습니다. 하지만 EXISTS는 오히려 성능이 약간 떨어 졌다는 걸 알 수 있습니다. 간단히 이유를 정리 해봤습니다.
A. EXISTS는 인덱스를 꼭 안 쓸수도 있다.
EXISTS는 "조건을 만족하는 첫 번째 레코드만 찾으면 끝"이기 때문에,
풀 스캔이 더 빠르다고 판단되면 오라클 옵티마이저가 인덱스를 안 쓰고 풀 테이블 스캔으로 가기도 합니다.
그런데 WRITER에 인덱스를 추가하면, 옵티마이저가 인덱스 존재로 인해 인덱스 레인지 스캔을 하다가 오히려 더 많은 I/O와 비효율적인 랜덤 액세스로 성능이 나빠질 수 있습니다.
B. EXISTS는 "인덱스만 보고 끝내는" 작업이 아니다.
EXISTS 쿼리 안에서 SELECT 1 FROM TB_BOARD WHERE WRITER = '작성자1'라고 하면,
인덱스로 "WRITER = '작성자1'" 조건은 탐색하지만
결국 TB_BOARD 테이블의 실제 행(ROWID)을 따라가서 실제로 있는지 확인까지 해야 됩니다.
즉, "인덱스 스캔 → ROWID 읽기 → 테이블 접근" 이런 랜덤 I/O가 많아질 수 있어서 오히려 느려질 수 있습니다.
C. 반대로 COUNT(*)는 인덱스가 있으면 빠르게 인덱스만 보고 셈 가능
WRITER 인덱스를 쓰면 조건에 맞는 ROWID 목록을 순차적으로 얻어서, 굳이 전체 행을 읽지 않고 인덱스 레벨에서 COUNT가 가능하기 때문에 성능이 향상 되었습니다.
3. 정리
A. EXISTS
- 조건을 만족하는 첫 번째 레코드만 찾으면 바로 TRUE 리턴하고 끝남.
- 인덱스로 인해 오히려 성능이 떨어 질 수 있음.
- 내부적으로 STOP 키워드처럼 작동.
B. COUNT
- 조건에 맞는 모든 레코드를 스캔해서 카운트함.
- 수백만 건이면 진짜 다 셈.
- 결과가 많을수록 시간 증가.
존재 여부만 체크할 때는 반드시 EXISTS 사용 (예: 중복 체크, 데이터 유무 확인)
정확한 숫자가 필요할 때만 COUNT 사용 (예: 페이지네이션, 통계)
존재 여부만 알고 싶은데 COUNT로 다 세면 불필요한 리소스 낭비이고, EXISTS가 더 좋은 선택 입니다.