COUNT(컬럼) 을 COUNT(*) 로 대체
NOT NULL 제약조건이 선언되어 있으면 옵티마이저가 COUNT(컬럼)을 COUNT(*)로 자동 변환해, 테이블 접근 없이 인덱스만으로 처리할 수 있습니다. 물리 모델 설계 단계에서 NOT NULL 명시가 곧 성능 최적화임을 실행계획으로 검증합니다.
NOT NULL 제약조건이 선언되어 있으면 옵티마이저가
COUNT(컬럼)을COUNT(*)로 자동 변환하여 테이블 접근 없이 인덱스만으로 처리할 수 있습니다.
핵심 정리
COUNT(컬럼)시, 해당 컬럼이 NOT NULL 이라면 옵티마이저가 강제로COUNT(*)로 변환합니다.- 물리 모델 설계 시 NOT NULL 컬럼이라면 반드시 Constraint 를 명시해야 합니다. 명시되지 않으면 옵티마이저는 NULL 가능성을 가정해야 하므로 인덱스만으로는 답을 낼 수 없습니다.
COUNT(컬럼)은 어떤 경우에도COUNT(*)보다 빠를 수 없습니다. 같거나 느릴 뿐.
시나리오
같은 테이블, 같은 인덱스, 같은 쿼리. 단 하나만 다릅니다 — FIRST_NAME 컬럼의 NOT NULL 제약 여부.
- 인덱스:
EMP_IDX_02 (JOB_ID, DEPARTMENT_ID) - 쿼리:
1
2
3
4
SELECT e.department_id, COUNT(e.first_name) cnt
FROM employee e
WHERE e.job_id = 'ST_CLERK'
GROUP BY e.department_id;
1) 원본 — FIRST_NAME NULL 허용
FIRST_NAME 컬럼에 NOT NULL 제약이 없으면 옵티마이저는 NULL 행을 카운트에서 제외해야 하므로, 실제 컬럼 값을 확인하기 위해 테이블 접근이 필요합니다.
1
2
3
4
5
6
7
8
9
10
11
12
--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 2 | |
| 1 | SORT GROUP BY NOSORT | | 5 | 95 | 2 | 00:00:01 |
| 2 | TABLE ACCESS BY INDEX ROWID | EMPLOYEE | 6 | 114 | 2 | 00:00:01 | ← 추가 Table Access
| 3 | INDEX RANGE SCAN | EMP_IDX_02 | 6 | | 1 | 00:00:01 |
--------------------------------------------------------------------------------------
Predicate Information:
----------------------
3 - access("E"."JOB_ID"='ST_CLERK')
Id 2 의 TABLE ACCESS BY INDEX ROWID 가 불필요한 추가 작업입니다. 인덱스로 행은 찾았지만, FIRST_NAME 의 NULL 여부를 확인하려면 실제 행을 봐야 하기 때문입니다.
2) 수정 — FIRST_NAME NOT NULL 적용
FIRST_NAME 에 NOT NULL 제약을 선언하면 옵티마이저는 “이 컬럼은 절대 NULL 이 아니다” 를 알게 되고, COUNT(FIRST_NAME) 을 COUNT(*) 로 내부 변환합니다. 결과적으로 인덱스 키만으로 답이 나오므로 테이블 접근이 사라집니다.
1
2
3
4
5
6
7
8
9
10
11
--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 2 | |
| 1 | SORT GROUP BY NOSORT | | 5 | 95 | 2 | 00:00:01 |
| 2 | INDEX RANGE SCAN | EMP_IDX_02 | 6 | | 1 | 00:00:01 | ← 인덱스만으로 커버
--------------------------------------------------------------------------------------
Predicate Information:
----------------------
2 - access("E"."JOB_ID"='ST_CLERK')
Id 2 의 INDEX RANGE SCAN 만으로 처리되어 테이블 접근이 사라졌습니다.
분석
왜 NULL 허용 컬럼은 테이블을 봐야 하는가
COUNT(컬럼) 의 정의는 “NULL 이 아닌 행의 수” 입니다. 인덱스에는 키 컬럼 값이 들어 있지만, 인덱스 키가 아닌 다른 컬럼(여기서는 FIRST_NAME)의 NULL 여부는 인덱스만 봐서는 알 수 없습니다. 따라서:
- NULL 허용: 인덱스로 행을 찾고 → 테이블에서
FIRST_NAME값 확인 → NULL 이면 제외 - NOT NULL: 옵티마이저가 컴파일 타임에
COUNT(*)로 변환 → 테이블 접근 불필요
물리 모델 설계 함의
NULL 가능성이 0% 인 컬럼이라도 DDL 에 NOT NULL 을 명시하지 않으면, 옵티마이저는 가능한 모든 케이스를 가정합니다. 즉, 운영상 NULL 이 없어도 NOT NULL 을 선언하지 않으면 이 변환은 일어나지 않습니다.
NOT NULL 제약은 단순한 무결성 규칙이 아니라 옵티마이저에게 주는 정보이기도 합니다. 선언하지 않으면 옵티마이저가 사용할 수 있는 변환 기회를 잃습니다.
COUNT(*) vs COUNT(컬럼) 성능 비교
| 컬럼 제약 | COUNT(*) | COUNT(컬럼) | 비고 |
|---|---|---|---|
| NOT NULL | 동일 | 동일 (내부 변환) | 옵티마이저가 같은 계획 생성 |
| NULL 허용 | 빠름 (인덱스 가능) | 느림 (테이블 접근 필요) | 의미가 다른 쿼리 — 결과도 다를 수 있음 |
COUNT(컬럼) 이 COUNT(*) 보다 빠를 수 있는 시나리오는 없습니다. 따라서 “행 수가 궁금하다” 면 항상 COUNT(*) 가, “NULL 이 아닌 값의 수가 궁금하다” 면 COUNT(컬럼) 이 의도에 부합합니다.
정리
세 줄로 압축하면:
- NULL 가능성 0% 인 컬럼은 반드시 NOT NULL 을 DDL 에 명시할 것.
- 단순 행 수는
COUNT(*), NOT-NULL 행 수가 의미상 명확할 때만COUNT(컬럼). - 인덱스로 커버 가능한 카운트 쿼리가 갑자기 테이블 접근으로 떨어진다면, 첫 의심 대상은 물리 모델의 NULL 제약 누락.