현재 선택한 SQL 과정

SQLP 이론 학습

이론 목록으로 돌아가기

최신 이력 조회: Top-1·ROW_NUMBER·DENSE_RANK·KEEP

이력 데이터에서 최신 한 건 또는 최신 시점의 여러 값을 효율적으로 찾는 대표 패턴을 비교합니다.

예상 읽기 24

핵심 요약

“최신 이력”은 한 가지 의미로 고정되지 않습니다. 먼저 다음 중 어떤 결과가 필요한지 정해야 합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
A. 특정 Key의 최신 행 한 건
B. 모든 Key별 최신 행 한 건
C. 최신 시각과 동률인 모든 행
D. 최신 행의 일부 Column 값만 Group별로 추출
E. 특정 기준 시점 이하의 최신 행

같은 변경 시각의 행이 여러 개 존재할 수 있으므로 변경 시각 + 변경 순번 + Primary Key처럼 업무적으로 최신을 완전히 결정하는 전체 순서를 확정해야 합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
정확히 한 행
→ Top-1·ROW_NUMBER
→ 유일한 전체 ORDER BY 필요

최신 시각의 동률 전체
→ DENSE_RANK
→ 동률을 일부러 유지

Group별 최신 Rank의 일부 값
→ KEEP(DENSE_RANK FIRST·LAST)
→ Rank 집합 안에서 Aggregate 수행

필요한 Key 수, 반환 Column 폭, Index Covering, 첫 행 응답과 전체 처리량에 따라 비용 구조가 달라집니다.

이 이론의 범위

SQLP SQL 고급 활용 및 튜닝 → 고급 SQL 활용 → 최신 이력 조회 범위에서 Top-1, ROW_NUMBER, DENSE_RANK, KEEP, MAX 후 Join, Outer Join 보존과 실행계획을 다룹니다.

학습 목표

이 이론을 학습한 뒤에는 다음을 설명할 수 있어야 합니다.

  1. 최신 행 한 건과 최신 시각의 모든 행을 구분한다.
  2. 결정적인 최신 순서를 만들기 위한 Tie-Breaker를 설계한다.
  3. 단일 Key Top-1 Query와 Stopkey의 실행 원리를 이해한다.
  4. 모든 Key별 최신 행을 ROW_NUMBER로 구한다.
  5. 동률 행을 보존할 때 DENSE_RANK를 사용한다.
  6. 최신 행의 여러 값을 KEEP (DENSE_RANK LAST ...)로 추출한다.
  7. MAX(날짜)와 다른 Column의 독립적인 MAX가 같은 행을 보장하지 않는 이유를 설명한다.
  8. 소수 Key 반복 Index Probe와 전체 Window·Group 처리의 비용을 비교한다.
  9. WITH TIES의 전체 Query 경계 동률과 Key별 DENSE_RANK를 구분한다.
  10. NULL 최신 Key·MAX 후 Join의 NULL 비교 문제를 설명한다.
  11. 0건·NULL·동점·기준 시점 조건에서 결과를 검증한다.

1. 최신 이력의 결과 의미를 먼저 정한다

다음 이력 데이터를 사용합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
DEVICE_ID | CHANGE_TIME        | CHANGE_SEQ | STATUS | HISTORY_ID
----------+--------------------+------------+--------+-----------
A         | 2026-07-01 09:00   | 1          | READY  | 101
A         | 2026-07-10 10:00   | 1          | RUN    | 102
A         | 2026-07-10 10:00   | 2          | STOP   | 103
B         | 2026-07-03 08:00   | 1          | READY  | 201

장비 A의 최신 변경 시각은 2026-07-10 10:00이며 해당 시각에 두 행이 있습니다.

  • 최신 행 한 건: CHANGE_SEQ = 2인 STOP 행
  • 최신 시각의 모든 행: RUN과 STOP 두 행

업무에서 “최신”이라는 표현만 사용하면 두 결과가 섞일 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
최신 시각
→ 시간 Key의 최대값

최신 행
→ 시간 + 순번 + PK의 전체 순서에서 첫 행

최신 상태값
→ 최신 행의 STATUS

최신 시각 동률 전체
→ 시간 Key가 최대인 모든 행

한 건이 필요하면 순서를 유일하게 만드는 기준을 정의합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
ORDER BY CHANGE_TIME DESC,
         CHANGE_SEQ DESC,
         HISTORY_ID DESC

CHANGE_TIME이 Nullable이면 Oracle의 DESC 정렬에서 NULL이 앞쪽에 올 수 있으므로 다음 중 하나를 적용합니다.

  • 최신 기준 Column을 NOT NULL로 설계
  • NULLS LAST를 명시
  • NULL의 업무 의미를 별도 상태로 정의

2. 특정 Key의 최신 행 한 건: Top-1

장비 하나의 최신 행을 조회합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT h.device_id,
       h.change_time,
       h.change_seq,
       h.status,
       h.history_id
FROM   device_status_history h
WHERE  h.device_id = :device_id
ORDER  BY h.change_time DESC NULLS LAST,
          h.change_seq DESC,
          h.history_id DESC
FETCH FIRST 1 ROW ONLY;

실행 흐름

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
DEVICE_ID 등치 탐색
→ 최신 방향으로 Index 범위 탐색
→ 첫 행에서 중단
→ 필요한 Table Column 반환

대표 Index 후보입니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
CREATE INDEX device_status_h_ix
ON device_status_history
   (device_id, change_time DESC, change_seq DESC, history_id DESC);

실제 Index에는 조회 빈도, 반환 Column, DML 비용을 함께 고려합니다. Oracle은 Ascending Index를 역방향으로 Scan할 수도 있으므로 Hint나 DESC 정의만으로 성능을 단정하지 않습니다.

실행계획에서 확인할 항목

  • INDEX RANGE SCAN DESCENDING
  • COUNT STOPKEY
  • WINDOW NOSORT STOPKEY
  • SORT ORDER BY STOPKEY
  • 하위 Index·Table Operation의 A-Rows
  • 첫 행까지와 전체 Query 완료까지의 Buffers

SORT ORDER BY STOPKEY가 보이면 결과는 한 건이어도 하위 후보를 많이 읽을 수 있습니다. Index 순서가 ORDER BY를 만족해 조기 중단하는지 확인합니다.

INDEX_DESC Hint의 위치

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT /*+ INDEX_DESC(h device_status_h_ix) */ ...

INDEX_DESC는 특정 Index의 역방향 접근을 시험하는 Hint입니다. 다음 역할까지 대신하지는 않습니다.

  • 최신 행의 업무 기준 정의
  • 동률을 해소하는 ORDER BY
  • 결과 한 건 제한
  • Hint 적용 여부 검증

먼저 올바른 SQL 의미와 Index를 설계하고, Hint는 Optimizer 판단을 비교하는 제한적인 실험 수단으로 사용합니다.

2.1 ONLYWITH TIES

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
FETCH FIRST 1 ROW ONLY

ONLY는 정확히 지정한 수만큼 반환합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
FETCH FIRST 1 ROW WITH TIES

WITH TIES는 전체 Query 결과에서 마지막으로 Fetch된 행과 ORDER BY Key가 같은 추가 행을 반환합니다. 반드시 ORDER BY가 있어야 동률 기준이 정의됩니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
전체 결과 Top-1 동률
→ WITH TIES

각 DEVICE_ID별 최신 시각 동률
→ PARTITION BY가 있는 DENSE_RANK

일반 WITH TIES를 모든 Key별 동률 보존 수단으로 혼동하지 않습니다.

2.2 최신 Key의 NULL 정책

Oracle의 기본 NULL 위치는 다음과 같습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
ASC  → NULLS LAST
DESC → NULLS FIRST

따라서 다음 SQL은 NULL CHANGE_TIME을 가장 최신처럼 앞에 둘 수 있습니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
ORDER BY change_time DESC

NULL이 “변경 시각 미확정”이라면 다음처럼 명시합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
ORDER BY change_time DESC NULLS LAST,
         change_seq DESC,
         history_id DESC

반대로 NULL이 실제 업무상 최우선 상태라면 그 의미를 문서화하고 그대로 정렬합니다. 단순히 성능을 위해 NULL 위치를 바꾸면 결과가 달라집니다.


3. 모든 Key별 최신 행 한 건: ROW_NUMBER

전체 장비의 최신 행을 한 번에 조회합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT device_id,
       change_time,
       change_seq,
       status,
       history_id
FROM (
    SELECT h.*,
           ROW_NUMBER() OVER (
               PARTITION BY h.device_id
               ORDER BY h.change_time DESC NULLS LAST,
                        h.change_seq DESC,
                        h.history_id DESC
           ) AS rn
    FROM   device_status_history h
)
WHERE rn = 1;

처리 의미

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
DEVICE_ID별 Partition 생성
→ 각 Partition에서 최신 순서 부여
→ 첫 번째 행만 반환

ROW_NUMBER는 동률이 있어도 각 행에 서로 다른 번호를 부여합니다. Oracle은 동률 행의 처리 순서에 따라 번호를 정할 수 있으므로 ORDER BY가 전체 순서를 만들지 못하면 어떤 행이 1번이 되는지 비결정적일 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
ORDER BY change_time DESC
→ 같은 시각 행의 1번 미정

ORDER BY change_time DESC,
         change_seq DESC,
         history_id DESC
→ 전체 순서 확정

Oracle AI Database 26ai에서 QUALIFY를 사용할 수 있는 환경이라면 Analytic 결과를 같은 Query Block에서 Filter하는 표현도 검토할 수 있습니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT h.*,
       ROW_NUMBER() OVER (
           PARTITION BY device_id
           ORDER BY change_time DESC NULLS LAST,
                    change_seq DESC,
                    history_id DESC
       ) AS rn
FROM device_status_history h
QUALIFY rn = 1;

운영 Version과 SQL 작성 표준을 확인하고, Inline View 방식과 실제 Plan을 비교합니다.

기준 시점 최신 행

2026-07-05 시점의 최신 행을 찾으려면 기준 시점 조건을 Analytic 함수가 처리할 입력에 먼저 적용합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT device_id,
       change_time,
       change_seq,
       status
FROM (
    SELECT h.*,
           ROW_NUMBER() OVER (
               PARTITION BY h.device_id
               ORDER BY h.change_time DESC NULLS LAST,
                        h.change_seq DESC,
                        h.history_id DESC
           ) AS rn
    FROM   device_status_history h
    WHERE  h.change_time <= :as_of
)
WHERE rn = 1;

다음 순서가 핵심입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
기준 시점 이하로 Filter
→ Key별 최신 순위 계산

순위를 먼저 계산하고 바깥에서 기준 시점 조건을 적용하면 미래 최신 행이 1번인 Key가 통째로 누락될 수 있습니다.

최종 결과 정렬

Analytic 함수 내부의 ORDER BY는 함수 계산 순서입니다. Client에 반환되는 전체 결과 순서를 보장하려면 바깥 Query에 별도 ORDER BY를 작성합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
ORDER BY device_id;

4. 최신 시각의 동률 행을 모두 보존: DENSE_RANK

최신 변경 시각의 모든 행이 필요하다면 DENSE_RANK를 사용합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT device_id,
       change_time,
       change_seq,
       status,
       history_id
FROM (
    SELECT h.*,
           DENSE_RANK() OVER (
               PARTITION BY h.device_id
               ORDER BY h.change_time DESC NULLS LAST
           ) AS dr
    FROM   device_status_history h
)
WHERE dr = 1;

장비 A에는 최신 시각 행이 두 개이므로 두 행 모두 반환됩니다.

DENSE_RANK는 ORDER BY 표현식이 같은 Row에 같은 Rank를 부여하고 다음 Rank를 건너뛰지 않습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
시간 10:00 → Rank 1, Rank 1
시간 09:00 → Rank 2

change_time DESC NULLS LAST에서 Non-NULL 시각이 하나라도 있으면 NULL Row는 최신 Rank가 아닙니다. 모든 Row의 change_time이 NULL이면 모두 같은 Rank가 될 수 있으므로, 한 행이 필요하다면 별도 Tie-Breaker와 ROW_NUMBER를 사용합니다.

목적함수와 정렬 기준
Key별 최신 행 정확히 한 건ROW_NUMBER + 유일한 전체 정렬
Key별 최신 시각의 모든 행DENSE_RANK + 최신 시각 기준
전체 결과에서 첫 행과 동률 보존FETCH FIRST 1 ROW WITH TIES 검토

WITH TIES는 하나의 Query 결과 전체에 대한 경계 동률을 보존합니다. Key별 동률은 PARTITION BY가 가능한 DENSE_RANK가 더 직접적입니다.


5. 최신 행의 값을 Group별로 추출: KEEP

장비별 최신 변경 시각과 최신 상태를 Aggregate 결과 한 행으로 만들 수 있습니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT h.device_id,
       MAX(h.change_time) AS latest_change_time,
       MAX(h.change_seq) KEEP (
           DENSE_RANK LAST
           ORDER BY h.change_time NULLS FIRST,
                    h.change_seq,
                    h.history_id
       ) AS latest_change_seq,
       MAX(h.status) KEEP (
           DENSE_RANK LAST
           ORDER BY h.change_time NULLS FIRST,
                    h.change_seq,
                    h.history_id
       ) AS latest_status,
       MAX(h.history_id) KEEP (
           DENSE_RANK LAST
           ORDER BY h.change_time NULLS FIRST,
                    h.change_seq,
                    h.history_id
       ) AS latest_history_id
FROM   device_status_history h
GROUP BY h.device_id;

KEEP의 의미

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
1. ORDER BY 기준으로 각 Group의 순위를 계산
2. DENSE_RANK LAST에 해당하는 행 집합 선택
3. 그 행 집합에서 MAX(status) 같은 Aggregate 계산

정렬 기준이 HISTORY_ID까지 포함해 한 행을 유일하게 결정하면 LAST Rank 행 집합도 한 행이 됩니다. 동률이 남아 있으면 MAX(status)는 그 동률 행 집합에서 가장 큰 값을 계산합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
KEEP 순서
1. ORDER BY로 Rank 계산
2. FIRST 또는 LAST Rank 행 집합 선택
3. 선택 집합에 MAX·MIN·SUM·AVG·COUNT 등 Aggregate 적용

따라서 KEEP는 “원본 최신 행 전체”를 자동 반환하는 함수가 아닙니다. 여러 Column을 각각 KEEP로 가져올 때는 모든 KEEP 식에 동일한 ORDER BY 전체 순서를 사용해야 같은 원본 행을 가리킵니다.

다음 두 표현은 방향을 반대로 정의한 동등 후보입니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
MAX(status) KEEP (
    DENSE_RANK LAST
    ORDER BY change_time ASC NULLS FIRST,
             change_seq ASC,
             history_id ASC
)
SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
MAX(status) KEEP (
    DENSE_RANK FIRST
    ORDER BY change_time DESC NULLS LAST,
             change_seq DESC,
             history_id DESC
)

NULL 위치까지 포함해 실제 업무 순서가 같은지 확인합니다.

KEEP Column별 ORDER BY 불일치 위험

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
MAX(status) KEEP (
    DENSE_RANK LAST ORDER BY change_time, history_id
),
MAX(operator_id) KEEP (
    DENSE_RANK LAST ORDER BY change_time
)

두 KEEP의 순서 정의가 다르면 STATUSOPERATOR_ID가 서로 다른 LAST Rank 집합에서 계산될 수 있습니다. 최신 행의 여러 값을 한 행처럼 조합하려면 동일한 정렬 Key·방향·NULL 정책을 사용합니다.

KEEP가 적합한 경우

  • Group별 결과가 한 행이어야 함
  • 최신 행에서 몇 개의 값만 필요함
  • 원본 행 전체를 반환할 필요가 없음
  • 여러 Scalar Subquery 대신 한 번의 Group 처리로 값을 구하고 싶음

원본 행의 많은 Column을 그대로 반환해야 하면 ROW_NUMBER 방식이 더 읽기 쉬운 경우가 많습니다.


6. 서로 독립적인 MAX의 오류

다음 SQL은 최신 행의 상태를 보장하지 않습니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT device_id,
       MAX(change_time) AS latest_change_time,
       MAX(status)      AS latest_status
FROM   device_status_history
GROUP BY device_id;

MAX(change_time)MAX(status)는 각 Column에서 독립적으로 가장 큰 값을 계산합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
최신 시각 행의 STATUS = READY
과거 행의 STATUS       = STOP
문자열 MAX(STATUS)     = STOP

결과가 다음처럼 서로 다른 행의 값을 결합할 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
최신 시각 + 과거 행의 상태

최신 행의 다른 값을 가져오려면 다음 중 하나를 사용합니다.

  • ROW_NUMBER로 실제 최신 행 선택
  • KEEP (DENSE_RANK LAST ORDER BY ...)
  • 최신 Key를 구한 뒤 전체 Key로 원본과 Join

7. MAX 시각을 구한 뒤 Join하는 패턴

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
WITH latest_time AS (
    SELECT device_id,
           MAX(change_time) AS max_change_time
    FROM   device_status_history
    GROUP BY device_id
)
SELECT h.*
FROM   device_status_history h
JOIN   latest_time x
  ON   x.device_id      = h.device_id
 AND   x.max_change_time = h.change_time;

같은 CHANGE_TIME의 행이 여러 개면 여러 행이 반환됩니다. 최신 행 한 건이 필요하다면 변경 순번까지 단계적으로 구하거나 유일한 최신 Key를 한 번에 결정합니다.

또한 Aggregate는 일반적으로 NULL을 무시합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
한 Key의 CHANGE_TIME이 전부 NULL
→ MAX(CHANGE_TIME) = NULL

다음 Join은 NULL과 NULL이 같지 않으므로 원본 Row를 찾지 못합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
x.max_change_time = h.change_time

NULL이 허용되는 최신 Key라면 IS NOT DISTINCT FROM을 지원하는 Version·문법, 명시적 NULL 조건, 또는 업무적으로 NOT NULL 제약을 검토합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
ON x.device_id = h.device_id
AND (
       x.max_change_time = h.change_time
    OR (x.max_change_time IS NULL AND h.change_time IS NULL)
)

그러나 NULL Row가 여러 개라면 여전히 여러 행이 반환되므로 한 행을 결정할 Tie-Breaker가 필요합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
MAX(CHANGE_TIME)만 Join
→ 최신 시각의 모든 행

CHANGE_TIME + CHANGE_SEQ + HISTORY_ID로 전체 순서 결정
→ 최신 행 한 건

이 패턴을 사용할 때는 Group By 결과와 원본 Join에서 읽는 행 수, Join Method, 동률 행 수를 확인합니다.

7.1 전체 최신 Key를 단계적으로 Join하는 경우

최신 한 건을 Aggregate와 Join으로 구현하려면 Lexicographic 전체 순서를 보존해야 합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
1. Key별 MAX(CHANGE_TIME)
2. 그 시각 Row에서 MAX(CHANGE_SEQ)
3. 그 시각·순번 Row에서 MAX(HISTORY_ID)
4. 전체 Key로 원본 Join

각 단계의 후보 범위를 이전 단계 결과로 제한하지 않고 Column별 MAX를 독립 계산하면 서로 다른 Row의 Key가 조합될 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
MAX(CHANGE_TIME)
MAX(CHANGE_SEQ)
MAX(HISTORY_ID)
→ 각각 독립 계산하면 실제 Row가 없는 복합 Key 가능

이 복잡성 때문에 원본 행 전체가 필요하면 ROW_NUMBER가 더 명확한 경우가 많습니다.


8. 소수 Key와 다수 Key의 비용 비교

소수 Key

마스터에서 소수 장비만 선별한 뒤 장비마다 최신 이력 한 건을 Index Stopkey로 찾는 방식이 유리할 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
선별된 장비 20건
→ 장비별 최신 Index Probe 20회

첫 행 응답이 빠르고 불필요한 전체 이력 Sort를 피할 수 있습니다.

다수 Key

수십만 개 장비의 최신 이력을 모두 구하면 반복 Probe가 커질 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
장비 500,000건
→ 최신 Index Probe 최대 500,000회

이 경우 전체 이력을 한 번 읽어 ROW_NUMBER, HASH GROUP BY, KEEP로 처리하는 방식과 비교합니다.

방식주된 비용
Key별 Top-1 Index Probe반복 Starts, Index·Table Random Access
ROW_NUMBER입력 Scan, WINDOW SORT 또는 NOSORT 가능성
KEEPGroup By Workarea, Hash·Sort Aggregate
MAX 후 JoinGroup 처리 + 원본 재Join

실제 선택은 Key 수, Key별 이력 개수, 반환 Column 폭, Index Covering, Clustering Factor, Memory·TEMP를 함께 측정합니다.

확인 지표해석
Outer Key 수·Starts반복 Index Probe 횟수
하위 Index A-RowsStopkey 전에 읽은 Index Entry
Table A-Rows·BuffersROWID 방문과 추가 Filter 비용
WINDOW SORT·NOSORTAnalytic 입력 정렬 여부
Group By OMem·Used-TmpKEEP·MAX 집계 Workarea
첫 행 시간화면·부분 Fetch 응답
전체 Fetch 시간Batch·전체 결과 처리

SORT ORDER BY STOPKEY는 전체 Sort보다 Memory를 줄일 수 있지만 하위 후보를 모두 또는 많이 읽을 수 있습니다. INDEX RANGE SCAN DESCENDING도 Table Access가 많으면 전체 비용이 커질 수 있습니다.


9. 마스터 행을 보존해야 하는 경우

이력이 없는 장비도 결과에 포함해야 한다면 최신 이력 결과를 Outer Join합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
WITH ranked_history AS (
    SELECT h.*,
           ROW_NUMBER() OVER (
               PARTITION BY h.device_id
               ORDER BY h.change_time DESC NULLS LAST,
                        h.change_seq DESC,
                        h.history_id DESC
           ) AS rn
    FROM   device_status_history h
)
SELECT d.device_id,
       d.device_name,
       h.change_time,
       h.status
FROM   device d
LEFT JOIN ranked_history h
  ON   h.device_id = d.device_id
 AND   h.rn = 1;

h.rn = 1을 WHERE 절에 두면 이력이 없는 장비의 NULL 확장 행이 제거될 수 있습니다. 보존할 쪽과 선택 조건의 위치를 함께 확인합니다.

기준 시점 조건도 최신 이력 Inline View 안에 적용해야 합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
WITH ranked_history AS (
    SELECT h.*,
           ROW_NUMBER() OVER (
               PARTITION BY h.device_id
               ORDER BY h.change_time DESC NULLS LAST,
                        h.change_seq DESC,
                        h.history_id DESC
           ) AS rn
    FROM device_status_history h
    WHERE h.change_time <= :as_of
)
...
LEFT JOIN ranked_history h
  ON h.device_id = d.device_id
 AND h.rn = 1

이력이 없거나 기준 시점 이전 이력이 없는 Master Row도 보존됩니다.


10. 실행계획과 결과 검증 순서

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
1. 최신 한 건인지 최신 시각의 모든 행인지 정한다.
2. 기준 시점이 있는지 확인한다.
3. 전체 순서를 만드는 Tie-Breaker를 정의한다.
4. NULL 정렬 정책을 정한다.
5. 0건·1건·동률 데이터를 만들고 결과를 검증한다.
6. 소수 Key와 전체 Key 요구를 구분한다.
7. Top-1·ROW_NUMBER·KEEP 후보를 작성한다.
8. Starts·A-Rows·Buffers·Memory·TEMP와 첫 행·전체 Fetch 시간을 비교한다.
9. KEEP 식마다 동일한 전체 ORDER BY를 사용하는지 확인한다.
10. MAX 후 Join의 동률·NULL·복합 Key 일관성을 확인한다.
11. 이력이 없는 마스터 행의 보존 여부를 확인한다.
12. 최종 출력 순서는 바깥 ORDER BY로 보장한다.

혼동하기 쉬운 판단

단순 판단정확한 기준
최신 날짜만 찾으면 최신 행도 정해진다같은 날짜·시각의 순번과 PK까지 확인
ROW_NUMBER는 동률에서 항상 같은 행을 1번으로 고른다ORDER BY가 전체 순서를 만들 때만 결정적
MAX(날짜), MAX(상태)는 최신 행을 반환한다두 MAX가 서로 다른 원본 행에서 올 수 있음
KEEP는 실제 행 전체를 자동 선택한다LAST 순위의 행 집합에서 지정 Aggregate를 계산
INDEX_DESC Hint가 최신 결과의 정확성을 보장한다ORDER BY·Tie-Breaker·행 제한이 결과를 정의
최신 Query 결과가 한 건이면 작업도 한 건이다하위 Scan·Sort에서 많은 후보를 처리할 수 있음
WITH TIES는 Key별 최신 동률을 모두 반환일반적으로 전체 Query의 Fetch 경계 동률을 반환
모든 KEEP 식은 자동으로 같은 행을 참조ORDER BY가 다르면 서로 다른 Rank 집합에서 계산될 수 있음
MAX(time)이 NULL이면 NULL Row와 Join된다=의 NULL 비교는 TRUE가 아니므로 별도 처리 필요
Analytic ORDER BY가 최종 결과도 정렬바깥 Query의 ORDER BY가 최종 출력 순서를 보장
DESC Index를 만들면 항상 Sort·Table Access가 사라진다실제 Plan·Covering·후보 Row·Buffers를 검증

스스로 확인하기

개념 확인 문제

문제를 누르면 바로 아래에서 정답과 해설을 확인할 수 있습니다.

01“최신 행 한 건”과 “최신 시각의 모든 행”은 어떻게 다른가?
정답 및 해설

최신 한 행은 Tie-Breaker까지 적용해 실제 Row 하나를 결정하고, 최신 시각의 모든 행은 가장 최근 시간과 동률인 Row들을 보존합니다.

02최신 순서에 변경 시각 외의 Tie-Breaker가 필요한 이유는 무엇인가?
정답 및 해설

같은 변경 시각의 Row가 여러 개일 수 있기 때문에 변경 순번·PK를 추가해 전체 순서를 만들어야 합니다.

03특정 Key의 최신 행 한 건을 조회하는 대표 SQL 구조는 무엇인가?
정답 및 해설

특정 Key는 등치 조건과 최신 순서의 ORDER BY를 사용하고 FETCH FIRST 1 ROW ONLY로 제한합니다. Index 순서가 맞으면 Stopkey 조기 중단 후보가 됩니다.

04ROWNUMBER에서 ORDER BY가 전체 순서를 만들지 못하면 어떤 문제가 생기는가?
정답 및 해설

ROW_NUMBER는 동률에도 서로 다른 번호를 주므로 ORDER BY가 Total Order가 아니면 1번 Row가 비결정적일 수 있습니다.

05특정 기준 시점의 최신 행을 구할 때 기준 시점 조건을 어디에 적용해야 하는가?
정답 및 해설

기준 시점 조건은 Analytic 함수가 처리할 입력 Row Source 안에서 Rank 계산 전에 적용합니다.

06Key별 최신 시각의 동률 행을 모두 보존하려면 어떤 함수를 사용할 수 있는가?
정답 및 해설

Key별 최신 시각 동률 전체는 DENSE_RANK() OVER(PARTITION BY key ORDER BY change_time DESC NULLS LAST)의 Rank 1을 선택합니다.

07KEEP (DENSERANK LAST ORDER BY ...)는 어떤 순서로 값을 계산하는가?
정답 및 해설

KEEP는 ORDER BY로 FIRST/LAST Dense Rank 행 집합을 정한 뒤 그 집합에 MAX·MIN 등의 Aggregate를 적용합니다. 실제 행 전체를 자동 선택하지 않습니다.

08MAX(changetime), MAX(status)가 최신 행의 두 값을 보장하지 않는 이유는 무엇인가?
정답 및 해설

MAX(change_time)MAX(status)는 Column별 독립 Aggregate이므로 서로 다른 원본 Row에서 값이 올 수 있습니다.

09소수 Key와 다수 Key 최신 조회에서 비교할 대표 방식과 비용은 무엇인가?
정답 및 해설

소수 Key는 반복 Index Probe·Stopkey, 다수 Key는 ROW_NUMBER·KEEP·Group 후 Join을 비교합니다. 전자는 Starts·Random Access, 후자는 Scan·Window/Group Workarea·TEMP가 핵심입니다.

10이력이 없는 마스터 행까지 보존하려면 최신 이력 조건을 Outer Join의 어느 위치에 두는 것이 안전한가?
정답 및 해설

이력이 없는 Master를 보존하려면 Ranking 결과의 rn=1 조건을 LEFT JOIN의 ON 절에 두고, 기준 시점 Filter는 Ranking 입력 안에 둡니다.