포스트

COUNT(컬럼) 을 COUNT(*) 로 대체

NOT NULL 제약조건이 선언되어 있으면 옵티마이저가 COUNT(컬럼)을 COUNT(*)로 자동 변환해, 테이블 접근 없이 인덱스만으로 처리할 수 있습니다. 물리 모델 설계 단계에서 NOT NULL 명시가 곧 성능 최적화임을 실행계획으로 검증합니다.

COUNT(컬럼) 을 COUNT(*) 로 대체

NOT NULL 제약조건이 선언되어 있으면 옵티마이저가 COUNT(컬럼)COUNT(*) 로 자동 변환하여 테이블 접근 없이 인덱스만으로 처리할 수 있습니다.

핵심 정리

  1. COUNT(컬럼) 시, 해당 컬럼이 NOT NULL 이라면 옵티마이저가 강제로 COUNT(*) 로 변환합니다.
  2. 물리 모델 설계 시 NOT NULL 컬럼이라면 반드시 Constraint 를 명시해야 합니다. 명시되지 않으면 옵티마이저는 NULL 가능성을 가정해야 하므로 인덱스만으로는 답을 낼 수 없습니다.
  3. 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 2TABLE 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 2INDEX 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(컬럼) 이 의도에 부합합니다.


정리

세 줄로 압축하면:

  1. NULL 가능성 0% 인 컬럼은 반드시 NOT NULL 을 DDL 에 명시할 것.
  2. 단순 행 수COUNT(*), NOT-NULL 행 수가 의미상 명확할 때만 COUNT(컬럼).
  3. 인덱스로 커버 가능한 카운트 쿼리가 갑자기 테이블 접근으로 떨어진다면, 첫 의심 대상은 물리 모델의 NULL 제약 누락.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.