MIN/MAX 최적화와 Top-1 조회: 값·행·동점의 차이
집계값 하나를 구하는 MIN/MAX 최적화와 정렬된 행 하나를 반환하는 Top-1의 의미·NULL·동점·인덱스 조건을 구분합니다.
핵심 요약
MIN·MAX와 Top-1은 비슷해 보이지만 결과의 의미가 다릅니다.
MAX(order_date)
→ 가장 큰 날짜 값 하나
ORDER BY order_date DESC, order_id DESC
FETCH FIRST 1 ROW ONLY
→ 정렬 기준의 첫 행 하나
KEEP (DENSE_RANK LAST ORDER BY order_date)
→ 마지막 순위에 속한 행 집합에서 지정 Aggregate 계산
핵심 차이입니다.
| 구분 | MIN·MAX | Top-1 |
|---|---|---|
| 반환 대상 | 값 | 행 |
| 빈 입력 | 1행, 값 NULL | 0행 |
| NULL | 비교 대상에서 제외 | ORDER BY NULL 위치에 영향 |
| 동점 | 같은 최대값 하나 | Tie-Breaker로 행 선택 또는 WITH TIES |
| 다른 Column | 별도 Join·KEEP 필요 | 같은 행 Column 반환 가능 |
성능 측면에서는 다음을 확인합니다.
Index 범위의 첫·마지막 Entry에서 종료 가능한가?
→ MIN/MAX 전용 Index Scan 가능성
정렬된 Index Entry에서 첫 행을 얻고 중단 가능한가?
→ COUNT STOPKEY·WINDOW NOSORT STOPKEY 가능성
추가 Filter·함수·Group이 있는가?
→ 더 많은 Index Entry·Table Row 처리 가능
이 이론의 범위
이 이론은 SQLP의
SQL 고급활용 및 튜닝 → 소트 튜닝 → MIN/MAX 최적화·Top-1범위에서 값과 행의 차이, NULL·동점, Index 조기 탐색, KEEP Aggregate와 실행계획 검증을 다룹니다.
학습 목표
이 이론을 학습한 뒤에는 다음을 설명할 수 있어야 합니다.
- MIN·MAX Aggregate와 Top-1 Row 조회의 결과 차이를 설명한다.
- 빈 집합과 NULL이 두 방식에 미치는 영향을 설명한다.
- B-tree Index로 MIN/MAX를 조기 탐색할 수 있는 조건을 설명한다.
- 선두 Index Column·함수·묵시적 변환·추가 Filter의 영향을 설명한다.
- Top-1의 NULL 정렬과 결정적 Tie-Breaker를 설계한다.
- WITH TIES와 최고값 Subquery의 동점 처리 차이를 설명한다.
- KEEP(DENSE_RANK FIRST/LAST)의 집계 의미를 설명한다.
- Group별 MIN/MAX와 전체 MIN/MAX의 처리량 차이를 설명한다.
- Index 전용 MIN/MAX Operation과 STOPKEY Plan을 구분한다.
- ALLSTATS LAST의 A-Rows·Buffers·Table Access로 실제 조기 종료를 검증한다.
1. MIN·MAX는 값을 계산한다
SELECT MAX(order_date) AS last_order_date
FROM orders
WHERE customer_id = :customer_id;
이 SQL은 날짜 값 하나를 반환합니다.
조건 만족 행 있음
→ Non-NULL order_date 중 최대값
조건 만족 행 없음
→ 결과 1행, 값 NULL
조건 만족 행은 있으나 모두 NULL
→ 결과 1행, 값 NULL
MIN·MAX는 NULL을 비교에서 제외합니다.
MAX(NULL, DATE '2026-01-01')
→ DATE '2026-01-01'
이 SQL이 답하는 질문입니다.
가장 최근 주문일 값은 무엇인가?
다음 질문과는 다릅니다.
가장 최근 주문 행의 order_id·amount·status는 무엇인가?
2. Top-1은 행을 반환한다
SELECT order_id,
order_date,
amount,
status
FROM orders
WHERE customer_id = :customer_id
ORDER BY order_date DESC NULLS LAST,
order_id DESC
FETCH FIRST 1 ROW ONLY;
특징입니다.
조건 만족 행 있음
→ 정렬 기준의 첫 행 1건
조건 만족 행 없음
→ 결과 0행
order_id는 같은 날짜의 여러 주문 중 한 행을 선택하는 결정적 Tie-Breaker입니다.
2.1 값과 행의 차이
MAX(order_date)
→ 2026-08-01
Top-1
→ order_id=950, order_date=2026-08-01, amount=...
MAX 값만 얻은 뒤 다른 Column을 찾으려면 다시 Join하거나 Analytic·KEEP 구조가 필요합니다.
3. NULL 정렬 차이
Oracle 기본 NULL 정렬입니다.
ASC → NULLS LAST
DESC → NULLS FIRST
따라서 다음 Top-1은 NULL 날짜 행을 먼저 반환할 수 있습니다.
ORDER BY order_date DESC
FETCH FIRST 1 ROW ONLY
최근 Non-NULL 날짜 행이 목적이라면 다음 중 하나를 명시합니다.
WHERE order_date IS NOT NULL
ORDER BY order_date DESC, order_id DESC
ORDER BY order_date DESC NULLS LAST,
order_id DESC
반면 MAX(order_date)는 NULL을 제외합니다.
NULL Row 존재
→ MAX는 최대 Non-NULL 값
→ 기본 DESC Top-1은 NULL Row 가능
4. 동점 처리
4.1 정확히 한 행
ORDER BY score DESC,
student_id ASC
FETCH FIRST 1 ROW ONLY;
Unique Tie-Breaker로 한 행을 결정합니다.
4.2 최고점 동점 모두
ORDER BY score DESC
FETCH FIRST 1 ROW WITH TIES;
경계 Row와 모든 ORDER BY 표현식 값이 같은 Row를 함께 반환합니다.
또는:
SELECT student_id,
score
FROM exam_result
WHERE score = (
SELECT MAX(score)
FROM exam_result
);
두 방법 모두 최고점 행 전체를 반환할 수 있습니다.
4.3 WITH TIES와 Unique Key
ORDER BY score DESC,
student_id ASC
FETCH FIRST 1 ROW WITH TIES;
Tie 기준은 (score, student_id) 전체입니다. student_id가 Unique이면 일반적으로 한 행만 반환됩니다.
5. B-tree Index의 MIN/MAX 조기 탐색
Index:
CREATE INDEX orders_cust_date_ix
ON orders(customer_id, order_date);
SQL:
SELECT MAX(order_date)
FROM orders
WHERE customer_id = :customer_id;
개념적인 흐름입니다.
customer_id 한 범위 탐색
→ 해당 범위의 마지막 Non-NULL order_date Entry
→ 값 반환
→ 나머지 Entry 탐색 중단
가능한 Plan입니다.
SORT AGGREGATE
INDEX RANGE SCAN (MIN/MAX) ORDERS_CUST_DATE_IX
환경에 따라 INDEX FULL SCAN (MIN/MAX) 또는 일반 Index Scan으로 나타날 수도 있습니다.
Operation 이름보다 다음을 봅니다.
- Index 자식 A-Rows
- Buffers·Reads
- Table Access 유무
- Predicate 위치
- Aggregate 결과
6. MIN/MAX 최적화 조건
6.1 Index Prefix 고정
Index:
(customer_id, order_date)
다음 조건은 한 고객 범위를 고정합니다.
WHERE customer_id = :customer_id
반면 다음 SQL은 선두 customer_id를 고정하지 못합니다.
SELECT MAX(order_date)
FROM orders
WHERE region_code = :region_code;
order_date가 고객별 구간에 흩어져 있어 첫·마지막 Entry 하나만으로 결과를 확정하기 어렵습니다.
6.2 대상 Column 순서
MIN/MAX 대상 Column이 Index Key 순서에 있어야 합니다.
(customer_id, order_date)
→ customer_id 고정 후 order_date 순서 활용
6.3 함수 적용
SELECT MAX(TRUNC(order_date))
FROM orders
WHERE customer_id=:customer_id;
일반 order_date Index는 TRUNC(order_date)의 정렬 순서를 직접 제공하지 못할 수 있습니다.
대안입니다.
- Function-Based Index
- 조건·표현식 재설계
- 원본 MAX 후 필요한 수준으로 변환 가능 여부 검토
결과 의미가 같은지 먼저 확인합니다.
6.4 묵시적 변환·Collation
Data Type 불일치나 함수·Collation 차이가 있으면 기존 Index Key 순서를 직접 활용하지 못할 수 있습니다.
Column Data Type
≠ Bind Data Type
→ Function·Conversion이 Column 쪽에 적용
→ Index Access 제한 가능
7. 추가 Filter와 후보 탐색
SELECT MAX(order_date)
FROM orders
WHERE customer_id=:customer_id
AND status='COMPLETED';
Index가 다음뿐이라고 가정합니다.
(customer_id, order_date)
최근 Entry부터 Table을 방문해 status를 확인해야 할 수 있습니다.
Index 마지막 Entry
→ Table Row
→ status Filter 실패
→ 이전 Entry
→ 반복
최근 주문 100,000건 중 완료 주문이 드물면 많은 후보를 탐색할 수 있습니다.
복합 Index 후보:
(customer_id, status, order_date)
검토합니다.
- status 선택도·Skew
- 다른 SQL의 Predicate
- Index 폭·LEAF_BLOCKS
- DML·Redo·Undo 비용
- Covering 가능성
- 실제 후보 A-Rows·Table Buffers
8. Top-1과 Index Order·STOPKEY
SELECT order_id,
order_date,
amount
FROM orders
WHERE customer_id=:customer_id
ORDER BY order_date DESC NULLS LAST,
order_id DESC
FETCH FIRST 1 ROW ONLY;
Index 후보입니다.
CREATE INDEX orders_latest_ix
ON orders(
customer_id,
order_date DESC,
order_id DESC
);
가능한 흐름입니다.
customer_id 범위 탐색
→ 첫 Entry
→ 필요한 경우 Table Row
→ 결과 1행
→ 하위 요청 중단
Plan 예시입니다.
COUNT STOPKEY
TABLE ACCESS BY INDEX ROWID ORDERS
INDEX RANGE SCAN ORDERS_LATEST_IX
또는 WINDOW NOSORT STOPKEY 등으로 나타날 수 있습니다.
실제 효율은 다음으로 판단합니다.
결과 A-Rows=1
Index A-Rows=1~소수
Table A-Rows=1~소수
Buffers 작음
결과가 1행이어도 Index 후보 A-Rows가 수십만이면 조기 종료가 효율적이지 않습니다.
9. Covering Top-1 Index
조회 Column이 Index에 모두 있으면 Table by ROWID를 생략할 수 있습니다.
(customer_id, order_date DESC, order_id DESC, amount)
가능한 Plan:
COUNT STOPKEY
INDEX RANGE SCAN ORDERS_LATEST_COVER_IX
장점입니다.
- Sort 제거
- Table Access 제거
- 첫 Match 후 중단 가능
Trade-off입니다.
- Index 폭 증가
- Leaf Block 증가
- Cache 점유
- DML·Redo·Undo 증가
- 중복 Index 가능성
한 SQL만 보고 무조건 Wide Index를 만들지 않습니다.
10. KEEP(DENSE_RANK FIRST/LAST)
SELECT customer_id,
MAX(order_date) AS last_order_date,
MAX(order_id) KEEP (
DENSE_RANK LAST
ORDER BY order_date
) AS selected_order_id
FROM orders
GROUP BY customer_id;
의미입니다.
customer_id별 order_date Dense Rank 계산
→ 마지막 Rank의 행 집합 선택
→ 그 집합에서 MAX(order_id) 계산
KEEP도 Aggregate입니다.
최대 날짜에 여러 Row가 있으면:
MAX(order_id) KEEP(...)
→ Tie 집합에서 가장 큰 order_id
여러 Column을 각각 MAX(...) KEEP(...)로 계산하면 Tie 집합에서 각 Aggregate가 독립 계산됩니다.
MAX(order_id) KEEP(...)
MAX(amount) KEEP(...)
두 값이 반드시 같은 실제 Row에서 나온다고 단정할 수 없습니다.
같은 한 행의 모든 Column이 필요하면:
- ORDER BY에 Unique Tie-Breaker 추가
- ROW_NUMBER로 행 확정
- Top-1 Inline View·Join 사용
11. Group별 MIN/MAX
SELECT customer_id,
MAX(order_date)
FROM orders
GROUP BY customer_id;
전체 MAX 하나와 다릅니다.
전체 MAX
→ 전체 Index 범위 끝에서 조기 종료 가능성
Group별 MAX
→ 모든 customer_id Group 식별
→ Group마다 최대값 계산
Index (customer_id, order_date)를 이용해 GROUP BY NOSORT 등이 가능할 수 있지만 모든 Group의 Entry를 상당 부분 처리할 수 있습니다.
MIN/MAX 함수 존재
≠ 항상 Index Entry 1개만 읽음
12. 빈 집합 처리와 SQL 설계
Aggregate:
SELECT MAX(order_date)
FROM orders
WHERE customer_id=:customer_id;
결과:
항상 1행
값은 날짜 또는 NULL
Top-1:
SELECT ...
FROM orders
WHERE customer_id=:customer_id
ORDER BY ...
FETCH FIRST 1 ROW ONLY;
결과:
0행 또는 1행
Application에서 구분해야 합니다.
고객이 없거나 주문이 없음
→ NULL 값 1행이 필요한가?
→ Result Set 0행이 필요한가?
MAX 결과 NULL은 다음 두 경우를 구분하지 못할 수 있습니다.
- 조건 만족 Row 0건
- 조건 만족 Row는 있으나 대상 Column 모두 NULL
필요하면 COUNT 등을 함께 반환합니다.
SELECT COUNT(*) AS row_count,
COUNT(order_date) AS non_null_count,
MAX(order_date) AS max_date
FROM orders
WHERE customer_id=:customer_id;
13. 실행계획 검증
SELECT /*+ GATHER_PLAN_STATISTICS */
MAX(order_date)
FROM orders
WHERE customer_id=:customer_id;
SELECT /*+ GATHER_PLAN_STATISTICS */
order_id, order_date, amount
FROM orders
WHERE customer_id=:customer_id
ORDER BY order_date DESC NULLS LAST,
order_id DESC
FETCH FIRST 1 ROW ONLY;
SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
:sql_id,
:child_no,
'ALLSTATS LAST +PREDICATE +ALIAS +NOTE'
)
);
확인 항목입니다.
| 지표 | 진단 질문 |
|---|---|
| Index Operation | MIN/MAX 전용 또는 순서 기반 Scan인가? |
| Index A-Rows | 값·행 하나를 찾기 위해 몇 Entry를 읽었는가? |
| Table A-Rows | Filter·Projection 때문에 몇 Table Row를 방문했는가? |
| Buffers·Reads | 실제 Block 접근량은 얼마인가? |
| STOPKEY | 결과를 얻은 뒤 하위 요청이 중단됐는가? |
| Predicate | Access와 Table Filter는 어디에 있는가? |
| Result Row | 빈 집합·NULL·Tie 요구와 일치하는가? |
상위 SORT AGGREGATE A-Rows=1만 보고 Index 작업량이 1이라고 단정하지 않습니다.
14. 대표 비교
효율적인 MAX
SORT AGGREGATE A-Rows 1
INDEX RANGE SCAN (MIN/MAX) A-Rows 1
Buffers 3
Filter가 비효율적인 MAX
SORT AGGREGATE A-Rows 1
TABLE ACCESS BY INDEX ROWID A-Rows 1
INDEX RANGE SCAN DESCENDING A-Rows 150,000
Buffers 180,000
효율적인 Top-1
COUNT STOPKEY A-Rows 1
INDEX RANGE SCAN A-Rows 1
Buffers 4
결과만 1행인 비효율 Plan
SORT ORDER BY STOPKEY A-Rows 1
TABLE ACCESS FULL A-Rows 50,000,000
Buffers 400,000
자주 혼동하는 판단
| 혼동 | 정확한 기준 |
|---|---|
| MAX와 최신 행은 동일 | 값 하나와 행 하나 |
| 빈 집합에서 둘 다 0행 | MAX는 NULL 1행, Top-1은 0행 |
| DESC는 Non-NULL 최대부터 | 기본 DESC는 NULLS FIRST |
| MIN/MAX 함수면 항상 1 Entry | Prefix·함수·Filter·Group 확인 |
| KEEP은 실제 한 행을 반환 | Tie 집합에 Aggregate 적용 |
| Group별 MAX도 전체 MAX처럼 조기 종료 | Group별 계산을 위해 많은 Entry 처리 가능 |
| 결과 A-Rows=1이면 효율적 | 하위 Index·Table A-Rows·Buffers 확인 |
| Index가 있으면 Top-1 최적 | 순서·NULL·Filter·Covering 조건 확인 |
| WITH TIES는 항상 1행 | 경계 Tie 수만큼 증가 가능 |
| Operation 이름만 보면 성공 | 실제 Runtime Statistics로 검증 |
개념 확인 문제
문제를 누르면 바로 아래에서 정답과 해설을 확인할 수 있습니다.
01MAX 값 조회와 Top-1 행 조회의 결과 의미 차이를 설명하시오.
값과 행
- MAX는 입력 집합의 최대값 하나를 계산합니다.
- Top-1은 정렬 기준의 첫 행과 그 행의 Column을 반환합니다.
02빈 집합에서 MAX와 Top-1의 반환 행 수를 설명하시오.
빈 집합
- MAX Aggregate는 결과 1행을 반환하며 값은 NULL입니다.
- Top-1은 조건 만족 Row가 없으면 0행을 반환합니다.
03MAX의 NULL 처리와 DESC Top-1의 기본 NULL 정렬 차이를 설명하시오.
NULL
- MAX는 NULL을 비교 대상에서 제외합니다.
- Oracle 기본 DESC 정렬은 NULLS FIRST이므로 Top-1이 NULL Row를 반환할 수 있습니다.
- Non-NULL 최신 행은 WHERE IS NOT NULL 또는 NULLS LAST를 명시합니다.
04Index MIN/MAX 조기 탐색에 필요한 Prefix와 Key 조건을 설명하시오.
Index 조건
- 선두 Column Predicate가 한 Index 범위를 고정해야 합니다.
- MIN/MAX 대상 Column이 그 범위 안에서 Index Key 순서를 제공해야 합니다.
- 실제 Plan과 Index A-Rows·Buffers를 확인합니다.
05함수 적용·묵시적 변환이 MIN/MAX Index 활용을 제한하는 이유를 설명하시오.
함수·변환
- 일반 Index는 원본 Column 값의 순서를 저장합니다.
- TRUNC·TO_CHAR 같은 함수 결과나 Column 쪽 묵시적 변환의 순서를 직접 제공하지 못할 수 있습니다.
- Function-Based Index 또는 SQL 재설계를 검토합니다.
06추가 Table Filter가 MIN/MAX·Top-1 후보 탐색을 늘리는 이유를 설명하시오.
추가 Filter
- Index 밖 조건은 후보 Entry의 Table Row를 읽은 뒤 평가됩니다.
- Match를 찾을 때까지 여러 Entry·Table Block을 계속 읽을 수 있습니다.
- 복합·Covering Index의 전체 Workload 비용을 비교합니다.
07WITH TIES와 Unique Tie-Breaker가 결과 행 수에 미치는 영향을 설명하시오.
Tie
- WITH TIES는 경계 Row와 모든 ORDER BY 표현식이 같은 Row를 추가 반환합니다.
- Unique Tie-Breaker를 포함하면 전체 Sort Key Tie가 없어져 정확히 한 행 방향이 됩니다.
08KEEP(DENSERANK LAST)의 Tie 집합 Aggregate 의미를 설명하시오.
KEEP
- FIRST·LAST 순위에 속한 행 집합을 선택합니다.
- 그 집합에서 MAX·MIN 등 지정 Aggregate를 계산합니다.
- 여러 KEEP Aggregate가 반드시 같은 실제 Row의 값을 선택하는 것은 아닙니다.
09Group별 MAX가 전체 MAX보다 많은 Entry를 처리하는 이유를 설명하시오.
Group별 MAX
- 고객별 Group을 모두 식별하고 각 Group의 최대값을 계산해야 합니다.
- Index 순서를 활용해 Sort를 줄여도 전체 Group Data를 상당 부분 처리할 수 있습니다.
10실행계획에서 MIN/MAX·Top-1 최적화를 검증하는 절차를 설명하시오.
검증 - Index Operation과 Predicate를 확인합니다. - Index·Table A-Rows, Buffers·Reads를 확인합니다. - STOPKEY 이후 하위 요청이 실제 중단됐는지 봅니다. - 빈 집합·NULL·Tie 결과 의미를 검증합니다. - Index 유지 비용과 다른 Bind·Workload 회귀도 확인합니다.