DISTINCT 를 INDEX UNIQUE SCAN으로 제거
옵티마이저가 결과 row의 유일성을 이미 보장한다고 판단하면 DISTINCT를 위한 HASH/SORT UNIQUE 단계가 자동으로 사라집니다. PK·UNIQUE 인덱스 활용이 곧 옵티마이저에게 '중복 제거 불필요'임을 알려주는 일임을 실행계획으로 보여줍니다.
옵티마이저가 결과 row 의 유일성을 이미 보장한다고 판단하면 — DISTINCT 를 위한 별도 단계(
HASH UNIQUE/SORT UNIQUE)는 자동으로 사라집니다.
PK / UNIQUE 인덱스를 잘 활용하는 것이 곧 옵티마이저에게 “이건 중복 제거 안 해도 됩니다” 라고 알려주는 일입니다.
핵심 정리
- SELECT 절의 컬럼이 PK / UNIQUE 인덱스로 유일성이 보장되면 옵티마이저는
DISTINCT를 자동 제거할 수 있습니다. - FROM 절의 모든 테이블이 PK / UNIQUE 등치 검색으로 0 또는 1 row 만 내놓으면, 조인 결과 자체가 1 row 이하로 보장되어
DISTINCT처리가 불필요해집니다. - 실행계획에
HASH UNIQUE/SORT UNIQUE가 보이면 — DISTINCT 처리에 별도 COST 가 들고 있다는 신호입니다. SELECT 절·WHERE 절·인덱스 구성을 점검해 옵티마이저가 유일성을 추론할 수 있게 정보를 보강할 여지가 있는지 확인합니다.
시나리오
같은 두 테이블·같은 인덱스. 옵티마이저가 어떤 조건에서 DISTINCT 를 별도 단계로 처리하고, 어떤 조건에서 그것을 제거하는지 비교하기 위한 예시입니다.
DEPARTMENTPK:DEPT_ID_PK (department_id)LOCATIONPK:LOC_ID_PK (location_id)DEPARTMENT.LOCATION_ID는 unique 하지 않음 (여러 부서가 같은 위치를 공유 가능)
아래 두 쿼리는 결과값이 다른 별개의 SQL 입니다. 같은 시나리오의 before/after 가 아니라,
DISTINCT가 옵티마이저 단계에서 어떻게 처리되는지를 비교하기 위한 예시입니다.
1) 원본 — d.location_id 가 unique 하지 않음
DEPARTMENT 의 PK 는 department_id 이지만 SELECT 절에는 d.department_id, d.location_id 두 컬럼이 노출됩니다. d.location_id 자체는 unique 하지 않으므로 옵티마이저는 결과 row 의 유일성을 보장할 수 없다고 보고 DISTINCT 를 위한 별도 단계를 추가합니다.
1
2
3
SELECT DISTINCT d.department_id, d.location_id
FROM department d, location l
WHERE d.location_id = l.location_id;
1
2
3
4
5
6
7
8
9
10
11
12
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 4 | |
| 1 | HASH UNIQUE | | 27 | 270 | 4 | 00:00:01 | ← DISTINCT 처리 비용
| 2 | NESTED LOOPS | | 27 | 270 | 3 | 00:00:01 |
| 3 | TABLE ACCESS FULL | DEPARTMENT | 27 | 189 | 3 | 00:00:01 |
| 4 | INDEX UNIQUE SCAN | LOC_ID_PK | 1 | 3 | 0 | |
-----------------------------------------------------------------------------
Predicate Information:
----------------------
4 - access("D"."LOCATION_ID"="L"."LOCATION_ID")
Id 1 의 HASH UNIQUE 가 DISTINCT 처리를 위한 추가 단계 입니다. NL 조인이 끝난 27 row 를 다시 해시 빌드하여 중복을 걷어냅니다.
2) 수정 — PK 등치 술어로 결과 1건 보장
d.department_id = 10 등치 술어가 추가됐습니다. DEPARTMENT 의 PK 가 department_id 이므로 DEPT_ID_PK INDEX UNIQUE SCAN 으로 0 또는 1 row 만 나옵니다. 그 1 row 의 location_id 로 LOCATION 도 LOC_ID_PK INDEX UNIQUE SCAN — 역시 0 또는 1 row.
1
2
3
4
SELECT DISTINCT d.department_id, l.city, l.location_id
FROM department d, location l
WHERE d.location_id = l.location_id
AND d.department_id = 10;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 3 | |
| 1 | NESTED LOOPS | | 1 | 22 | 2 | 00:00:01 | ← UNIQUE 단계 사라짐
| 2 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT | 27 | 270 | 3 | 00:00:01 |
| 3 | INDEX UNIQUE SCAN | DEPT_ID_PK | 1 | | 0 | |
| 4 | TABLE ACCESS BY INDEX ROWID | LOCATION | 23 | 345 | 1 | 00:00:01 |
| 5 | INDEX UNIQUE SCAN | LOC_ID_PK | 1 | 3 | 0 | |
------------------------------------------------------------------------------------
Predicate Information:
----------------------
3 - access("D"."DEPARTMENT_ID"=10)
5 - access("D"."LOCATION_ID"="L"."LOCATION_ID")
Id 1 위에 있던 HASH UNIQUE 가 사라졌습니다. NL 조인 결과 자체가 0 또는 1 row 임을 옵티마이저가 알아차렸기 때문입니다.
분석
왜 원본은 HASH UNIQUE 가 추가되는가
옵티마이저는 SELECT 결과 row 가 이미 unique 한지를 다음 정보로 판단합니다.
- SELECT 절에 노출된 컬럼이 어떤 PK / UNIQUE 인덱스를 cover 하는가
- FROM 절의 각 테이블이 결과에 몇 row 기여하는가 (cardinality estimation)
원본 쿼리는 d.department_id(PK) + d.location_id(non-unique) 두 컬럼을 SELECT 합니다. department_id 만 봤을 때는 이미 unique 하지만, location_id 가 SELECT 에 추가되면서 옵티마이저는 보수적으로 “이 두 컬럼 조합이 unique 한지 확정 못 한다” 고 판단해 HASH UNIQUE 를 끼워 넣습니다. 또한 DEPARTMENT 에 대한 등치 술어가 없어 TABLE ACCESS FULL 로 27 row 를 모두 읽으니, 조인 결과 row 수도 27 로 추정되어 중복 가능성이 더욱 살아 있는 셈입니다.
왜 수정 쿼리는 HASH UNIQUE 가 사라지는가
d.department_id = 10 술어 + DEPT_ID_PK 가 결합하면 옵티마이저는 다음을 추론합니다.
DEPARTMENT에서 0 또는 1 row 만 나옴 (PK 등치 검색)- 그 1 row 의
location_id로LOCATION을 PK 조회 → 역시 0 또는 1 row - 두 1-row 의 NL 조인 결과 = 최대 1 row →
DISTINCT적용 대상이 애초에 없음
결과 cardinality 가 1 임을 옵티마이저가 알면, DISTINCT 는 이미 만족된 제약 이 되어 별도 단계로 변환되지 않습니다.
DISTINCT는 단순히 “중복을 지워줘” 가 아니라 “이 SELECT 결과가 unique 하지 않을 가능성이 있다” 는 정보를 옵티마이저에게 주는 신호입니다.
PK / UNIQUE 인덱스로 유일성이 이미 보장된 자리에서는 옵티마이저가 알아서 비용 0 으로 처리합니다.
비교 요약
| 조건 | 원본 | 수정 |
|---|---|---|
| WHERE 술어 | join 조건만 | join 조건 + department_id = 10 (PK 등치) |
| SELECT 결과 unique 보장 | 부분 (department_id 만) | 전체 (PK 등치로 결과 1 row 확정) |
| DISTINCT 처리 단계 | HASH UNIQUE (Id 1) | (없음) |
DEPARTMENT 접근 | TABLE ACCESS FULL | INDEX UNIQUE SCAN + ROWID |
| 총 Cost | 4 | 3 |
정리
세 줄로 압축하면:
- SELECT 결과의 유일성이 PK / UNIQUE 인덱스로 보장되면 옵티마이저는
DISTINCT를 자동 제거합니다. - WHERE 절의 PK 등치 술어 (
= 상수) 는 cardinality = 1 을 보장하여 위 효과를 강력하게 trigger 합니다. - 실행계획에서
HASH UNIQUE/SORT UNIQUE가 보이면 — SELECT 절·WHERE 절·인덱스 구성을 점검해 옵티마이저가 유일성을 추론할 수 있게 정보를 보강할 수 있는지 확인합니다.